@i4ctime/q-ring 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -7
- package/dist/{chunk-C2TFJ2EH.js → chunk-MLBJCPX2.js} +322 -119
- package/dist/chunk-MLBJCPX2.js.map +1 -0
- package/dist/{chunk-NNIEXAW5.js → chunk-TPPPTL4J.js} +319 -116
- package/dist/chunk-TPPPTL4J.js.map +1 -0
- package/dist/{dashboard-KAUEPLXO.js → dashboard-T2UG23KI.js} +2 -2
- package/dist/{dashboard-PYYAED45.js → dashboard-TFWCG23R.js} +2 -2
- package/dist/index.js +647 -18
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +102 -7
- package/dist/mcp.js.map +1 -1
- package/package.json +5 -4
- package/dist/chunk-C2TFJ2EH.js.map +0 -1
- package/dist/chunk-NNIEXAW5.js.map +0 -1
- /package/dist/{dashboard-KAUEPLXO.js.map → dashboard-T2UG23KI.js.map} +0 -0
- /package/dist/{dashboard-PYYAED45.js.map → dashboard-TFWCG23R.js.map} +0 -0
|
@@ -287,21 +287,27 @@ function mapEnvName(raw) {
|
|
|
287
287
|
|
|
288
288
|
// src/core/observer.ts
|
|
289
289
|
import {
|
|
290
|
-
existsSync,
|
|
291
|
-
mkdirSync as
|
|
290
|
+
existsSync as existsSync2,
|
|
291
|
+
mkdirSync as mkdirSync3,
|
|
292
292
|
appendFileSync,
|
|
293
|
-
chmodSync,
|
|
294
|
-
readFileSync as
|
|
293
|
+
chmodSync as chmodSync2,
|
|
294
|
+
readFileSync as readFileSync5,
|
|
295
295
|
openSync,
|
|
296
296
|
fstatSync,
|
|
297
297
|
readSync,
|
|
298
298
|
closeSync,
|
|
299
299
|
statSync as statSync3
|
|
300
300
|
} from "fs";
|
|
301
|
-
import { join as
|
|
301
|
+
import { join as join5 } from "path";
|
|
302
|
+
import { homedir as homedir3 } from "os";
|
|
303
|
+
import { createHash, createHmac, randomBytes as randomBytes2 } from "crypto";
|
|
304
|
+
|
|
305
|
+
// src/core/backend.ts
|
|
306
|
+
import { Entry as NapiEntry, findCredentials as napiFindCredentials } from "@napi-rs/keyring";
|
|
307
|
+
import { existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "fs";
|
|
308
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
302
309
|
import { homedir as homedir2 } from "os";
|
|
303
|
-
import {
|
|
304
|
-
import { Entry } from "@napi-rs/keyring";
|
|
310
|
+
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from "crypto";
|
|
305
311
|
|
|
306
312
|
// src/utils/file-lock.ts
|
|
307
313
|
import {
|
|
@@ -332,17 +338,11 @@ function withFileLock(name, fn, opts = {}) {
|
|
|
332
338
|
const deadline = Date.now() + (opts.timeoutMs ?? 8e3);
|
|
333
339
|
const staleMs = opts.staleMs ?? 3e4;
|
|
334
340
|
while (Date.now() < deadline) {
|
|
341
|
+
let acquired = false;
|
|
335
342
|
try {
|
|
336
343
|
writeFileSync(lockPath, `${process.pid}
|
|
337
344
|
`, { flag: "wx", mode: 384 });
|
|
338
|
-
|
|
339
|
-
return fn();
|
|
340
|
-
} finally {
|
|
341
|
-
try {
|
|
342
|
-
unlinkSync(lockPath);
|
|
343
|
-
} catch {
|
|
344
|
-
}
|
|
345
|
-
}
|
|
345
|
+
acquired = true;
|
|
346
346
|
} catch {
|
|
347
347
|
try {
|
|
348
348
|
const holderPid = parseInt(readFileSync3(lockPath, "utf8").trim(), 10);
|
|
@@ -356,10 +356,172 @@ function withFileLock(name, fn, opts = {}) {
|
|
|
356
356
|
}
|
|
357
357
|
sleepSync(15);
|
|
358
358
|
}
|
|
359
|
+
if (acquired) {
|
|
360
|
+
try {
|
|
361
|
+
return fn();
|
|
362
|
+
} finally {
|
|
363
|
+
try {
|
|
364
|
+
unlinkSync(lockPath);
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
359
369
|
}
|
|
360
370
|
throw new Error(`Could not acquire lock "${name}" (timeout)`);
|
|
361
371
|
}
|
|
362
372
|
|
|
373
|
+
// src/core/backend.ts
|
|
374
|
+
var PASSPHRASE_ENV = "QRING_FILE_PASSPHRASE";
|
|
375
|
+
var BACKEND_ENV = "QRING_BACKEND";
|
|
376
|
+
var PATH_ENV = "QRING_FILE_BACKEND_PATH";
|
|
377
|
+
var PBKDF2_ITERATIONS = 21e4;
|
|
378
|
+
var KEY_LENGTH = 32;
|
|
379
|
+
var FILE_PREFIX = "qfile1";
|
|
380
|
+
var BackendUnavailableError = class extends Error {
|
|
381
|
+
constructor(message) {
|
|
382
|
+
super(message);
|
|
383
|
+
this.name = "BackendUnavailableError";
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
function activeBackend() {
|
|
387
|
+
const requested = process.env[BACKEND_ENV];
|
|
388
|
+
if (requested === "file") return "file";
|
|
389
|
+
if (requested && requested !== "keyring") {
|
|
390
|
+
throw new BackendUnavailableError(
|
|
391
|
+
`Unknown ${BACKEND_ENV} "${requested}" \u2014 expected "keyring" or "file".`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return "keyring";
|
|
395
|
+
}
|
|
396
|
+
function storePath() {
|
|
397
|
+
return process.env[PATH_ENV] ?? join4(homedir2(), ".config", "q-ring", "file-backend.enc");
|
|
398
|
+
}
|
|
399
|
+
function passphrase() {
|
|
400
|
+
const p = process.env[PASSPHRASE_ENV];
|
|
401
|
+
if (!p) {
|
|
402
|
+
throw new BackendUnavailableError(
|
|
403
|
+
`${BACKEND_ENV}=file requires ${PASSPHRASE_ENV} to be set. q-ring refuses to store secrets under a machine-derivable key \u2014 set a strong passphrase, or use the OS keyring backend.`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
return p;
|
|
407
|
+
}
|
|
408
|
+
var keyCache = null;
|
|
409
|
+
function deriveKey(saltB64) {
|
|
410
|
+
const pass = passphrase();
|
|
411
|
+
if (keyCache && keyCache.salt === saltB64 && keyCache.pass === pass) {
|
|
412
|
+
return keyCache.key;
|
|
413
|
+
}
|
|
414
|
+
const key = pbkdf2Sync(pass, Buffer.from(saltB64, "base64"), PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
415
|
+
keyCache = { salt: saltB64, pass, key };
|
|
416
|
+
return key;
|
|
417
|
+
}
|
|
418
|
+
function loadStore() {
|
|
419
|
+
const path = storePath();
|
|
420
|
+
if (!existsSync(path)) {
|
|
421
|
+
return { map: {}, salt: randomBytes(16).toString("base64") };
|
|
422
|
+
}
|
|
423
|
+
const blob = readFileSync4(path, "utf8").trim();
|
|
424
|
+
const parts = blob.split(":");
|
|
425
|
+
if (parts.length !== 5 || parts[0] !== FILE_PREFIX) {
|
|
426
|
+
throw new BackendUnavailableError(
|
|
427
|
+
`${path} is not a valid q-ring file-backend store (expected ${FILE_PREFIX}:...).`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const [, salt, ivB64, tagB64, ctB64] = parts;
|
|
431
|
+
const decipher = createDecipheriv("aes-256-gcm", deriveKey(salt), Buffer.from(ivB64, "base64"));
|
|
432
|
+
decipher.setAuthTag(Buffer.from(tagB64, "base64"));
|
|
433
|
+
let plaintext;
|
|
434
|
+
try {
|
|
435
|
+
plaintext = decipher.update(Buffer.from(ctB64, "base64")) + decipher.final("utf8");
|
|
436
|
+
} catch {
|
|
437
|
+
throw new BackendUnavailableError(
|
|
438
|
+
`Cannot decrypt ${path} \u2014 wrong ${PASSPHRASE_ENV}, or the store was tampered with.`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
return { map: JSON.parse(plaintext), salt };
|
|
442
|
+
}
|
|
443
|
+
function saveStore(map, salt) {
|
|
444
|
+
const path = storePath();
|
|
445
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
446
|
+
const iv = randomBytes(12);
|
|
447
|
+
const cipher = createCipheriv("aes-256-gcm", deriveKey(salt), iv);
|
|
448
|
+
const ct = Buffer.concat([cipher.update(JSON.stringify(map), "utf8"), cipher.final()]);
|
|
449
|
+
const blob = [
|
|
450
|
+
FILE_PREFIX,
|
|
451
|
+
salt,
|
|
452
|
+
iv.toString("base64"),
|
|
453
|
+
cipher.getAuthTag().toString("base64"),
|
|
454
|
+
ct.toString("base64")
|
|
455
|
+
].join(":");
|
|
456
|
+
writeFileSync2(path, blob + "\n", { mode: 384 });
|
|
457
|
+
try {
|
|
458
|
+
chmodSync(path, 384);
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
var SEP = "\0";
|
|
463
|
+
function fileMutate(fn) {
|
|
464
|
+
return withFileLock("file-backend", () => {
|
|
465
|
+
const { map, salt } = loadStore();
|
|
466
|
+
const result = fn(map);
|
|
467
|
+
saveStore(map, salt);
|
|
468
|
+
return result;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
var Entry = class {
|
|
472
|
+
constructor(service, account) {
|
|
473
|
+
this.service = service;
|
|
474
|
+
this.account = account;
|
|
475
|
+
}
|
|
476
|
+
service;
|
|
477
|
+
account;
|
|
478
|
+
napi;
|
|
479
|
+
delegate() {
|
|
480
|
+
this.napi ??= new NapiEntry(this.service, this.account);
|
|
481
|
+
return this.napi;
|
|
482
|
+
}
|
|
483
|
+
storeKey() {
|
|
484
|
+
return `${this.service}${SEP}${this.account}`;
|
|
485
|
+
}
|
|
486
|
+
getPassword() {
|
|
487
|
+
if (activeBackend() === "file") {
|
|
488
|
+
return loadStore().map[this.storeKey()] ?? null;
|
|
489
|
+
}
|
|
490
|
+
return this.delegate().getPassword();
|
|
491
|
+
}
|
|
492
|
+
setPassword(password) {
|
|
493
|
+
if (activeBackend() === "file") {
|
|
494
|
+
fileMutate((map) => {
|
|
495
|
+
map[this.storeKey()] = password;
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
this.delegate().setPassword(password);
|
|
500
|
+
}
|
|
501
|
+
deleteCredential() {
|
|
502
|
+
if (activeBackend() === "file") {
|
|
503
|
+
return fileMutate((map) => {
|
|
504
|
+
const existed = this.storeKey() in map;
|
|
505
|
+
delete map[this.storeKey()];
|
|
506
|
+
return existed;
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return this.delegate().deleteCredential();
|
|
510
|
+
}
|
|
511
|
+
// @napi-rs/keyring's other delete alias, used by `qring doctor`.
|
|
512
|
+
deletePassword() {
|
|
513
|
+
return this.deleteCredential();
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
function findCredentials(service) {
|
|
517
|
+
if (activeBackend() === "file") {
|
|
518
|
+
const { map } = loadStore();
|
|
519
|
+
const prefix = `${service}${SEP}`;
|
|
520
|
+
return Object.entries(map).filter(([k]) => k.startsWith(prefix)).map(([k, password]) => ({ account: k.slice(prefix.length), password }));
|
|
521
|
+
}
|
|
522
|
+
return napiFindCredentials(service);
|
|
523
|
+
}
|
|
524
|
+
|
|
363
525
|
// src/core/observer.ts
|
|
364
526
|
var AUDIT_KEYRING_SERVICE = "qring-audit-chain";
|
|
365
527
|
var AUDIT_KEY_ACCOUNT = "hmac-key";
|
|
@@ -370,7 +532,7 @@ function getAuditKey() {
|
|
|
370
532
|
const entry = new Entry(AUDIT_KEYRING_SERVICE, AUDIT_KEY_ACCOUNT);
|
|
371
533
|
const stored = entry.getPassword();
|
|
372
534
|
if (stored) return Buffer.from(stored, "base64");
|
|
373
|
-
const key =
|
|
535
|
+
const key = randomBytes2(32);
|
|
374
536
|
entry.setPassword(key.toString("base64"));
|
|
375
537
|
return key;
|
|
376
538
|
} catch {
|
|
@@ -401,23 +563,23 @@ function writeStoredAnchor(hash) {
|
|
|
401
563
|
}
|
|
402
564
|
function getAuditDir() {
|
|
403
565
|
if (process.env.QRING_AUDIT_DIR) {
|
|
404
|
-
if (!
|
|
405
|
-
|
|
566
|
+
if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
|
|
567
|
+
mkdirSync3(process.env.QRING_AUDIT_DIR, { recursive: true, mode: 448 });
|
|
406
568
|
}
|
|
407
569
|
return process.env.QRING_AUDIT_DIR;
|
|
408
570
|
}
|
|
409
|
-
const dir =
|
|
410
|
-
if (!
|
|
411
|
-
|
|
571
|
+
const dir = join5(homedir3(), ".config", "q-ring");
|
|
572
|
+
if (!existsSync2(dir)) {
|
|
573
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
412
574
|
}
|
|
413
575
|
return dir;
|
|
414
576
|
}
|
|
415
577
|
function getAuditPath() {
|
|
416
|
-
return
|
|
578
|
+
return join5(getAuditDir(), "audit.jsonl");
|
|
417
579
|
}
|
|
418
580
|
function getLastLineHash() {
|
|
419
581
|
const path = getAuditPath();
|
|
420
|
-
if (!
|
|
582
|
+
if (!existsSync2(path)) return void 0;
|
|
421
583
|
try {
|
|
422
584
|
const fd = openSync(path, "r");
|
|
423
585
|
const stat = fstatSync(fd);
|
|
@@ -454,7 +616,7 @@ function logAudit(event) {
|
|
|
454
616
|
const path = getAuditPath();
|
|
455
617
|
appendFileSync(path, line + "\n", { mode: 384 });
|
|
456
618
|
try {
|
|
457
|
-
|
|
619
|
+
chmodSync2(path, 384);
|
|
458
620
|
} catch {
|
|
459
621
|
}
|
|
460
622
|
const key = getAuditKey();
|
|
@@ -468,7 +630,7 @@ function logAudit(event) {
|
|
|
468
630
|
var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
|
|
469
631
|
function queryAudit(query = {}) {
|
|
470
632
|
const path = getAuditPath();
|
|
471
|
-
if (!
|
|
633
|
+
if (!existsSync2(path)) return [];
|
|
472
634
|
try {
|
|
473
635
|
const st = statSync3(path);
|
|
474
636
|
const readStart = st.size > MAX_AUDIT_BYTES ? st.size - MAX_AUDIT_BYTES : 0;
|
|
@@ -509,10 +671,10 @@ function queryAudit(query = {}) {
|
|
|
509
671
|
}
|
|
510
672
|
function verifyAuditChain() {
|
|
511
673
|
const path = getAuditPath();
|
|
512
|
-
if (!
|
|
674
|
+
if (!existsSync2(path)) {
|
|
513
675
|
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
514
676
|
}
|
|
515
|
-
const lines =
|
|
677
|
+
const lines = readFileSync5(path, "utf8").split("\n").filter((l) => l.trim());
|
|
516
678
|
if (lines.length === 0) {
|
|
517
679
|
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
518
680
|
}
|
|
@@ -563,8 +725,8 @@ function verifyAuditChain() {
|
|
|
563
725
|
}
|
|
564
726
|
function exportAudit(opts = {}) {
|
|
565
727
|
const path = getAuditPath();
|
|
566
|
-
if (!
|
|
567
|
-
const lines =
|
|
728
|
+
if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
|
|
729
|
+
const lines = readFileSync5(path, "utf8").split("\n").filter((l) => l.trim());
|
|
568
730
|
let events = lines.map((l) => {
|
|
569
731
|
try {
|
|
570
732
|
return JSON.parse(l);
|
|
@@ -629,15 +791,15 @@ function detectAnomalies(key) {
|
|
|
629
791
|
}
|
|
630
792
|
|
|
631
793
|
// src/core/entanglement.ts
|
|
632
|
-
import { existsSync as
|
|
633
|
-
import { join as
|
|
634
|
-
import { homedir as
|
|
794
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
795
|
+
import { join as join6 } from "path";
|
|
796
|
+
import { homedir as homedir4 } from "os";
|
|
635
797
|
|
|
636
798
|
// src/utils/registry.ts
|
|
637
|
-
import { existsSync as
|
|
799
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6, renameSync } from "fs";
|
|
638
800
|
function loadJsonRegistry(path, empty) {
|
|
639
|
-
if (!
|
|
640
|
-
const raw =
|
|
801
|
+
if (!existsSync3(path)) return empty;
|
|
802
|
+
const raw = readFileSync6(path, "utf8");
|
|
641
803
|
try {
|
|
642
804
|
return JSON.parse(raw);
|
|
643
805
|
} catch (err) {
|
|
@@ -661,18 +823,18 @@ function loadJsonRegistry(path, empty) {
|
|
|
661
823
|
// src/core/entanglement.ts
|
|
662
824
|
var REGISTRY_VERSION = 1;
|
|
663
825
|
function getRegistryPath() {
|
|
664
|
-
const dir =
|
|
665
|
-
if (!
|
|
666
|
-
|
|
826
|
+
const dir = join6(homedir4(), ".config", "q-ring");
|
|
827
|
+
if (!existsSync4(dir)) {
|
|
828
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
667
829
|
}
|
|
668
|
-
return
|
|
830
|
+
return join6(dir, "entanglement.json");
|
|
669
831
|
}
|
|
670
832
|
function loadRegistry() {
|
|
671
833
|
return loadJsonRegistry(getRegistryPath(), { pairs: [] });
|
|
672
834
|
}
|
|
673
835
|
function saveRegistry(registry2) {
|
|
674
836
|
registry2.version = REGISTRY_VERSION;
|
|
675
|
-
|
|
837
|
+
writeFileSync3(getRegistryPath(), JSON.stringify(registry2, null, 2), {
|
|
676
838
|
mode: 384
|
|
677
839
|
});
|
|
678
840
|
}
|
|
@@ -706,9 +868,9 @@ function listEntanglements() {
|
|
|
706
868
|
}
|
|
707
869
|
|
|
708
870
|
// src/core/hooks.ts
|
|
709
|
-
import { existsSync as
|
|
710
|
-
import { join as
|
|
711
|
-
import { homedir as
|
|
871
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
|
|
872
|
+
import { join as join7 } from "path";
|
|
873
|
+
import { homedir as homedir5 } from "os";
|
|
712
874
|
import { execFile, spawn } from "child_process";
|
|
713
875
|
import { randomUUID } from "crypto";
|
|
714
876
|
|
|
@@ -932,17 +1094,17 @@ function httpRequest(opts) {
|
|
|
932
1094
|
|
|
933
1095
|
// src/core/hooks.ts
|
|
934
1096
|
function getRegistryPath2() {
|
|
935
|
-
const dir =
|
|
936
|
-
if (!
|
|
937
|
-
|
|
1097
|
+
const dir = join7(homedir5(), ".config", "q-ring");
|
|
1098
|
+
if (!existsSync5(dir)) {
|
|
1099
|
+
mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
938
1100
|
}
|
|
939
|
-
return
|
|
1101
|
+
return join7(dir, "hooks.json");
|
|
940
1102
|
}
|
|
941
1103
|
function loadRegistry2() {
|
|
942
1104
|
return loadJsonRegistry(getRegistryPath2(), { hooks: [] });
|
|
943
1105
|
}
|
|
944
1106
|
function saveRegistry2(registry2) {
|
|
945
|
-
|
|
1107
|
+
writeFileSync4(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
|
|
946
1108
|
mode: 384
|
|
947
1109
|
});
|
|
948
1110
|
}
|
|
@@ -1189,19 +1351,19 @@ async function fireHooks(payload, tags) {
|
|
|
1189
1351
|
}
|
|
1190
1352
|
|
|
1191
1353
|
// src/core/approval.ts
|
|
1192
|
-
import { existsSync as
|
|
1193
|
-
import { join as
|
|
1194
|
-
import { homedir as
|
|
1195
|
-
import { createHmac as createHmac2, randomBytes as
|
|
1354
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1355
|
+
import { join as join8 } from "path";
|
|
1356
|
+
import { homedir as homedir6 } from "os";
|
|
1357
|
+
import { createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
1196
1358
|
function getHmacSecret() {
|
|
1197
|
-
const dir =
|
|
1198
|
-
const secretPath =
|
|
1199
|
-
if (!
|
|
1200
|
-
if (
|
|
1201
|
-
return
|
|
1202
|
-
}
|
|
1203
|
-
const secret =
|
|
1204
|
-
|
|
1359
|
+
const dir = join8(homedir6(), ".config", "q-ring");
|
|
1360
|
+
const secretPath = join8(dir, ".approval-key");
|
|
1361
|
+
if (!existsSync6(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
1362
|
+
if (existsSync6(secretPath)) {
|
|
1363
|
+
return readFileSync7(secretPath, "utf8").trim();
|
|
1364
|
+
}
|
|
1365
|
+
const secret = randomBytes3(32).toString("hex");
|
|
1366
|
+
writeFileSync5(secretPath, secret, { mode: 384 });
|
|
1205
1367
|
return secret;
|
|
1206
1368
|
}
|
|
1207
1369
|
function computeHmac(entry) {
|
|
@@ -1231,11 +1393,11 @@ function verifyHmac(entry) {
|
|
|
1231
1393
|
}
|
|
1232
1394
|
}
|
|
1233
1395
|
function getRegistryPath3() {
|
|
1234
|
-
const dir =
|
|
1235
|
-
if (!
|
|
1236
|
-
|
|
1396
|
+
const dir = join8(homedir6(), ".config", "q-ring");
|
|
1397
|
+
if (!existsSync6(dir)) {
|
|
1398
|
+
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
1237
1399
|
}
|
|
1238
|
-
return
|
|
1400
|
+
return join8(dir, "approvals.json");
|
|
1239
1401
|
}
|
|
1240
1402
|
function loadRegistry3() {
|
|
1241
1403
|
return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
|
|
@@ -1262,7 +1424,7 @@ function listApprovals() {
|
|
|
1262
1424
|
|
|
1263
1425
|
// src/core/policy.ts
|
|
1264
1426
|
import { statSync as statSync4 } from "fs";
|
|
1265
|
-
import { join as
|
|
1427
|
+
import { join as join9 } from "path";
|
|
1266
1428
|
import { z as z2 } from "zod";
|
|
1267
1429
|
var stringArray = z2.array(z2.string());
|
|
1268
1430
|
var mcpPolicySchema = z2.object({
|
|
@@ -1308,7 +1470,7 @@ function resolvePolicyPath(projectPath) {
|
|
|
1308
1470
|
}
|
|
1309
1471
|
function configMtime(pp) {
|
|
1310
1472
|
try {
|
|
1311
|
-
return statSync4(
|
|
1473
|
+
return statSync4(join9(pp, ".q-ring.json")).mtimeMs;
|
|
1312
1474
|
} catch {
|
|
1313
1475
|
return 0;
|
|
1314
1476
|
}
|
|
@@ -1331,7 +1493,7 @@ function loadPolicy(projectPath) {
|
|
|
1331
1493
|
if (!parsed.success) {
|
|
1332
1494
|
const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
|
|
1333
1495
|
const error = new PolicyConfigError(
|
|
1334
|
-
`Invalid policy in ${
|
|
1496
|
+
`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
1497
|
);
|
|
1336
1498
|
console.error(`q-ring: ${error.message}`);
|
|
1337
1499
|
cachedPolicy = { path: pp, mtimeMs, error };
|
|
@@ -1466,9 +1628,6 @@ function getPolicySummary(projectPath) {
|
|
|
1466
1628
|
};
|
|
1467
1629
|
}
|
|
1468
1630
|
|
|
1469
|
-
// src/core/keyring.ts
|
|
1470
|
-
import { Entry as Entry2, findCredentials } from "@napi-rs/keyring";
|
|
1471
|
-
|
|
1472
1631
|
// src/utils/hash.ts
|
|
1473
1632
|
import { createHash as createHash2 } from "crypto";
|
|
1474
1633
|
function hashProjectPath(projectPath) {
|
|
@@ -1524,6 +1683,50 @@ function serviceForScope(scope, opts = {}) {
|
|
|
1524
1683
|
return resolveScope({ ...opts, scope })[0].service;
|
|
1525
1684
|
}
|
|
1526
1685
|
|
|
1686
|
+
// src/core/notify.ts
|
|
1687
|
+
import { spawn as spawn2 } from "child_process";
|
|
1688
|
+
var DISABLE_ENV = "QRING_NOTIFY";
|
|
1689
|
+
var THROTTLE_MS = 5 * 60 * 1e3;
|
|
1690
|
+
var lastNotified = /* @__PURE__ */ new Map();
|
|
1691
|
+
function notificationsEnabled() {
|
|
1692
|
+
const value = process.env[DISABLE_ENV];
|
|
1693
|
+
return value !== "off" && value !== "0" && value !== "false";
|
|
1694
|
+
}
|
|
1695
|
+
function notifyUser(title, body) {
|
|
1696
|
+
try {
|
|
1697
|
+
let command;
|
|
1698
|
+
let args;
|
|
1699
|
+
if (process.platform === "linux") {
|
|
1700
|
+
command = "notify-send";
|
|
1701
|
+
args = ["--app-name=q-ring", "--urgency=critical", title, body];
|
|
1702
|
+
} else if (process.platform === "darwin") {
|
|
1703
|
+
command = "osascript";
|
|
1704
|
+
const clean = (s) => s.replace(/["\\]/g, "");
|
|
1705
|
+
args = ["-e", `display notification "${clean(body)}" with title "${clean(title)}"`];
|
|
1706
|
+
} else {
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1709
|
+
const child = spawn2(command, args, { detached: true, stdio: "ignore" });
|
|
1710
|
+
child.on("error", () => {
|
|
1711
|
+
});
|
|
1712
|
+
child.unref();
|
|
1713
|
+
return true;
|
|
1714
|
+
} catch {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function notifyApprovalRequested(key, source) {
|
|
1719
|
+
if (!notificationsEnabled()) return;
|
|
1720
|
+
const now = Date.now();
|
|
1721
|
+
const last = lastNotified.get(key);
|
|
1722
|
+
if (last !== void 0 && now - last < THROTTLE_MS) return;
|
|
1723
|
+
lastNotified.set(key, now);
|
|
1724
|
+
notifyUser(
|
|
1725
|
+
"q-ring: approval requested",
|
|
1726
|
+
`An ${source} agent wants to read "${key}". Allow with: qring approve ${key}`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1527
1730
|
// src/core/provision.ts
|
|
1528
1731
|
import { execFileSync, spawnSync } from "child_process";
|
|
1529
1732
|
import { z as z3 } from "zod";
|
|
@@ -1676,14 +1879,14 @@ function withJitEnvelopeLock(service, key, fn) {
|
|
|
1676
1879
|
return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
|
|
1677
1880
|
}
|
|
1678
1881
|
function readEnvelope(service, key) {
|
|
1679
|
-
const entry = new
|
|
1882
|
+
const entry = new Entry(service, key);
|
|
1680
1883
|
const raw = entry.getPassword();
|
|
1681
1884
|
if (raw === null) return null;
|
|
1682
1885
|
const envelope = parseEnvelope(raw);
|
|
1683
1886
|
return envelope ?? wrapLegacy(raw);
|
|
1684
1887
|
}
|
|
1685
1888
|
function writeEnvelope(service, key, envelope) {
|
|
1686
|
-
const entry = new
|
|
1889
|
+
const entry = new Entry(service, key);
|
|
1687
1890
|
entry.setPassword(serializeEnvelope(envelope));
|
|
1688
1891
|
}
|
|
1689
1892
|
function resolveEnv(opts) {
|
|
@@ -1760,6 +1963,7 @@ function getSecret(key, opts = {}) {
|
|
|
1760
1963
|
source,
|
|
1761
1964
|
detail: "blocked: requires user approval"
|
|
1762
1965
|
});
|
|
1966
|
+
notifyApprovalRequested(key, source);
|
|
1763
1967
|
}
|
|
1764
1968
|
throw new Error(`Access Denied: This secret requires user approval. Please ask the user to run 'qring approve ${key}'`);
|
|
1765
1969
|
}
|
|
@@ -1941,7 +2145,7 @@ function deleteSecret(key, opts = {}) {
|
|
|
1941
2145
|
}
|
|
1942
2146
|
let deleted = false;
|
|
1943
2147
|
for (const { service, scope } of scopes) {
|
|
1944
|
-
const entry = new
|
|
2148
|
+
const entry = new Entry(service, key);
|
|
1945
2149
|
try {
|
|
1946
2150
|
if (entry.deleteCredential()) {
|
|
1947
2151
|
deleted = true;
|
|
@@ -2121,7 +2325,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
2121
2325
|
}
|
|
2122
2326
|
|
|
2123
2327
|
// src/core/tunnel.ts
|
|
2124
|
-
import { randomBytes as
|
|
2328
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2125
2329
|
var tunnelStore = /* @__PURE__ */ new Map();
|
|
2126
2330
|
var cleanupInterval = null;
|
|
2127
2331
|
function ensureCleanup() {
|
|
@@ -2143,7 +2347,7 @@ function ensureCleanup() {
|
|
|
2143
2347
|
}
|
|
2144
2348
|
}
|
|
2145
2349
|
function tunnelCreate(value, opts = {}) {
|
|
2146
|
-
const id = `tun_${Date.now().toString(36)}_${
|
|
2350
|
+
const id = `tun_${Date.now().toString(36)}_${randomBytes4(6).toString("base64url")}`;
|
|
2147
2351
|
const now = Date.now();
|
|
2148
2352
|
tunnelStore.set(id, {
|
|
2149
2353
|
value,
|
|
@@ -2193,40 +2397,39 @@ function tunnelList() {
|
|
|
2193
2397
|
}
|
|
2194
2398
|
|
|
2195
2399
|
// src/core/memory.ts
|
|
2196
|
-
import { existsSync as
|
|
2197
|
-
import { join as
|
|
2198
|
-
import { homedir as
|
|
2400
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, chmodSync as chmodSync3 } from "fs";
|
|
2401
|
+
import { join as join10 } from "path";
|
|
2402
|
+
import { homedir as homedir7, hostname, userInfo } from "os";
|
|
2199
2403
|
import {
|
|
2200
|
-
createCipheriv,
|
|
2201
|
-
createDecipheriv,
|
|
2404
|
+
createCipheriv as createCipheriv2,
|
|
2405
|
+
createDecipheriv as createDecipheriv2,
|
|
2202
2406
|
createHash as createHash3,
|
|
2203
|
-
randomBytes as
|
|
2204
|
-
pbkdf2Sync
|
|
2407
|
+
randomBytes as randomBytes5,
|
|
2408
|
+
pbkdf2Sync as pbkdf2Sync2
|
|
2205
2409
|
} from "crypto";
|
|
2206
|
-
import { Entry as Entry3 } from "@napi-rs/keyring";
|
|
2207
2410
|
var MEMORY_FILE = "agent-memory.enc";
|
|
2208
2411
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
2209
2412
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
2210
2413
|
function getMemoryDir() {
|
|
2211
|
-
const dir =
|
|
2212
|
-
if (!
|
|
2213
|
-
|
|
2414
|
+
const dir = join10(homedir7(), ".config", "q-ring");
|
|
2415
|
+
if (!existsSync7(dir)) {
|
|
2416
|
+
mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
2214
2417
|
}
|
|
2215
2418
|
return dir;
|
|
2216
2419
|
}
|
|
2217
2420
|
function writeMemoryFile(path, data) {
|
|
2218
|
-
|
|
2421
|
+
writeFileSync6(path, data, { mode: 384 });
|
|
2219
2422
|
try {
|
|
2220
|
-
|
|
2423
|
+
chmodSync3(path, 384);
|
|
2221
2424
|
} catch {
|
|
2222
2425
|
}
|
|
2223
2426
|
}
|
|
2224
2427
|
function getMemoryPath() {
|
|
2225
|
-
return
|
|
2428
|
+
return join10(getMemoryDir(), MEMORY_FILE);
|
|
2226
2429
|
}
|
|
2227
|
-
var
|
|
2228
|
-
var
|
|
2229
|
-
var
|
|
2430
|
+
var PBKDF2_ITERATIONS2 = 21e4;
|
|
2431
|
+
var KEY_LENGTH2 = 32;
|
|
2432
|
+
var PASSPHRASE_ENV2 = "QRING_MEMORY_PASSPHRASE";
|
|
2230
2433
|
var V2_PREFIX = "qmem2";
|
|
2231
2434
|
var MemoryKeyUnavailableError = class extends Error {
|
|
2232
2435
|
constructor(message) {
|
|
@@ -2238,19 +2441,19 @@ function deriveLegacyKey() {
|
|
|
2238
2441
|
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
2239
2442
|
return createHash3("sha256").update(fingerprint).digest();
|
|
2240
2443
|
}
|
|
2241
|
-
function
|
|
2242
|
-
const p = process.env[
|
|
2444
|
+
function passphrase2() {
|
|
2445
|
+
const p = process.env[PASSPHRASE_ENV2];
|
|
2243
2446
|
return p && p.length > 0 ? p : void 0;
|
|
2244
2447
|
}
|
|
2245
2448
|
function derivePassphraseKey(salt) {
|
|
2246
|
-
return
|
|
2449
|
+
return pbkdf2Sync2(passphrase2(), salt, PBKDF2_ITERATIONS2, KEY_LENGTH2, "sha512");
|
|
2247
2450
|
}
|
|
2248
2451
|
function keyringKey() {
|
|
2249
2452
|
try {
|
|
2250
|
-
const entry = new
|
|
2453
|
+
const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
2251
2454
|
const stored = entry.getPassword();
|
|
2252
2455
|
if (stored) return Buffer.from(stored, "base64");
|
|
2253
|
-
const key =
|
|
2456
|
+
const key = randomBytes5(KEY_LENGTH2);
|
|
2254
2457
|
entry.setPassword(key.toString("base64"));
|
|
2255
2458
|
return key;
|
|
2256
2459
|
} catch {
|
|
@@ -2258,8 +2461,8 @@ function keyringKey() {
|
|
|
2258
2461
|
}
|
|
2259
2462
|
}
|
|
2260
2463
|
function encryptWith(data, key) {
|
|
2261
|
-
const iv =
|
|
2262
|
-
const cipher =
|
|
2464
|
+
const iv = randomBytes5(12);
|
|
2465
|
+
const cipher = createCipheriv2("aes-256-gcm", key, iv);
|
|
2263
2466
|
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
2264
2467
|
const tag = cipher.getAuthTag();
|
|
2265
2468
|
return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
|
|
@@ -2270,27 +2473,27 @@ function decryptWith(blob, key) {
|
|
|
2270
2473
|
const iv = Buffer.from(parts[0], "base64");
|
|
2271
2474
|
const tag = Buffer.from(parts[1], "base64");
|
|
2272
2475
|
const encrypted = Buffer.from(parts[2], "base64");
|
|
2273
|
-
const decipher =
|
|
2476
|
+
const decipher = createDecipheriv2("aes-256-gcm", key, iv);
|
|
2274
2477
|
decipher.setAuthTag(tag);
|
|
2275
2478
|
return decipher.update(encrypted) + decipher.final("utf8");
|
|
2276
2479
|
}
|
|
2277
2480
|
function encrypt(data) {
|
|
2278
2481
|
const kk = keyringKey();
|
|
2279
2482
|
if (kk) return encryptWith(data, kk);
|
|
2280
|
-
if (
|
|
2281
|
-
const salt =
|
|
2483
|
+
if (passphrase2()) {
|
|
2484
|
+
const salt = randomBytes5(16);
|
|
2282
2485
|
const key = derivePassphraseKey(salt);
|
|
2283
2486
|
return `${V2_PREFIX}:${salt.toString("base64")}:${encryptWith(data, key)}`;
|
|
2284
2487
|
}
|
|
2285
2488
|
throw new MemoryKeyUnavailableError(
|
|
2286
|
-
`Cannot persist agent memory: the OS keyring is unavailable and ${
|
|
2489
|
+
`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
2490
|
);
|
|
2288
2491
|
}
|
|
2289
2492
|
function decrypt(blob) {
|
|
2290
2493
|
if (blob.startsWith(`${V2_PREFIX}:`)) {
|
|
2291
|
-
if (!
|
|
2494
|
+
if (!passphrase2()) {
|
|
2292
2495
|
throw new MemoryKeyUnavailableError(
|
|
2293
|
-
`Agent memory was encrypted with ${
|
|
2496
|
+
`Agent memory was encrypted with ${PASSPHRASE_ENV2} but it is not set \u2014 cannot decrypt.`
|
|
2294
2497
|
);
|
|
2295
2498
|
}
|
|
2296
2499
|
const rest = blob.slice(V2_PREFIX.length + 1);
|
|
@@ -2314,13 +2517,13 @@ function decrypt(blob) {
|
|
|
2314
2517
|
}
|
|
2315
2518
|
return plain;
|
|
2316
2519
|
}
|
|
2317
|
-
function
|
|
2520
|
+
function loadStore2() {
|
|
2318
2521
|
const path = getMemoryPath();
|
|
2319
|
-
if (!
|
|
2522
|
+
if (!existsSync7(path)) {
|
|
2320
2523
|
return { entries: {} };
|
|
2321
2524
|
}
|
|
2322
2525
|
try {
|
|
2323
|
-
const raw =
|
|
2526
|
+
const raw = readFileSync8(path, "utf8");
|
|
2324
2527
|
const decrypted = decrypt(raw);
|
|
2325
2528
|
return JSON.parse(decrypted);
|
|
2326
2529
|
} catch (err) {
|
|
@@ -2330,35 +2533,35 @@ function loadStore() {
|
|
|
2330
2533
|
return { entries: {} };
|
|
2331
2534
|
}
|
|
2332
2535
|
}
|
|
2333
|
-
function
|
|
2536
|
+
function saveStore2(store) {
|
|
2334
2537
|
const json = JSON.stringify(store);
|
|
2335
2538
|
const encrypted = encrypt(json);
|
|
2336
2539
|
writeMemoryFile(getMemoryPath(), encrypted);
|
|
2337
2540
|
}
|
|
2338
2541
|
function remember(key, value) {
|
|
2339
|
-
const store =
|
|
2542
|
+
const store = loadStore2();
|
|
2340
2543
|
store.entries[key] = {
|
|
2341
2544
|
value,
|
|
2342
2545
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2343
2546
|
};
|
|
2344
|
-
|
|
2547
|
+
saveStore2(store);
|
|
2345
2548
|
}
|
|
2346
2549
|
function recall(key) {
|
|
2347
|
-
const store =
|
|
2550
|
+
const store = loadStore2();
|
|
2348
2551
|
return store.entries[key]?.value ?? null;
|
|
2349
2552
|
}
|
|
2350
2553
|
function listMemory() {
|
|
2351
|
-
const store =
|
|
2554
|
+
const store = loadStore2();
|
|
2352
2555
|
return Object.entries(store.entries).map(([key, entry]) => ({
|
|
2353
2556
|
key,
|
|
2354
2557
|
updatedAt: entry.updatedAt
|
|
2355
2558
|
}));
|
|
2356
2559
|
}
|
|
2357
2560
|
function forget(key) {
|
|
2358
|
-
const store =
|
|
2561
|
+
const store = loadStore2();
|
|
2359
2562
|
if (key in store.entries) {
|
|
2360
2563
|
delete store.entries[key];
|
|
2361
|
-
|
|
2564
|
+
saveStore2(store);
|
|
2362
2565
|
return true;
|
|
2363
2566
|
}
|
|
2364
2567
|
return false;
|
|
@@ -2407,4 +2610,4 @@ export {
|
|
|
2407
2610
|
listMemory,
|
|
2408
2611
|
forget
|
|
2409
2612
|
};
|
|
2410
|
-
//# sourceMappingURL=chunk-
|
|
2613
|
+
//# sourceMappingURL=chunk-TPPPTL4J.js.map
|