@i4ctime/q-ring 0.14.1 → 0.16.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.
@@ -40,7 +40,9 @@ var SecretMetadataSchema = z.object({
40
40
  validationUrl: z.string().optional(),
41
41
  requiresApproval: z.boolean().optional(),
42
42
  jitProvider: z.string().optional(),
43
- jitExpiresAt: z.string().optional()
43
+ jitExpiresAt: z.string().optional(),
44
+ canary: z.boolean().optional(),
45
+ canaryFormat: z.string().optional()
44
46
  });
45
47
  var QuantumEnvelopeSchema = z.object({
46
48
  v: z.literal(1),
@@ -73,7 +75,9 @@ function createEnvelope(value, opts) {
73
75
  rotationPrefix: opts?.rotationPrefix,
74
76
  provider: opts?.provider,
75
77
  requiresApproval: opts?.requiresApproval,
76
- jitProvider: opts?.jitProvider
78
+ jitProvider: opts?.jitProvider,
79
+ canary: opts?.canary,
80
+ canaryFormat: opts?.canaryFormat
77
81
  }
78
82
  };
79
83
  }
@@ -287,21 +291,27 @@ function mapEnvName(raw) {
287
291
 
288
292
  // src/core/observer.ts
289
293
  import {
290
- existsSync,
291
- mkdirSync as mkdirSync2,
294
+ existsSync as existsSync2,
295
+ mkdirSync as mkdirSync3,
292
296
  appendFileSync,
293
- chmodSync,
294
- readFileSync as readFileSync4,
297
+ chmodSync as chmodSync2,
298
+ readFileSync as readFileSync5,
295
299
  openSync,
296
300
  fstatSync,
297
301
  readSync,
298
302
  closeSync,
299
303
  statSync as statSync3
300
304
  } from "fs";
301
- import { join as join4 } from "path";
305
+ import { join as join5 } from "path";
306
+ import { homedir as homedir3 } from "os";
307
+ import { createHash, createHmac, randomBytes as randomBytes2 } from "crypto";
308
+
309
+ // src/core/backend.ts
310
+ import { Entry as NapiEntry, findCredentials as napiFindCredentials } from "@napi-rs/keyring";
311
+ import { existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "fs";
312
+ import { dirname as dirname2, join as join4 } from "path";
302
313
  import { homedir as homedir2 } from "os";
303
- import { createHash, createHmac, randomBytes } from "crypto";
304
- import { Entry } from "@napi-rs/keyring";
314
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from "crypto";
305
315
 
306
316
  // src/utils/file-lock.ts
307
317
  import {
@@ -332,17 +342,11 @@ function withFileLock(name, fn, opts = {}) {
332
342
  const deadline = Date.now() + (opts.timeoutMs ?? 8e3);
333
343
  const staleMs = opts.staleMs ?? 3e4;
334
344
  while (Date.now() < deadline) {
345
+ let acquired = false;
335
346
  try {
336
347
  writeFileSync(lockPath, `${process.pid}
337
348
  `, { flag: "wx", mode: 384 });
338
- try {
339
- return fn();
340
- } finally {
341
- try {
342
- unlinkSync(lockPath);
343
- } catch {
344
- }
345
- }
349
+ acquired = true;
346
350
  } catch {
347
351
  try {
348
352
  const holderPid = parseInt(readFileSync3(lockPath, "utf8").trim(), 10);
@@ -356,10 +360,172 @@ function withFileLock(name, fn, opts = {}) {
356
360
  }
357
361
  sleepSync(15);
358
362
  }
363
+ if (acquired) {
364
+ try {
365
+ return fn();
366
+ } finally {
367
+ try {
368
+ unlinkSync(lockPath);
369
+ } catch {
370
+ }
371
+ }
372
+ }
359
373
  }
360
374
  throw new Error(`Could not acquire lock "${name}" (timeout)`);
361
375
  }
362
376
 
377
+ // src/core/backend.ts
378
+ var PASSPHRASE_ENV = "QRING_FILE_PASSPHRASE";
379
+ var BACKEND_ENV = "QRING_BACKEND";
380
+ var PATH_ENV = "QRING_FILE_BACKEND_PATH";
381
+ var PBKDF2_ITERATIONS = 21e4;
382
+ var KEY_LENGTH = 32;
383
+ var FILE_PREFIX = "qfile1";
384
+ var BackendUnavailableError = class extends Error {
385
+ constructor(message) {
386
+ super(message);
387
+ this.name = "BackendUnavailableError";
388
+ }
389
+ };
390
+ function activeBackend() {
391
+ const requested = process.env[BACKEND_ENV];
392
+ if (requested === "file") return "file";
393
+ if (requested && requested !== "keyring") {
394
+ throw new BackendUnavailableError(
395
+ `Unknown ${BACKEND_ENV} "${requested}" \u2014 expected "keyring" or "file".`
396
+ );
397
+ }
398
+ return "keyring";
399
+ }
400
+ function storePath() {
401
+ return process.env[PATH_ENV] ?? join4(homedir2(), ".config", "q-ring", "file-backend.enc");
402
+ }
403
+ function passphrase() {
404
+ const p = process.env[PASSPHRASE_ENV];
405
+ if (!p) {
406
+ throw new BackendUnavailableError(
407
+ `${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.`
408
+ );
409
+ }
410
+ return p;
411
+ }
412
+ var keyCache = null;
413
+ function deriveKey(saltB64) {
414
+ const pass = passphrase();
415
+ if (keyCache && keyCache.salt === saltB64 && keyCache.pass === pass) {
416
+ return keyCache.key;
417
+ }
418
+ const key = pbkdf2Sync(pass, Buffer.from(saltB64, "base64"), PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
419
+ keyCache = { salt: saltB64, pass, key };
420
+ return key;
421
+ }
422
+ function loadStore() {
423
+ const path = storePath();
424
+ if (!existsSync(path)) {
425
+ return { map: {}, salt: randomBytes(16).toString("base64") };
426
+ }
427
+ const blob = readFileSync4(path, "utf8").trim();
428
+ const parts = blob.split(":");
429
+ if (parts.length !== 5 || parts[0] !== FILE_PREFIX) {
430
+ throw new BackendUnavailableError(
431
+ `${path} is not a valid q-ring file-backend store (expected ${FILE_PREFIX}:...).`
432
+ );
433
+ }
434
+ const [, salt, ivB64, tagB64, ctB64] = parts;
435
+ const decipher = createDecipheriv("aes-256-gcm", deriveKey(salt), Buffer.from(ivB64, "base64"));
436
+ decipher.setAuthTag(Buffer.from(tagB64, "base64"));
437
+ let plaintext;
438
+ try {
439
+ plaintext = decipher.update(Buffer.from(ctB64, "base64")) + decipher.final("utf8");
440
+ } catch {
441
+ throw new BackendUnavailableError(
442
+ `Cannot decrypt ${path} \u2014 wrong ${PASSPHRASE_ENV}, or the store was tampered with.`
443
+ );
444
+ }
445
+ return { map: JSON.parse(plaintext), salt };
446
+ }
447
+ function saveStore(map, salt) {
448
+ const path = storePath();
449
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
450
+ const iv = randomBytes(12);
451
+ const cipher = createCipheriv("aes-256-gcm", deriveKey(salt), iv);
452
+ const ct = Buffer.concat([cipher.update(JSON.stringify(map), "utf8"), cipher.final()]);
453
+ const blob = [
454
+ FILE_PREFIX,
455
+ salt,
456
+ iv.toString("base64"),
457
+ cipher.getAuthTag().toString("base64"),
458
+ ct.toString("base64")
459
+ ].join(":");
460
+ writeFileSync2(path, blob + "\n", { mode: 384 });
461
+ try {
462
+ chmodSync(path, 384);
463
+ } catch {
464
+ }
465
+ }
466
+ var SEP = "\0";
467
+ function fileMutate(fn) {
468
+ return withFileLock("file-backend", () => {
469
+ const { map, salt } = loadStore();
470
+ const result = fn(map);
471
+ saveStore(map, salt);
472
+ return result;
473
+ });
474
+ }
475
+ var Entry = class {
476
+ constructor(service, account) {
477
+ this.service = service;
478
+ this.account = account;
479
+ }
480
+ service;
481
+ account;
482
+ napi;
483
+ delegate() {
484
+ this.napi ??= new NapiEntry(this.service, this.account);
485
+ return this.napi;
486
+ }
487
+ storeKey() {
488
+ return `${this.service}${SEP}${this.account}`;
489
+ }
490
+ getPassword() {
491
+ if (activeBackend() === "file") {
492
+ return loadStore().map[this.storeKey()] ?? null;
493
+ }
494
+ return this.delegate().getPassword();
495
+ }
496
+ setPassword(password) {
497
+ if (activeBackend() === "file") {
498
+ fileMutate((map) => {
499
+ map[this.storeKey()] = password;
500
+ });
501
+ return;
502
+ }
503
+ this.delegate().setPassword(password);
504
+ }
505
+ deleteCredential() {
506
+ if (activeBackend() === "file") {
507
+ return fileMutate((map) => {
508
+ const existed = this.storeKey() in map;
509
+ delete map[this.storeKey()];
510
+ return existed;
511
+ });
512
+ }
513
+ return this.delegate().deleteCredential();
514
+ }
515
+ // @napi-rs/keyring's other delete alias, used by `qring doctor`.
516
+ deletePassword() {
517
+ return this.deleteCredential();
518
+ }
519
+ };
520
+ function findCredentials(service) {
521
+ if (activeBackend() === "file") {
522
+ const { map } = loadStore();
523
+ const prefix = `${service}${SEP}`;
524
+ return Object.entries(map).filter(([k]) => k.startsWith(prefix)).map(([k, password]) => ({ account: k.slice(prefix.length), password }));
525
+ }
526
+ return napiFindCredentials(service);
527
+ }
528
+
363
529
  // src/core/observer.ts
364
530
  var AUDIT_KEYRING_SERVICE = "qring-audit-chain";
365
531
  var AUDIT_KEY_ACCOUNT = "hmac-key";
@@ -370,7 +536,7 @@ function getAuditKey() {
370
536
  const entry = new Entry(AUDIT_KEYRING_SERVICE, AUDIT_KEY_ACCOUNT);
371
537
  const stored = entry.getPassword();
372
538
  if (stored) return Buffer.from(stored, "base64");
373
- const key = randomBytes(32);
539
+ const key = randomBytes2(32);
374
540
  entry.setPassword(key.toString("base64"));
375
541
  return key;
376
542
  } catch {
@@ -399,25 +565,37 @@ function writeStoredAnchor(hash) {
399
565
  } catch {
400
566
  }
401
567
  }
568
+ var auditAgentLabel = null;
569
+ function setAuditAgentLabel(label) {
570
+ if (label === null) {
571
+ auditAgentLabel = null;
572
+ return;
573
+ }
574
+ const cleaned = label.replace(/[^\x20-\x7e]/g, "").trim().slice(0, 128);
575
+ auditAgentLabel = cleaned.length > 0 ? cleaned : null;
576
+ }
577
+ function getAuditAgentLabel() {
578
+ return auditAgentLabel;
579
+ }
402
580
  function getAuditDir() {
403
581
  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 });
582
+ if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
583
+ mkdirSync3(process.env.QRING_AUDIT_DIR, { recursive: true, mode: 448 });
406
584
  }
407
585
  return process.env.QRING_AUDIT_DIR;
408
586
  }
409
- const dir = join4(homedir2(), ".config", "q-ring");
410
- if (!existsSync(dir)) {
411
- mkdirSync2(dir, { recursive: true, mode: 448 });
587
+ const dir = join5(homedir3(), ".config", "q-ring");
588
+ if (!existsSync2(dir)) {
589
+ mkdirSync3(dir, { recursive: true, mode: 448 });
412
590
  }
413
591
  return dir;
414
592
  }
415
593
  function getAuditPath() {
416
- return join4(getAuditDir(), "audit.jsonl");
594
+ return join5(getAuditDir(), "audit.jsonl");
417
595
  }
418
596
  function getLastLineHash() {
419
597
  const path = getAuditPath();
420
- if (!existsSync(path)) return void 0;
598
+ if (!existsSync2(path)) return void 0;
421
599
  try {
422
600
  const fd = openSync(path, "r");
423
601
  const stat = fstatSync(fd);
@@ -450,11 +628,14 @@ function logAudit(event) {
450
628
  pid: process.pid,
451
629
  prevHash
452
630
  };
631
+ if (full.agent === void 0 && auditAgentLabel) {
632
+ full.agent = auditAgentLabel;
633
+ }
453
634
  const line = JSON.stringify(full);
454
635
  const path = getAuditPath();
455
636
  appendFileSync(path, line + "\n", { mode: 384 });
456
637
  try {
457
- chmodSync(path, 384);
638
+ chmodSync2(path, 384);
458
639
  } catch {
459
640
  }
460
641
  const key = getAuditKey();
@@ -468,7 +649,7 @@ function logAudit(event) {
468
649
  var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
469
650
  function queryAudit(query = {}) {
470
651
  const path = getAuditPath();
471
- if (!existsSync(path)) return [];
652
+ if (!existsSync2(path)) return [];
472
653
  try {
473
654
  const st = statSync3(path);
474
655
  const readStart = st.size > MAX_AUDIT_BYTES ? st.size - MAX_AUDIT_BYTES : 0;
@@ -494,6 +675,7 @@ function queryAudit(query = {}) {
494
675
  if (query.action) events = events.filter((e) => e.action === query.action);
495
676
  if (query.source) events = events.filter((e) => e.source === query.source);
496
677
  if (query.correlationId) events = events.filter((e) => e.correlationId === query.correlationId);
678
+ if (query.agent) events = events.filter((e) => e.agent === query.agent);
497
679
  if (query.since) {
498
680
  const since = new Date(query.since).getTime();
499
681
  events = events.filter((e) => new Date(e.timestamp).getTime() >= since);
@@ -509,10 +691,10 @@ function queryAudit(query = {}) {
509
691
  }
510
692
  function verifyAuditChain() {
511
693
  const path = getAuditPath();
512
- if (!existsSync(path)) {
694
+ if (!existsSync2(path)) {
513
695
  return { totalEvents: 0, validEvents: 0, intact: true };
514
696
  }
515
- const lines = readFileSync4(path, "utf8").split("\n").filter((l) => l.trim());
697
+ const lines = readFileSync5(path, "utf8").split("\n").filter((l) => l.trim());
516
698
  if (lines.length === 0) {
517
699
  return { totalEvents: 0, validEvents: 0, intact: true };
518
700
  }
@@ -563,8 +745,8 @@ function verifyAuditChain() {
563
745
  }
564
746
  function exportAudit(opts = {}) {
565
747
  const path = getAuditPath();
566
- if (!existsSync(path)) return opts.format === "json" ? "[]" : "";
567
- const lines = readFileSync4(path, "utf8").split("\n").filter((l) => l.trim());
748
+ if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
749
+ const lines = readFileSync5(path, "utf8").split("\n").filter((l) => l.trim());
568
750
  let events = lines.map((l) => {
569
751
  try {
570
752
  return JSON.parse(l);
@@ -584,9 +766,9 @@ function exportAudit(opts = {}) {
584
766
  return JSON.stringify(events, null, 2);
585
767
  }
586
768
  if (opts.format === "csv") {
587
- const header = "timestamp,action,key,scope,env,source,pid,correlationId,detail";
769
+ const header = "timestamp,action,key,scope,env,source,agent,pid,correlationId,detail";
588
770
  const rows = events.map(
589
- (e) => `${e.timestamp},${e.action},${e.key ?? ""},${e.scope ?? ""},${e.env ?? ""},${e.source},${e.pid},${e.correlationId ?? ""},${(e.detail ?? "").replace(/,/g, ";")}`
771
+ (e) => `${e.timestamp},${e.action},${e.key ?? ""},${e.scope ?? ""},${e.env ?? ""},${e.source},${(e.agent ?? "").replace(/,/g, ";")},${e.pid},${e.correlationId ?? ""},${(e.detail ?? "").replace(/,/g, ";")}`
590
772
  );
591
773
  return [header, ...rows].join("\n");
592
774
  }
@@ -629,15 +811,15 @@ function detectAnomalies(key) {
629
811
  }
630
812
 
631
813
  // 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";
814
+ import { existsSync as existsSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
815
+ import { join as join6 } from "path";
816
+ import { homedir as homedir4 } from "os";
635
817
 
636
818
  // src/utils/registry.ts
637
- import { existsSync as existsSync2, readFileSync as readFileSync5, renameSync } from "fs";
819
+ import { existsSync as existsSync3, readFileSync as readFileSync6, renameSync } from "fs";
638
820
  function loadJsonRegistry(path, empty) {
639
- if (!existsSync2(path)) return empty;
640
- const raw = readFileSync5(path, "utf8");
821
+ if (!existsSync3(path)) return empty;
822
+ const raw = readFileSync6(path, "utf8");
641
823
  try {
642
824
  return JSON.parse(raw);
643
825
  } catch (err) {
@@ -661,18 +843,18 @@ function loadJsonRegistry(path, empty) {
661
843
  // src/core/entanglement.ts
662
844
  var REGISTRY_VERSION = 1;
663
845
  function getRegistryPath() {
664
- const dir = join5(homedir3(), ".config", "q-ring");
665
- if (!existsSync3(dir)) {
666
- mkdirSync3(dir, { recursive: true, mode: 448 });
846
+ const dir = join6(homedir4(), ".config", "q-ring");
847
+ if (!existsSync4(dir)) {
848
+ mkdirSync4(dir, { recursive: true, mode: 448 });
667
849
  }
668
- return join5(dir, "entanglement.json");
850
+ return join6(dir, "entanglement.json");
669
851
  }
670
852
  function loadRegistry() {
671
853
  return loadJsonRegistry(getRegistryPath(), { pairs: [] });
672
854
  }
673
855
  function saveRegistry(registry2) {
674
856
  registry2.version = REGISTRY_VERSION;
675
- writeFileSync2(getRegistryPath(), JSON.stringify(registry2, null, 2), {
857
+ writeFileSync3(getRegistryPath(), JSON.stringify(registry2, null, 2), {
676
858
  mode: 384
677
859
  });
678
860
  }
@@ -706,9 +888,9 @@ function listEntanglements() {
706
888
  }
707
889
 
708
890
  // 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";
891
+ import { existsSync as existsSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
892
+ import { join as join7 } from "path";
893
+ import { homedir as homedir5 } from "os";
712
894
  import { execFile, spawn } from "child_process";
713
895
  import { randomUUID } from "crypto";
714
896
 
@@ -932,17 +1114,17 @@ function httpRequest(opts) {
932
1114
 
933
1115
  // src/core/hooks.ts
934
1116
  function getRegistryPath2() {
935
- const dir = join6(homedir4(), ".config", "q-ring");
936
- if (!existsSync4(dir)) {
937
- mkdirSync4(dir, { recursive: true, mode: 448 });
1117
+ const dir = join7(homedir5(), ".config", "q-ring");
1118
+ if (!existsSync5(dir)) {
1119
+ mkdirSync5(dir, { recursive: true, mode: 448 });
938
1120
  }
939
- return join6(dir, "hooks.json");
1121
+ return join7(dir, "hooks.json");
940
1122
  }
941
1123
  function loadRegistry2() {
942
1124
  return loadJsonRegistry(getRegistryPath2(), { hooks: [] });
943
1125
  }
944
1126
  function saveRegistry2(registry2) {
945
- writeFileSync3(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
1127
+ writeFileSync4(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
946
1128
  mode: 384
947
1129
  });
948
1130
  }
@@ -1205,19 +1387,19 @@ async function fireHooks(payload, tags) {
1205
1387
  }
1206
1388
 
1207
1389
  // 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";
1390
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
1391
+ import { join as join8 } from "path";
1392
+ import { homedir as homedir6 } from "os";
1393
+ import { createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1212
1394
  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 });
1395
+ const dir = join8(homedir6(), ".config", "q-ring");
1396
+ const secretPath = join8(dir, ".approval-key");
1397
+ if (!existsSync6(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
1398
+ if (existsSync6(secretPath)) {
1399
+ return readFileSync7(secretPath, "utf8").trim();
1400
+ }
1401
+ const secret = randomBytes3(32).toString("hex");
1402
+ writeFileSync5(secretPath, secret, { mode: 384 });
1221
1403
  return secret;
1222
1404
  }
1223
1405
  function computeHmac(entry) {
@@ -1247,17 +1429,17 @@ function verifyHmac(entry) {
1247
1429
  }
1248
1430
  }
1249
1431
  function getRegistryPath3() {
1250
- const dir = join7(homedir5(), ".config", "q-ring");
1251
- if (!existsSync5(dir)) {
1252
- mkdirSync5(dir, { recursive: true, mode: 448 });
1432
+ const dir = join8(homedir6(), ".config", "q-ring");
1433
+ if (!existsSync6(dir)) {
1434
+ mkdirSync6(dir, { recursive: true, mode: 448 });
1253
1435
  }
1254
- return join7(dir, "approvals.json");
1436
+ return join8(dir, "approvals.json");
1255
1437
  }
1256
1438
  function loadRegistry3() {
1257
1439
  return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
1258
1440
  }
1259
1441
  function saveRegistry3(registry2) {
1260
- writeFileSync4(getRegistryPath3(), JSON.stringify(registry2, null, 2), { mode: 384 });
1442
+ writeFileSync5(getRegistryPath3(), JSON.stringify(registry2, null, 2), { mode: 384 });
1261
1443
  }
1262
1444
  function cleanup(registry2) {
1263
1445
  const now = Date.now();
@@ -1268,7 +1450,7 @@ function cleanup(registry2) {
1268
1450
  function grantApproval(key, scope, service, ttlSeconds = 3600, grantOpts = {}) {
1269
1451
  const registry2 = loadRegistry3();
1270
1452
  cleanup(registry2);
1271
- const id = randomBytes2(8).toString("hex");
1453
+ const id = randomBytes3(8).toString("hex");
1272
1454
  const grantedAt = (/* @__PURE__ */ new Date()).toISOString();
1273
1455
  const expiresAt = new Date(Date.now() + ttlSeconds * 1e3).toISOString();
1274
1456
  const partial = {
@@ -1329,7 +1511,7 @@ function listApprovals() {
1329
1511
 
1330
1512
  // src/core/policy.ts
1331
1513
  import { statSync as statSync4 } from "fs";
1332
- import { join as join8 } from "path";
1514
+ import { join as join9 } from "path";
1333
1515
  import { z as z2 } from "zod";
1334
1516
  var stringArray = z2.array(z2.string());
1335
1517
  var mcpPolicySchema = z2.object({
@@ -1371,7 +1553,7 @@ function resolvePolicyPath(projectPath) {
1371
1553
  }
1372
1554
  function configMtime(pp) {
1373
1555
  try {
1374
- return statSync4(join8(pp, ".q-ring.json")).mtimeMs;
1556
+ return statSync4(join9(pp, ".q-ring.json")).mtimeMs;
1375
1557
  } catch {
1376
1558
  return 0;
1377
1559
  }
@@ -1394,7 +1576,7 @@ function loadPolicy(projectPath) {
1394
1576
  if (!parsed.success) {
1395
1577
  const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
1396
1578
  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}`
1579
+ `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
1580
  );
1399
1581
  console.error(`q-ring: ${error.message}`);
1400
1582
  cachedPolicy = { path: pp, mtimeMs, error };
@@ -1510,9 +1692,6 @@ function getPolicySummary(projectPath) {
1510
1692
  };
1511
1693
  }
1512
1694
 
1513
- // src/core/keyring.ts
1514
- import { Entry as Entry2, findCredentials } from "@napi-rs/keyring";
1515
-
1516
1695
  // src/utils/hash.ts
1517
1696
  import { createHash as createHash2 } from "crypto";
1518
1697
  function hashProjectPath(projectPath) {
@@ -1568,6 +1747,75 @@ function serviceForScope(scope, opts = {}) {
1568
1747
  return resolveScope({ ...opts, scope })[0].service;
1569
1748
  }
1570
1749
 
1750
+ // src/core/notify.ts
1751
+ import { spawn as spawn2 } from "child_process";
1752
+ var DISABLE_ENV = "QRING_NOTIFY";
1753
+ var THROTTLE_MS = 5 * 60 * 1e3;
1754
+ var lastNotified = /* @__PURE__ */ new Map();
1755
+ function notificationsEnabled() {
1756
+ const value = process.env[DISABLE_ENV];
1757
+ return value !== "off" && value !== "0" && value !== "false";
1758
+ }
1759
+ function notifyUser(title, body) {
1760
+ try {
1761
+ let command;
1762
+ let args;
1763
+ if (process.platform === "linux") {
1764
+ command = "notify-send";
1765
+ args = ["--app-name=q-ring", "--urgency=critical", title, body];
1766
+ } else if (process.platform === "darwin") {
1767
+ command = "osascript";
1768
+ const clean = (s) => s.replace(/["\\]/g, "");
1769
+ args = ["-e", `display notification "${clean(body)}" with title "${clean(title)}"`];
1770
+ } else {
1771
+ return false;
1772
+ }
1773
+ const child = spawn2(command, args, { detached: true, stdio: "ignore" });
1774
+ child.on("error", () => {
1775
+ });
1776
+ child.unref();
1777
+ return true;
1778
+ } catch {
1779
+ return false;
1780
+ }
1781
+ }
1782
+ function notifyApprovalRequested(key, source) {
1783
+ if (!notificationsEnabled()) return;
1784
+ const now = Date.now();
1785
+ const last = lastNotified.get(key);
1786
+ if (last !== void 0 && now - last < THROTTLE_MS) return;
1787
+ lastNotified.set(key, now);
1788
+ notifyUser(
1789
+ "q-ring: approval requested",
1790
+ `An ${source} agent wants to read "${key}". Allow with: qring approve ${key}`
1791
+ );
1792
+ }
1793
+
1794
+ // src/core/canary-alert.ts
1795
+ var TRIP_THROTTLE_MS = 30 * 1e3;
1796
+ var lastAlerted = /* @__PURE__ */ new Map();
1797
+ function recordCanaryTrip(trip) {
1798
+ const agent = getAuditAgentLabel();
1799
+ logAudit({
1800
+ action: "canary",
1801
+ key: trip.key,
1802
+ scope: trip.scope,
1803
+ env: trip.env,
1804
+ source: trip.source,
1805
+ detail: `CANARY TRIPPED: honeytoken read via ${trip.source}`
1806
+ });
1807
+ if (!notificationsEnabled()) return;
1808
+ const now = Date.now();
1809
+ const last = lastAlerted.get(trip.key);
1810
+ if (last !== void 0 && now - last < TRIP_THROTTLE_MS) return;
1811
+ lastAlerted.set(trip.key, now);
1812
+ const who = agent ? `${trip.source} (${agent})` : trip.source;
1813
+ notifyUser(
1814
+ "q-ring: CANARY TRIPPED",
1815
+ `Honeytoken "${trip.key}" was read by ${who}. This credential is fake \u2014 but something reached for it. Investigate: qring audit --action canary`
1816
+ );
1817
+ }
1818
+
1571
1819
  // src/core/provision.ts
1572
1820
  import { execFileSync, spawnSync } from "child_process";
1573
1821
  import { z as z3 } from "zod";
@@ -1720,14 +1968,14 @@ function withJitEnvelopeLock(service, key, fn) {
1720
1968
  return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
1721
1969
  }
1722
1970
  function readEnvelope(service, key) {
1723
- const entry = new Entry2(service, key);
1971
+ const entry = new Entry(service, key);
1724
1972
  const raw = entry.getPassword();
1725
1973
  if (raw === null) return null;
1726
1974
  const envelope = parseEnvelope(raw);
1727
1975
  return envelope ?? wrapLegacy(raw);
1728
1976
  }
1729
1977
  function writeEnvelope(service, key, envelope) {
1730
- const entry = new Entry2(service, key);
1978
+ const entry = new Entry(service, key);
1731
1979
  entry.setPassword(serializeEnvelope(envelope));
1732
1980
  }
1733
1981
  function resolveEnv(opts) {
@@ -1804,6 +2052,7 @@ function getSecret(key, opts = {}) {
1804
2052
  source,
1805
2053
  detail: "blocked: requires user approval"
1806
2054
  });
2055
+ notifyApprovalRequested(key, source);
1807
2056
  }
1808
2057
  throw new Error(`Access Denied: This secret requires user approval. Please ask the user to run 'qring approve ${key}'`);
1809
2058
  }
@@ -1848,6 +2097,9 @@ function getSecret(key, opts = {}) {
1848
2097
  const latest = readEnvelope(service, key) ?? envelope;
1849
2098
  writeEnvelope(service, key, recordAccess(latest));
1850
2099
  logAudit({ action: "read", key, scope, env, source });
2100
+ if (envelope.meta.canary) {
2101
+ recordCanaryTrip({ key, scope, env, source });
2102
+ }
1851
2103
  }
1852
2104
  return value;
1853
2105
  }
@@ -1880,6 +2132,8 @@ function setSecret(key, value, opts = {}) {
1880
2132
  const prov = opts.provider ?? existing?.meta.provider;
1881
2133
  const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
1882
2134
  const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
2135
+ const canaryFlag = opts.canary ?? existing?.meta.canary;
2136
+ const canaryFmt = opts.canaryFormat ?? existing?.meta.canaryFormat;
1883
2137
  const mergedTags = opts.tags ?? existing?.meta.tags;
1884
2138
  const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
1885
2139
  const life = checkSecretLifecyclePolicy(
@@ -1907,7 +2161,9 @@ function setSecret(key, value, opts = {}) {
1907
2161
  rotationPrefix: rotPfx,
1908
2162
  provider: prov,
1909
2163
  requiresApproval: reqApp,
1910
- jitProvider: jitProv
2164
+ jitProvider: jitProv,
2165
+ canary: canaryFlag,
2166
+ canaryFormat: canaryFmt
1911
2167
  });
1912
2168
  } else {
1913
2169
  envelope = createEnvelope(value, {
@@ -1920,7 +2176,9 @@ function setSecret(key, value, opts = {}) {
1920
2176
  rotationPrefix: rotPfx,
1921
2177
  provider: prov,
1922
2178
  requiresApproval: reqApp,
1923
- jitProvider: jitProv
2179
+ jitProvider: jitProv,
2180
+ canary: canaryFlag,
2181
+ canaryFormat: canaryFmt
1924
2182
  });
1925
2183
  }
1926
2184
  if (existing) {
@@ -1985,7 +2243,7 @@ function deleteSecret(key, opts = {}) {
1985
2243
  }
1986
2244
  let deleted = false;
1987
2245
  for (const { service, scope } of scopes) {
1988
- const entry = new Entry2(service, key);
2246
+ const entry = new Entry(service, key);
1989
2247
  try {
1990
2248
  if (entry.deleteCredential()) {
1991
2249
  deleted = true;
@@ -2165,7 +2423,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
2165
2423
  }
2166
2424
 
2167
2425
  // src/core/tunnel.ts
2168
- import { randomBytes as randomBytes3 } from "crypto";
2426
+ import { randomBytes as randomBytes4 } from "crypto";
2169
2427
  var tunnelStore = /* @__PURE__ */ new Map();
2170
2428
  var cleanupInterval = null;
2171
2429
  function ensureCleanup() {
@@ -2187,7 +2445,7 @@ function ensureCleanup() {
2187
2445
  }
2188
2446
  }
2189
2447
  function tunnelCreate(value, opts = {}) {
2190
- const id = `tun_${Date.now().toString(36)}_${randomBytes3(6).toString("base64url")}`;
2448
+ const id = `tun_${Date.now().toString(36)}_${randomBytes4(6).toString("base64url")}`;
2191
2449
  const now = Date.now();
2192
2450
  tunnelStore.set(id, {
2193
2451
  value,
@@ -2237,40 +2495,39 @@ function tunnelList() {
2237
2495
  }
2238
2496
 
2239
2497
  // 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";
2498
+ import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, chmodSync as chmodSync3 } from "fs";
2499
+ import { join as join10 } from "path";
2500
+ import { homedir as homedir7, hostname, userInfo } from "os";
2243
2501
  import {
2244
- createCipheriv,
2245
- createDecipheriv,
2502
+ createCipheriv as createCipheriv2,
2503
+ createDecipheriv as createDecipheriv2,
2246
2504
  createHash as createHash3,
2247
- randomBytes as randomBytes4,
2248
- pbkdf2Sync
2505
+ randomBytes as randomBytes5,
2506
+ pbkdf2Sync as pbkdf2Sync2
2249
2507
  } from "crypto";
2250
- import { Entry as Entry3 } from "@napi-rs/keyring";
2251
2508
  var MEMORY_FILE = "agent-memory.enc";
2252
2509
  var KEYRING_SERVICE = "qring-memory-key";
2253
2510
  var KEYRING_ACCOUNT = "encryption-key";
2254
2511
  function getMemoryDir() {
2255
- const dir = join9(homedir6(), ".config", "q-ring");
2256
- if (!existsSync6(dir)) {
2257
- mkdirSync6(dir, { recursive: true, mode: 448 });
2512
+ const dir = join10(homedir7(), ".config", "q-ring");
2513
+ if (!existsSync7(dir)) {
2514
+ mkdirSync7(dir, { recursive: true, mode: 448 });
2258
2515
  }
2259
2516
  return dir;
2260
2517
  }
2261
2518
  function writeMemoryFile(path, data) {
2262
- writeFileSync5(path, data, { mode: 384 });
2519
+ writeFileSync6(path, data, { mode: 384 });
2263
2520
  try {
2264
- chmodSync2(path, 384);
2521
+ chmodSync3(path, 384);
2265
2522
  } catch {
2266
2523
  }
2267
2524
  }
2268
2525
  function getMemoryPath() {
2269
- return join9(getMemoryDir(), MEMORY_FILE);
2526
+ return join10(getMemoryDir(), MEMORY_FILE);
2270
2527
  }
2271
- var PBKDF2_ITERATIONS = 21e4;
2272
- var KEY_LENGTH = 32;
2273
- var PASSPHRASE_ENV = "QRING_MEMORY_PASSPHRASE";
2528
+ var PBKDF2_ITERATIONS2 = 21e4;
2529
+ var KEY_LENGTH2 = 32;
2530
+ var PASSPHRASE_ENV2 = "QRING_MEMORY_PASSPHRASE";
2274
2531
  var V2_PREFIX = "qmem2";
2275
2532
  var MemoryKeyUnavailableError = class extends Error {
2276
2533
  constructor(message) {
@@ -2282,19 +2539,19 @@ function deriveLegacyKey() {
2282
2539
  const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
2283
2540
  return createHash3("sha256").update(fingerprint).digest();
2284
2541
  }
2285
- function passphrase() {
2286
- const p = process.env[PASSPHRASE_ENV];
2542
+ function passphrase2() {
2543
+ const p = process.env[PASSPHRASE_ENV2];
2287
2544
  return p && p.length > 0 ? p : void 0;
2288
2545
  }
2289
2546
  function derivePassphraseKey(salt) {
2290
- return pbkdf2Sync(passphrase(), salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
2547
+ return pbkdf2Sync2(passphrase2(), salt, PBKDF2_ITERATIONS2, KEY_LENGTH2, "sha512");
2291
2548
  }
2292
2549
  function keyringKey() {
2293
2550
  try {
2294
- const entry = new Entry3(KEYRING_SERVICE, KEYRING_ACCOUNT);
2551
+ const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
2295
2552
  const stored = entry.getPassword();
2296
2553
  if (stored) return Buffer.from(stored, "base64");
2297
- const key = randomBytes4(KEY_LENGTH);
2554
+ const key = randomBytes5(KEY_LENGTH2);
2298
2555
  entry.setPassword(key.toString("base64"));
2299
2556
  return key;
2300
2557
  } catch {
@@ -2302,8 +2559,8 @@ function keyringKey() {
2302
2559
  }
2303
2560
  }
2304
2561
  function encryptWith(data, key) {
2305
- const iv = randomBytes4(12);
2306
- const cipher = createCipheriv("aes-256-gcm", key, iv);
2562
+ const iv = randomBytes5(12);
2563
+ const cipher = createCipheriv2("aes-256-gcm", key, iv);
2307
2564
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
2308
2565
  const tag = cipher.getAuthTag();
2309
2566
  return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
@@ -2314,27 +2571,27 @@ function decryptWith(blob, key) {
2314
2571
  const iv = Buffer.from(parts[0], "base64");
2315
2572
  const tag = Buffer.from(parts[1], "base64");
2316
2573
  const encrypted = Buffer.from(parts[2], "base64");
2317
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
2574
+ const decipher = createDecipheriv2("aes-256-gcm", key, iv);
2318
2575
  decipher.setAuthTag(tag);
2319
2576
  return decipher.update(encrypted) + decipher.final("utf8");
2320
2577
  }
2321
2578
  function encrypt(data) {
2322
2579
  const kk = keyringKey();
2323
2580
  if (kk) return encryptWith(data, kk);
2324
- if (passphrase()) {
2325
- const salt = randomBytes4(16);
2581
+ if (passphrase2()) {
2582
+ const salt = randomBytes5(16);
2326
2583
  const key = derivePassphraseKey(salt);
2327
2584
  return `${V2_PREFIX}:${salt.toString("base64")}:${encryptWith(data, key)}`;
2328
2585
  }
2329
2586
  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.`
2587
+ `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
2588
  );
2332
2589
  }
2333
2590
  function decrypt(blob) {
2334
2591
  if (blob.startsWith(`${V2_PREFIX}:`)) {
2335
- if (!passphrase()) {
2592
+ if (!passphrase2()) {
2336
2593
  throw new MemoryKeyUnavailableError(
2337
- `Agent memory was encrypted with ${PASSPHRASE_ENV} but it is not set \u2014 cannot decrypt.`
2594
+ `Agent memory was encrypted with ${PASSPHRASE_ENV2} but it is not set \u2014 cannot decrypt.`
2338
2595
  );
2339
2596
  }
2340
2597
  const rest = blob.slice(V2_PREFIX.length + 1);
@@ -2358,13 +2615,13 @@ function decrypt(blob) {
2358
2615
  }
2359
2616
  return plain;
2360
2617
  }
2361
- function loadStore() {
2618
+ function loadStore2() {
2362
2619
  const path = getMemoryPath();
2363
- if (!existsSync6(path)) {
2620
+ if (!existsSync7(path)) {
2364
2621
  return { entries: {} };
2365
2622
  }
2366
2623
  try {
2367
- const raw = readFileSync7(path, "utf8");
2624
+ const raw = readFileSync8(path, "utf8");
2368
2625
  const decrypted = decrypt(raw);
2369
2626
  return JSON.parse(decrypted);
2370
2627
  } catch (err) {
@@ -2374,41 +2631,41 @@ function loadStore() {
2374
2631
  return { entries: {} };
2375
2632
  }
2376
2633
  }
2377
- function saveStore(store) {
2634
+ function saveStore2(store) {
2378
2635
  const json = JSON.stringify(store);
2379
2636
  const encrypted = encrypt(json);
2380
2637
  writeMemoryFile(getMemoryPath(), encrypted);
2381
2638
  }
2382
2639
  function remember(key, value) {
2383
- const store = loadStore();
2640
+ const store = loadStore2();
2384
2641
  store.entries[key] = {
2385
2642
  value,
2386
2643
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2387
2644
  };
2388
- saveStore(store);
2645
+ saveStore2(store);
2389
2646
  }
2390
2647
  function recall(key) {
2391
- const store = loadStore();
2648
+ const store = loadStore2();
2392
2649
  return store.entries[key]?.value ?? null;
2393
2650
  }
2394
2651
  function listMemory() {
2395
- const store = loadStore();
2652
+ const store = loadStore2();
2396
2653
  return Object.entries(store.entries).map(([key, entry]) => ({
2397
2654
  key,
2398
2655
  updatedAt: entry.updatedAt
2399
2656
  }));
2400
2657
  }
2401
2658
  function forget(key) {
2402
- const store = loadStore();
2659
+ const store = loadStore2();
2403
2660
  if (key in store.entries) {
2404
2661
  delete store.entries[key];
2405
- saveStore(store);
2662
+ saveStore2(store);
2406
2663
  return true;
2407
2664
  }
2408
2665
  return false;
2409
2666
  }
2410
2667
  function clearMemory() {
2411
- saveStore({ entries: {} });
2668
+ saveStore2({ entries: {} });
2412
2669
  }
2413
2670
 
2414
2671
  export {
@@ -2417,6 +2674,7 @@ export {
2417
2674
  checkDecay,
2418
2675
  readProjectConfig,
2419
2676
  collapseEnvironment,
2677
+ setAuditAgentLabel,
2420
2678
  logAudit,
2421
2679
  queryAudit,
2422
2680
  verifyAuditChain,
@@ -2458,4 +2716,4 @@ export {
2458
2716
  forget,
2459
2717
  clearMemory
2460
2718
  };
2461
- //# sourceMappingURL=chunk-C2TFJ2EH.js.map
2719
+ //# sourceMappingURL=chunk-5LFKBZ3Q.js.map