@i4ctime/q-ring 0.17.5 → 0.18.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.
@@ -36,6 +36,10 @@ var SecretMetadataSchema = z.object({
36
36
  ephemeral: z.boolean().optional(),
37
37
  rotationFormat: z.string().optional(),
38
38
  rotationPrefix: z.string().optional(),
39
+ rotatedAt: z.string().optional(),
40
+ // `.catch(undefined)`: a bad interval strips the reminder rather than
41
+ // rejecting the whole envelope (which would surface the JSON as the value).
42
+ rotateEveryDays: z.number().int().positive().max(3650).optional().catch(void 0),
39
43
  provider: z.string().optional(),
40
44
  validationUrl: z.string().optional(),
41
45
  requiresApproval: z.boolean().optional(),
@@ -73,6 +77,8 @@ function createEnvelope(value, opts) {
73
77
  accessCount: 0,
74
78
  rotationFormat: opts?.rotationFormat,
75
79
  rotationPrefix: opts?.rotationPrefix,
80
+ rotatedAt: opts?.rotatedAt ?? now,
81
+ rotateEveryDays: opts?.rotateEveryDays,
76
82
  provider: opts?.provider,
77
83
  requiresApproval: opts?.requiresApproval,
78
84
  jitProvider: opts?.jitProvider,
@@ -302,14 +308,13 @@ import {
302
308
  closeSync,
303
309
  statSync as statSync3
304
310
  } from "fs";
305
- import { join as join5 } from "path";
306
- import { homedir as homedir3 } from "os";
311
+ import { join as join6 } from "path";
307
312
  import { createHash, createHmac, randomBytes as randomBytes2 } from "crypto";
308
313
 
309
314
  // src/core/backend.ts
310
315
  import { Entry as NapiEntry, findCredentials as napiFindCredentials } from "@napi-rs/keyring";
311
316
  import { existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "fs";
312
- import { dirname as dirname2, join as join4 } from "path";
317
+ import { dirname as dirname2, join as join5 } from "path";
313
318
  import { homedir as homedir2 } from "os";
314
319
  import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from "crypto";
315
320
 
@@ -321,8 +326,19 @@ import {
321
326
  readFileSync as readFileSync3,
322
327
  statSync as statSync2
323
328
  } from "fs";
324
- import { join as join3 } from "path";
329
+ import { join as join4 } from "path";
330
+
331
+ // src/utils/config-dir.ts
325
332
  import { homedir } from "os";
333
+ import { join as join3 } from "path";
334
+ var CONFIG_DIR_ENV = "QRING_CONFIG_DIR";
335
+ function configDir() {
336
+ const override = process.env[CONFIG_DIR_ENV];
337
+ if (override && override.trim()) return override;
338
+ return join3(homedir(), ".config", "q-ring");
339
+ }
340
+
341
+ // src/utils/file-lock.ts
326
342
  function sleepSync(ms) {
327
343
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
328
344
  }
@@ -335,11 +351,11 @@ function isProcessAlive(pid) {
335
351
  }
336
352
  }
337
353
  function withFileLock(name, fn, opts = {}) {
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");
354
+ const baseDir = process.env.QRING_LOCK_DIR ?? (process.env.QRING_AUDIT_DIR ? join4(process.env.QRING_AUDIT_DIR, ".locks") : configDir());
355
+ const lockDir = join4(baseDir, opts.dir ?? "locks");
340
356
  mkdirSync(lockDir, { recursive: true, mode: 448 });
341
357
  const safe = Buffer.from(name, "utf8").toString("base64url");
342
- const lockPath = join3(lockDir, `${safe}.lock`);
358
+ const lockPath = join4(lockDir, `${safe}.lock`);
343
359
  const deadline = Date.now() + (opts.timeoutMs ?? 8e3);
344
360
  const staleMs = opts.staleMs ?? 3e4;
345
361
  while (Date.now() < deadline) {
@@ -382,6 +398,12 @@ var PATH_ENV = "QRING_FILE_BACKEND_PATH";
382
398
  var PBKDF2_ITERATIONS = 21e4;
383
399
  var KEY_LENGTH = 32;
384
400
  var FILE_PREFIX = "qfile1";
401
+ function keyringFailure(op, err) {
402
+ const detail = err instanceof Error ? err.message : String(err);
403
+ return new BackendUnavailableError(
404
+ `OS keyring ${op} failed \u2014 the credential store is locked or unreachable (${detail}). Unlock your keyring (or set ${BACKEND_ENV}=file with ${PASSPHRASE_ENV} for a headless store) and retry.`
405
+ );
406
+ }
385
407
  var BackendUnavailableError = class extends Error {
386
408
  constructor(message) {
387
409
  super(message);
@@ -399,7 +421,7 @@ function activeBackend() {
399
421
  return "keyring";
400
422
  }
401
423
  function storePath() {
402
- return process.env[PATH_ENV] ?? join4(homedir2(), ".config", "q-ring", "file-backend.enc");
424
+ return process.env[PATH_ENV] ?? join5(homedir2(), ".config", "q-ring", "file-backend.enc");
403
425
  }
404
426
  function passphrase() {
405
427
  const p = process.env[PASSPHRASE_ENV];
@@ -492,7 +514,11 @@ var Entry = class {
492
514
  if (activeBackend() === "file") {
493
515
  return loadStore().map[this.storeKey()] ?? null;
494
516
  }
495
- return this.delegate().getPassword();
517
+ try {
518
+ return this.delegate().getPassword();
519
+ } catch (err) {
520
+ throw keyringFailure("read", err);
521
+ }
496
522
  }
497
523
  setPassword(password) {
498
524
  if (activeBackend() === "file") {
@@ -511,7 +537,11 @@ var Entry = class {
511
537
  return existed;
512
538
  });
513
539
  }
514
- return this.delegate().deleteCredential();
540
+ try {
541
+ return this.delegate().deleteCredential();
542
+ } catch (err) {
543
+ throw keyringFailure("delete", err);
544
+ }
515
545
  }
516
546
  // @napi-rs/keyring's other delete alias, used by `qring doctor`.
517
547
  deletePassword() {
@@ -592,14 +622,14 @@ function getAuditDir() {
592
622
  }
593
623
  return process.env.QRING_AUDIT_DIR;
594
624
  }
595
- const dir = join5(homedir3(), ".config", "q-ring");
625
+ const dir = configDir();
596
626
  if (!existsSync2(dir)) {
597
627
  mkdirSync3(dir, { recursive: true, mode: 448 });
598
628
  }
599
629
  return dir;
600
630
  }
601
631
  function getAuditPath() {
602
- return join5(getAuditDir(), "audit.jsonl");
632
+ return join6(getAuditDir(), "audit.jsonl");
603
633
  }
604
634
  function getLastLineHash() {
605
635
  const path = getAuditPath();
@@ -849,8 +879,7 @@ function detectAnomalies(key) {
849
879
 
850
880
  // src/core/entanglement.ts
851
881
  import { existsSync as existsSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
852
- import { join as join6 } from "path";
853
- import { homedir as homedir4 } from "os";
882
+ import { join as join7 } from "path";
854
883
 
855
884
  // src/utils/registry.ts
856
885
  import { existsSync as existsSync3, readFileSync as readFileSync6, renameSync } from "fs";
@@ -880,11 +909,11 @@ function loadJsonRegistry(path, empty) {
880
909
  // src/core/entanglement.ts
881
910
  var REGISTRY_VERSION = 1;
882
911
  function getRegistryPath() {
883
- const dir = join6(homedir4(), ".config", "q-ring");
912
+ const dir = configDir();
884
913
  if (!existsSync4(dir)) {
885
914
  mkdirSync4(dir, { recursive: true, mode: 448 });
886
915
  }
887
- return join6(dir, "entanglement.json");
916
+ return join7(dir, "entanglement.json");
888
917
  }
889
918
  function loadRegistry() {
890
919
  return loadJsonRegistry(getRegistryPath(), { pairs: [] });
@@ -926,8 +955,7 @@ function listEntanglements() {
926
955
 
927
956
  // src/core/hooks.ts
928
957
  import { existsSync as existsSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
929
- import { join as join7 } from "path";
930
- import { homedir as homedir5 } from "os";
958
+ import { join as join8 } from "path";
931
959
  import { execFile, spawn } from "child_process";
932
960
  import { randomUUID } from "crypto";
933
961
 
@@ -1151,11 +1179,11 @@ function httpRequest(opts) {
1151
1179
 
1152
1180
  // src/core/hooks.ts
1153
1181
  function getRegistryPath2() {
1154
- const dir = join7(homedir5(), ".config", "q-ring");
1182
+ const dir = configDir();
1155
1183
  if (!existsSync5(dir)) {
1156
1184
  mkdirSync5(dir, { recursive: true, mode: 448 });
1157
1185
  }
1158
- return join7(dir, "hooks.json");
1186
+ return join8(dir, "hooks.json");
1159
1187
  }
1160
1188
  function loadRegistry2() {
1161
1189
  return loadJsonRegistry(getRegistryPath2(), { hooks: [] });
@@ -1425,12 +1453,11 @@ async function fireHooks(payload, tags) {
1425
1453
 
1426
1454
  // src/core/approval.ts
1427
1455
  import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
1428
- import { join as join8 } from "path";
1429
- import { homedir as homedir6 } from "os";
1456
+ import { join as join9 } from "path";
1430
1457
  import { createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1431
1458
  function getHmacSecret() {
1432
- const dir = join8(homedir6(), ".config", "q-ring");
1433
- const secretPath = join8(dir, ".approval-key");
1459
+ const dir = configDir();
1460
+ const secretPath = join9(dir, ".approval-key");
1434
1461
  if (!existsSync6(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
1435
1462
  if (existsSync6(secretPath)) {
1436
1463
  return readFileSync7(secretPath, "utf8").trim();
@@ -1466,11 +1493,11 @@ function verifyHmac(entry) {
1466
1493
  }
1467
1494
  }
1468
1495
  function getRegistryPath3() {
1469
- const dir = join8(homedir6(), ".config", "q-ring");
1496
+ const dir = configDir();
1470
1497
  if (!existsSync6(dir)) {
1471
1498
  mkdirSync6(dir, { recursive: true, mode: 448 });
1472
1499
  }
1473
- return join8(dir, "approvals.json");
1500
+ return join9(dir, "approvals.json");
1474
1501
  }
1475
1502
  function loadRegistry3() {
1476
1503
  return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
@@ -1548,7 +1575,7 @@ function listApprovals() {
1548
1575
 
1549
1576
  // src/core/policy.ts
1550
1577
  import { statSync as statSync4 } from "fs";
1551
- import { join as join9 } from "path";
1578
+ import { join as join10 } from "path";
1552
1579
  import { z as z2 } from "zod";
1553
1580
  var stringArray = z2.array(z2.string());
1554
1581
  var mcpPolicySchema = z2.object({
@@ -1607,7 +1634,7 @@ function resolvePolicyPath(projectPath) {
1607
1634
  }
1608
1635
  function configMtime(pp) {
1609
1636
  try {
1610
- return statSync4(join9(pp, ".q-ring.json")).mtimeMs;
1637
+ return statSync4(join10(pp, ".q-ring.json")).mtimeMs;
1611
1638
  } catch {
1612
1639
  return 0;
1613
1640
  }
@@ -1630,7 +1657,7 @@ function loadPolicy(projectPath) {
1630
1657
  if (!parsed.success) {
1631
1658
  const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
1632
1659
  const error = new PolicyConfigError(
1633
- `Invalid policy in ${join9(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
1660
+ `Invalid policy in ${join10(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
1634
1661
  );
1635
1662
  console.error(`q-ring: ${error.message}`);
1636
1663
  cachedPolicy = { path: pp, mtimeMs, error };
@@ -1894,16 +1921,15 @@ function notifyApprovalRequested(key, source) {
1894
1921
 
1895
1922
  // src/core/canary-webhooks.ts
1896
1923
  import { existsSync as existsSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
1897
- import { join as join10 } from "path";
1898
- import { homedir as homedir7 } from "os";
1924
+ import { join as join11 } from "path";
1899
1925
  import { randomUUID as randomUUID2 } from "crypto";
1900
1926
  var CANARY_ALERT_TYPES = ["discord", "slack", "ntfy", "generic"];
1901
1927
  function getRegistryPath4() {
1902
1928
  const override = process.env.QRING_CANARY_ALERTS_PATH;
1903
1929
  if (override) return override;
1904
- const dir = join10(homedir7(), ".config", "q-ring");
1930
+ const dir = configDir();
1905
1931
  if (!existsSync7(dir)) mkdirSync7(dir, { recursive: true, mode: 448 });
1906
- return join10(dir, "canary-alerts.json");
1932
+ return join11(dir, "canary-alerts.json");
1907
1933
  }
1908
1934
  function loadRegistry4() {
1909
1935
  return loadJsonRegistry(getRegistryPath4(), { channels: [] });
@@ -2253,6 +2279,10 @@ registry.register(httpProvider);
2253
2279
  function withJitEnvelopeLock(service, key, fn) {
2254
2280
  return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
2255
2281
  }
2282
+ var MAX_ROTATE_EVERY_DAYS = 3650;
2283
+ function isValidRotateEveryDays(days) {
2284
+ return Number.isInteger(days) && days >= 1 && days <= MAX_ROTATE_EVERY_DAYS;
2285
+ }
2256
2286
  function readEnvelope(service, key) {
2257
2287
  const entry = new Entry(service, key);
2258
2288
  const raw = entry.getPassword();
@@ -2415,6 +2445,14 @@ function setSecret(key, value, opts = {}) {
2415
2445
  let envelope;
2416
2446
  const rotFmt = opts.rotationFormat ?? existing?.meta.rotationFormat;
2417
2447
  const rotPfx = opts.rotationPrefix ?? existing?.meta.rotationPrefix;
2448
+ const rotEvery = opts.rotateEveryDays ?? existing?.meta.rotateEveryDays;
2449
+ if (rotEvery !== void 0 && !isValidRotateEveryDays(rotEvery)) {
2450
+ throw new Error(
2451
+ `rotateEveryDays must be a whole number of days between 1 and ${MAX_ROTATE_EVERY_DAYS}`
2452
+ );
2453
+ }
2454
+ const valueChanged = !existing || (opts.states ? JSON.stringify(existing.states ?? null) !== JSON.stringify(opts.states) : existing.states !== void 0 || existing.value !== value);
2455
+ const rotatedAt = valueChanged ? (/* @__PURE__ */ new Date()).toISOString() : existing?.meta.rotatedAt ?? existing?.meta.updatedAt ?? existing?.meta.createdAt;
2418
2456
  const prov = opts.provider ?? existing?.meta.provider;
2419
2457
  const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
2420
2458
  const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
@@ -2445,6 +2483,8 @@ function setSecret(key, value, opts = {}) {
2445
2483
  entangled: existing?.meta.entangled,
2446
2484
  rotationFormat: rotFmt,
2447
2485
  rotationPrefix: rotPfx,
2486
+ rotatedAt,
2487
+ rotateEveryDays: rotEvery,
2448
2488
  provider: prov,
2449
2489
  requiresApproval: reqApp,
2450
2490
  jitProvider: jitProv,
@@ -2460,6 +2500,8 @@ function setSecret(key, value, opts = {}) {
2460
2500
  entangled: existing?.meta.entangled,
2461
2501
  rotationFormat: rotFmt,
2462
2502
  rotationPrefix: rotPfx,
2503
+ rotatedAt,
2504
+ rotateEveryDays: rotEvery,
2463
2505
  provider: prov,
2464
2506
  requiresApproval: reqApp,
2465
2507
  jitProvider: jitProv,
@@ -2742,6 +2784,61 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
2742
2784
  });
2743
2785
  }
2744
2786
 
2787
+ // src/core/rotation.ts
2788
+ var DAY_MS = 864e5;
2789
+ var DUE_SOON_FRACTION = 0.2;
2790
+ var DUE_SOON_MAX_DAYS = 7;
2791
+ function dueSoonWindowDays(rotateEveryDays) {
2792
+ return Math.min(rotateEveryDays * DUE_SOON_FRACTION, DUE_SOON_MAX_DAYS);
2793
+ }
2794
+ function parseMs(iso) {
2795
+ if (!iso) return null;
2796
+ const ms = new Date(iso).getTime();
2797
+ return Number.isFinite(ms) ? ms : null;
2798
+ }
2799
+ function rotationStatus(meta, now = Date.now()) {
2800
+ const nowMs = typeof now === "number" ? now : now.getTime();
2801
+ const lastRotatedAt = meta.rotatedAt ?? meta.updatedAt ?? meta.createdAt;
2802
+ const lastMs = parseMs(lastRotatedAt) ?? nowMs;
2803
+ const ageDays = Math.max(0, Math.floor((nowMs - lastMs) / DAY_MS));
2804
+ const every = meta.rotateEveryDays;
2805
+ if (!every || !Number.isFinite(every) || every <= 0) {
2806
+ return {
2807
+ lastRotatedAt,
2808
+ ageDays,
2809
+ rotateEveryDays: null,
2810
+ dueAt: null,
2811
+ daysUntilDue: null,
2812
+ state: "unscheduled"
2813
+ };
2814
+ }
2815
+ const dueMs = lastMs + every * DAY_MS;
2816
+ const untilMs = dueMs - nowMs;
2817
+ const daysUntilDue = Math.ceil(untilMs / DAY_MS);
2818
+ let state = "ok";
2819
+ if (untilMs <= 0) state = "overdue";
2820
+ else if (untilMs <= dueSoonWindowDays(every) * DAY_MS) state = "due-soon";
2821
+ return {
2822
+ lastRotatedAt,
2823
+ ageDays,
2824
+ rotateEveryDays: every,
2825
+ dueAt: new Date(dueMs).toISOString(),
2826
+ daysUntilDue,
2827
+ state
2828
+ };
2829
+ }
2830
+ function describeRotation(status) {
2831
+ if (status.state === "unscheduled" || status.daysUntilDue === null) return "unscheduled";
2832
+ if (status.daysUntilDue > 0) return `due in ${status.daysUntilDue}d`;
2833
+ if (status.daysUntilDue === 0) return "due now";
2834
+ return `overdue ${-status.daysUntilDue}d`;
2835
+ }
2836
+ function compareRotationUrgency(a, b) {
2837
+ const ad = a.daysUntilDue ?? Number.POSITIVE_INFINITY;
2838
+ const bd = b.daysUntilDue ?? Number.POSITIVE_INFINITY;
2839
+ return ad - bd;
2840
+ }
2841
+
2745
2842
  // src/core/tunnel.ts
2746
2843
  import { randomBytes as randomBytes4 } from "crypto";
2747
2844
  var tunnelStore = /* @__PURE__ */ new Map();
@@ -2816,8 +2913,8 @@ function tunnelList() {
2816
2913
 
2817
2914
  // src/core/memory.ts
2818
2915
  import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8, chmodSync as chmodSync3 } from "fs";
2819
- import { join as join11 } from "path";
2820
- import { homedir as homedir8, hostname, userInfo } from "os";
2916
+ import { join as join12 } from "path";
2917
+ import { hostname, userInfo } from "os";
2821
2918
  import {
2822
2919
  createCipheriv as createCipheriv2,
2823
2920
  createDecipheriv as createDecipheriv2,
@@ -2829,7 +2926,7 @@ var MEMORY_FILE = "agent-memory.enc";
2829
2926
  var KEYRING_SERVICE = "qring-memory-key";
2830
2927
  var KEYRING_ACCOUNT = "encryption-key";
2831
2928
  function getMemoryDir() {
2832
- const dir = join11(homedir8(), ".config", "q-ring");
2929
+ const dir = configDir();
2833
2930
  if (!existsSync8(dir)) {
2834
2931
  mkdirSync8(dir, { recursive: true, mode: 448 });
2835
2932
  }
@@ -2843,7 +2940,7 @@ function writeMemoryFile(path, data) {
2843
2940
  }
2844
2941
  }
2845
2942
  function getMemoryPath() {
2846
- return join11(getMemoryDir(), MEMORY_FILE);
2943
+ return join12(getMemoryDir(), MEMORY_FILE);
2847
2944
  }
2848
2945
  var PBKDF2_ITERATIONS2 = 21e4;
2849
2946
  var KEY_LENGTH2 = 32;
@@ -3061,6 +3158,8 @@ function listAgentSessions(query = {}) {
3061
3158
 
3062
3159
  export {
3063
3160
  PACKAGE_VERSION,
3161
+ configDir,
3162
+ Entry,
3064
3163
  hashProjectPath,
3065
3164
  serviceForScope,
3066
3165
  checkDecay,
@@ -3101,6 +3200,8 @@ export {
3101
3200
  wrapToolRequiresApproval,
3102
3201
  getWrapRateLimit,
3103
3202
  wrapRedactsResults,
3203
+ MAX_ROTATE_EVERY_DAYS,
3204
+ isValidRotateEveryDays,
3104
3205
  getSecret,
3105
3206
  getEnvelope,
3106
3207
  setSecret,
@@ -3111,6 +3212,9 @@ export {
3111
3212
  exportSecrets,
3112
3213
  entangleSecrets,
3113
3214
  disentangleSecrets,
3215
+ rotationStatus,
3216
+ describeRotation,
3217
+ compareRotationUrgency,
3114
3218
  tunnelCreate,
3115
3219
  tunnelRead,
3116
3220
  tunnelDestroy,
@@ -3123,4 +3227,4 @@ export {
3123
3227
  buildSessions,
3124
3228
  listAgentSessions
3125
3229
  };
3126
- //# sourceMappingURL=chunk-5ZCQSBVS.js.map
3230
+ //# sourceMappingURL=chunk-ZBPMEJRO.js.map