@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
  }
@@ -1189,19 +1371,19 @@ async function fireHooks(payload, tags) {
1189
1371
  }
1190
1372
 
1191
1373
  // 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";
1374
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
1375
+ import { join as join8 } from "path";
1376
+ import { homedir as homedir6 } from "os";
1377
+ import { createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1196
1378
  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 });
1379
+ const dir = join8(homedir6(), ".config", "q-ring");
1380
+ const secretPath = join8(dir, ".approval-key");
1381
+ if (!existsSync6(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
1382
+ if (existsSync6(secretPath)) {
1383
+ return readFileSync7(secretPath, "utf8").trim();
1384
+ }
1385
+ const secret = randomBytes3(32).toString("hex");
1386
+ writeFileSync5(secretPath, secret, { mode: 384 });
1205
1387
  return secret;
1206
1388
  }
1207
1389
  function computeHmac(entry) {
@@ -1231,11 +1413,11 @@ function verifyHmac(entry) {
1231
1413
  }
1232
1414
  }
1233
1415
  function getRegistryPath3() {
1234
- const dir = join7(homedir5(), ".config", "q-ring");
1235
- if (!existsSync5(dir)) {
1236
- mkdirSync5(dir, { recursive: true, mode: 448 });
1416
+ const dir = join8(homedir6(), ".config", "q-ring");
1417
+ if (!existsSync6(dir)) {
1418
+ mkdirSync6(dir, { recursive: true, mode: 448 });
1237
1419
  }
1238
- return join7(dir, "approvals.json");
1420
+ return join8(dir, "approvals.json");
1239
1421
  }
1240
1422
  function loadRegistry3() {
1241
1423
  return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
@@ -1262,7 +1444,7 @@ function listApprovals() {
1262
1444
 
1263
1445
  // src/core/policy.ts
1264
1446
  import { statSync as statSync4 } from "fs";
1265
- import { join as join8 } from "path";
1447
+ import { join as join9 } from "path";
1266
1448
  import { z as z2 } from "zod";
1267
1449
  var stringArray = z2.array(z2.string());
1268
1450
  var mcpPolicySchema = z2.object({
@@ -1308,7 +1490,7 @@ function resolvePolicyPath(projectPath) {
1308
1490
  }
1309
1491
  function configMtime(pp) {
1310
1492
  try {
1311
- return statSync4(join8(pp, ".q-ring.json")).mtimeMs;
1493
+ return statSync4(join9(pp, ".q-ring.json")).mtimeMs;
1312
1494
  } catch {
1313
1495
  return 0;
1314
1496
  }
@@ -1331,7 +1513,7 @@ function loadPolicy(projectPath) {
1331
1513
  if (!parsed.success) {
1332
1514
  const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
1333
1515
  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}`
1516
+ `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
1517
  );
1336
1518
  console.error(`q-ring: ${error.message}`);
1337
1519
  cachedPolicy = { path: pp, mtimeMs, error };
@@ -1466,9 +1648,6 @@ function getPolicySummary(projectPath) {
1466
1648
  };
1467
1649
  }
1468
1650
 
1469
- // src/core/keyring.ts
1470
- import { Entry as Entry2, findCredentials } from "@napi-rs/keyring";
1471
-
1472
1651
  // src/utils/hash.ts
1473
1652
  import { createHash as createHash2 } from "crypto";
1474
1653
  function hashProjectPath(projectPath) {
@@ -1524,6 +1703,75 @@ function serviceForScope(scope, opts = {}) {
1524
1703
  return resolveScope({ ...opts, scope })[0].service;
1525
1704
  }
1526
1705
 
1706
+ // src/core/notify.ts
1707
+ import { spawn as spawn2 } from "child_process";
1708
+ var DISABLE_ENV = "QRING_NOTIFY";
1709
+ var THROTTLE_MS = 5 * 60 * 1e3;
1710
+ var lastNotified = /* @__PURE__ */ new Map();
1711
+ function notificationsEnabled() {
1712
+ const value = process.env[DISABLE_ENV];
1713
+ return value !== "off" && value !== "0" && value !== "false";
1714
+ }
1715
+ function notifyUser(title, body) {
1716
+ try {
1717
+ let command;
1718
+ let args;
1719
+ if (process.platform === "linux") {
1720
+ command = "notify-send";
1721
+ args = ["--app-name=q-ring", "--urgency=critical", title, body];
1722
+ } else if (process.platform === "darwin") {
1723
+ command = "osascript";
1724
+ const clean = (s) => s.replace(/["\\]/g, "");
1725
+ args = ["-e", `display notification "${clean(body)}" with title "${clean(title)}"`];
1726
+ } else {
1727
+ return false;
1728
+ }
1729
+ const child = spawn2(command, args, { detached: true, stdio: "ignore" });
1730
+ child.on("error", () => {
1731
+ });
1732
+ child.unref();
1733
+ return true;
1734
+ } catch {
1735
+ return false;
1736
+ }
1737
+ }
1738
+ function notifyApprovalRequested(key, source) {
1739
+ if (!notificationsEnabled()) return;
1740
+ const now = Date.now();
1741
+ const last = lastNotified.get(key);
1742
+ if (last !== void 0 && now - last < THROTTLE_MS) return;
1743
+ lastNotified.set(key, now);
1744
+ notifyUser(
1745
+ "q-ring: approval requested",
1746
+ `An ${source} agent wants to read "${key}". Allow with: qring approve ${key}`
1747
+ );
1748
+ }
1749
+
1750
+ // src/core/canary-alert.ts
1751
+ var TRIP_THROTTLE_MS = 30 * 1e3;
1752
+ var lastAlerted = /* @__PURE__ */ new Map();
1753
+ function recordCanaryTrip(trip) {
1754
+ const agent = getAuditAgentLabel();
1755
+ logAudit({
1756
+ action: "canary",
1757
+ key: trip.key,
1758
+ scope: trip.scope,
1759
+ env: trip.env,
1760
+ source: trip.source,
1761
+ detail: `CANARY TRIPPED: honeytoken read via ${trip.source}`
1762
+ });
1763
+ if (!notificationsEnabled()) return;
1764
+ const now = Date.now();
1765
+ const last = lastAlerted.get(trip.key);
1766
+ if (last !== void 0 && now - last < TRIP_THROTTLE_MS) return;
1767
+ lastAlerted.set(trip.key, now);
1768
+ const who = agent ? `${trip.source} (${agent})` : trip.source;
1769
+ notifyUser(
1770
+ "q-ring: CANARY TRIPPED",
1771
+ `Honeytoken "${trip.key}" was read by ${who}. This credential is fake \u2014 but something reached for it. Investigate: qring audit --action canary`
1772
+ );
1773
+ }
1774
+
1527
1775
  // src/core/provision.ts
1528
1776
  import { execFileSync, spawnSync } from "child_process";
1529
1777
  import { z as z3 } from "zod";
@@ -1676,14 +1924,14 @@ function withJitEnvelopeLock(service, key, fn) {
1676
1924
  return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
1677
1925
  }
1678
1926
  function readEnvelope(service, key) {
1679
- const entry = new Entry2(service, key);
1927
+ const entry = new Entry(service, key);
1680
1928
  const raw = entry.getPassword();
1681
1929
  if (raw === null) return null;
1682
1930
  const envelope = parseEnvelope(raw);
1683
1931
  return envelope ?? wrapLegacy(raw);
1684
1932
  }
1685
1933
  function writeEnvelope(service, key, envelope) {
1686
- const entry = new Entry2(service, key);
1934
+ const entry = new Entry(service, key);
1687
1935
  entry.setPassword(serializeEnvelope(envelope));
1688
1936
  }
1689
1937
  function resolveEnv(opts) {
@@ -1760,6 +2008,7 @@ function getSecret(key, opts = {}) {
1760
2008
  source,
1761
2009
  detail: "blocked: requires user approval"
1762
2010
  });
2011
+ notifyApprovalRequested(key, source);
1763
2012
  }
1764
2013
  throw new Error(`Access Denied: This secret requires user approval. Please ask the user to run 'qring approve ${key}'`);
1765
2014
  }
@@ -1804,6 +2053,9 @@ function getSecret(key, opts = {}) {
1804
2053
  const latest = readEnvelope(service, key) ?? envelope;
1805
2054
  writeEnvelope(service, key, recordAccess(latest));
1806
2055
  logAudit({ action: "read", key, scope, env, source });
2056
+ if (envelope.meta.canary) {
2057
+ recordCanaryTrip({ key, scope, env, source });
2058
+ }
1807
2059
  }
1808
2060
  return value;
1809
2061
  }
@@ -1836,6 +2088,8 @@ function setSecret(key, value, opts = {}) {
1836
2088
  const prov = opts.provider ?? existing?.meta.provider;
1837
2089
  const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
1838
2090
  const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
2091
+ const canaryFlag = opts.canary ?? existing?.meta.canary;
2092
+ const canaryFmt = opts.canaryFormat ?? existing?.meta.canaryFormat;
1839
2093
  const mergedTags = opts.tags ?? existing?.meta.tags;
1840
2094
  const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
1841
2095
  const life = checkSecretLifecyclePolicy(
@@ -1863,7 +2117,9 @@ function setSecret(key, value, opts = {}) {
1863
2117
  rotationPrefix: rotPfx,
1864
2118
  provider: prov,
1865
2119
  requiresApproval: reqApp,
1866
- jitProvider: jitProv
2120
+ jitProvider: jitProv,
2121
+ canary: canaryFlag,
2122
+ canaryFormat: canaryFmt
1867
2123
  });
1868
2124
  } else {
1869
2125
  envelope = createEnvelope(value, {
@@ -1876,7 +2132,9 @@ function setSecret(key, value, opts = {}) {
1876
2132
  rotationPrefix: rotPfx,
1877
2133
  provider: prov,
1878
2134
  requiresApproval: reqApp,
1879
- jitProvider: jitProv
2135
+ jitProvider: jitProv,
2136
+ canary: canaryFlag,
2137
+ canaryFormat: canaryFmt
1880
2138
  });
1881
2139
  }
1882
2140
  if (existing) {
@@ -1941,7 +2199,7 @@ function deleteSecret(key, opts = {}) {
1941
2199
  }
1942
2200
  let deleted = false;
1943
2201
  for (const { service, scope } of scopes) {
1944
- const entry = new Entry2(service, key);
2202
+ const entry = new Entry(service, key);
1945
2203
  try {
1946
2204
  if (entry.deleteCredential()) {
1947
2205
  deleted = true;
@@ -2121,7 +2379,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
2121
2379
  }
2122
2380
 
2123
2381
  // src/core/tunnel.ts
2124
- import { randomBytes as randomBytes3 } from "crypto";
2382
+ import { randomBytes as randomBytes4 } from "crypto";
2125
2383
  var tunnelStore = /* @__PURE__ */ new Map();
2126
2384
  var cleanupInterval = null;
2127
2385
  function ensureCleanup() {
@@ -2143,7 +2401,7 @@ function ensureCleanup() {
2143
2401
  }
2144
2402
  }
2145
2403
  function tunnelCreate(value, opts = {}) {
2146
- const id = `tun_${Date.now().toString(36)}_${randomBytes3(6).toString("base64url")}`;
2404
+ const id = `tun_${Date.now().toString(36)}_${randomBytes4(6).toString("base64url")}`;
2147
2405
  const now = Date.now();
2148
2406
  tunnelStore.set(id, {
2149
2407
  value,
@@ -2193,40 +2451,39 @@ function tunnelList() {
2193
2451
  }
2194
2452
 
2195
2453
  // 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";
2454
+ import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, chmodSync as chmodSync3 } from "fs";
2455
+ import { join as join10 } from "path";
2456
+ import { homedir as homedir7, hostname, userInfo } from "os";
2199
2457
  import {
2200
- createCipheriv,
2201
- createDecipheriv,
2458
+ createCipheriv as createCipheriv2,
2459
+ createDecipheriv as createDecipheriv2,
2202
2460
  createHash as createHash3,
2203
- randomBytes as randomBytes4,
2204
- pbkdf2Sync
2461
+ randomBytes as randomBytes5,
2462
+ pbkdf2Sync as pbkdf2Sync2
2205
2463
  } from "crypto";
2206
- import { Entry as Entry3 } from "@napi-rs/keyring";
2207
2464
  var MEMORY_FILE = "agent-memory.enc";
2208
2465
  var KEYRING_SERVICE = "qring-memory-key";
2209
2466
  var KEYRING_ACCOUNT = "encryption-key";
2210
2467
  function getMemoryDir() {
2211
- const dir = join9(homedir6(), ".config", "q-ring");
2212
- if (!existsSync6(dir)) {
2213
- mkdirSync6(dir, { recursive: true, mode: 448 });
2468
+ const dir = join10(homedir7(), ".config", "q-ring");
2469
+ if (!existsSync7(dir)) {
2470
+ mkdirSync7(dir, { recursive: true, mode: 448 });
2214
2471
  }
2215
2472
  return dir;
2216
2473
  }
2217
2474
  function writeMemoryFile(path, data) {
2218
- writeFileSync5(path, data, { mode: 384 });
2475
+ writeFileSync6(path, data, { mode: 384 });
2219
2476
  try {
2220
- chmodSync2(path, 384);
2477
+ chmodSync3(path, 384);
2221
2478
  } catch {
2222
2479
  }
2223
2480
  }
2224
2481
  function getMemoryPath() {
2225
- return join9(getMemoryDir(), MEMORY_FILE);
2482
+ return join10(getMemoryDir(), MEMORY_FILE);
2226
2483
  }
2227
- var PBKDF2_ITERATIONS = 21e4;
2228
- var KEY_LENGTH = 32;
2229
- var PASSPHRASE_ENV = "QRING_MEMORY_PASSPHRASE";
2484
+ var PBKDF2_ITERATIONS2 = 21e4;
2485
+ var KEY_LENGTH2 = 32;
2486
+ var PASSPHRASE_ENV2 = "QRING_MEMORY_PASSPHRASE";
2230
2487
  var V2_PREFIX = "qmem2";
2231
2488
  var MemoryKeyUnavailableError = class extends Error {
2232
2489
  constructor(message) {
@@ -2238,19 +2495,19 @@ function deriveLegacyKey() {
2238
2495
  const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
2239
2496
  return createHash3("sha256").update(fingerprint).digest();
2240
2497
  }
2241
- function passphrase() {
2242
- const p = process.env[PASSPHRASE_ENV];
2498
+ function passphrase2() {
2499
+ const p = process.env[PASSPHRASE_ENV2];
2243
2500
  return p && p.length > 0 ? p : void 0;
2244
2501
  }
2245
2502
  function derivePassphraseKey(salt) {
2246
- return pbkdf2Sync(passphrase(), salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
2503
+ return pbkdf2Sync2(passphrase2(), salt, PBKDF2_ITERATIONS2, KEY_LENGTH2, "sha512");
2247
2504
  }
2248
2505
  function keyringKey() {
2249
2506
  try {
2250
- const entry = new Entry3(KEYRING_SERVICE, KEYRING_ACCOUNT);
2507
+ const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
2251
2508
  const stored = entry.getPassword();
2252
2509
  if (stored) return Buffer.from(stored, "base64");
2253
- const key = randomBytes4(KEY_LENGTH);
2510
+ const key = randomBytes5(KEY_LENGTH2);
2254
2511
  entry.setPassword(key.toString("base64"));
2255
2512
  return key;
2256
2513
  } catch {
@@ -2258,8 +2515,8 @@ function keyringKey() {
2258
2515
  }
2259
2516
  }
2260
2517
  function encryptWith(data, key) {
2261
- const iv = randomBytes4(12);
2262
- const cipher = createCipheriv("aes-256-gcm", key, iv);
2518
+ const iv = randomBytes5(12);
2519
+ const cipher = createCipheriv2("aes-256-gcm", key, iv);
2263
2520
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
2264
2521
  const tag = cipher.getAuthTag();
2265
2522
  return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
@@ -2270,27 +2527,27 @@ function decryptWith(blob, key) {
2270
2527
  const iv = Buffer.from(parts[0], "base64");
2271
2528
  const tag = Buffer.from(parts[1], "base64");
2272
2529
  const encrypted = Buffer.from(parts[2], "base64");
2273
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
2530
+ const decipher = createDecipheriv2("aes-256-gcm", key, iv);
2274
2531
  decipher.setAuthTag(tag);
2275
2532
  return decipher.update(encrypted) + decipher.final("utf8");
2276
2533
  }
2277
2534
  function encrypt(data) {
2278
2535
  const kk = keyringKey();
2279
2536
  if (kk) return encryptWith(data, kk);
2280
- if (passphrase()) {
2281
- const salt = randomBytes4(16);
2537
+ if (passphrase2()) {
2538
+ const salt = randomBytes5(16);
2282
2539
  const key = derivePassphraseKey(salt);
2283
2540
  return `${V2_PREFIX}:${salt.toString("base64")}:${encryptWith(data, key)}`;
2284
2541
  }
2285
2542
  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.`
2543
+ `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
2544
  );
2288
2545
  }
2289
2546
  function decrypt(blob) {
2290
2547
  if (blob.startsWith(`${V2_PREFIX}:`)) {
2291
- if (!passphrase()) {
2548
+ if (!passphrase2()) {
2292
2549
  throw new MemoryKeyUnavailableError(
2293
- `Agent memory was encrypted with ${PASSPHRASE_ENV} but it is not set \u2014 cannot decrypt.`
2550
+ `Agent memory was encrypted with ${PASSPHRASE_ENV2} but it is not set \u2014 cannot decrypt.`
2294
2551
  );
2295
2552
  }
2296
2553
  const rest = blob.slice(V2_PREFIX.length + 1);
@@ -2314,13 +2571,13 @@ function decrypt(blob) {
2314
2571
  }
2315
2572
  return plain;
2316
2573
  }
2317
- function loadStore() {
2574
+ function loadStore2() {
2318
2575
  const path = getMemoryPath();
2319
- if (!existsSync6(path)) {
2576
+ if (!existsSync7(path)) {
2320
2577
  return { entries: {} };
2321
2578
  }
2322
2579
  try {
2323
- const raw = readFileSync7(path, "utf8");
2580
+ const raw = readFileSync8(path, "utf8");
2324
2581
  const decrypted = decrypt(raw);
2325
2582
  return JSON.parse(decrypted);
2326
2583
  } catch (err) {
@@ -2330,35 +2587,35 @@ function loadStore() {
2330
2587
  return { entries: {} };
2331
2588
  }
2332
2589
  }
2333
- function saveStore(store) {
2590
+ function saveStore2(store) {
2334
2591
  const json = JSON.stringify(store);
2335
2592
  const encrypted = encrypt(json);
2336
2593
  writeMemoryFile(getMemoryPath(), encrypted);
2337
2594
  }
2338
2595
  function remember(key, value) {
2339
- const store = loadStore();
2596
+ const store = loadStore2();
2340
2597
  store.entries[key] = {
2341
2598
  value,
2342
2599
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2343
2600
  };
2344
- saveStore(store);
2601
+ saveStore2(store);
2345
2602
  }
2346
2603
  function recall(key) {
2347
- const store = loadStore();
2604
+ const store = loadStore2();
2348
2605
  return store.entries[key]?.value ?? null;
2349
2606
  }
2350
2607
  function listMemory() {
2351
- const store = loadStore();
2608
+ const store = loadStore2();
2352
2609
  return Object.entries(store.entries).map(([key, entry]) => ({
2353
2610
  key,
2354
2611
  updatedAt: entry.updatedAt
2355
2612
  }));
2356
2613
  }
2357
2614
  function forget(key) {
2358
- const store = loadStore();
2615
+ const store = loadStore2();
2359
2616
  if (key in store.entries) {
2360
2617
  delete store.entries[key];
2361
- saveStore(store);
2618
+ saveStore2(store);
2362
2619
  return true;
2363
2620
  }
2364
2621
  return false;
@@ -2369,6 +2626,7 @@ export {
2369
2626
  checkDecay,
2370
2627
  readProjectConfig,
2371
2628
  collapseEnvironment,
2629
+ setAuditAgentLabel,
2372
2630
  logAudit,
2373
2631
  queryAudit,
2374
2632
  verifyAuditChain,
@@ -2407,4 +2665,4 @@ export {
2407
2665
  listMemory,
2408
2666
  forget
2409
2667
  };
2410
- //# sourceMappingURL=chunk-NNIEXAW5.js.map
2668
+ //# sourceMappingURL=chunk-NCM5GHNW.js.map