@i4ctime/q-ring 0.16.0 → 0.16.2

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.
@@ -335,7 +335,8 @@ function isProcessAlive(pid) {
335
335
  }
336
336
  }
337
337
  function withFileLock(name, fn, opts = {}) {
338
- const lockDir = join3(homedir(), ".config", "q-ring", opts.dir ?? "locks");
338
+ const baseDir = process.env.QRING_LOCK_DIR ?? (process.env.QRING_AUDIT_DIR ? join3(process.env.QRING_AUDIT_DIR, ".locks") : join3(homedir(), ".config", "q-ring"));
339
+ const lockDir = join3(baseDir, opts.dir ?? "locks");
339
340
  mkdirSync(lockDir, { recursive: true, mode: 448 });
340
341
  const safe = Buffer.from(name, "utf8").toString("base64url");
341
342
  const lockPath = join3(lockDir, `${safe}.lock`);
@@ -577,6 +578,13 @@ function setAuditAgentLabel(label) {
577
578
  function getAuditAgentLabel() {
578
579
  return auditAgentLabel;
579
580
  }
581
+ function sanitizeAuditText(value, max) {
582
+ const cleaned = Array.from(value).filter((ch) => {
583
+ const cp = ch.codePointAt(0) ?? 0;
584
+ return cp >= 32 && cp !== 127;
585
+ }).join("");
586
+ return cleaned.length > max ? `${cleaned.slice(0, max)}\u2026` : cleaned;
587
+ }
580
588
  function getAuditDir() {
581
589
  if (process.env.QRING_AUDIT_DIR) {
582
590
  if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
@@ -631,6 +639,10 @@ function logAudit(event) {
631
639
  if (full.agent === void 0 && auditAgentLabel) {
632
640
  full.agent = auditAgentLabel;
633
641
  }
642
+ if (full.key !== void 0) full.key = sanitizeAuditText(full.key, 256);
643
+ if (full.detail !== void 0) full.detail = sanitizeAuditText(full.detail, 600);
644
+ if (full.scope !== void 0) full.scope = sanitizeAuditText(full.scope, 128);
645
+ if (full.env !== void 0) full.env = sanitizeAuditText(full.env, 128);
634
646
  const line = JSON.stringify(full);
635
647
  const path = getAuditPath();
636
648
  appendFileSync(path, line + "\n", { mode: 384 });
@@ -643,7 +655,12 @@ function logAudit(event) {
643
655
  },
644
656
  { timeoutMs: 5e3 }
645
657
  );
646
- } catch {
658
+ } catch (err) {
659
+ if (process.env.QRING_DEBUG) {
660
+ console.error(
661
+ `q-ring: audit event dropped (${event.action}${event.key ? ` ${event.key}` : ""}): ${err instanceof Error ? err.message : String(err)}`
662
+ );
663
+ }
647
664
  }
648
665
  }
649
666
  var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
@@ -762,13 +779,33 @@ function exportAudit(opts = {}) {
762
779
  const until = new Date(opts.until).getTime();
763
780
  events = events.filter((e) => new Date(e.timestamp).getTime() <= until);
764
781
  }
782
+ if (opts.excludeActions?.length) {
783
+ const excluded = new Set(opts.excludeActions);
784
+ events = events.filter((e) => !excluded.has(e.action));
785
+ }
765
786
  if (opts.format === "json") {
766
787
  return JSON.stringify(events, null, 2);
767
788
  }
768
789
  if (opts.format === "csv") {
790
+ const csvField = (v) => {
791
+ let s = v === void 0 ? "" : String(v);
792
+ if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
793
+ return `"${s.replace(/"/g, '""')}"`;
794
+ };
769
795
  const header = "timestamp,action,key,scope,env,source,agent,pid,correlationId,detail";
770
796
  const rows = events.map(
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, ";")}`
797
+ (e) => [
798
+ csvField(e.timestamp),
799
+ csvField(e.action),
800
+ csvField(e.key),
801
+ csvField(e.scope),
802
+ csvField(e.env),
803
+ csvField(e.source),
804
+ csvField(e.agent),
805
+ csvField(e.pid),
806
+ csvField(e.correlationId),
807
+ csvField(e.detail)
808
+ ].join(",")
772
809
  );
773
810
  return [header, ...rows].join("\n");
774
811
  }
@@ -1762,7 +1799,8 @@ function notifyUser(title, body) {
1762
1799
  let args;
1763
1800
  if (process.platform === "linux") {
1764
1801
  command = "notify-send";
1765
- args = ["--app-name=q-ring", "--urgency=critical", title, body];
1802
+ const pango = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1803
+ args = ["--app-name=q-ring", "--urgency=critical", pango(title), pango(body)];
1766
1804
  } else if (process.platform === "darwin") {
1767
1805
  command = "osascript";
1768
1806
  const clean = (s) => s.replace(/["\\]/g, "");
@@ -1802,7 +1840,7 @@ function recordCanaryTrip(trip) {
1802
1840
  scope: trip.scope,
1803
1841
  env: trip.env,
1804
1842
  source: trip.source,
1805
- detail: `CANARY TRIPPED: honeytoken read via ${trip.source}`
1843
+ detail: `CANARY TRIPPED: ${trip.detail ?? `honeytoken read via ${trip.source}`}`
1806
1844
  });
1807
1845
  if (!notificationsEnabled()) return;
1808
1846
  const now = Date.now();
@@ -2245,8 +2283,12 @@ function deleteSecret(key, opts = {}) {
2245
2283
  for (const { service, scope } of scopes) {
2246
2284
  const entry = new Entry(service, key);
2247
2285
  try {
2286
+ const existing = readEnvelope(service, key);
2248
2287
  if (entry.deleteCredential()) {
2249
2288
  deleted = true;
2289
+ if (existing?.meta.canary) {
2290
+ recordCanaryTrip({ key, scope, source, detail: "canary deleted" });
2291
+ }
2250
2292
  logAudit({ action: "delete", key, scope, source });
2251
2293
  fireHooks({
2252
2294
  action: "delete",
@@ -2262,6 +2304,27 @@ function deleteSecret(key, opts = {}) {
2262
2304
  }
2263
2305
  return deleted;
2264
2306
  }
2307
+ function disarmCanary(key, opts = {}) {
2308
+ const scopes = resolveScope(opts);
2309
+ for (const { service, scope } of scopes) {
2310
+ const envelope = readEnvelope(service, key);
2311
+ if (!envelope) continue;
2312
+ if (!envelope.meta.canary) return false;
2313
+ delete envelope.meta.canary;
2314
+ delete envelope.meta.canaryFormat;
2315
+ envelope.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2316
+ writeEnvelope(service, key, envelope);
2317
+ logAudit({
2318
+ action: "write",
2319
+ key,
2320
+ scope,
2321
+ source: opts.source ?? "cli",
2322
+ detail: "canary disarmed"
2323
+ });
2324
+ return true;
2325
+ }
2326
+ return false;
2327
+ }
2265
2328
  function hasSecret(key, opts = {}) {
2266
2329
  const source = opts.source ?? "cli";
2267
2330
  if (source === "mcp") {
@@ -2365,6 +2428,9 @@ function exportSecrets(opts = {}) {
2365
2428
  const value = collapseValue(entry.envelope, env);
2366
2429
  if (value !== null) {
2367
2430
  rawValues.set(entry.key, value);
2431
+ if (entry.envelope.meta.canary) {
2432
+ recordCanaryTrip({ key: entry.key, scope: entry.scope, env, source });
2433
+ }
2368
2434
  }
2369
2435
  }
2370
2436
  }
@@ -2377,7 +2443,13 @@ function exportSecrets(opts = {}) {
2377
2443
  console.warn(`Warning: skipped exporting ${key} due to template error: ${err instanceof Error ? err.message : String(err)}`);
2378
2444
  }
2379
2445
  }
2380
- logAudit({ action: "export", source, detail: `format=${format}` });
2446
+ const exportedKeys = [...merged.keys()];
2447
+ const keyList = exportedKeys.length > 20 ? `${exportedKeys.slice(0, 20).join(",")} +${exportedKeys.length - 20} more` : exportedKeys.join(",");
2448
+ logAudit({
2449
+ action: "export",
2450
+ source,
2451
+ detail: `format=${format} keys=${keyList}`
2452
+ });
2381
2453
  if (format === "json") {
2382
2454
  const obj = {};
2383
2455
  for (const [key, value] of merged) {
@@ -2701,6 +2773,7 @@ export {
2701
2773
  getEnvelope,
2702
2774
  setSecret,
2703
2775
  deleteSecret,
2776
+ disarmCanary,
2704
2777
  hasSecret,
2705
2778
  listSecrets,
2706
2779
  exportSecrets,
@@ -2716,4 +2789,4 @@ export {
2716
2789
  forget,
2717
2790
  clearMemory
2718
2791
  };
2719
- //# sourceMappingURL=chunk-5LFKBZ3Q.js.map
2792
+ //# sourceMappingURL=chunk-SQKTDYSO.js.map