@i4ctime/q-ring 0.13.0 → 0.14.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 +9 -8
- package/assets/icon-192.png +0 -0
- package/assets/icon-512.png +0 -0
- package/assets/mark-mono.svg +27 -0
- package/assets/mark-small.svg +15 -0
- package/assets/mark.svg +44 -0
- package/assets/repo-social-preview.png +0 -0
- package/assets/social-card-optimized.jpg +0 -0
- package/dist/{chunk-SRESNRML.js → chunk-C2TFJ2EH.js} +466 -200
- package/dist/chunk-C2TFJ2EH.js.map +1 -0
- package/dist/{chunk-CWV3WTPF.js → chunk-NNIEXAW5.js} +454 -194
- package/dist/chunk-NNIEXAW5.js.map +1 -0
- package/dist/{dashboard-II2ILNXA.js → dashboard-KAUEPLXO.js} +2 -2
- package/dist/{dashboard-RLMROWIU.js → dashboard-PYYAED45.js} +2 -2
- package/dist/index.js +60 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +49 -7
- package/dist/mcp.js.map +1 -1
- package/package.json +21 -8
- package/dist/chunk-CWV3WTPF.js.map +0 -1
- package/dist/chunk-SRESNRML.js.map +0 -1
- /package/dist/{dashboard-II2ILNXA.js.map → dashboard-KAUEPLXO.js.map} +0 -0
- /package/dist/{dashboard-RLMROWIU.js.map → dashboard-PYYAED45.js.map} +0 -0
|
@@ -177,7 +177,7 @@ function recordAccess(envelope) {
|
|
|
177
177
|
|
|
178
178
|
// src/core/collapse.ts
|
|
179
179
|
import { execSync } from "child_process";
|
|
180
|
-
import {
|
|
180
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
181
181
|
import { join as join2 } from "path";
|
|
182
182
|
var BRANCH_ENV_MAP = {
|
|
183
183
|
main: "prod",
|
|
@@ -191,28 +191,49 @@ var BRANCH_ENV_MAP = {
|
|
|
191
191
|
test: "test",
|
|
192
192
|
testing: "test"
|
|
193
193
|
};
|
|
194
|
+
var BRANCH_CACHE_TTL_MS = 2e3;
|
|
195
|
+
var branchCache = null;
|
|
194
196
|
function detectGitBranch(cwd) {
|
|
197
|
+
const dir = cwd ?? process.cwd();
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
if (branchCache && branchCache.cwd === dir && now - branchCache.at < BRANCH_CACHE_TTL_MS) {
|
|
200
|
+
return branchCache.branch;
|
|
201
|
+
}
|
|
202
|
+
let branch = null;
|
|
195
203
|
try {
|
|
196
|
-
const
|
|
197
|
-
cwd:
|
|
204
|
+
const out = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
205
|
+
cwd: dir,
|
|
198
206
|
stdio: ["pipe", "pipe", "pipe"],
|
|
199
207
|
encoding: "utf8",
|
|
200
208
|
timeout: 3e3
|
|
201
209
|
}).trim();
|
|
202
|
-
|
|
210
|
+
branch = out || null;
|
|
203
211
|
} catch {
|
|
204
|
-
return null;
|
|
205
212
|
}
|
|
213
|
+
branchCache = { cwd: dir, at: now, branch };
|
|
214
|
+
return branch;
|
|
206
215
|
}
|
|
216
|
+
var configCache = null;
|
|
207
217
|
function readProjectConfig(projectPath) {
|
|
208
218
|
const configPath = join2(projectPath ?? process.cwd(), ".q-ring.json");
|
|
219
|
+
let mtimeMs = 0;
|
|
209
220
|
try {
|
|
210
|
-
|
|
211
|
-
return JSON.parse(readFileSync2(configPath, "utf8"));
|
|
212
|
-
}
|
|
221
|
+
mtimeMs = statSync(configPath).mtimeMs;
|
|
213
222
|
} catch {
|
|
214
223
|
}
|
|
215
|
-
|
|
224
|
+
if (configCache && configCache.path === configPath && configCache.mtimeMs === mtimeMs) {
|
|
225
|
+
return configCache.config;
|
|
226
|
+
}
|
|
227
|
+
let config = null;
|
|
228
|
+
if (mtimeMs > 0) {
|
|
229
|
+
try {
|
|
230
|
+
config = JSON.parse(readFileSync2(configPath, "utf8"));
|
|
231
|
+
} catch {
|
|
232
|
+
config = null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
configCache = { path: configPath, mtimeMs, config };
|
|
236
|
+
return config;
|
|
216
237
|
}
|
|
217
238
|
function collapseEnvironment(ctx = {}) {
|
|
218
239
|
if (ctx.explicit) {
|
|
@@ -266,38 +287,137 @@ function mapEnvName(raw) {
|
|
|
266
287
|
|
|
267
288
|
// src/core/observer.ts
|
|
268
289
|
import {
|
|
269
|
-
existsSync
|
|
270
|
-
mkdirSync,
|
|
290
|
+
existsSync,
|
|
291
|
+
mkdirSync as mkdirSync2,
|
|
271
292
|
appendFileSync,
|
|
272
|
-
|
|
293
|
+
chmodSync,
|
|
294
|
+
readFileSync as readFileSync4,
|
|
273
295
|
openSync,
|
|
274
296
|
fstatSync,
|
|
275
297
|
readSync,
|
|
276
298
|
closeSync,
|
|
277
|
-
statSync
|
|
299
|
+
statSync as statSync3
|
|
300
|
+
} from "fs";
|
|
301
|
+
import { join as join4 } from "path";
|
|
302
|
+
import { homedir as homedir2 } from "os";
|
|
303
|
+
import { createHash, createHmac, randomBytes } from "crypto";
|
|
304
|
+
import { Entry } from "@napi-rs/keyring";
|
|
305
|
+
|
|
306
|
+
// src/utils/file-lock.ts
|
|
307
|
+
import {
|
|
308
|
+
mkdirSync,
|
|
309
|
+
writeFileSync,
|
|
310
|
+
unlinkSync,
|
|
311
|
+
readFileSync as readFileSync3,
|
|
312
|
+
statSync as statSync2
|
|
278
313
|
} from "fs";
|
|
279
314
|
import { join as join3 } from "path";
|
|
280
315
|
import { homedir } from "os";
|
|
281
|
-
|
|
316
|
+
function sleepSync(ms) {
|
|
317
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
318
|
+
}
|
|
319
|
+
function isProcessAlive(pid) {
|
|
320
|
+
try {
|
|
321
|
+
process.kill(pid, 0);
|
|
322
|
+
return true;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return err.code === "EPERM";
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function withFileLock(name, fn, opts = {}) {
|
|
328
|
+
const lockDir = join3(homedir(), ".config", "q-ring", opts.dir ?? "locks");
|
|
329
|
+
mkdirSync(lockDir, { recursive: true, mode: 448 });
|
|
330
|
+
const safe = Buffer.from(name, "utf8").toString("base64url");
|
|
331
|
+
const lockPath = join3(lockDir, `${safe}.lock`);
|
|
332
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 8e3);
|
|
333
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
334
|
+
while (Date.now() < deadline) {
|
|
335
|
+
try {
|
|
336
|
+
writeFileSync(lockPath, `${process.pid}
|
|
337
|
+
`, { flag: "wx", mode: 384 });
|
|
338
|
+
try {
|
|
339
|
+
return fn();
|
|
340
|
+
} finally {
|
|
341
|
+
try {
|
|
342
|
+
unlinkSync(lockPath);
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
try {
|
|
348
|
+
const holderPid = parseInt(readFileSync3(lockPath, "utf8").trim(), 10);
|
|
349
|
+
const ageMs = Date.now() - statSync2(lockPath).mtimeMs;
|
|
350
|
+
const stale = Number.isInteger(holderPid) && holderPid > 0 && !isProcessAlive(holderPid) || ageMs > staleMs;
|
|
351
|
+
if (stale) {
|
|
352
|
+
unlinkSync(lockPath);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
sleepSync(15);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
throw new Error(`Could not acquire lock "${name}" (timeout)`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/core/observer.ts
|
|
364
|
+
var AUDIT_KEYRING_SERVICE = "qring-audit-chain";
|
|
365
|
+
var AUDIT_KEY_ACCOUNT = "hmac-key";
|
|
366
|
+
var AUDIT_ANCHOR_ACCOUNT = "chain-head";
|
|
367
|
+
var warnedNoKeyring = false;
|
|
368
|
+
function getAuditKey() {
|
|
369
|
+
try {
|
|
370
|
+
const entry = new Entry(AUDIT_KEYRING_SERVICE, AUDIT_KEY_ACCOUNT);
|
|
371
|
+
const stored = entry.getPassword();
|
|
372
|
+
if (stored) return Buffer.from(stored, "base64");
|
|
373
|
+
const key = randomBytes(32);
|
|
374
|
+
entry.setPassword(key.toString("base64"));
|
|
375
|
+
return key;
|
|
376
|
+
} catch {
|
|
377
|
+
if (!warnedNoKeyring) {
|
|
378
|
+
console.error(
|
|
379
|
+
"q-ring: WARNING \u2014 OS keyring unavailable; the audit chain has no keyed anchor on this host and is NOT tamper-evident against truncation or full-file rewrite. (In-file SHA-256 chaining still detects edits.)"
|
|
380
|
+
);
|
|
381
|
+
warnedNoKeyring = true;
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function headAnchor(line, key) {
|
|
387
|
+
return createHmac("sha256", key).update(line).digest("hex");
|
|
388
|
+
}
|
|
389
|
+
function readStoredAnchor() {
|
|
390
|
+
try {
|
|
391
|
+
return new Entry(AUDIT_KEYRING_SERVICE, AUDIT_ANCHOR_ACCOUNT).getPassword() ?? null;
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function writeStoredAnchor(hash) {
|
|
397
|
+
try {
|
|
398
|
+
new Entry(AUDIT_KEYRING_SERVICE, AUDIT_ANCHOR_ACCOUNT).setPassword(hash);
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
}
|
|
282
402
|
function getAuditDir() {
|
|
283
403
|
if (process.env.QRING_AUDIT_DIR) {
|
|
284
|
-
if (!
|
|
285
|
-
|
|
404
|
+
if (!existsSync(process.env.QRING_AUDIT_DIR)) {
|
|
405
|
+
mkdirSync2(process.env.QRING_AUDIT_DIR, { recursive: true, mode: 448 });
|
|
286
406
|
}
|
|
287
407
|
return process.env.QRING_AUDIT_DIR;
|
|
288
408
|
}
|
|
289
|
-
const dir =
|
|
290
|
-
if (!
|
|
291
|
-
|
|
409
|
+
const dir = join4(homedir2(), ".config", "q-ring");
|
|
410
|
+
if (!existsSync(dir)) {
|
|
411
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
292
412
|
}
|
|
293
413
|
return dir;
|
|
294
414
|
}
|
|
295
415
|
function getAuditPath() {
|
|
296
|
-
return
|
|
416
|
+
return join4(getAuditDir(), "audit.jsonl");
|
|
297
417
|
}
|
|
298
418
|
function getLastLineHash() {
|
|
299
419
|
const path = getAuditPath();
|
|
300
|
-
if (!
|
|
420
|
+
if (!existsSync(path)) return void 0;
|
|
301
421
|
try {
|
|
302
422
|
const fd = openSync(path, "r");
|
|
303
423
|
const stat = fstatSync(fd);
|
|
@@ -319,24 +439,38 @@ function getLastLineHash() {
|
|
|
319
439
|
}
|
|
320
440
|
}
|
|
321
441
|
function logAudit(event) {
|
|
322
|
-
const prevHash = getLastLineHash();
|
|
323
|
-
const full = {
|
|
324
|
-
...event,
|
|
325
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
326
|
-
pid: process.pid,
|
|
327
|
-
prevHash
|
|
328
|
-
};
|
|
329
442
|
try {
|
|
330
|
-
|
|
443
|
+
withFileLock(
|
|
444
|
+
"audit-chain",
|
|
445
|
+
() => {
|
|
446
|
+
const prevHash = getLastLineHash();
|
|
447
|
+
const full = {
|
|
448
|
+
...event,
|
|
449
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
450
|
+
pid: process.pid,
|
|
451
|
+
prevHash
|
|
452
|
+
};
|
|
453
|
+
const line = JSON.stringify(full);
|
|
454
|
+
const path = getAuditPath();
|
|
455
|
+
appendFileSync(path, line + "\n", { mode: 384 });
|
|
456
|
+
try {
|
|
457
|
+
chmodSync(path, 384);
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
const key = getAuditKey();
|
|
461
|
+
if (key) writeStoredAnchor(headAnchor(line, key));
|
|
462
|
+
},
|
|
463
|
+
{ timeoutMs: 5e3 }
|
|
464
|
+
);
|
|
331
465
|
} catch {
|
|
332
466
|
}
|
|
333
467
|
}
|
|
334
468
|
var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
|
|
335
469
|
function queryAudit(query = {}) {
|
|
336
470
|
const path = getAuditPath();
|
|
337
|
-
if (!
|
|
471
|
+
if (!existsSync(path)) return [];
|
|
338
472
|
try {
|
|
339
|
-
const st =
|
|
473
|
+
const st = statSync3(path);
|
|
340
474
|
const readStart = st.size > MAX_AUDIT_BYTES ? st.size - MAX_AUDIT_BYTES : 0;
|
|
341
475
|
const readLen = st.size > MAX_AUDIT_BYTES ? MAX_AUDIT_BYTES : st.size;
|
|
342
476
|
const buf = Buffer.alloc(readLen);
|
|
@@ -375,10 +509,10 @@ function queryAudit(query = {}) {
|
|
|
375
509
|
}
|
|
376
510
|
function verifyAuditChain() {
|
|
377
511
|
const path = getAuditPath();
|
|
378
|
-
if (!
|
|
512
|
+
if (!existsSync(path)) {
|
|
379
513
|
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
380
514
|
}
|
|
381
|
-
const lines =
|
|
515
|
+
const lines = readFileSync4(path, "utf8").split("\n").filter((l) => l.trim());
|
|
382
516
|
if (lines.length === 0) {
|
|
383
517
|
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
384
518
|
}
|
|
@@ -411,12 +545,26 @@ function verifyAuditChain() {
|
|
|
411
545
|
}
|
|
412
546
|
validEvents++;
|
|
413
547
|
}
|
|
548
|
+
const key = getAuditKey();
|
|
549
|
+
const anchor = key ? readStoredAnchor() : null;
|
|
550
|
+
if (key && anchor !== null) {
|
|
551
|
+
const head = headAnchor(lines[lines.length - 1], key);
|
|
552
|
+
if (head !== anchor) {
|
|
553
|
+
return {
|
|
554
|
+
totalEvents: lines.length,
|
|
555
|
+
validEvents,
|
|
556
|
+
brokenAt: lines.length - 1,
|
|
557
|
+
intact: false,
|
|
558
|
+
reason: "audit head does not match the keyed anchor in the OS keyring \u2014 the log was truncated or rewritten"
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
414
562
|
return { totalEvents: lines.length, validEvents, intact: true };
|
|
415
563
|
}
|
|
416
564
|
function exportAudit(opts = {}) {
|
|
417
565
|
const path = getAuditPath();
|
|
418
|
-
if (!
|
|
419
|
-
const lines =
|
|
566
|
+
if (!existsSync(path)) return opts.format === "json" ? "[]" : "";
|
|
567
|
+
const lines = readFileSync4(path, "utf8").split("\n").filter((l) => l.trim());
|
|
420
568
|
let events = lines.map((l) => {
|
|
421
569
|
try {
|
|
422
570
|
return JSON.parse(l);
|
|
@@ -481,48 +629,62 @@ function detectAnomalies(key) {
|
|
|
481
629
|
}
|
|
482
630
|
|
|
483
631
|
// src/core/entanglement.ts
|
|
484
|
-
import { existsSync as existsSync3,
|
|
485
|
-
import { join as
|
|
486
|
-
import { homedir as
|
|
632
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
633
|
+
import { join as join5 } from "path";
|
|
634
|
+
import { homedir as homedir3 } from "os";
|
|
635
|
+
|
|
636
|
+
// src/utils/registry.ts
|
|
637
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5, renameSync } from "fs";
|
|
638
|
+
function loadJsonRegistry(path, empty) {
|
|
639
|
+
if (!existsSync2(path)) return empty;
|
|
640
|
+
const raw = readFileSync5(path, "utf8");
|
|
641
|
+
try {
|
|
642
|
+
return JSON.parse(raw);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
645
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
646
|
+
const backup = `${path}.corrupt-${stamp}`;
|
|
647
|
+
try {
|
|
648
|
+
renameSync(path, backup);
|
|
649
|
+
} catch {
|
|
650
|
+
throw new Error(
|
|
651
|
+
`q-ring: registry ${path} is corrupt (${reason}) and could not be moved aside \u2014 refusing to continue so a later write cannot overwrite it. Inspect or remove the file manually.`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
console.error(
|
|
655
|
+
`q-ring: WARNING \u2014 registry ${path} was corrupt (${reason}); moved it to ${backup} and reinitialized from empty. Previous entries are preserved in the backup file.`
|
|
656
|
+
);
|
|
657
|
+
return empty;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/core/entanglement.ts
|
|
662
|
+
var REGISTRY_VERSION = 1;
|
|
487
663
|
function getRegistryPath() {
|
|
488
|
-
const dir =
|
|
664
|
+
const dir = join5(homedir3(), ".config", "q-ring");
|
|
489
665
|
if (!existsSync3(dir)) {
|
|
490
|
-
|
|
666
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
491
667
|
}
|
|
492
|
-
return
|
|
668
|
+
return join5(dir, "entanglement.json");
|
|
493
669
|
}
|
|
494
670
|
function loadRegistry() {
|
|
495
|
-
|
|
496
|
-
if (!existsSync3(path)) {
|
|
497
|
-
return { pairs: [] };
|
|
498
|
-
}
|
|
499
|
-
try {
|
|
500
|
-
return JSON.parse(readFileSync4(path, "utf8"));
|
|
501
|
-
} catch {
|
|
502
|
-
return { pairs: [] };
|
|
503
|
-
}
|
|
671
|
+
return loadJsonRegistry(getRegistryPath(), { pairs: [] });
|
|
504
672
|
}
|
|
505
673
|
function saveRegistry(registry2) {
|
|
506
|
-
|
|
674
|
+
registry2.version = REGISTRY_VERSION;
|
|
675
|
+
writeFileSync2(getRegistryPath(), JSON.stringify(registry2, null, 2), {
|
|
507
676
|
mode: 384
|
|
508
677
|
});
|
|
509
678
|
}
|
|
510
|
-
function entangle(source, target) {
|
|
679
|
+
function entangle(source, target, createdBy) {
|
|
511
680
|
const registry2 = loadRegistry();
|
|
512
681
|
const exists = registry2.pairs.some(
|
|
513
682
|
(p) => p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key
|
|
514
683
|
);
|
|
515
684
|
if (!exists) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
520
|
-
});
|
|
521
|
-
registry2.pairs.push({
|
|
522
|
-
source: target,
|
|
523
|
-
target: source,
|
|
524
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
525
|
-
});
|
|
685
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
686
|
+
registry2.pairs.push({ source, target, createdAt, createdBy });
|
|
687
|
+
registry2.pairs.push({ source: target, target: source, createdAt, createdBy });
|
|
526
688
|
saveRegistry(registry2);
|
|
527
689
|
}
|
|
528
690
|
}
|
|
@@ -544,9 +706,9 @@ function listEntanglements() {
|
|
|
544
706
|
}
|
|
545
707
|
|
|
546
708
|
// src/core/hooks.ts
|
|
547
|
-
import { existsSync as existsSync4,
|
|
548
|
-
import { join as
|
|
549
|
-
import { homedir as
|
|
709
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
710
|
+
import { join as join6 } from "path";
|
|
711
|
+
import { homedir as homedir4 } from "os";
|
|
550
712
|
import { execFile, spawn } from "child_process";
|
|
551
713
|
import { randomUUID } from "crypto";
|
|
552
714
|
|
|
@@ -559,6 +721,7 @@ import { lookup } from "dns/promises";
|
|
|
559
721
|
import * as dns from "dns";
|
|
560
722
|
import { lookup as dnsLookup } from "dns";
|
|
561
723
|
import { isIPv4, isIPv6 } from "net";
|
|
724
|
+
import ipaddr from "ipaddr.js";
|
|
562
725
|
function lookupAddressesSync(hostname2) {
|
|
563
726
|
const lookupSync2 = dns.lookupSync;
|
|
564
727
|
return lookupSync2(hostname2, { all: true });
|
|
@@ -570,21 +733,32 @@ function isHostnameIpLiteral(hostname2) {
|
|
|
570
733
|
}
|
|
571
734
|
return isIPv6(hostname2);
|
|
572
735
|
}
|
|
736
|
+
var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
737
|
+
"unspecified",
|
|
738
|
+
"broadcast",
|
|
739
|
+
"linkLocal",
|
|
740
|
+
"loopback",
|
|
741
|
+
"carrierGradeNat",
|
|
742
|
+
"private"
|
|
743
|
+
]);
|
|
573
744
|
function isPrivateIP(ip) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return
|
|
745
|
+
let addr;
|
|
746
|
+
try {
|
|
747
|
+
addr = ipaddr.parse(ip);
|
|
748
|
+
} catch {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
if (addr.kind() === "ipv6") {
|
|
752
|
+
const v6 = addr;
|
|
753
|
+
if (v6.isIPv4MappedAddress()) {
|
|
754
|
+
return isBlockedIPv4(v6.toIPv4Address());
|
|
755
|
+
}
|
|
756
|
+
return v6.range() !== "unicast";
|
|
757
|
+
}
|
|
758
|
+
return isBlockedIPv4(addr);
|
|
759
|
+
}
|
|
760
|
+
function isBlockedIPv4(addr) {
|
|
761
|
+
return BLOCKED_IPV4_RANGES.has(addr.range());
|
|
588
762
|
}
|
|
589
763
|
async function checkSSRF(url) {
|
|
590
764
|
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
@@ -758,25 +932,17 @@ function httpRequest(opts) {
|
|
|
758
932
|
|
|
759
933
|
// src/core/hooks.ts
|
|
760
934
|
function getRegistryPath2() {
|
|
761
|
-
const dir =
|
|
935
|
+
const dir = join6(homedir4(), ".config", "q-ring");
|
|
762
936
|
if (!existsSync4(dir)) {
|
|
763
|
-
|
|
937
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
764
938
|
}
|
|
765
|
-
return
|
|
939
|
+
return join6(dir, "hooks.json");
|
|
766
940
|
}
|
|
767
941
|
function loadRegistry2() {
|
|
768
|
-
|
|
769
|
-
if (!existsSync4(path)) {
|
|
770
|
-
return { hooks: [] };
|
|
771
|
-
}
|
|
772
|
-
try {
|
|
773
|
-
return JSON.parse(readFileSync5(path, "utf8"));
|
|
774
|
-
} catch {
|
|
775
|
-
return { hooks: [] };
|
|
776
|
-
}
|
|
942
|
+
return loadJsonRegistry(getRegistryPath2(), { hooks: [] });
|
|
777
943
|
}
|
|
778
944
|
function saveRegistry2(registry2) {
|
|
779
|
-
|
|
945
|
+
writeFileSync3(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
|
|
780
946
|
mode: 384
|
|
781
947
|
});
|
|
782
948
|
}
|
|
@@ -1023,19 +1189,19 @@ async function fireHooks(payload, tags) {
|
|
|
1023
1189
|
}
|
|
1024
1190
|
|
|
1025
1191
|
// src/core/approval.ts
|
|
1026
|
-
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as
|
|
1027
|
-
import { join as
|
|
1028
|
-
import { homedir as
|
|
1029
|
-
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1192
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
|
|
1193
|
+
import { join as join7 } from "path";
|
|
1194
|
+
import { homedir as homedir5 } from "os";
|
|
1195
|
+
import { createHmac as createHmac2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1030
1196
|
function getHmacSecret() {
|
|
1031
|
-
const dir =
|
|
1032
|
-
const secretPath =
|
|
1033
|
-
if (!existsSync5(dir))
|
|
1197
|
+
const dir = join7(homedir5(), ".config", "q-ring");
|
|
1198
|
+
const secretPath = join7(dir, ".approval-key");
|
|
1199
|
+
if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
1034
1200
|
if (existsSync5(secretPath)) {
|
|
1035
1201
|
return readFileSync6(secretPath, "utf8").trim();
|
|
1036
1202
|
}
|
|
1037
|
-
const secret =
|
|
1038
|
-
|
|
1203
|
+
const secret = randomBytes2(32).toString("hex");
|
|
1204
|
+
writeFileSync4(secretPath, secret, { mode: 384 });
|
|
1039
1205
|
return secret;
|
|
1040
1206
|
}
|
|
1041
1207
|
function computeHmac(entry) {
|
|
@@ -1043,6 +1209,7 @@ function computeHmac(entry) {
|
|
|
1043
1209
|
entry.id,
|
|
1044
1210
|
entry.key,
|
|
1045
1211
|
entry.scope,
|
|
1212
|
+
entry.service ?? "",
|
|
1046
1213
|
entry.reason,
|
|
1047
1214
|
entry.grantedBy,
|
|
1048
1215
|
entry.grantedAt,
|
|
@@ -1050,7 +1217,7 @@ function computeHmac(entry) {
|
|
|
1050
1217
|
entry.workspace ?? "",
|
|
1051
1218
|
entry.sessionId ?? ""
|
|
1052
1219
|
].join("|");
|
|
1053
|
-
return
|
|
1220
|
+
return createHmac2("sha256", getHmacSecret()).update(payload).digest("hex");
|
|
1054
1221
|
}
|
|
1055
1222
|
function verifyHmac(entry) {
|
|
1056
1223
|
const expected = computeHmac(entry);
|
|
@@ -1064,27 +1231,19 @@ function verifyHmac(entry) {
|
|
|
1064
1231
|
}
|
|
1065
1232
|
}
|
|
1066
1233
|
function getRegistryPath3() {
|
|
1067
|
-
const dir =
|
|
1234
|
+
const dir = join7(homedir5(), ".config", "q-ring");
|
|
1068
1235
|
if (!existsSync5(dir)) {
|
|
1069
|
-
|
|
1236
|
+
mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
1070
1237
|
}
|
|
1071
|
-
return
|
|
1238
|
+
return join7(dir, "approvals.json");
|
|
1072
1239
|
}
|
|
1073
1240
|
function loadRegistry3() {
|
|
1074
|
-
|
|
1075
|
-
if (!existsSync5(path)) {
|
|
1076
|
-
return { approvals: [] };
|
|
1077
|
-
}
|
|
1078
|
-
try {
|
|
1079
|
-
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1080
|
-
} catch {
|
|
1081
|
-
return { approvals: [] };
|
|
1082
|
-
}
|
|
1241
|
+
return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
|
|
1083
1242
|
}
|
|
1084
|
-
function hasApproval(key, scope) {
|
|
1243
|
+
function hasApproval(key, scope, service) {
|
|
1085
1244
|
const registry2 = loadRegistry3();
|
|
1086
1245
|
const entry = registry2.approvals.find(
|
|
1087
|
-
(a) => a.key === key && a.scope === scope
|
|
1246
|
+
(a) => a.key === key && a.scope === scope && a.service === service
|
|
1088
1247
|
);
|
|
1089
1248
|
if (!entry) return false;
|
|
1090
1249
|
if (new Date(entry.expiresAt).getTime() < Date.now()) return false;
|
|
@@ -1102,20 +1261,54 @@ function listApprovals() {
|
|
|
1102
1261
|
}
|
|
1103
1262
|
|
|
1104
1263
|
// src/core/policy.ts
|
|
1105
|
-
import { statSync as
|
|
1106
|
-
import { join as
|
|
1264
|
+
import { statSync as statSync4 } from "fs";
|
|
1265
|
+
import { join as join8 } from "path";
|
|
1266
|
+
import { z as z2 } from "zod";
|
|
1267
|
+
var stringArray = z2.array(z2.string());
|
|
1268
|
+
var mcpPolicySchema = z2.object({
|
|
1269
|
+
allowTools: stringArray.optional(),
|
|
1270
|
+
denyTools: stringArray.optional(),
|
|
1271
|
+
readableKeys: stringArray.optional(),
|
|
1272
|
+
deniedKeys: stringArray.optional(),
|
|
1273
|
+
deniedTags: stringArray.optional()
|
|
1274
|
+
}).strict();
|
|
1275
|
+
var execPolicySchema = z2.object({
|
|
1276
|
+
allowCommands: stringArray.optional(),
|
|
1277
|
+
denyCommands: stringArray.optional(),
|
|
1278
|
+
maxRuntimeSeconds: z2.number().optional(),
|
|
1279
|
+
allowNetwork: z2.boolean().optional()
|
|
1280
|
+
}).strict();
|
|
1281
|
+
var secretsPolicySchema = z2.object({
|
|
1282
|
+
requireApprovalForTags: stringArray.optional(),
|
|
1283
|
+
requireRotationFormatForTags: stringArray.optional(),
|
|
1284
|
+
maxTtlSeconds: z2.number().optional()
|
|
1285
|
+
}).strict();
|
|
1286
|
+
var policySchema = z2.object({
|
|
1287
|
+
mcp: mcpPolicySchema.optional(),
|
|
1288
|
+
exec: execPolicySchema.optional(),
|
|
1289
|
+
secrets: secretsPolicySchema.optional()
|
|
1290
|
+
}).strict();
|
|
1291
|
+
var PolicyConfigError = class extends Error {
|
|
1292
|
+
constructor(message) {
|
|
1293
|
+
super(message);
|
|
1294
|
+
this.name = "PolicyConfigError";
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1107
1297
|
var cachedPolicy = null;
|
|
1108
1298
|
var policyRoot = null;
|
|
1109
1299
|
function setPolicyRoot(root) {
|
|
1110
1300
|
policyRoot = root;
|
|
1111
1301
|
cachedPolicy = null;
|
|
1112
1302
|
}
|
|
1303
|
+
function getPolicyRoot() {
|
|
1304
|
+
return policyRoot;
|
|
1305
|
+
}
|
|
1113
1306
|
function resolvePolicyPath(projectPath) {
|
|
1114
1307
|
return policyRoot ?? projectPath ?? process.cwd();
|
|
1115
1308
|
}
|
|
1116
1309
|
function configMtime(pp) {
|
|
1117
1310
|
try {
|
|
1118
|
-
return
|
|
1311
|
+
return statSync4(join8(pp, ".q-ring.json")).mtimeMs;
|
|
1119
1312
|
} catch {
|
|
1120
1313
|
return 0;
|
|
1121
1314
|
}
|
|
@@ -1124,12 +1317,28 @@ function loadPolicy(projectPath) {
|
|
|
1124
1317
|
const pp = resolvePolicyPath(projectPath);
|
|
1125
1318
|
const mtimeMs = configMtime(pp);
|
|
1126
1319
|
if (cachedPolicy && cachedPolicy.path === pp && cachedPolicy.mtimeMs === mtimeMs) {
|
|
1320
|
+
if (cachedPolicy.error) throw cachedPolicy.error;
|
|
1127
1321
|
return cachedPolicy.policy;
|
|
1128
1322
|
}
|
|
1129
1323
|
const config = readProjectConfig(pp);
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1324
|
+
const rawPolicy = config?.policy;
|
|
1325
|
+
if (rawPolicy === void 0 || rawPolicy === null) {
|
|
1326
|
+
const policy = {};
|
|
1327
|
+
cachedPolicy = { path: pp, mtimeMs, policy };
|
|
1328
|
+
return policy;
|
|
1329
|
+
}
|
|
1330
|
+
const parsed = policySchema.safeParse(rawPolicy);
|
|
1331
|
+
if (!parsed.success) {
|
|
1332
|
+
const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
|
|
1333
|
+
const error = new PolicyConfigError(
|
|
1334
|
+
`Invalid policy in ${join8(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
|
|
1335
|
+
);
|
|
1336
|
+
console.error(`q-ring: ${error.message}`);
|
|
1337
|
+
cachedPolicy = { path: pp, mtimeMs, error };
|
|
1338
|
+
throw error;
|
|
1339
|
+
}
|
|
1340
|
+
cachedPolicy = { path: pp, mtimeMs, policy: parsed.data };
|
|
1341
|
+
return parsed.data;
|
|
1133
1342
|
}
|
|
1134
1343
|
function checkToolPolicy(toolName, projectPath) {
|
|
1135
1344
|
const policy = loadPolicy(projectPath);
|
|
@@ -1258,10 +1467,7 @@ function getPolicySummary(projectPath) {
|
|
|
1258
1467
|
}
|
|
1259
1468
|
|
|
1260
1469
|
// src/core/keyring.ts
|
|
1261
|
-
import {
|
|
1262
|
-
import { homedir as homedir5 } from "os";
|
|
1263
|
-
import { join as join8 } from "path";
|
|
1264
|
-
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
1470
|
+
import { Entry as Entry2, findCredentials } from "@napi-rs/keyring";
|
|
1265
1471
|
|
|
1266
1472
|
// src/utils/hash.ts
|
|
1267
1473
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -1314,22 +1520,25 @@ function resolveScope(opts) {
|
|
|
1314
1520
|
chain.push({ scope: "global", service: globalService() });
|
|
1315
1521
|
return chain;
|
|
1316
1522
|
}
|
|
1523
|
+
function serviceForScope(scope, opts = {}) {
|
|
1524
|
+
return resolveScope({ ...opts, scope })[0].service;
|
|
1525
|
+
}
|
|
1317
1526
|
|
|
1318
1527
|
// src/core/provision.ts
|
|
1319
1528
|
import { execFileSync, spawnSync } from "child_process";
|
|
1320
|
-
import { z as
|
|
1321
|
-
var AwsStsConfigSchema =
|
|
1322
|
-
roleArn:
|
|
1323
|
-
sessionName:
|
|
1324
|
-
durationSeconds:
|
|
1529
|
+
import { z as z3 } from "zod";
|
|
1530
|
+
var AwsStsConfigSchema = z3.object({
|
|
1531
|
+
roleArn: z3.string(),
|
|
1532
|
+
sessionName: z3.string().optional(),
|
|
1533
|
+
durationSeconds: z3.number().optional()
|
|
1325
1534
|
});
|
|
1326
|
-
var HttpJitConfigSchema =
|
|
1327
|
-
url:
|
|
1328
|
-
method:
|
|
1329
|
-
valuePath:
|
|
1330
|
-
expiresInSeconds:
|
|
1331
|
-
headers:
|
|
1332
|
-
body:
|
|
1535
|
+
var HttpJitConfigSchema = z3.object({
|
|
1536
|
+
url: z3.string(),
|
|
1537
|
+
method: z3.string().optional(),
|
|
1538
|
+
valuePath: z3.string().optional(),
|
|
1539
|
+
expiresInSeconds: z3.number().optional(),
|
|
1540
|
+
headers: z3.record(z3.string(), z3.string()).optional(),
|
|
1541
|
+
body: z3.unknown().optional()
|
|
1333
1542
|
});
|
|
1334
1543
|
var ProvisionRegistry = class {
|
|
1335
1544
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -1464,40 +1673,17 @@ registry.register(httpProvider);
|
|
|
1464
1673
|
|
|
1465
1674
|
// src/core/keyring.ts
|
|
1466
1675
|
function withJitEnvelopeLock(service, key, fn) {
|
|
1467
|
-
|
|
1468
|
-
mkdirSync5(dir, { recursive: true });
|
|
1469
|
-
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1470
|
-
const lockPath = join8(dir, `${safe}.lock`);
|
|
1471
|
-
const deadline = Date.now() + 8e3;
|
|
1472
|
-
while (Date.now() < deadline) {
|
|
1473
|
-
try {
|
|
1474
|
-
writeFileSync4(lockPath, `${process.pid}
|
|
1475
|
-
`, { flag: "wx", mode: 384 });
|
|
1476
|
-
try {
|
|
1477
|
-
return fn();
|
|
1478
|
-
} finally {
|
|
1479
|
-
try {
|
|
1480
|
-
unlinkSync(lockPath);
|
|
1481
|
-
} catch {
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
} catch {
|
|
1485
|
-
const start = Date.now();
|
|
1486
|
-
while (Date.now() - start < 15) {
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
throw new Error("Could not acquire JIT envelope lock (timeout)");
|
|
1676
|
+
return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
|
|
1491
1677
|
}
|
|
1492
1678
|
function readEnvelope(service, key) {
|
|
1493
|
-
const entry = new
|
|
1679
|
+
const entry = new Entry2(service, key);
|
|
1494
1680
|
const raw = entry.getPassword();
|
|
1495
1681
|
if (raw === null) return null;
|
|
1496
1682
|
const envelope = parseEnvelope(raw);
|
|
1497
1683
|
return envelope ?? wrapLegacy(raw);
|
|
1498
1684
|
}
|
|
1499
1685
|
function writeEnvelope(service, key, envelope) {
|
|
1500
|
-
const entry = new
|
|
1686
|
+
const entry = new Entry2(service, key);
|
|
1501
1687
|
entry.setPassword(serializeEnvelope(envelope));
|
|
1502
1688
|
}
|
|
1503
1689
|
function resolveEnv(opts) {
|
|
@@ -1565,7 +1751,7 @@ function getSecret(key, opts = {}) {
|
|
|
1565
1751
|
continue;
|
|
1566
1752
|
}
|
|
1567
1753
|
if (envelope.meta.requiresApproval && source === "mcp") {
|
|
1568
|
-
if (!hasApproval(key, scope)) {
|
|
1754
|
+
if (!hasApproval(key, scope, service)) {
|
|
1569
1755
|
if (!opts.silent) {
|
|
1570
1756
|
logAudit({
|
|
1571
1757
|
action: "read",
|
|
@@ -1615,8 +1801,8 @@ function getSecret(key, opts = {}) {
|
|
|
1615
1801
|
}
|
|
1616
1802
|
value = resolveTemplates(value, { ...opts, _seen: nextSeen }, nextSeen);
|
|
1617
1803
|
if (!opts.silent) {
|
|
1618
|
-
const
|
|
1619
|
-
writeEnvelope(service, key,
|
|
1804
|
+
const latest = readEnvelope(service, key) ?? envelope;
|
|
1805
|
+
writeEnvelope(service, key, recordAccess(latest));
|
|
1620
1806
|
logAudit({ action: "read", key, scope, env, source });
|
|
1621
1807
|
}
|
|
1622
1808
|
return value;
|
|
@@ -1702,6 +1888,19 @@ function setSecret(key, value, opts = {}) {
|
|
|
1702
1888
|
const entangled = findEntangled({ service, key });
|
|
1703
1889
|
for (const target of entangled) {
|
|
1704
1890
|
try {
|
|
1891
|
+
if (source === "mcp") {
|
|
1892
|
+
const decision = checkKeyReadPolicy(target.key, void 0, opts.projectPath);
|
|
1893
|
+
if (!decision.allowed) {
|
|
1894
|
+
logAudit({
|
|
1895
|
+
action: "entangle",
|
|
1896
|
+
key: target.key,
|
|
1897
|
+
scope: "global",
|
|
1898
|
+
source,
|
|
1899
|
+
detail: `blocked propagation from ${key}: ${decision.reason}`
|
|
1900
|
+
});
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1705
1904
|
const targetEnvelope = readEnvelope(target.service, target.key);
|
|
1706
1905
|
if (targetEnvelope) {
|
|
1707
1906
|
if (opts.states) {
|
|
@@ -1742,7 +1941,7 @@ function deleteSecret(key, opts = {}) {
|
|
|
1742
1941
|
}
|
|
1743
1942
|
let deleted = false;
|
|
1744
1943
|
for (const { service, scope } of scopes) {
|
|
1745
|
-
const entry = new
|
|
1944
|
+
const entry = new Entry2(service, key);
|
|
1746
1945
|
try {
|
|
1747
1946
|
if (entry.deleteCredential()) {
|
|
1748
1947
|
deleted = true;
|
|
@@ -1851,7 +2050,7 @@ function exportSecrets(opts = {}) {
|
|
|
1851
2050
|
if (entry.envelope) {
|
|
1852
2051
|
const decay = checkDecay(entry.envelope);
|
|
1853
2052
|
if (decay.isExpired) continue;
|
|
1854
|
-
if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope)) {
|
|
2053
|
+
if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope, serviceForScope(entry.scope, opts))) {
|
|
1855
2054
|
logAudit({
|
|
1856
2055
|
action: "read",
|
|
1857
2056
|
key: entry.key,
|
|
@@ -1896,7 +2095,10 @@ function entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
1896
2095
|
const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
|
|
1897
2096
|
const source = { service: sourceScopes[0].service, key: sourceKey };
|
|
1898
2097
|
const target = { service: targetScopes[0].service, key: targetKey };
|
|
1899
|
-
entangle(source, target
|
|
2098
|
+
entangle(source, target, {
|
|
2099
|
+
source: sourceOpts.source ?? "cli",
|
|
2100
|
+
policyRoot: getPolicyRoot()
|
|
2101
|
+
});
|
|
1900
2102
|
logAudit({
|
|
1901
2103
|
action: "entangle",
|
|
1902
2104
|
key: sourceKey,
|
|
@@ -1919,7 +2121,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
1919
2121
|
}
|
|
1920
2122
|
|
|
1921
2123
|
// src/core/tunnel.ts
|
|
1922
|
-
import { randomBytes as
|
|
2124
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1923
2125
|
var tunnelStore = /* @__PURE__ */ new Map();
|
|
1924
2126
|
var cleanupInterval = null;
|
|
1925
2127
|
function ensureCleanup() {
|
|
@@ -1941,7 +2143,7 @@ function ensureCleanup() {
|
|
|
1941
2143
|
}
|
|
1942
2144
|
}
|
|
1943
2145
|
function tunnelCreate(value, opts = {}) {
|
|
1944
|
-
const id = `tun_${Date.now().toString(36)}_${
|
|
2146
|
+
const id = `tun_${Date.now().toString(36)}_${randomBytes3(6).toString("base64url")}`;
|
|
1945
2147
|
const now = Date.now();
|
|
1946
2148
|
tunnelStore.set(id, {
|
|
1947
2149
|
value,
|
|
@@ -1991,43 +2193,72 @@ function tunnelList() {
|
|
|
1991
2193
|
}
|
|
1992
2194
|
|
|
1993
2195
|
// src/core/memory.ts
|
|
1994
|
-
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
2196
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, chmodSync as chmodSync2 } from "fs";
|
|
1995
2197
|
import { join as join9 } from "path";
|
|
1996
2198
|
import { homedir as homedir6, hostname, userInfo } from "os";
|
|
1997
|
-
import {
|
|
1998
|
-
|
|
2199
|
+
import {
|
|
2200
|
+
createCipheriv,
|
|
2201
|
+
createDecipheriv,
|
|
2202
|
+
createHash as createHash3,
|
|
2203
|
+
randomBytes as randomBytes4,
|
|
2204
|
+
pbkdf2Sync
|
|
2205
|
+
} from "crypto";
|
|
2206
|
+
import { Entry as Entry3 } from "@napi-rs/keyring";
|
|
1999
2207
|
var MEMORY_FILE = "agent-memory.enc";
|
|
2000
2208
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
2001
2209
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
2002
2210
|
function getMemoryDir() {
|
|
2003
2211
|
const dir = join9(homedir6(), ".config", "q-ring");
|
|
2004
2212
|
if (!existsSync6(dir)) {
|
|
2005
|
-
mkdirSync6(dir, { recursive: true });
|
|
2213
|
+
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
2006
2214
|
}
|
|
2007
2215
|
return dir;
|
|
2008
2216
|
}
|
|
2217
|
+
function writeMemoryFile(path, data) {
|
|
2218
|
+
writeFileSync5(path, data, { mode: 384 });
|
|
2219
|
+
try {
|
|
2220
|
+
chmodSync2(path, 384);
|
|
2221
|
+
} catch {
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2009
2224
|
function getMemoryPath() {
|
|
2010
2225
|
return join9(getMemoryDir(), MEMORY_FILE);
|
|
2011
2226
|
}
|
|
2227
|
+
var PBKDF2_ITERATIONS = 21e4;
|
|
2228
|
+
var KEY_LENGTH = 32;
|
|
2229
|
+
var PASSPHRASE_ENV = "QRING_MEMORY_PASSPHRASE";
|
|
2230
|
+
var V2_PREFIX = "qmem2";
|
|
2231
|
+
var MemoryKeyUnavailableError = class extends Error {
|
|
2232
|
+
constructor(message) {
|
|
2233
|
+
super(message);
|
|
2234
|
+
this.name = "MemoryKeyUnavailableError";
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2012
2237
|
function deriveLegacyKey() {
|
|
2013
2238
|
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
2014
2239
|
return createHash3("sha256").update(fingerprint).digest();
|
|
2015
2240
|
}
|
|
2016
|
-
function
|
|
2241
|
+
function passphrase() {
|
|
2242
|
+
const p = process.env[PASSPHRASE_ENV];
|
|
2243
|
+
return p && p.length > 0 ? p : void 0;
|
|
2244
|
+
}
|
|
2245
|
+
function derivePassphraseKey(salt) {
|
|
2246
|
+
return pbkdf2Sync(passphrase(), salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
2247
|
+
}
|
|
2248
|
+
function keyringKey() {
|
|
2017
2249
|
try {
|
|
2018
|
-
const entry = new
|
|
2250
|
+
const entry = new Entry3(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
2019
2251
|
const stored = entry.getPassword();
|
|
2020
2252
|
if (stored) return Buffer.from(stored, "base64");
|
|
2021
|
-
const key =
|
|
2253
|
+
const key = randomBytes4(KEY_LENGTH);
|
|
2022
2254
|
entry.setPassword(key.toString("base64"));
|
|
2023
2255
|
return key;
|
|
2024
2256
|
} catch {
|
|
2025
|
-
|
|
2026
|
-
return deriveLegacyKey();
|
|
2257
|
+
return null;
|
|
2027
2258
|
}
|
|
2028
2259
|
}
|
|
2029
2260
|
function encryptWith(data, key) {
|
|
2030
|
-
const iv =
|
|
2261
|
+
const iv = randomBytes4(12);
|
|
2031
2262
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
2032
2263
|
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
2033
2264
|
const tag = cipher.getAuthTag();
|
|
@@ -2044,18 +2275,44 @@ function decryptWith(blob, key) {
|
|
|
2044
2275
|
return decipher.update(encrypted) + decipher.final("utf8");
|
|
2045
2276
|
}
|
|
2046
2277
|
function encrypt(data) {
|
|
2047
|
-
|
|
2278
|
+
const kk = keyringKey();
|
|
2279
|
+
if (kk) return encryptWith(data, kk);
|
|
2280
|
+
if (passphrase()) {
|
|
2281
|
+
const salt = randomBytes4(16);
|
|
2282
|
+
const key = derivePassphraseKey(salt);
|
|
2283
|
+
return `${V2_PREFIX}:${salt.toString("base64")}:${encryptWith(data, key)}`;
|
|
2284
|
+
}
|
|
2285
|
+
throw new MemoryKeyUnavailableError(
|
|
2286
|
+
`Cannot persist agent memory: the OS keyring is unavailable and ${PASSPHRASE_ENV} is not set. Refusing to encrypt with a machine-derivable key (any local process could recompute it and read your memory). Set ${PASSPHRASE_ENV} to a strong passphrase, or run on a host with an OS keyring.`
|
|
2287
|
+
);
|
|
2048
2288
|
}
|
|
2049
2289
|
function decrypt(blob) {
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2290
|
+
if (blob.startsWith(`${V2_PREFIX}:`)) {
|
|
2291
|
+
if (!passphrase()) {
|
|
2292
|
+
throw new MemoryKeyUnavailableError(
|
|
2293
|
+
`Agent memory was encrypted with ${PASSPHRASE_ENV} but it is not set \u2014 cannot decrypt.`
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
const rest = blob.slice(V2_PREFIX.length + 1);
|
|
2297
|
+
const sep = rest.indexOf(":");
|
|
2298
|
+
const salt = Buffer.from(rest.slice(0, sep), "base64");
|
|
2299
|
+
return decryptWith(rest.slice(sep + 1), derivePassphraseKey(salt));
|
|
2300
|
+
}
|
|
2301
|
+
const kk = keyringKey();
|
|
2302
|
+
if (kk) {
|
|
2303
|
+
try {
|
|
2304
|
+
return decryptWith(blob, kk);
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const plain = decryptWith(blob, deriveLegacyKey());
|
|
2309
|
+
if (kk) {
|
|
2310
|
+
try {
|
|
2311
|
+
writeMemoryFile(getMemoryPath(), encryptWith(plain, kk));
|
|
2312
|
+
} catch {
|
|
2313
|
+
}
|
|
2058
2314
|
}
|
|
2315
|
+
return plain;
|
|
2059
2316
|
}
|
|
2060
2317
|
function loadStore() {
|
|
2061
2318
|
const path = getMemoryPath();
|
|
@@ -2066,14 +2323,17 @@ function loadStore() {
|
|
|
2066
2323
|
const raw = readFileSync7(path, "utf8");
|
|
2067
2324
|
const decrypted = decrypt(raw);
|
|
2068
2325
|
return JSON.parse(decrypted);
|
|
2069
|
-
} catch {
|
|
2326
|
+
} catch (err) {
|
|
2327
|
+
if (err instanceof MemoryKeyUnavailableError) {
|
|
2328
|
+
console.error(`q-ring: ${err.message}`);
|
|
2329
|
+
}
|
|
2070
2330
|
return { entries: {} };
|
|
2071
2331
|
}
|
|
2072
2332
|
}
|
|
2073
2333
|
function saveStore(store) {
|
|
2074
2334
|
const json = JSON.stringify(store);
|
|
2075
2335
|
const encrypted = encrypt(json);
|
|
2076
|
-
|
|
2336
|
+
writeMemoryFile(getMemoryPath(), encrypted);
|
|
2077
2337
|
}
|
|
2078
2338
|
function remember(key, value) {
|
|
2079
2339
|
const store = loadStore();
|
|
@@ -2147,4 +2407,4 @@ export {
|
|
|
2147
2407
|
listMemory,
|
|
2148
2408
|
forget
|
|
2149
2409
|
};
|
|
2150
|
-
//# sourceMappingURL=chunk-
|
|
2410
|
+
//# sourceMappingURL=chunk-NNIEXAW5.js.map
|