@i4ctime/q-ring 0.14.1 → 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 +78 -1
- 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
|
}
|
|
@@ -1205,19 +1367,19 @@ async function fireHooks(payload, tags) {
|
|
|
1205
1367
|
}
|
|
1206
1368
|
|
|
1207
1369
|
// src/core/approval.ts
|
|
1208
|
-
import { existsSync as
|
|
1209
|
-
import { join as
|
|
1210
|
-
import { homedir as
|
|
1211
|
-
import { createHmac as createHmac2, randomBytes as
|
|
1370
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1371
|
+
import { join as join8 } from "path";
|
|
1372
|
+
import { homedir as homedir6 } from "os";
|
|
1373
|
+
import { createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
1212
1374
|
function getHmacSecret() {
|
|
1213
|
-
const dir =
|
|
1214
|
-
const secretPath =
|
|
1215
|
-
if (!
|
|
1216
|
-
if (
|
|
1217
|
-
return
|
|
1218
|
-
}
|
|
1219
|
-
const secret =
|
|
1220
|
-
|
|
1375
|
+
const dir = join8(homedir6(), ".config", "q-ring");
|
|
1376
|
+
const secretPath = join8(dir, ".approval-key");
|
|
1377
|
+
if (!existsSync6(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
1378
|
+
if (existsSync6(secretPath)) {
|
|
1379
|
+
return readFileSync7(secretPath, "utf8").trim();
|
|
1380
|
+
}
|
|
1381
|
+
const secret = randomBytes3(32).toString("hex");
|
|
1382
|
+
writeFileSync5(secretPath, secret, { mode: 384 });
|
|
1221
1383
|
return secret;
|
|
1222
1384
|
}
|
|
1223
1385
|
function computeHmac(entry) {
|
|
@@ -1247,17 +1409,17 @@ function verifyHmac(entry) {
|
|
|
1247
1409
|
}
|
|
1248
1410
|
}
|
|
1249
1411
|
function getRegistryPath3() {
|
|
1250
|
-
const dir =
|
|
1251
|
-
if (!
|
|
1252
|
-
|
|
1412
|
+
const dir = join8(homedir6(), ".config", "q-ring");
|
|
1413
|
+
if (!existsSync6(dir)) {
|
|
1414
|
+
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
1253
1415
|
}
|
|
1254
|
-
return
|
|
1416
|
+
return join8(dir, "approvals.json");
|
|
1255
1417
|
}
|
|
1256
1418
|
function loadRegistry3() {
|
|
1257
1419
|
return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
|
|
1258
1420
|
}
|
|
1259
1421
|
function saveRegistry3(registry2) {
|
|
1260
|
-
|
|
1422
|
+
writeFileSync5(getRegistryPath3(), JSON.stringify(registry2, null, 2), { mode: 384 });
|
|
1261
1423
|
}
|
|
1262
1424
|
function cleanup(registry2) {
|
|
1263
1425
|
const now = Date.now();
|
|
@@ -1268,7 +1430,7 @@ function cleanup(registry2) {
|
|
|
1268
1430
|
function grantApproval(key, scope, service, ttlSeconds = 3600, grantOpts = {}) {
|
|
1269
1431
|
const registry2 = loadRegistry3();
|
|
1270
1432
|
cleanup(registry2);
|
|
1271
|
-
const id =
|
|
1433
|
+
const id = randomBytes3(8).toString("hex");
|
|
1272
1434
|
const grantedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1273
1435
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3).toISOString();
|
|
1274
1436
|
const partial = {
|
|
@@ -1329,7 +1491,7 @@ function listApprovals() {
|
|
|
1329
1491
|
|
|
1330
1492
|
// src/core/policy.ts
|
|
1331
1493
|
import { statSync as statSync4 } from "fs";
|
|
1332
|
-
import { join as
|
|
1494
|
+
import { join as join9 } from "path";
|
|
1333
1495
|
import { z as z2 } from "zod";
|
|
1334
1496
|
var stringArray = z2.array(z2.string());
|
|
1335
1497
|
var mcpPolicySchema = z2.object({
|
|
@@ -1371,7 +1533,7 @@ function resolvePolicyPath(projectPath) {
|
|
|
1371
1533
|
}
|
|
1372
1534
|
function configMtime(pp) {
|
|
1373
1535
|
try {
|
|
1374
|
-
return statSync4(
|
|
1536
|
+
return statSync4(join9(pp, ".q-ring.json")).mtimeMs;
|
|
1375
1537
|
} catch {
|
|
1376
1538
|
return 0;
|
|
1377
1539
|
}
|
|
@@ -1394,7 +1556,7 @@ function loadPolicy(projectPath) {
|
|
|
1394
1556
|
if (!parsed.success) {
|
|
1395
1557
|
const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
|
|
1396
1558
|
const error = new PolicyConfigError(
|
|
1397
|
-
`Invalid policy in ${
|
|
1559
|
+
`Invalid policy in ${join9(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
|
|
1398
1560
|
);
|
|
1399
1561
|
console.error(`q-ring: ${error.message}`);
|
|
1400
1562
|
cachedPolicy = { path: pp, mtimeMs, error };
|
|
@@ -1510,9 +1672,6 @@ function getPolicySummary(projectPath) {
|
|
|
1510
1672
|
};
|
|
1511
1673
|
}
|
|
1512
1674
|
|
|
1513
|
-
// src/core/keyring.ts
|
|
1514
|
-
import { Entry as Entry2, findCredentials } from "@napi-rs/keyring";
|
|
1515
|
-
|
|
1516
1675
|
// src/utils/hash.ts
|
|
1517
1676
|
import { createHash as createHash2 } from "crypto";
|
|
1518
1677
|
function hashProjectPath(projectPath) {
|
|
@@ -1568,6 +1727,50 @@ function serviceForScope(scope, opts = {}) {
|
|
|
1568
1727
|
return resolveScope({ ...opts, scope })[0].service;
|
|
1569
1728
|
}
|
|
1570
1729
|
|
|
1730
|
+
// src/core/notify.ts
|
|
1731
|
+
import { spawn as spawn2 } from "child_process";
|
|
1732
|
+
var DISABLE_ENV = "QRING_NOTIFY";
|
|
1733
|
+
var THROTTLE_MS = 5 * 60 * 1e3;
|
|
1734
|
+
var lastNotified = /* @__PURE__ */ new Map();
|
|
1735
|
+
function notificationsEnabled() {
|
|
1736
|
+
const value = process.env[DISABLE_ENV];
|
|
1737
|
+
return value !== "off" && value !== "0" && value !== "false";
|
|
1738
|
+
}
|
|
1739
|
+
function notifyUser(title, body) {
|
|
1740
|
+
try {
|
|
1741
|
+
let command;
|
|
1742
|
+
let args;
|
|
1743
|
+
if (process.platform === "linux") {
|
|
1744
|
+
command = "notify-send";
|
|
1745
|
+
args = ["--app-name=q-ring", "--urgency=critical", title, body];
|
|
1746
|
+
} else if (process.platform === "darwin") {
|
|
1747
|
+
command = "osascript";
|
|
1748
|
+
const clean = (s) => s.replace(/["\\]/g, "");
|
|
1749
|
+
args = ["-e", `display notification "${clean(body)}" with title "${clean(title)}"`];
|
|
1750
|
+
} else {
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
const child = spawn2(command, args, { detached: true, stdio: "ignore" });
|
|
1754
|
+
child.on("error", () => {
|
|
1755
|
+
});
|
|
1756
|
+
child.unref();
|
|
1757
|
+
return true;
|
|
1758
|
+
} catch {
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
function notifyApprovalRequested(key, source) {
|
|
1763
|
+
if (!notificationsEnabled()) return;
|
|
1764
|
+
const now = Date.now();
|
|
1765
|
+
const last = lastNotified.get(key);
|
|
1766
|
+
if (last !== void 0 && now - last < THROTTLE_MS) return;
|
|
1767
|
+
lastNotified.set(key, now);
|
|
1768
|
+
notifyUser(
|
|
1769
|
+
"q-ring: approval requested",
|
|
1770
|
+
`An ${source} agent wants to read "${key}". Allow with: qring approve ${key}`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1571
1774
|
// src/core/provision.ts
|
|
1572
1775
|
import { execFileSync, spawnSync } from "child_process";
|
|
1573
1776
|
import { z as z3 } from "zod";
|
|
@@ -1720,14 +1923,14 @@ function withJitEnvelopeLock(service, key, fn) {
|
|
|
1720
1923
|
return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
|
|
1721
1924
|
}
|
|
1722
1925
|
function readEnvelope(service, key) {
|
|
1723
|
-
const entry = new
|
|
1926
|
+
const entry = new Entry(service, key);
|
|
1724
1927
|
const raw = entry.getPassword();
|
|
1725
1928
|
if (raw === null) return null;
|
|
1726
1929
|
const envelope = parseEnvelope(raw);
|
|
1727
1930
|
return envelope ?? wrapLegacy(raw);
|
|
1728
1931
|
}
|
|
1729
1932
|
function writeEnvelope(service, key, envelope) {
|
|
1730
|
-
const entry = new
|
|
1933
|
+
const entry = new Entry(service, key);
|
|
1731
1934
|
entry.setPassword(serializeEnvelope(envelope));
|
|
1732
1935
|
}
|
|
1733
1936
|
function resolveEnv(opts) {
|
|
@@ -1804,6 +2007,7 @@ function getSecret(key, opts = {}) {
|
|
|
1804
2007
|
source,
|
|
1805
2008
|
detail: "blocked: requires user approval"
|
|
1806
2009
|
});
|
|
2010
|
+
notifyApprovalRequested(key, source);
|
|
1807
2011
|
}
|
|
1808
2012
|
throw new Error(`Access Denied: This secret requires user approval. Please ask the user to run 'qring approve ${key}'`);
|
|
1809
2013
|
}
|
|
@@ -1985,7 +2189,7 @@ function deleteSecret(key, opts = {}) {
|
|
|
1985
2189
|
}
|
|
1986
2190
|
let deleted = false;
|
|
1987
2191
|
for (const { service, scope } of scopes) {
|
|
1988
|
-
const entry = new
|
|
2192
|
+
const entry = new Entry(service, key);
|
|
1989
2193
|
try {
|
|
1990
2194
|
if (entry.deleteCredential()) {
|
|
1991
2195
|
deleted = true;
|
|
@@ -2165,7 +2369,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
2165
2369
|
}
|
|
2166
2370
|
|
|
2167
2371
|
// src/core/tunnel.ts
|
|
2168
|
-
import { randomBytes as
|
|
2372
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2169
2373
|
var tunnelStore = /* @__PURE__ */ new Map();
|
|
2170
2374
|
var cleanupInterval = null;
|
|
2171
2375
|
function ensureCleanup() {
|
|
@@ -2187,7 +2391,7 @@ function ensureCleanup() {
|
|
|
2187
2391
|
}
|
|
2188
2392
|
}
|
|
2189
2393
|
function tunnelCreate(value, opts = {}) {
|
|
2190
|
-
const id = `tun_${Date.now().toString(36)}_${
|
|
2394
|
+
const id = `tun_${Date.now().toString(36)}_${randomBytes4(6).toString("base64url")}`;
|
|
2191
2395
|
const now = Date.now();
|
|
2192
2396
|
tunnelStore.set(id, {
|
|
2193
2397
|
value,
|
|
@@ -2237,40 +2441,39 @@ function tunnelList() {
|
|
|
2237
2441
|
}
|
|
2238
2442
|
|
|
2239
2443
|
// src/core/memory.ts
|
|
2240
|
-
import { existsSync as
|
|
2241
|
-
import { join as
|
|
2242
|
-
import { homedir as
|
|
2444
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, chmodSync as chmodSync3 } from "fs";
|
|
2445
|
+
import { join as join10 } from "path";
|
|
2446
|
+
import { homedir as homedir7, hostname, userInfo } from "os";
|
|
2243
2447
|
import {
|
|
2244
|
-
createCipheriv,
|
|
2245
|
-
createDecipheriv,
|
|
2448
|
+
createCipheriv as createCipheriv2,
|
|
2449
|
+
createDecipheriv as createDecipheriv2,
|
|
2246
2450
|
createHash as createHash3,
|
|
2247
|
-
randomBytes as
|
|
2248
|
-
pbkdf2Sync
|
|
2451
|
+
randomBytes as randomBytes5,
|
|
2452
|
+
pbkdf2Sync as pbkdf2Sync2
|
|
2249
2453
|
} from "crypto";
|
|
2250
|
-
import { Entry as Entry3 } from "@napi-rs/keyring";
|
|
2251
2454
|
var MEMORY_FILE = "agent-memory.enc";
|
|
2252
2455
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
2253
2456
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
2254
2457
|
function getMemoryDir() {
|
|
2255
|
-
const dir =
|
|
2256
|
-
if (!
|
|
2257
|
-
|
|
2458
|
+
const dir = join10(homedir7(), ".config", "q-ring");
|
|
2459
|
+
if (!existsSync7(dir)) {
|
|
2460
|
+
mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
2258
2461
|
}
|
|
2259
2462
|
return dir;
|
|
2260
2463
|
}
|
|
2261
2464
|
function writeMemoryFile(path, data) {
|
|
2262
|
-
|
|
2465
|
+
writeFileSync6(path, data, { mode: 384 });
|
|
2263
2466
|
try {
|
|
2264
|
-
|
|
2467
|
+
chmodSync3(path, 384);
|
|
2265
2468
|
} catch {
|
|
2266
2469
|
}
|
|
2267
2470
|
}
|
|
2268
2471
|
function getMemoryPath() {
|
|
2269
|
-
return
|
|
2472
|
+
return join10(getMemoryDir(), MEMORY_FILE);
|
|
2270
2473
|
}
|
|
2271
|
-
var
|
|
2272
|
-
var
|
|
2273
|
-
var
|
|
2474
|
+
var PBKDF2_ITERATIONS2 = 21e4;
|
|
2475
|
+
var KEY_LENGTH2 = 32;
|
|
2476
|
+
var PASSPHRASE_ENV2 = "QRING_MEMORY_PASSPHRASE";
|
|
2274
2477
|
var V2_PREFIX = "qmem2";
|
|
2275
2478
|
var MemoryKeyUnavailableError = class extends Error {
|
|
2276
2479
|
constructor(message) {
|
|
@@ -2282,19 +2485,19 @@ function deriveLegacyKey() {
|
|
|
2282
2485
|
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
2283
2486
|
return createHash3("sha256").update(fingerprint).digest();
|
|
2284
2487
|
}
|
|
2285
|
-
function
|
|
2286
|
-
const p = process.env[
|
|
2488
|
+
function passphrase2() {
|
|
2489
|
+
const p = process.env[PASSPHRASE_ENV2];
|
|
2287
2490
|
return p && p.length > 0 ? p : void 0;
|
|
2288
2491
|
}
|
|
2289
2492
|
function derivePassphraseKey(salt) {
|
|
2290
|
-
return
|
|
2493
|
+
return pbkdf2Sync2(passphrase2(), salt, PBKDF2_ITERATIONS2, KEY_LENGTH2, "sha512");
|
|
2291
2494
|
}
|
|
2292
2495
|
function keyringKey() {
|
|
2293
2496
|
try {
|
|
2294
|
-
const entry = new
|
|
2497
|
+
const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
2295
2498
|
const stored = entry.getPassword();
|
|
2296
2499
|
if (stored) return Buffer.from(stored, "base64");
|
|
2297
|
-
const key =
|
|
2500
|
+
const key = randomBytes5(KEY_LENGTH2);
|
|
2298
2501
|
entry.setPassword(key.toString("base64"));
|
|
2299
2502
|
return key;
|
|
2300
2503
|
} catch {
|
|
@@ -2302,8 +2505,8 @@ function keyringKey() {
|
|
|
2302
2505
|
}
|
|
2303
2506
|
}
|
|
2304
2507
|
function encryptWith(data, key) {
|
|
2305
|
-
const iv =
|
|
2306
|
-
const cipher =
|
|
2508
|
+
const iv = randomBytes5(12);
|
|
2509
|
+
const cipher = createCipheriv2("aes-256-gcm", key, iv);
|
|
2307
2510
|
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
2308
2511
|
const tag = cipher.getAuthTag();
|
|
2309
2512
|
return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
|
|
@@ -2314,27 +2517,27 @@ function decryptWith(blob, key) {
|
|
|
2314
2517
|
const iv = Buffer.from(parts[0], "base64");
|
|
2315
2518
|
const tag = Buffer.from(parts[1], "base64");
|
|
2316
2519
|
const encrypted = Buffer.from(parts[2], "base64");
|
|
2317
|
-
const decipher =
|
|
2520
|
+
const decipher = createDecipheriv2("aes-256-gcm", key, iv);
|
|
2318
2521
|
decipher.setAuthTag(tag);
|
|
2319
2522
|
return decipher.update(encrypted) + decipher.final("utf8");
|
|
2320
2523
|
}
|
|
2321
2524
|
function encrypt(data) {
|
|
2322
2525
|
const kk = keyringKey();
|
|
2323
2526
|
if (kk) return encryptWith(data, kk);
|
|
2324
|
-
if (
|
|
2325
|
-
const salt =
|
|
2527
|
+
if (passphrase2()) {
|
|
2528
|
+
const salt = randomBytes5(16);
|
|
2326
2529
|
const key = derivePassphraseKey(salt);
|
|
2327
2530
|
return `${V2_PREFIX}:${salt.toString("base64")}:${encryptWith(data, key)}`;
|
|
2328
2531
|
}
|
|
2329
2532
|
throw new MemoryKeyUnavailableError(
|
|
2330
|
-
`Cannot persist agent memory: the OS keyring is unavailable and ${
|
|
2533
|
+
`Cannot persist agent memory: the OS keyring is unavailable and ${PASSPHRASE_ENV2} is not set. Refusing to encrypt with a machine-derivable key (any local process could recompute it and read your memory). Set ${PASSPHRASE_ENV2} to a strong passphrase, or run on a host with an OS keyring.`
|
|
2331
2534
|
);
|
|
2332
2535
|
}
|
|
2333
2536
|
function decrypt(blob) {
|
|
2334
2537
|
if (blob.startsWith(`${V2_PREFIX}:`)) {
|
|
2335
|
-
if (!
|
|
2538
|
+
if (!passphrase2()) {
|
|
2336
2539
|
throw new MemoryKeyUnavailableError(
|
|
2337
|
-
`Agent memory was encrypted with ${
|
|
2540
|
+
`Agent memory was encrypted with ${PASSPHRASE_ENV2} but it is not set \u2014 cannot decrypt.`
|
|
2338
2541
|
);
|
|
2339
2542
|
}
|
|
2340
2543
|
const rest = blob.slice(V2_PREFIX.length + 1);
|
|
@@ -2358,13 +2561,13 @@ function decrypt(blob) {
|
|
|
2358
2561
|
}
|
|
2359
2562
|
return plain;
|
|
2360
2563
|
}
|
|
2361
|
-
function
|
|
2564
|
+
function loadStore2() {
|
|
2362
2565
|
const path = getMemoryPath();
|
|
2363
|
-
if (!
|
|
2566
|
+
if (!existsSync7(path)) {
|
|
2364
2567
|
return { entries: {} };
|
|
2365
2568
|
}
|
|
2366
2569
|
try {
|
|
2367
|
-
const raw =
|
|
2570
|
+
const raw = readFileSync8(path, "utf8");
|
|
2368
2571
|
const decrypted = decrypt(raw);
|
|
2369
2572
|
return JSON.parse(decrypted);
|
|
2370
2573
|
} catch (err) {
|
|
@@ -2374,41 +2577,41 @@ function loadStore() {
|
|
|
2374
2577
|
return { entries: {} };
|
|
2375
2578
|
}
|
|
2376
2579
|
}
|
|
2377
|
-
function
|
|
2580
|
+
function saveStore2(store) {
|
|
2378
2581
|
const json = JSON.stringify(store);
|
|
2379
2582
|
const encrypted = encrypt(json);
|
|
2380
2583
|
writeMemoryFile(getMemoryPath(), encrypted);
|
|
2381
2584
|
}
|
|
2382
2585
|
function remember(key, value) {
|
|
2383
|
-
const store =
|
|
2586
|
+
const store = loadStore2();
|
|
2384
2587
|
store.entries[key] = {
|
|
2385
2588
|
value,
|
|
2386
2589
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2387
2590
|
};
|
|
2388
|
-
|
|
2591
|
+
saveStore2(store);
|
|
2389
2592
|
}
|
|
2390
2593
|
function recall(key) {
|
|
2391
|
-
const store =
|
|
2594
|
+
const store = loadStore2();
|
|
2392
2595
|
return store.entries[key]?.value ?? null;
|
|
2393
2596
|
}
|
|
2394
2597
|
function listMemory() {
|
|
2395
|
-
const store =
|
|
2598
|
+
const store = loadStore2();
|
|
2396
2599
|
return Object.entries(store.entries).map(([key, entry]) => ({
|
|
2397
2600
|
key,
|
|
2398
2601
|
updatedAt: entry.updatedAt
|
|
2399
2602
|
}));
|
|
2400
2603
|
}
|
|
2401
2604
|
function forget(key) {
|
|
2402
|
-
const store =
|
|
2605
|
+
const store = loadStore2();
|
|
2403
2606
|
if (key in store.entries) {
|
|
2404
2607
|
delete store.entries[key];
|
|
2405
|
-
|
|
2608
|
+
saveStore2(store);
|
|
2406
2609
|
return true;
|
|
2407
2610
|
}
|
|
2408
2611
|
return false;
|
|
2409
2612
|
}
|
|
2410
2613
|
function clearMemory() {
|
|
2411
|
-
|
|
2614
|
+
saveStore2({ entries: {} });
|
|
2412
2615
|
}
|
|
2413
2616
|
|
|
2414
2617
|
export {
|
|
@@ -2458,4 +2661,4 @@ export {
|
|
|
2458
2661
|
forget,
|
|
2459
2662
|
clearMemory
|
|
2460
2663
|
};
|
|
2461
|
-
//# sourceMappingURL=chunk-
|
|
2664
|
+
//# sourceMappingURL=chunk-MLBJCPX2.js.map
|