@i4ctime/q-ring 0.14.0 → 0.15.0

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.
@@ -287,21 +287,27 @@ function mapEnvName(raw) {
287
287
 
288
288
  // src/core/observer.ts
289
289
  import {
290
- existsSync,
291
- mkdirSync as mkdirSync2,
290
+ existsSync as existsSync2,
291
+ mkdirSync as mkdirSync3,
292
292
  appendFileSync,
293
- chmodSync,
294
- readFileSync as readFileSync4,
293
+ chmodSync as chmodSync2,
294
+ readFileSync as readFileSync5,
295
295
  openSync,
296
296
  fstatSync,
297
297
  readSync,
298
298
  closeSync,
299
299
  statSync as statSync3
300
300
  } from "fs";
301
- import { join as join4 } from "path";
301
+ import { join as join5 } from "path";
302
+ import { homedir as homedir3 } from "os";
303
+ import { createHash, createHmac, randomBytes as randomBytes2 } from "crypto";
304
+
305
+ // src/core/backend.ts
306
+ import { Entry as NapiEntry, findCredentials as napiFindCredentials } from "@napi-rs/keyring";
307
+ import { existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "fs";
308
+ import { dirname as dirname2, join as join4 } from "path";
302
309
  import { homedir as homedir2 } from "os";
303
- import { createHash, createHmac, randomBytes } from "crypto";
304
- import { Entry } from "@napi-rs/keyring";
310
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from "crypto";
305
311
 
306
312
  // src/utils/file-lock.ts
307
313
  import {
@@ -332,17 +338,11 @@ function withFileLock(name, fn, opts = {}) {
332
338
  const deadline = Date.now() + (opts.timeoutMs ?? 8e3);
333
339
  const staleMs = opts.staleMs ?? 3e4;
334
340
  while (Date.now() < deadline) {
341
+ let acquired = false;
335
342
  try {
336
343
  writeFileSync(lockPath, `${process.pid}
337
344
  `, { flag: "wx", mode: 384 });
338
- try {
339
- return fn();
340
- } finally {
341
- try {
342
- unlinkSync(lockPath);
343
- } catch {
344
- }
345
- }
345
+ acquired = true;
346
346
  } catch {
347
347
  try {
348
348
  const holderPid = parseInt(readFileSync3(lockPath, "utf8").trim(), 10);
@@ -356,10 +356,172 @@ function withFileLock(name, fn, opts = {}) {
356
356
  }
357
357
  sleepSync(15);
358
358
  }
359
+ if (acquired) {
360
+ try {
361
+ return fn();
362
+ } finally {
363
+ try {
364
+ unlinkSync(lockPath);
365
+ } catch {
366
+ }
367
+ }
368
+ }
359
369
  }
360
370
  throw new Error(`Could not acquire lock "${name}" (timeout)`);
361
371
  }
362
372
 
373
+ // src/core/backend.ts
374
+ var PASSPHRASE_ENV = "QRING_FILE_PASSPHRASE";
375
+ var BACKEND_ENV = "QRING_BACKEND";
376
+ var PATH_ENV = "QRING_FILE_BACKEND_PATH";
377
+ var PBKDF2_ITERATIONS = 21e4;
378
+ var KEY_LENGTH = 32;
379
+ var FILE_PREFIX = "qfile1";
380
+ var BackendUnavailableError = class extends Error {
381
+ constructor(message) {
382
+ super(message);
383
+ this.name = "BackendUnavailableError";
384
+ }
385
+ };
386
+ function activeBackend() {
387
+ const requested = process.env[BACKEND_ENV];
388
+ if (requested === "file") return "file";
389
+ if (requested && requested !== "keyring") {
390
+ throw new BackendUnavailableError(
391
+ `Unknown ${BACKEND_ENV} "${requested}" \u2014 expected "keyring" or "file".`
392
+ );
393
+ }
394
+ return "keyring";
395
+ }
396
+ function storePath() {
397
+ return process.env[PATH_ENV] ?? join4(homedir2(), ".config", "q-ring", "file-backend.enc");
398
+ }
399
+ function passphrase() {
400
+ const p = process.env[PASSPHRASE_ENV];
401
+ if (!p) {
402
+ throw new BackendUnavailableError(
403
+ `${BACKEND_ENV}=file requires ${PASSPHRASE_ENV} to be set. q-ring refuses to store secrets under a machine-derivable key \u2014 set a strong passphrase, or use the OS keyring backend.`
404
+ );
405
+ }
406
+ return p;
407
+ }
408
+ var keyCache = null;
409
+ function deriveKey(saltB64) {
410
+ const pass = passphrase();
411
+ if (keyCache && keyCache.salt === saltB64 && keyCache.pass === pass) {
412
+ return keyCache.key;
413
+ }
414
+ const key = pbkdf2Sync(pass, Buffer.from(saltB64, "base64"), PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
415
+ keyCache = { salt: saltB64, pass, key };
416
+ return key;
417
+ }
418
+ function loadStore() {
419
+ const path = storePath();
420
+ if (!existsSync(path)) {
421
+ return { map: {}, salt: randomBytes(16).toString("base64") };
422
+ }
423
+ const blob = readFileSync4(path, "utf8").trim();
424
+ const parts = blob.split(":");
425
+ if (parts.length !== 5 || parts[0] !== FILE_PREFIX) {
426
+ throw new BackendUnavailableError(
427
+ `${path} is not a valid q-ring file-backend store (expected ${FILE_PREFIX}:...).`
428
+ );
429
+ }
430
+ const [, salt, ivB64, tagB64, ctB64] = parts;
431
+ const decipher = createDecipheriv("aes-256-gcm", deriveKey(salt), Buffer.from(ivB64, "base64"));
432
+ decipher.setAuthTag(Buffer.from(tagB64, "base64"));
433
+ let plaintext;
434
+ try {
435
+ plaintext = decipher.update(Buffer.from(ctB64, "base64")) + decipher.final("utf8");
436
+ } catch {
437
+ throw new BackendUnavailableError(
438
+ `Cannot decrypt ${path} \u2014 wrong ${PASSPHRASE_ENV}, or the store was tampered with.`
439
+ );
440
+ }
441
+ return { map: JSON.parse(plaintext), salt };
442
+ }
443
+ function saveStore(map, salt) {
444
+ const path = storePath();
445
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
446
+ const iv = randomBytes(12);
447
+ const cipher = createCipheriv("aes-256-gcm", deriveKey(salt), iv);
448
+ const ct = Buffer.concat([cipher.update(JSON.stringify(map), "utf8"), cipher.final()]);
449
+ const blob = [
450
+ FILE_PREFIX,
451
+ salt,
452
+ iv.toString("base64"),
453
+ cipher.getAuthTag().toString("base64"),
454
+ ct.toString("base64")
455
+ ].join(":");
456
+ writeFileSync2(path, blob + "\n", { mode: 384 });
457
+ try {
458
+ chmodSync(path, 384);
459
+ } catch {
460
+ }
461
+ }
462
+ var SEP = "\0";
463
+ function fileMutate(fn) {
464
+ return withFileLock("file-backend", () => {
465
+ const { map, salt } = loadStore();
466
+ const result = fn(map);
467
+ saveStore(map, salt);
468
+ return result;
469
+ });
470
+ }
471
+ var Entry = class {
472
+ constructor(service, account) {
473
+ this.service = service;
474
+ this.account = account;
475
+ }
476
+ service;
477
+ account;
478
+ napi;
479
+ delegate() {
480
+ this.napi ??= new NapiEntry(this.service, this.account);
481
+ return this.napi;
482
+ }
483
+ storeKey() {
484
+ return `${this.service}${SEP}${this.account}`;
485
+ }
486
+ getPassword() {
487
+ if (activeBackend() === "file") {
488
+ return loadStore().map[this.storeKey()] ?? null;
489
+ }
490
+ return this.delegate().getPassword();
491
+ }
492
+ setPassword(password) {
493
+ if (activeBackend() === "file") {
494
+ fileMutate((map) => {
495
+ map[this.storeKey()] = password;
496
+ });
497
+ return;
498
+ }
499
+ this.delegate().setPassword(password);
500
+ }
501
+ deleteCredential() {
502
+ if (activeBackend() === "file") {
503
+ return fileMutate((map) => {
504
+ const existed = this.storeKey() in map;
505
+ delete map[this.storeKey()];
506
+ return existed;
507
+ });
508
+ }
509
+ return this.delegate().deleteCredential();
510
+ }
511
+ // @napi-rs/keyring's other delete alias, used by `qring doctor`.
512
+ deletePassword() {
513
+ return this.deleteCredential();
514
+ }
515
+ };
516
+ function findCredentials(service) {
517
+ if (activeBackend() === "file") {
518
+ const { map } = loadStore();
519
+ const prefix = `${service}${SEP}`;
520
+ return Object.entries(map).filter(([k]) => k.startsWith(prefix)).map(([k, password]) => ({ account: k.slice(prefix.length), password }));
521
+ }
522
+ return napiFindCredentials(service);
523
+ }
524
+
363
525
  // src/core/observer.ts
364
526
  var AUDIT_KEYRING_SERVICE = "qring-audit-chain";
365
527
  var AUDIT_KEY_ACCOUNT = "hmac-key";
@@ -370,7 +532,7 @@ function getAuditKey() {
370
532
  const entry = new Entry(AUDIT_KEYRING_SERVICE, AUDIT_KEY_ACCOUNT);
371
533
  const stored = entry.getPassword();
372
534
  if (stored) return Buffer.from(stored, "base64");
373
- const key = randomBytes(32);
535
+ const key = randomBytes2(32);
374
536
  entry.setPassword(key.toString("base64"));
375
537
  return key;
376
538
  } catch {
@@ -401,23 +563,23 @@ function writeStoredAnchor(hash) {
401
563
  }
402
564
  function getAuditDir() {
403
565
  if (process.env.QRING_AUDIT_DIR) {
404
- if (!existsSync(process.env.QRING_AUDIT_DIR)) {
405
- mkdirSync2(process.env.QRING_AUDIT_DIR, { recursive: true, mode: 448 });
566
+ if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
567
+ mkdirSync3(process.env.QRING_AUDIT_DIR, { recursive: true, mode: 448 });
406
568
  }
407
569
  return process.env.QRING_AUDIT_DIR;
408
570
  }
409
- const dir = join4(homedir2(), ".config", "q-ring");
410
- if (!existsSync(dir)) {
411
- mkdirSync2(dir, { recursive: true, mode: 448 });
571
+ const dir = join5(homedir3(), ".config", "q-ring");
572
+ if (!existsSync2(dir)) {
573
+ mkdirSync3(dir, { recursive: true, mode: 448 });
412
574
  }
413
575
  return dir;
414
576
  }
415
577
  function getAuditPath() {
416
- return join4(getAuditDir(), "audit.jsonl");
578
+ return join5(getAuditDir(), "audit.jsonl");
417
579
  }
418
580
  function getLastLineHash() {
419
581
  const path = getAuditPath();
420
- if (!existsSync(path)) return void 0;
582
+ if (!existsSync2(path)) return void 0;
421
583
  try {
422
584
  const fd = openSync(path, "r");
423
585
  const stat = fstatSync(fd);
@@ -454,7 +616,7 @@ function logAudit(event) {
454
616
  const path = getAuditPath();
455
617
  appendFileSync(path, line + "\n", { mode: 384 });
456
618
  try {
457
- chmodSync(path, 384);
619
+ chmodSync2(path, 384);
458
620
  } catch {
459
621
  }
460
622
  const key = getAuditKey();
@@ -468,7 +630,7 @@ function logAudit(event) {
468
630
  var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
469
631
  function queryAudit(query = {}) {
470
632
  const path = getAuditPath();
471
- if (!existsSync(path)) return [];
633
+ if (!existsSync2(path)) return [];
472
634
  try {
473
635
  const st = statSync3(path);
474
636
  const readStart = st.size > MAX_AUDIT_BYTES ? st.size - MAX_AUDIT_BYTES : 0;
@@ -509,10 +671,10 @@ function queryAudit(query = {}) {
509
671
  }
510
672
  function verifyAuditChain() {
511
673
  const path = getAuditPath();
512
- if (!existsSync(path)) {
674
+ if (!existsSync2(path)) {
513
675
  return { totalEvents: 0, validEvents: 0, intact: true };
514
676
  }
515
- const lines = readFileSync4(path, "utf8").split("\n").filter((l) => l.trim());
677
+ const lines = readFileSync5(path, "utf8").split("\n").filter((l) => l.trim());
516
678
  if (lines.length === 0) {
517
679
  return { totalEvents: 0, validEvents: 0, intact: true };
518
680
  }
@@ -563,8 +725,8 @@ function verifyAuditChain() {
563
725
  }
564
726
  function exportAudit(opts = {}) {
565
727
  const path = getAuditPath();
566
- if (!existsSync(path)) return opts.format === "json" ? "[]" : "";
567
- const lines = readFileSync4(path, "utf8").split("\n").filter((l) => l.trim());
728
+ if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
729
+ const lines = readFileSync5(path, "utf8").split("\n").filter((l) => l.trim());
568
730
  let events = lines.map((l) => {
569
731
  try {
570
732
  return JSON.parse(l);
@@ -629,15 +791,15 @@ function detectAnomalies(key) {
629
791
  }
630
792
 
631
793
  // src/core/entanglement.ts
632
- import { existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
633
- import { join as join5 } from "path";
634
- import { homedir as homedir3 } from "os";
794
+ import { existsSync as existsSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
795
+ import { join as join6 } from "path";
796
+ import { homedir as homedir4 } from "os";
635
797
 
636
798
  // src/utils/registry.ts
637
- import { existsSync as existsSync2, readFileSync as readFileSync5, renameSync } from "fs";
799
+ import { existsSync as existsSync3, readFileSync as readFileSync6, renameSync } from "fs";
638
800
  function loadJsonRegistry(path, empty) {
639
- if (!existsSync2(path)) return empty;
640
- const raw = readFileSync5(path, "utf8");
801
+ if (!existsSync3(path)) return empty;
802
+ const raw = readFileSync6(path, "utf8");
641
803
  try {
642
804
  return JSON.parse(raw);
643
805
  } catch (err) {
@@ -661,18 +823,18 @@ function loadJsonRegistry(path, empty) {
661
823
  // src/core/entanglement.ts
662
824
  var REGISTRY_VERSION = 1;
663
825
  function getRegistryPath() {
664
- const dir = join5(homedir3(), ".config", "q-ring");
665
- if (!existsSync3(dir)) {
666
- mkdirSync3(dir, { recursive: true, mode: 448 });
826
+ const dir = join6(homedir4(), ".config", "q-ring");
827
+ if (!existsSync4(dir)) {
828
+ mkdirSync4(dir, { recursive: true, mode: 448 });
667
829
  }
668
- return join5(dir, "entanglement.json");
830
+ return join6(dir, "entanglement.json");
669
831
  }
670
832
  function loadRegistry() {
671
833
  return loadJsonRegistry(getRegistryPath(), { pairs: [] });
672
834
  }
673
835
  function saveRegistry(registry2) {
674
836
  registry2.version = REGISTRY_VERSION;
675
- writeFileSync2(getRegistryPath(), JSON.stringify(registry2, null, 2), {
837
+ writeFileSync3(getRegistryPath(), JSON.stringify(registry2, null, 2), {
676
838
  mode: 384
677
839
  });
678
840
  }
@@ -706,9 +868,9 @@ function listEntanglements() {
706
868
  }
707
869
 
708
870
  // src/core/hooks.ts
709
- import { existsSync as existsSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
710
- import { join as join6 } from "path";
711
- import { homedir as homedir4 } from "os";
871
+ import { existsSync as existsSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
872
+ import { join as join7 } from "path";
873
+ import { homedir as homedir5 } from "os";
712
874
  import { execFile, spawn } from "child_process";
713
875
  import { randomUUID } from "crypto";
714
876
 
@@ -932,17 +1094,17 @@ function httpRequest(opts) {
932
1094
 
933
1095
  // src/core/hooks.ts
934
1096
  function getRegistryPath2() {
935
- const dir = join6(homedir4(), ".config", "q-ring");
936
- if (!existsSync4(dir)) {
937
- mkdirSync4(dir, { recursive: true, mode: 448 });
1097
+ const dir = join7(homedir5(), ".config", "q-ring");
1098
+ if (!existsSync5(dir)) {
1099
+ mkdirSync5(dir, { recursive: true, mode: 448 });
938
1100
  }
939
- return join6(dir, "hooks.json");
1101
+ return join7(dir, "hooks.json");
940
1102
  }
941
1103
  function loadRegistry2() {
942
1104
  return loadJsonRegistry(getRegistryPath2(), { hooks: [] });
943
1105
  }
944
1106
  function saveRegistry2(registry2) {
945
- writeFileSync3(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
1107
+ writeFileSync4(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
946
1108
  mode: 384
947
1109
  });
948
1110
  }
@@ -1189,19 +1351,19 @@ async function fireHooks(payload, tags) {
1189
1351
  }
1190
1352
 
1191
1353
  // src/core/approval.ts
1192
- import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
1193
- import { join as join7 } from "path";
1194
- import { homedir as homedir5 } from "os";
1195
- import { createHmac as createHmac2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1354
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
1355
+ import { join as join8 } from "path";
1356
+ import { homedir as homedir6 } from "os";
1357
+ import { createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1196
1358
  function getHmacSecret() {
1197
- const dir = join7(homedir5(), ".config", "q-ring");
1198
- const secretPath = join7(dir, ".approval-key");
1199
- if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true, mode: 448 });
1200
- if (existsSync5(secretPath)) {
1201
- return readFileSync6(secretPath, "utf8").trim();
1202
- }
1203
- const secret = randomBytes2(32).toString("hex");
1204
- writeFileSync4(secretPath, secret, { mode: 384 });
1359
+ const dir = join8(homedir6(), ".config", "q-ring");
1360
+ const secretPath = join8(dir, ".approval-key");
1361
+ if (!existsSync6(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
1362
+ if (existsSync6(secretPath)) {
1363
+ return readFileSync7(secretPath, "utf8").trim();
1364
+ }
1365
+ const secret = randomBytes3(32).toString("hex");
1366
+ writeFileSync5(secretPath, secret, { mode: 384 });
1205
1367
  return secret;
1206
1368
  }
1207
1369
  function computeHmac(entry) {
@@ -1231,11 +1393,11 @@ function verifyHmac(entry) {
1231
1393
  }
1232
1394
  }
1233
1395
  function getRegistryPath3() {
1234
- const dir = join7(homedir5(), ".config", "q-ring");
1235
- if (!existsSync5(dir)) {
1236
- mkdirSync5(dir, { recursive: true, mode: 448 });
1396
+ const dir = join8(homedir6(), ".config", "q-ring");
1397
+ if (!existsSync6(dir)) {
1398
+ mkdirSync6(dir, { recursive: true, mode: 448 });
1237
1399
  }
1238
- return join7(dir, "approvals.json");
1400
+ return join8(dir, "approvals.json");
1239
1401
  }
1240
1402
  function loadRegistry3() {
1241
1403
  return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
@@ -1262,7 +1424,7 @@ function listApprovals() {
1262
1424
 
1263
1425
  // src/core/policy.ts
1264
1426
  import { statSync as statSync4 } from "fs";
1265
- import { join as join8 } from "path";
1427
+ import { join as join9 } from "path";
1266
1428
  import { z as z2 } from "zod";
1267
1429
  var stringArray = z2.array(z2.string());
1268
1430
  var mcpPolicySchema = z2.object({
@@ -1308,7 +1470,7 @@ function resolvePolicyPath(projectPath) {
1308
1470
  }
1309
1471
  function configMtime(pp) {
1310
1472
  try {
1311
- return statSync4(join8(pp, ".q-ring.json")).mtimeMs;
1473
+ return statSync4(join9(pp, ".q-ring.json")).mtimeMs;
1312
1474
  } catch {
1313
1475
  return 0;
1314
1476
  }
@@ -1331,7 +1493,7 @@ function loadPolicy(projectPath) {
1331
1493
  if (!parsed.success) {
1332
1494
  const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
1333
1495
  const error = new PolicyConfigError(
1334
- `Invalid policy in ${join8(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
1496
+ `Invalid policy in ${join9(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
1335
1497
  );
1336
1498
  console.error(`q-ring: ${error.message}`);
1337
1499
  cachedPolicy = { path: pp, mtimeMs, error };
@@ -1466,9 +1628,6 @@ function getPolicySummary(projectPath) {
1466
1628
  };
1467
1629
  }
1468
1630
 
1469
- // src/core/keyring.ts
1470
- import { Entry as Entry2, findCredentials } from "@napi-rs/keyring";
1471
-
1472
1631
  // src/utils/hash.ts
1473
1632
  import { createHash as createHash2 } from "crypto";
1474
1633
  function hashProjectPath(projectPath) {
@@ -1524,6 +1683,50 @@ function serviceForScope(scope, opts = {}) {
1524
1683
  return resolveScope({ ...opts, scope })[0].service;
1525
1684
  }
1526
1685
 
1686
+ // src/core/notify.ts
1687
+ import { spawn as spawn2 } from "child_process";
1688
+ var DISABLE_ENV = "QRING_NOTIFY";
1689
+ var THROTTLE_MS = 5 * 60 * 1e3;
1690
+ var lastNotified = /* @__PURE__ */ new Map();
1691
+ function notificationsEnabled() {
1692
+ const value = process.env[DISABLE_ENV];
1693
+ return value !== "off" && value !== "0" && value !== "false";
1694
+ }
1695
+ function notifyUser(title, body) {
1696
+ try {
1697
+ let command;
1698
+ let args;
1699
+ if (process.platform === "linux") {
1700
+ command = "notify-send";
1701
+ args = ["--app-name=q-ring", "--urgency=critical", title, body];
1702
+ } else if (process.platform === "darwin") {
1703
+ command = "osascript";
1704
+ const clean = (s) => s.replace(/["\\]/g, "");
1705
+ args = ["-e", `display notification "${clean(body)}" with title "${clean(title)}"`];
1706
+ } else {
1707
+ return false;
1708
+ }
1709
+ const child = spawn2(command, args, { detached: true, stdio: "ignore" });
1710
+ child.on("error", () => {
1711
+ });
1712
+ child.unref();
1713
+ return true;
1714
+ } catch {
1715
+ return false;
1716
+ }
1717
+ }
1718
+ function notifyApprovalRequested(key, source) {
1719
+ if (!notificationsEnabled()) return;
1720
+ const now = Date.now();
1721
+ const last = lastNotified.get(key);
1722
+ if (last !== void 0 && now - last < THROTTLE_MS) return;
1723
+ lastNotified.set(key, now);
1724
+ notifyUser(
1725
+ "q-ring: approval requested",
1726
+ `An ${source} agent wants to read "${key}". Allow with: qring approve ${key}`
1727
+ );
1728
+ }
1729
+
1527
1730
  // src/core/provision.ts
1528
1731
  import { execFileSync, spawnSync } from "child_process";
1529
1732
  import { z as z3 } from "zod";
@@ -1676,14 +1879,14 @@ function withJitEnvelopeLock(service, key, fn) {
1676
1879
  return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
1677
1880
  }
1678
1881
  function readEnvelope(service, key) {
1679
- const entry = new Entry2(service, key);
1882
+ const entry = new Entry(service, key);
1680
1883
  const raw = entry.getPassword();
1681
1884
  if (raw === null) return null;
1682
1885
  const envelope = parseEnvelope(raw);
1683
1886
  return envelope ?? wrapLegacy(raw);
1684
1887
  }
1685
1888
  function writeEnvelope(service, key, envelope) {
1686
- const entry = new Entry2(service, key);
1889
+ const entry = new Entry(service, key);
1687
1890
  entry.setPassword(serializeEnvelope(envelope));
1688
1891
  }
1689
1892
  function resolveEnv(opts) {
@@ -1760,6 +1963,7 @@ function getSecret(key, opts = {}) {
1760
1963
  source,
1761
1964
  detail: "blocked: requires user approval"
1762
1965
  });
1966
+ notifyApprovalRequested(key, source);
1763
1967
  }
1764
1968
  throw new Error(`Access Denied: This secret requires user approval. Please ask the user to run 'qring approve ${key}'`);
1765
1969
  }
@@ -1941,7 +2145,7 @@ function deleteSecret(key, opts = {}) {
1941
2145
  }
1942
2146
  let deleted = false;
1943
2147
  for (const { service, scope } of scopes) {
1944
- const entry = new Entry2(service, key);
2148
+ const entry = new Entry(service, key);
1945
2149
  try {
1946
2150
  if (entry.deleteCredential()) {
1947
2151
  deleted = true;
@@ -2121,7 +2325,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
2121
2325
  }
2122
2326
 
2123
2327
  // src/core/tunnel.ts
2124
- import { randomBytes as randomBytes3 } from "crypto";
2328
+ import { randomBytes as randomBytes4 } from "crypto";
2125
2329
  var tunnelStore = /* @__PURE__ */ new Map();
2126
2330
  var cleanupInterval = null;
2127
2331
  function ensureCleanup() {
@@ -2143,7 +2347,7 @@ function ensureCleanup() {
2143
2347
  }
2144
2348
  }
2145
2349
  function tunnelCreate(value, opts = {}) {
2146
- const id = `tun_${Date.now().toString(36)}_${randomBytes3(6).toString("base64url")}`;
2350
+ const id = `tun_${Date.now().toString(36)}_${randomBytes4(6).toString("base64url")}`;
2147
2351
  const now = Date.now();
2148
2352
  tunnelStore.set(id, {
2149
2353
  value,
@@ -2193,40 +2397,39 @@ function tunnelList() {
2193
2397
  }
2194
2398
 
2195
2399
  // src/core/memory.ts
2196
- import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, chmodSync as chmodSync2 } from "fs";
2197
- import { join as join9 } from "path";
2198
- import { homedir as homedir6, hostname, userInfo } from "os";
2400
+ import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, chmodSync as chmodSync3 } from "fs";
2401
+ import { join as join10 } from "path";
2402
+ import { homedir as homedir7, hostname, userInfo } from "os";
2199
2403
  import {
2200
- createCipheriv,
2201
- createDecipheriv,
2404
+ createCipheriv as createCipheriv2,
2405
+ createDecipheriv as createDecipheriv2,
2202
2406
  createHash as createHash3,
2203
- randomBytes as randomBytes4,
2204
- pbkdf2Sync
2407
+ randomBytes as randomBytes5,
2408
+ pbkdf2Sync as pbkdf2Sync2
2205
2409
  } from "crypto";
2206
- import { Entry as Entry3 } from "@napi-rs/keyring";
2207
2410
  var MEMORY_FILE = "agent-memory.enc";
2208
2411
  var KEYRING_SERVICE = "qring-memory-key";
2209
2412
  var KEYRING_ACCOUNT = "encryption-key";
2210
2413
  function getMemoryDir() {
2211
- const dir = join9(homedir6(), ".config", "q-ring");
2212
- if (!existsSync6(dir)) {
2213
- mkdirSync6(dir, { recursive: true, mode: 448 });
2414
+ const dir = join10(homedir7(), ".config", "q-ring");
2415
+ if (!existsSync7(dir)) {
2416
+ mkdirSync7(dir, { recursive: true, mode: 448 });
2214
2417
  }
2215
2418
  return dir;
2216
2419
  }
2217
2420
  function writeMemoryFile(path, data) {
2218
- writeFileSync5(path, data, { mode: 384 });
2421
+ writeFileSync6(path, data, { mode: 384 });
2219
2422
  try {
2220
- chmodSync2(path, 384);
2423
+ chmodSync3(path, 384);
2221
2424
  } catch {
2222
2425
  }
2223
2426
  }
2224
2427
  function getMemoryPath() {
2225
- return join9(getMemoryDir(), MEMORY_FILE);
2428
+ return join10(getMemoryDir(), MEMORY_FILE);
2226
2429
  }
2227
- var PBKDF2_ITERATIONS = 21e4;
2228
- var KEY_LENGTH = 32;
2229
- var PASSPHRASE_ENV = "QRING_MEMORY_PASSPHRASE";
2430
+ var PBKDF2_ITERATIONS2 = 21e4;
2431
+ var KEY_LENGTH2 = 32;
2432
+ var PASSPHRASE_ENV2 = "QRING_MEMORY_PASSPHRASE";
2230
2433
  var V2_PREFIX = "qmem2";
2231
2434
  var MemoryKeyUnavailableError = class extends Error {
2232
2435
  constructor(message) {
@@ -2238,19 +2441,19 @@ function deriveLegacyKey() {
2238
2441
  const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
2239
2442
  return createHash3("sha256").update(fingerprint).digest();
2240
2443
  }
2241
- function passphrase() {
2242
- const p = process.env[PASSPHRASE_ENV];
2444
+ function passphrase2() {
2445
+ const p = process.env[PASSPHRASE_ENV2];
2243
2446
  return p && p.length > 0 ? p : void 0;
2244
2447
  }
2245
2448
  function derivePassphraseKey(salt) {
2246
- return pbkdf2Sync(passphrase(), salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
2449
+ return pbkdf2Sync2(passphrase2(), salt, PBKDF2_ITERATIONS2, KEY_LENGTH2, "sha512");
2247
2450
  }
2248
2451
  function keyringKey() {
2249
2452
  try {
2250
- const entry = new Entry3(KEYRING_SERVICE, KEYRING_ACCOUNT);
2453
+ const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
2251
2454
  const stored = entry.getPassword();
2252
2455
  if (stored) return Buffer.from(stored, "base64");
2253
- const key = randomBytes4(KEY_LENGTH);
2456
+ const key = randomBytes5(KEY_LENGTH2);
2254
2457
  entry.setPassword(key.toString("base64"));
2255
2458
  return key;
2256
2459
  } catch {
@@ -2258,8 +2461,8 @@ function keyringKey() {
2258
2461
  }
2259
2462
  }
2260
2463
  function encryptWith(data, key) {
2261
- const iv = randomBytes4(12);
2262
- const cipher = createCipheriv("aes-256-gcm", key, iv);
2464
+ const iv = randomBytes5(12);
2465
+ const cipher = createCipheriv2("aes-256-gcm", key, iv);
2263
2466
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
2264
2467
  const tag = cipher.getAuthTag();
2265
2468
  return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
@@ -2270,27 +2473,27 @@ function decryptWith(blob, key) {
2270
2473
  const iv = Buffer.from(parts[0], "base64");
2271
2474
  const tag = Buffer.from(parts[1], "base64");
2272
2475
  const encrypted = Buffer.from(parts[2], "base64");
2273
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
2476
+ const decipher = createDecipheriv2("aes-256-gcm", key, iv);
2274
2477
  decipher.setAuthTag(tag);
2275
2478
  return decipher.update(encrypted) + decipher.final("utf8");
2276
2479
  }
2277
2480
  function encrypt(data) {
2278
2481
  const kk = keyringKey();
2279
2482
  if (kk) return encryptWith(data, kk);
2280
- if (passphrase()) {
2281
- const salt = randomBytes4(16);
2483
+ if (passphrase2()) {
2484
+ const salt = randomBytes5(16);
2282
2485
  const key = derivePassphraseKey(salt);
2283
2486
  return `${V2_PREFIX}:${salt.toString("base64")}:${encryptWith(data, key)}`;
2284
2487
  }
2285
2488
  throw new MemoryKeyUnavailableError(
2286
- `Cannot persist agent memory: the OS keyring is unavailable and ${PASSPHRASE_ENV} is not set. Refusing to encrypt with a machine-derivable key (any local process could recompute it and read your memory). Set ${PASSPHRASE_ENV} to a strong passphrase, or run on a host with an OS keyring.`
2489
+ `Cannot persist agent memory: the OS keyring is unavailable and ${PASSPHRASE_ENV2} is not set. Refusing to encrypt with a machine-derivable key (any local process could recompute it and read your memory). Set ${PASSPHRASE_ENV2} to a strong passphrase, or run on a host with an OS keyring.`
2287
2490
  );
2288
2491
  }
2289
2492
  function decrypt(blob) {
2290
2493
  if (blob.startsWith(`${V2_PREFIX}:`)) {
2291
- if (!passphrase()) {
2494
+ if (!passphrase2()) {
2292
2495
  throw new MemoryKeyUnavailableError(
2293
- `Agent memory was encrypted with ${PASSPHRASE_ENV} but it is not set \u2014 cannot decrypt.`
2496
+ `Agent memory was encrypted with ${PASSPHRASE_ENV2} but it is not set \u2014 cannot decrypt.`
2294
2497
  );
2295
2498
  }
2296
2499
  const rest = blob.slice(V2_PREFIX.length + 1);
@@ -2314,13 +2517,13 @@ function decrypt(blob) {
2314
2517
  }
2315
2518
  return plain;
2316
2519
  }
2317
- function loadStore() {
2520
+ function loadStore2() {
2318
2521
  const path = getMemoryPath();
2319
- if (!existsSync6(path)) {
2522
+ if (!existsSync7(path)) {
2320
2523
  return { entries: {} };
2321
2524
  }
2322
2525
  try {
2323
- const raw = readFileSync7(path, "utf8");
2526
+ const raw = readFileSync8(path, "utf8");
2324
2527
  const decrypted = decrypt(raw);
2325
2528
  return JSON.parse(decrypted);
2326
2529
  } catch (err) {
@@ -2330,35 +2533,35 @@ function loadStore() {
2330
2533
  return { entries: {} };
2331
2534
  }
2332
2535
  }
2333
- function saveStore(store) {
2536
+ function saveStore2(store) {
2334
2537
  const json = JSON.stringify(store);
2335
2538
  const encrypted = encrypt(json);
2336
2539
  writeMemoryFile(getMemoryPath(), encrypted);
2337
2540
  }
2338
2541
  function remember(key, value) {
2339
- const store = loadStore();
2542
+ const store = loadStore2();
2340
2543
  store.entries[key] = {
2341
2544
  value,
2342
2545
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2343
2546
  };
2344
- saveStore(store);
2547
+ saveStore2(store);
2345
2548
  }
2346
2549
  function recall(key) {
2347
- const store = loadStore();
2550
+ const store = loadStore2();
2348
2551
  return store.entries[key]?.value ?? null;
2349
2552
  }
2350
2553
  function listMemory() {
2351
- const store = loadStore();
2554
+ const store = loadStore2();
2352
2555
  return Object.entries(store.entries).map(([key, entry]) => ({
2353
2556
  key,
2354
2557
  updatedAt: entry.updatedAt
2355
2558
  }));
2356
2559
  }
2357
2560
  function forget(key) {
2358
- const store = loadStore();
2561
+ const store = loadStore2();
2359
2562
  if (key in store.entries) {
2360
2563
  delete store.entries[key];
2361
- saveStore(store);
2564
+ saveStore2(store);
2362
2565
  return true;
2363
2566
  }
2364
2567
  return false;
@@ -2407,4 +2610,4 @@ export {
2407
2610
  listMemory,
2408
2611
  forget
2409
2612
  };
2410
- //# sourceMappingURL=chunk-NNIEXAW5.js.map
2613
+ //# sourceMappingURL=chunk-TPPPTL4J.js.map