@i4ctime/q-ring 0.14.1 → 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
  }
@@ -1205,19 +1367,19 @@ async function fireHooks(payload, tags) {
1205
1367
  }
1206
1368
 
1207
1369
  // src/core/approval.ts
1208
- import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
1209
- import { join as join7 } from "path";
1210
- import { homedir as homedir5 } from "os";
1211
- import { createHmac as createHmac2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1370
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
1371
+ import { join as join8 } from "path";
1372
+ import { homedir as homedir6 } from "os";
1373
+ import { createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1212
1374
  function getHmacSecret() {
1213
- const dir = join7(homedir5(), ".config", "q-ring");
1214
- const secretPath = join7(dir, ".approval-key");
1215
- if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true, mode: 448 });
1216
- if (existsSync5(secretPath)) {
1217
- return readFileSync6(secretPath, "utf8").trim();
1218
- }
1219
- const secret = randomBytes2(32).toString("hex");
1220
- writeFileSync4(secretPath, secret, { mode: 384 });
1375
+ const dir = join8(homedir6(), ".config", "q-ring");
1376
+ const secretPath = join8(dir, ".approval-key");
1377
+ if (!existsSync6(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
1378
+ if (existsSync6(secretPath)) {
1379
+ return readFileSync7(secretPath, "utf8").trim();
1380
+ }
1381
+ const secret = randomBytes3(32).toString("hex");
1382
+ writeFileSync5(secretPath, secret, { mode: 384 });
1221
1383
  return secret;
1222
1384
  }
1223
1385
  function computeHmac(entry) {
@@ -1247,17 +1409,17 @@ function verifyHmac(entry) {
1247
1409
  }
1248
1410
  }
1249
1411
  function getRegistryPath3() {
1250
- const dir = join7(homedir5(), ".config", "q-ring");
1251
- if (!existsSync5(dir)) {
1252
- mkdirSync5(dir, { recursive: true, mode: 448 });
1412
+ const dir = join8(homedir6(), ".config", "q-ring");
1413
+ if (!existsSync6(dir)) {
1414
+ mkdirSync6(dir, { recursive: true, mode: 448 });
1253
1415
  }
1254
- return join7(dir, "approvals.json");
1416
+ return join8(dir, "approvals.json");
1255
1417
  }
1256
1418
  function loadRegistry3() {
1257
1419
  return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
1258
1420
  }
1259
1421
  function saveRegistry3(registry2) {
1260
- writeFileSync4(getRegistryPath3(), JSON.stringify(registry2, null, 2), { mode: 384 });
1422
+ writeFileSync5(getRegistryPath3(), JSON.stringify(registry2, null, 2), { mode: 384 });
1261
1423
  }
1262
1424
  function cleanup(registry2) {
1263
1425
  const now = Date.now();
@@ -1268,7 +1430,7 @@ function cleanup(registry2) {
1268
1430
  function grantApproval(key, scope, service, ttlSeconds = 3600, grantOpts = {}) {
1269
1431
  const registry2 = loadRegistry3();
1270
1432
  cleanup(registry2);
1271
- const id = randomBytes2(8).toString("hex");
1433
+ const id = randomBytes3(8).toString("hex");
1272
1434
  const grantedAt = (/* @__PURE__ */ new Date()).toISOString();
1273
1435
  const expiresAt = new Date(Date.now() + ttlSeconds * 1e3).toISOString();
1274
1436
  const partial = {
@@ -1329,7 +1491,7 @@ function listApprovals() {
1329
1491
 
1330
1492
  // src/core/policy.ts
1331
1493
  import { statSync as statSync4 } from "fs";
1332
- import { join as join8 } from "path";
1494
+ import { join as join9 } from "path";
1333
1495
  import { z as z2 } from "zod";
1334
1496
  var stringArray = z2.array(z2.string());
1335
1497
  var mcpPolicySchema = z2.object({
@@ -1371,7 +1533,7 @@ function resolvePolicyPath(projectPath) {
1371
1533
  }
1372
1534
  function configMtime(pp) {
1373
1535
  try {
1374
- return statSync4(join8(pp, ".q-ring.json")).mtimeMs;
1536
+ return statSync4(join9(pp, ".q-ring.json")).mtimeMs;
1375
1537
  } catch {
1376
1538
  return 0;
1377
1539
  }
@@ -1394,7 +1556,7 @@ function loadPolicy(projectPath) {
1394
1556
  if (!parsed.success) {
1395
1557
  const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
1396
1558
  const error = new PolicyConfigError(
1397
- `Invalid policy in ${join8(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
1559
+ `Invalid policy in ${join9(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
1398
1560
  );
1399
1561
  console.error(`q-ring: ${error.message}`);
1400
1562
  cachedPolicy = { path: pp, mtimeMs, error };
@@ -1510,9 +1672,6 @@ function getPolicySummary(projectPath) {
1510
1672
  };
1511
1673
  }
1512
1674
 
1513
- // src/core/keyring.ts
1514
- import { Entry as Entry2, findCredentials } from "@napi-rs/keyring";
1515
-
1516
1675
  // src/utils/hash.ts
1517
1676
  import { createHash as createHash2 } from "crypto";
1518
1677
  function hashProjectPath(projectPath) {
@@ -1568,6 +1727,50 @@ function serviceForScope(scope, opts = {}) {
1568
1727
  return resolveScope({ ...opts, scope })[0].service;
1569
1728
  }
1570
1729
 
1730
+ // src/core/notify.ts
1731
+ import { spawn as spawn2 } from "child_process";
1732
+ var DISABLE_ENV = "QRING_NOTIFY";
1733
+ var THROTTLE_MS = 5 * 60 * 1e3;
1734
+ var lastNotified = /* @__PURE__ */ new Map();
1735
+ function notificationsEnabled() {
1736
+ const value = process.env[DISABLE_ENV];
1737
+ return value !== "off" && value !== "0" && value !== "false";
1738
+ }
1739
+ function notifyUser(title, body) {
1740
+ try {
1741
+ let command;
1742
+ let args;
1743
+ if (process.platform === "linux") {
1744
+ command = "notify-send";
1745
+ args = ["--app-name=q-ring", "--urgency=critical", title, body];
1746
+ } else if (process.platform === "darwin") {
1747
+ command = "osascript";
1748
+ const clean = (s) => s.replace(/["\\]/g, "");
1749
+ args = ["-e", `display notification "${clean(body)}" with title "${clean(title)}"`];
1750
+ } else {
1751
+ return false;
1752
+ }
1753
+ const child = spawn2(command, args, { detached: true, stdio: "ignore" });
1754
+ child.on("error", () => {
1755
+ });
1756
+ child.unref();
1757
+ return true;
1758
+ } catch {
1759
+ return false;
1760
+ }
1761
+ }
1762
+ function notifyApprovalRequested(key, source) {
1763
+ if (!notificationsEnabled()) return;
1764
+ const now = Date.now();
1765
+ const last = lastNotified.get(key);
1766
+ if (last !== void 0 && now - last < THROTTLE_MS) return;
1767
+ lastNotified.set(key, now);
1768
+ notifyUser(
1769
+ "q-ring: approval requested",
1770
+ `An ${source} agent wants to read "${key}". Allow with: qring approve ${key}`
1771
+ );
1772
+ }
1773
+
1571
1774
  // src/core/provision.ts
1572
1775
  import { execFileSync, spawnSync } from "child_process";
1573
1776
  import { z as z3 } from "zod";
@@ -1720,14 +1923,14 @@ function withJitEnvelopeLock(service, key, fn) {
1720
1923
  return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
1721
1924
  }
1722
1925
  function readEnvelope(service, key) {
1723
- const entry = new Entry2(service, key);
1926
+ const entry = new Entry(service, key);
1724
1927
  const raw = entry.getPassword();
1725
1928
  if (raw === null) return null;
1726
1929
  const envelope = parseEnvelope(raw);
1727
1930
  return envelope ?? wrapLegacy(raw);
1728
1931
  }
1729
1932
  function writeEnvelope(service, key, envelope) {
1730
- const entry = new Entry2(service, key);
1933
+ const entry = new Entry(service, key);
1731
1934
  entry.setPassword(serializeEnvelope(envelope));
1732
1935
  }
1733
1936
  function resolveEnv(opts) {
@@ -1804,6 +2007,7 @@ function getSecret(key, opts = {}) {
1804
2007
  source,
1805
2008
  detail: "blocked: requires user approval"
1806
2009
  });
2010
+ notifyApprovalRequested(key, source);
1807
2011
  }
1808
2012
  throw new Error(`Access Denied: This secret requires user approval. Please ask the user to run 'qring approve ${key}'`);
1809
2013
  }
@@ -1985,7 +2189,7 @@ function deleteSecret(key, opts = {}) {
1985
2189
  }
1986
2190
  let deleted = false;
1987
2191
  for (const { service, scope } of scopes) {
1988
- const entry = new Entry2(service, key);
2192
+ const entry = new Entry(service, key);
1989
2193
  try {
1990
2194
  if (entry.deleteCredential()) {
1991
2195
  deleted = true;
@@ -2165,7 +2369,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
2165
2369
  }
2166
2370
 
2167
2371
  // src/core/tunnel.ts
2168
- import { randomBytes as randomBytes3 } from "crypto";
2372
+ import { randomBytes as randomBytes4 } from "crypto";
2169
2373
  var tunnelStore = /* @__PURE__ */ new Map();
2170
2374
  var cleanupInterval = null;
2171
2375
  function ensureCleanup() {
@@ -2187,7 +2391,7 @@ function ensureCleanup() {
2187
2391
  }
2188
2392
  }
2189
2393
  function tunnelCreate(value, opts = {}) {
2190
- const id = `tun_${Date.now().toString(36)}_${randomBytes3(6).toString("base64url")}`;
2394
+ const id = `tun_${Date.now().toString(36)}_${randomBytes4(6).toString("base64url")}`;
2191
2395
  const now = Date.now();
2192
2396
  tunnelStore.set(id, {
2193
2397
  value,
@@ -2237,40 +2441,39 @@ function tunnelList() {
2237
2441
  }
2238
2442
 
2239
2443
  // src/core/memory.ts
2240
- import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, chmodSync as chmodSync2 } from "fs";
2241
- import { join as join9 } from "path";
2242
- import { homedir as homedir6, hostname, userInfo } from "os";
2444
+ import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, chmodSync as chmodSync3 } from "fs";
2445
+ import { join as join10 } from "path";
2446
+ import { homedir as homedir7, hostname, userInfo } from "os";
2243
2447
  import {
2244
- createCipheriv,
2245
- createDecipheriv,
2448
+ createCipheriv as createCipheriv2,
2449
+ createDecipheriv as createDecipheriv2,
2246
2450
  createHash as createHash3,
2247
- randomBytes as randomBytes4,
2248
- pbkdf2Sync
2451
+ randomBytes as randomBytes5,
2452
+ pbkdf2Sync as pbkdf2Sync2
2249
2453
  } from "crypto";
2250
- import { Entry as Entry3 } from "@napi-rs/keyring";
2251
2454
  var MEMORY_FILE = "agent-memory.enc";
2252
2455
  var KEYRING_SERVICE = "qring-memory-key";
2253
2456
  var KEYRING_ACCOUNT = "encryption-key";
2254
2457
  function getMemoryDir() {
2255
- const dir = join9(homedir6(), ".config", "q-ring");
2256
- if (!existsSync6(dir)) {
2257
- mkdirSync6(dir, { recursive: true, mode: 448 });
2458
+ const dir = join10(homedir7(), ".config", "q-ring");
2459
+ if (!existsSync7(dir)) {
2460
+ mkdirSync7(dir, { recursive: true, mode: 448 });
2258
2461
  }
2259
2462
  return dir;
2260
2463
  }
2261
2464
  function writeMemoryFile(path, data) {
2262
- writeFileSync5(path, data, { mode: 384 });
2465
+ writeFileSync6(path, data, { mode: 384 });
2263
2466
  try {
2264
- chmodSync2(path, 384);
2467
+ chmodSync3(path, 384);
2265
2468
  } catch {
2266
2469
  }
2267
2470
  }
2268
2471
  function getMemoryPath() {
2269
- return join9(getMemoryDir(), MEMORY_FILE);
2472
+ return join10(getMemoryDir(), MEMORY_FILE);
2270
2473
  }
2271
- var PBKDF2_ITERATIONS = 21e4;
2272
- var KEY_LENGTH = 32;
2273
- var PASSPHRASE_ENV = "QRING_MEMORY_PASSPHRASE";
2474
+ var PBKDF2_ITERATIONS2 = 21e4;
2475
+ var KEY_LENGTH2 = 32;
2476
+ var PASSPHRASE_ENV2 = "QRING_MEMORY_PASSPHRASE";
2274
2477
  var V2_PREFIX = "qmem2";
2275
2478
  var MemoryKeyUnavailableError = class extends Error {
2276
2479
  constructor(message) {
@@ -2282,19 +2485,19 @@ function deriveLegacyKey() {
2282
2485
  const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
2283
2486
  return createHash3("sha256").update(fingerprint).digest();
2284
2487
  }
2285
- function passphrase() {
2286
- const p = process.env[PASSPHRASE_ENV];
2488
+ function passphrase2() {
2489
+ const p = process.env[PASSPHRASE_ENV2];
2287
2490
  return p && p.length > 0 ? p : void 0;
2288
2491
  }
2289
2492
  function derivePassphraseKey(salt) {
2290
- return pbkdf2Sync(passphrase(), salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
2493
+ return pbkdf2Sync2(passphrase2(), salt, PBKDF2_ITERATIONS2, KEY_LENGTH2, "sha512");
2291
2494
  }
2292
2495
  function keyringKey() {
2293
2496
  try {
2294
- const entry = new Entry3(KEYRING_SERVICE, KEYRING_ACCOUNT);
2497
+ const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
2295
2498
  const stored = entry.getPassword();
2296
2499
  if (stored) return Buffer.from(stored, "base64");
2297
- const key = randomBytes4(KEY_LENGTH);
2500
+ const key = randomBytes5(KEY_LENGTH2);
2298
2501
  entry.setPassword(key.toString("base64"));
2299
2502
  return key;
2300
2503
  } catch {
@@ -2302,8 +2505,8 @@ function keyringKey() {
2302
2505
  }
2303
2506
  }
2304
2507
  function encryptWith(data, key) {
2305
- const iv = randomBytes4(12);
2306
- const cipher = createCipheriv("aes-256-gcm", key, iv);
2508
+ const iv = randomBytes5(12);
2509
+ const cipher = createCipheriv2("aes-256-gcm", key, iv);
2307
2510
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
2308
2511
  const tag = cipher.getAuthTag();
2309
2512
  return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
@@ -2314,27 +2517,27 @@ function decryptWith(blob, key) {
2314
2517
  const iv = Buffer.from(parts[0], "base64");
2315
2518
  const tag = Buffer.from(parts[1], "base64");
2316
2519
  const encrypted = Buffer.from(parts[2], "base64");
2317
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
2520
+ const decipher = createDecipheriv2("aes-256-gcm", key, iv);
2318
2521
  decipher.setAuthTag(tag);
2319
2522
  return decipher.update(encrypted) + decipher.final("utf8");
2320
2523
  }
2321
2524
  function encrypt(data) {
2322
2525
  const kk = keyringKey();
2323
2526
  if (kk) return encryptWith(data, kk);
2324
- if (passphrase()) {
2325
- const salt = randomBytes4(16);
2527
+ if (passphrase2()) {
2528
+ const salt = randomBytes5(16);
2326
2529
  const key = derivePassphraseKey(salt);
2327
2530
  return `${V2_PREFIX}:${salt.toString("base64")}:${encryptWith(data, key)}`;
2328
2531
  }
2329
2532
  throw new MemoryKeyUnavailableError(
2330
- `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.`
2533
+ `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.`
2331
2534
  );
2332
2535
  }
2333
2536
  function decrypt(blob) {
2334
2537
  if (blob.startsWith(`${V2_PREFIX}:`)) {
2335
- if (!passphrase()) {
2538
+ if (!passphrase2()) {
2336
2539
  throw new MemoryKeyUnavailableError(
2337
- `Agent memory was encrypted with ${PASSPHRASE_ENV} but it is not set \u2014 cannot decrypt.`
2540
+ `Agent memory was encrypted with ${PASSPHRASE_ENV2} but it is not set \u2014 cannot decrypt.`
2338
2541
  );
2339
2542
  }
2340
2543
  const rest = blob.slice(V2_PREFIX.length + 1);
@@ -2358,13 +2561,13 @@ function decrypt(blob) {
2358
2561
  }
2359
2562
  return plain;
2360
2563
  }
2361
- function loadStore() {
2564
+ function loadStore2() {
2362
2565
  const path = getMemoryPath();
2363
- if (!existsSync6(path)) {
2566
+ if (!existsSync7(path)) {
2364
2567
  return { entries: {} };
2365
2568
  }
2366
2569
  try {
2367
- const raw = readFileSync7(path, "utf8");
2570
+ const raw = readFileSync8(path, "utf8");
2368
2571
  const decrypted = decrypt(raw);
2369
2572
  return JSON.parse(decrypted);
2370
2573
  } catch (err) {
@@ -2374,41 +2577,41 @@ function loadStore() {
2374
2577
  return { entries: {} };
2375
2578
  }
2376
2579
  }
2377
- function saveStore(store) {
2580
+ function saveStore2(store) {
2378
2581
  const json = JSON.stringify(store);
2379
2582
  const encrypted = encrypt(json);
2380
2583
  writeMemoryFile(getMemoryPath(), encrypted);
2381
2584
  }
2382
2585
  function remember(key, value) {
2383
- const store = loadStore();
2586
+ const store = loadStore2();
2384
2587
  store.entries[key] = {
2385
2588
  value,
2386
2589
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2387
2590
  };
2388
- saveStore(store);
2591
+ saveStore2(store);
2389
2592
  }
2390
2593
  function recall(key) {
2391
- const store = loadStore();
2594
+ const store = loadStore2();
2392
2595
  return store.entries[key]?.value ?? null;
2393
2596
  }
2394
2597
  function listMemory() {
2395
- const store = loadStore();
2598
+ const store = loadStore2();
2396
2599
  return Object.entries(store.entries).map(([key, entry]) => ({
2397
2600
  key,
2398
2601
  updatedAt: entry.updatedAt
2399
2602
  }));
2400
2603
  }
2401
2604
  function forget(key) {
2402
- const store = loadStore();
2605
+ const store = loadStore2();
2403
2606
  if (key in store.entries) {
2404
2607
  delete store.entries[key];
2405
- saveStore(store);
2608
+ saveStore2(store);
2406
2609
  return true;
2407
2610
  }
2408
2611
  return false;
2409
2612
  }
2410
2613
  function clearMemory() {
2411
- saveStore({ entries: {} });
2614
+ saveStore2({ entries: {} });
2412
2615
  }
2413
2616
 
2414
2617
  export {
@@ -2458,4 +2661,4 @@ export {
2458
2661
  forget,
2459
2662
  clearMemory
2460
2663
  };
2461
- //# sourceMappingURL=chunk-C2TFJ2EH.js.map
2664
+ //# sourceMappingURL=chunk-MLBJCPX2.js.map