@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
|
}
|
|
@@ -1039,19 +1205,19 @@ async function fireHooks(payload, tags) {
|
|
|
1039
1205
|
}
|
|
1040
1206
|
|
|
1041
1207
|
// src/core/approval.ts
|
|
1042
|
-
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as
|
|
1043
|
-
import { join as
|
|
1044
|
-
import { homedir as
|
|
1045
|
-
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1208
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
|
|
1209
|
+
import { join as join7 } from "path";
|
|
1210
|
+
import { homedir as homedir5 } from "os";
|
|
1211
|
+
import { createHmac as createHmac2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1046
1212
|
function getHmacSecret() {
|
|
1047
|
-
const dir =
|
|
1048
|
-
const secretPath =
|
|
1049
|
-
if (!existsSync5(dir))
|
|
1213
|
+
const dir = join7(homedir5(), ".config", "q-ring");
|
|
1214
|
+
const secretPath = join7(dir, ".approval-key");
|
|
1215
|
+
if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
1050
1216
|
if (existsSync5(secretPath)) {
|
|
1051
1217
|
return readFileSync6(secretPath, "utf8").trim();
|
|
1052
1218
|
}
|
|
1053
|
-
const secret =
|
|
1054
|
-
|
|
1219
|
+
const secret = randomBytes2(32).toString("hex");
|
|
1220
|
+
writeFileSync4(secretPath, secret, { mode: 384 });
|
|
1055
1221
|
return secret;
|
|
1056
1222
|
}
|
|
1057
1223
|
function computeHmac(entry) {
|
|
@@ -1059,6 +1225,7 @@ function computeHmac(entry) {
|
|
|
1059
1225
|
entry.id,
|
|
1060
1226
|
entry.key,
|
|
1061
1227
|
entry.scope,
|
|
1228
|
+
entry.service ?? "",
|
|
1062
1229
|
entry.reason,
|
|
1063
1230
|
entry.grantedBy,
|
|
1064
1231
|
entry.grantedAt,
|
|
@@ -1066,7 +1233,7 @@ function computeHmac(entry) {
|
|
|
1066
1233
|
entry.workspace ?? "",
|
|
1067
1234
|
entry.sessionId ?? ""
|
|
1068
1235
|
].join("|");
|
|
1069
|
-
return
|
|
1236
|
+
return createHmac2("sha256", getHmacSecret()).update(payload).digest("hex");
|
|
1070
1237
|
}
|
|
1071
1238
|
function verifyHmac(entry) {
|
|
1072
1239
|
const expected = computeHmac(entry);
|
|
@@ -1080,25 +1247,17 @@ function verifyHmac(entry) {
|
|
|
1080
1247
|
}
|
|
1081
1248
|
}
|
|
1082
1249
|
function getRegistryPath3() {
|
|
1083
|
-
const dir =
|
|
1250
|
+
const dir = join7(homedir5(), ".config", "q-ring");
|
|
1084
1251
|
if (!existsSync5(dir)) {
|
|
1085
|
-
|
|
1252
|
+
mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
1086
1253
|
}
|
|
1087
|
-
return
|
|
1254
|
+
return join7(dir, "approvals.json");
|
|
1088
1255
|
}
|
|
1089
1256
|
function loadRegistry3() {
|
|
1090
|
-
|
|
1091
|
-
if (!existsSync5(path)) {
|
|
1092
|
-
return { approvals: [] };
|
|
1093
|
-
}
|
|
1094
|
-
try {
|
|
1095
|
-
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1096
|
-
} catch {
|
|
1097
|
-
return { approvals: [] };
|
|
1098
|
-
}
|
|
1257
|
+
return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
|
|
1099
1258
|
}
|
|
1100
1259
|
function saveRegistry3(registry2) {
|
|
1101
|
-
|
|
1260
|
+
writeFileSync4(getRegistryPath3(), JSON.stringify(registry2, null, 2), { mode: 384 });
|
|
1102
1261
|
}
|
|
1103
1262
|
function cleanup(registry2) {
|
|
1104
1263
|
const now = Date.now();
|
|
@@ -1106,16 +1265,17 @@ function cleanup(registry2) {
|
|
|
1106
1265
|
(a) => new Date(a.expiresAt).getTime() > now
|
|
1107
1266
|
);
|
|
1108
1267
|
}
|
|
1109
|
-
function grantApproval(key, scope, ttlSeconds = 3600, grantOpts = {}) {
|
|
1268
|
+
function grantApproval(key, scope, service, ttlSeconds = 3600, grantOpts = {}) {
|
|
1110
1269
|
const registry2 = loadRegistry3();
|
|
1111
1270
|
cleanup(registry2);
|
|
1112
|
-
const id =
|
|
1271
|
+
const id = randomBytes2(8).toString("hex");
|
|
1113
1272
|
const grantedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1114
1273
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3).toISOString();
|
|
1115
1274
|
const partial = {
|
|
1116
1275
|
id,
|
|
1117
1276
|
key,
|
|
1118
1277
|
scope,
|
|
1278
|
+
service,
|
|
1119
1279
|
reason: grantOpts.reason ?? "no reason provided",
|
|
1120
1280
|
grantedBy: grantOpts.grantedBy ?? "cli-user",
|
|
1121
1281
|
grantedAt,
|
|
@@ -1125,7 +1285,7 @@ function grantApproval(key, scope, ttlSeconds = 3600, grantOpts = {}) {
|
|
|
1125
1285
|
};
|
|
1126
1286
|
const entry = { ...partial, hmac: computeHmac(partial) };
|
|
1127
1287
|
const existingIdx = registry2.approvals.findIndex(
|
|
1128
|
-
(a) => a.key === key && a.scope === scope
|
|
1288
|
+
(a) => a.key === key && a.scope === scope && a.service === service
|
|
1129
1289
|
);
|
|
1130
1290
|
if (existingIdx >= 0) {
|
|
1131
1291
|
registry2.approvals[existingIdx] = entry;
|
|
@@ -1135,25 +1295,28 @@ function grantApproval(key, scope, ttlSeconds = 3600, grantOpts = {}) {
|
|
|
1135
1295
|
saveRegistry3(registry2);
|
|
1136
1296
|
return entry;
|
|
1137
1297
|
}
|
|
1138
|
-
function revokeApproval(key, scope) {
|
|
1298
|
+
function revokeApproval(key, scope, service) {
|
|
1139
1299
|
const registry2 = loadRegistry3();
|
|
1140
1300
|
const before = registry2.approvals.length;
|
|
1141
1301
|
registry2.approvals = registry2.approvals.filter(
|
|
1142
|
-
(a) => !(a.key === key && a.scope === scope)
|
|
1302
|
+
(a) => !(a.key === key && a.scope === scope && a.service === service)
|
|
1143
1303
|
);
|
|
1144
1304
|
saveRegistry3(registry2);
|
|
1145
1305
|
return registry2.approvals.length < before;
|
|
1146
1306
|
}
|
|
1147
|
-
function hasApproval(key, scope) {
|
|
1307
|
+
function hasApproval(key, scope, service) {
|
|
1148
1308
|
const registry2 = loadRegistry3();
|
|
1149
1309
|
const entry = registry2.approvals.find(
|
|
1150
|
-
(a) => a.key === key && a.scope === scope
|
|
1310
|
+
(a) => a.key === key && a.scope === scope && a.service === service
|
|
1151
1311
|
);
|
|
1152
1312
|
if (!entry) return false;
|
|
1153
1313
|
if (new Date(entry.expiresAt).getTime() < Date.now()) return false;
|
|
1154
1314
|
if (!verifyHmac(entry)) return false;
|
|
1155
1315
|
return true;
|
|
1156
1316
|
}
|
|
1317
|
+
function countLegacyApprovals() {
|
|
1318
|
+
return loadRegistry3().approvals.filter((a) => a.service === void 0).length;
|
|
1319
|
+
}
|
|
1157
1320
|
function listApprovals() {
|
|
1158
1321
|
const registry2 = loadRegistry3();
|
|
1159
1322
|
const now = Date.now();
|
|
@@ -1165,16 +1328,50 @@ function listApprovals() {
|
|
|
1165
1328
|
}
|
|
1166
1329
|
|
|
1167
1330
|
// src/core/policy.ts
|
|
1168
|
-
import { statSync as
|
|
1169
|
-
import { join as
|
|
1331
|
+
import { statSync as statSync4 } from "fs";
|
|
1332
|
+
import { join as join8 } from "path";
|
|
1333
|
+
import { z as z2 } from "zod";
|
|
1334
|
+
var stringArray = z2.array(z2.string());
|
|
1335
|
+
var mcpPolicySchema = z2.object({
|
|
1336
|
+
allowTools: stringArray.optional(),
|
|
1337
|
+
denyTools: stringArray.optional(),
|
|
1338
|
+
readableKeys: stringArray.optional(),
|
|
1339
|
+
deniedKeys: stringArray.optional(),
|
|
1340
|
+
deniedTags: stringArray.optional()
|
|
1341
|
+
}).strict();
|
|
1342
|
+
var execPolicySchema = z2.object({
|
|
1343
|
+
allowCommands: stringArray.optional(),
|
|
1344
|
+
denyCommands: stringArray.optional(),
|
|
1345
|
+
maxRuntimeSeconds: z2.number().optional(),
|
|
1346
|
+
allowNetwork: z2.boolean().optional()
|
|
1347
|
+
}).strict();
|
|
1348
|
+
var secretsPolicySchema = z2.object({
|
|
1349
|
+
requireApprovalForTags: stringArray.optional(),
|
|
1350
|
+
requireRotationFormatForTags: stringArray.optional(),
|
|
1351
|
+
maxTtlSeconds: z2.number().optional()
|
|
1352
|
+
}).strict();
|
|
1353
|
+
var policySchema = z2.object({
|
|
1354
|
+
mcp: mcpPolicySchema.optional(),
|
|
1355
|
+
exec: execPolicySchema.optional(),
|
|
1356
|
+
secrets: secretsPolicySchema.optional()
|
|
1357
|
+
}).strict();
|
|
1358
|
+
var PolicyConfigError = class extends Error {
|
|
1359
|
+
constructor(message) {
|
|
1360
|
+
super(message);
|
|
1361
|
+
this.name = "PolicyConfigError";
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1170
1364
|
var cachedPolicy = null;
|
|
1171
1365
|
var policyRoot = null;
|
|
1366
|
+
function getPolicyRoot() {
|
|
1367
|
+
return policyRoot;
|
|
1368
|
+
}
|
|
1172
1369
|
function resolvePolicyPath(projectPath) {
|
|
1173
1370
|
return policyRoot ?? projectPath ?? process.cwd();
|
|
1174
1371
|
}
|
|
1175
1372
|
function configMtime(pp) {
|
|
1176
1373
|
try {
|
|
1177
|
-
return
|
|
1374
|
+
return statSync4(join8(pp, ".q-ring.json")).mtimeMs;
|
|
1178
1375
|
} catch {
|
|
1179
1376
|
return 0;
|
|
1180
1377
|
}
|
|
@@ -1183,12 +1380,28 @@ function loadPolicy(projectPath) {
|
|
|
1183
1380
|
const pp = resolvePolicyPath(projectPath);
|
|
1184
1381
|
const mtimeMs = configMtime(pp);
|
|
1185
1382
|
if (cachedPolicy && cachedPolicy.path === pp && cachedPolicy.mtimeMs === mtimeMs) {
|
|
1383
|
+
if (cachedPolicy.error) throw cachedPolicy.error;
|
|
1186
1384
|
return cachedPolicy.policy;
|
|
1187
1385
|
}
|
|
1188
1386
|
const config = readProjectConfig(pp);
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1387
|
+
const rawPolicy = config?.policy;
|
|
1388
|
+
if (rawPolicy === void 0 || rawPolicy === null) {
|
|
1389
|
+
const policy = {};
|
|
1390
|
+
cachedPolicy = { path: pp, mtimeMs, policy };
|
|
1391
|
+
return policy;
|
|
1392
|
+
}
|
|
1393
|
+
const parsed = policySchema.safeParse(rawPolicy);
|
|
1394
|
+
if (!parsed.success) {
|
|
1395
|
+
const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
|
|
1396
|
+
const error = new PolicyConfigError(
|
|
1397
|
+
`Invalid policy in ${join8(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
|
|
1398
|
+
);
|
|
1399
|
+
console.error(`q-ring: ${error.message}`);
|
|
1400
|
+
cachedPolicy = { path: pp, mtimeMs, error };
|
|
1401
|
+
throw error;
|
|
1402
|
+
}
|
|
1403
|
+
cachedPolicy = { path: pp, mtimeMs, policy: parsed.data };
|
|
1404
|
+
return parsed.data;
|
|
1192
1405
|
}
|
|
1193
1406
|
function checkSecretLifecyclePolicy(input, projectPath) {
|
|
1194
1407
|
const policy = loadPolicy(projectPath);
|
|
@@ -1298,10 +1511,7 @@ function getPolicySummary(projectPath) {
|
|
|
1298
1511
|
}
|
|
1299
1512
|
|
|
1300
1513
|
// src/core/keyring.ts
|
|
1301
|
-
import {
|
|
1302
|
-
import { homedir as homedir5 } from "os";
|
|
1303
|
-
import { join as join8 } from "path";
|
|
1304
|
-
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
1514
|
+
import { Entry as Entry2, findCredentials } from "@napi-rs/keyring";
|
|
1305
1515
|
|
|
1306
1516
|
// src/utils/hash.ts
|
|
1307
1517
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -1354,22 +1564,25 @@ function resolveScope(opts) {
|
|
|
1354
1564
|
chain.push({ scope: "global", service: globalService() });
|
|
1355
1565
|
return chain;
|
|
1356
1566
|
}
|
|
1567
|
+
function serviceForScope(scope, opts = {}) {
|
|
1568
|
+
return resolveScope({ ...opts, scope })[0].service;
|
|
1569
|
+
}
|
|
1357
1570
|
|
|
1358
1571
|
// src/core/provision.ts
|
|
1359
1572
|
import { execFileSync, spawnSync } from "child_process";
|
|
1360
|
-
import { z as
|
|
1361
|
-
var AwsStsConfigSchema =
|
|
1362
|
-
roleArn:
|
|
1363
|
-
sessionName:
|
|
1364
|
-
durationSeconds:
|
|
1573
|
+
import { z as z3 } from "zod";
|
|
1574
|
+
var AwsStsConfigSchema = z3.object({
|
|
1575
|
+
roleArn: z3.string(),
|
|
1576
|
+
sessionName: z3.string().optional(),
|
|
1577
|
+
durationSeconds: z3.number().optional()
|
|
1365
1578
|
});
|
|
1366
|
-
var HttpJitConfigSchema =
|
|
1367
|
-
url:
|
|
1368
|
-
method:
|
|
1369
|
-
valuePath:
|
|
1370
|
-
expiresInSeconds:
|
|
1371
|
-
headers:
|
|
1372
|
-
body:
|
|
1579
|
+
var HttpJitConfigSchema = z3.object({
|
|
1580
|
+
url: z3.string(),
|
|
1581
|
+
method: z3.string().optional(),
|
|
1582
|
+
valuePath: z3.string().optional(),
|
|
1583
|
+
expiresInSeconds: z3.number().optional(),
|
|
1584
|
+
headers: z3.record(z3.string(), z3.string()).optional(),
|
|
1585
|
+
body: z3.unknown().optional()
|
|
1373
1586
|
});
|
|
1374
1587
|
var ProvisionRegistry = class {
|
|
1375
1588
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -1504,40 +1717,17 @@ registry.register(httpProvider);
|
|
|
1504
1717
|
|
|
1505
1718
|
// src/core/keyring.ts
|
|
1506
1719
|
function withJitEnvelopeLock(service, key, fn) {
|
|
1507
|
-
|
|
1508
|
-
mkdirSync5(dir, { recursive: true });
|
|
1509
|
-
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1510
|
-
const lockPath = join8(dir, `${safe}.lock`);
|
|
1511
|
-
const deadline = Date.now() + 8e3;
|
|
1512
|
-
while (Date.now() < deadline) {
|
|
1513
|
-
try {
|
|
1514
|
-
writeFileSync4(lockPath, `${process.pid}
|
|
1515
|
-
`, { flag: "wx", mode: 384 });
|
|
1516
|
-
try {
|
|
1517
|
-
return fn();
|
|
1518
|
-
} finally {
|
|
1519
|
-
try {
|
|
1520
|
-
unlinkSync(lockPath);
|
|
1521
|
-
} catch {
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
} catch {
|
|
1525
|
-
const start = Date.now();
|
|
1526
|
-
while (Date.now() - start < 15) {
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
throw new Error("Could not acquire JIT envelope lock (timeout)");
|
|
1720
|
+
return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
|
|
1531
1721
|
}
|
|
1532
1722
|
function readEnvelope(service, key) {
|
|
1533
|
-
const entry = new
|
|
1723
|
+
const entry = new Entry2(service, key);
|
|
1534
1724
|
const raw = entry.getPassword();
|
|
1535
1725
|
if (raw === null) return null;
|
|
1536
1726
|
const envelope = parseEnvelope(raw);
|
|
1537
1727
|
return envelope ?? wrapLegacy(raw);
|
|
1538
1728
|
}
|
|
1539
1729
|
function writeEnvelope(service, key, envelope) {
|
|
1540
|
-
const entry = new
|
|
1730
|
+
const entry = new Entry2(service, key);
|
|
1541
1731
|
entry.setPassword(serializeEnvelope(envelope));
|
|
1542
1732
|
}
|
|
1543
1733
|
function resolveEnv(opts) {
|
|
@@ -1605,7 +1795,7 @@ function getSecret(key, opts = {}) {
|
|
|
1605
1795
|
continue;
|
|
1606
1796
|
}
|
|
1607
1797
|
if (envelope.meta.requiresApproval && source === "mcp") {
|
|
1608
|
-
if (!hasApproval(key, scope)) {
|
|
1798
|
+
if (!hasApproval(key, scope, service)) {
|
|
1609
1799
|
if (!opts.silent) {
|
|
1610
1800
|
logAudit({
|
|
1611
1801
|
action: "read",
|
|
@@ -1655,8 +1845,8 @@ function getSecret(key, opts = {}) {
|
|
|
1655
1845
|
}
|
|
1656
1846
|
value = resolveTemplates(value, { ...opts, _seen: nextSeen }, nextSeen);
|
|
1657
1847
|
if (!opts.silent) {
|
|
1658
|
-
const
|
|
1659
|
-
writeEnvelope(service, key,
|
|
1848
|
+
const latest = readEnvelope(service, key) ?? envelope;
|
|
1849
|
+
writeEnvelope(service, key, recordAccess(latest));
|
|
1660
1850
|
logAudit({ action: "read", key, scope, env, source });
|
|
1661
1851
|
}
|
|
1662
1852
|
return value;
|
|
@@ -1742,6 +1932,19 @@ function setSecret(key, value, opts = {}) {
|
|
|
1742
1932
|
const entangled = findEntangled({ service, key });
|
|
1743
1933
|
for (const target of entangled) {
|
|
1744
1934
|
try {
|
|
1935
|
+
if (source === "mcp") {
|
|
1936
|
+
const decision = checkKeyReadPolicy(target.key, void 0, opts.projectPath);
|
|
1937
|
+
if (!decision.allowed) {
|
|
1938
|
+
logAudit({
|
|
1939
|
+
action: "entangle",
|
|
1940
|
+
key: target.key,
|
|
1941
|
+
scope: "global",
|
|
1942
|
+
source,
|
|
1943
|
+
detail: `blocked propagation from ${key}: ${decision.reason}`
|
|
1944
|
+
});
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1745
1948
|
const targetEnvelope = readEnvelope(target.service, target.key);
|
|
1746
1949
|
if (targetEnvelope) {
|
|
1747
1950
|
if (opts.states) {
|
|
@@ -1782,7 +1985,7 @@ function deleteSecret(key, opts = {}) {
|
|
|
1782
1985
|
}
|
|
1783
1986
|
let deleted = false;
|
|
1784
1987
|
for (const { service, scope } of scopes) {
|
|
1785
|
-
const entry = new
|
|
1988
|
+
const entry = new Entry2(service, key);
|
|
1786
1989
|
try {
|
|
1787
1990
|
if (entry.deleteCredential()) {
|
|
1788
1991
|
deleted = true;
|
|
@@ -1891,7 +2094,7 @@ function exportSecrets(opts = {}) {
|
|
|
1891
2094
|
if (entry.envelope) {
|
|
1892
2095
|
const decay = checkDecay(entry.envelope);
|
|
1893
2096
|
if (decay.isExpired) continue;
|
|
1894
|
-
if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope)) {
|
|
2097
|
+
if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope, serviceForScope(entry.scope, opts))) {
|
|
1895
2098
|
logAudit({
|
|
1896
2099
|
action: "read",
|
|
1897
2100
|
key: entry.key,
|
|
@@ -1936,7 +2139,10 @@ function entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
1936
2139
|
const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
|
|
1937
2140
|
const source = { service: sourceScopes[0].service, key: sourceKey };
|
|
1938
2141
|
const target = { service: targetScopes[0].service, key: targetKey };
|
|
1939
|
-
entangle(source, target
|
|
2142
|
+
entangle(source, target, {
|
|
2143
|
+
source: sourceOpts.source ?? "cli",
|
|
2144
|
+
policyRoot: getPolicyRoot()
|
|
2145
|
+
});
|
|
1940
2146
|
logAudit({
|
|
1941
2147
|
action: "entangle",
|
|
1942
2148
|
key: sourceKey,
|
|
@@ -1959,7 +2165,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
1959
2165
|
}
|
|
1960
2166
|
|
|
1961
2167
|
// src/core/tunnel.ts
|
|
1962
|
-
import { randomBytes as
|
|
2168
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1963
2169
|
var tunnelStore = /* @__PURE__ */ new Map();
|
|
1964
2170
|
var cleanupInterval = null;
|
|
1965
2171
|
function ensureCleanup() {
|
|
@@ -1981,7 +2187,7 @@ function ensureCleanup() {
|
|
|
1981
2187
|
}
|
|
1982
2188
|
}
|
|
1983
2189
|
function tunnelCreate(value, opts = {}) {
|
|
1984
|
-
const id = `tun_${Date.now().toString(36)}_${
|
|
2190
|
+
const id = `tun_${Date.now().toString(36)}_${randomBytes3(6).toString("base64url")}`;
|
|
1985
2191
|
const now = Date.now();
|
|
1986
2192
|
tunnelStore.set(id, {
|
|
1987
2193
|
value,
|
|
@@ -2031,43 +2237,72 @@ function tunnelList() {
|
|
|
2031
2237
|
}
|
|
2032
2238
|
|
|
2033
2239
|
// src/core/memory.ts
|
|
2034
|
-
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
2240
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, chmodSync as chmodSync2 } from "fs";
|
|
2035
2241
|
import { join as join9 } from "path";
|
|
2036
2242
|
import { homedir as homedir6, hostname, userInfo } from "os";
|
|
2037
|
-
import {
|
|
2038
|
-
|
|
2243
|
+
import {
|
|
2244
|
+
createCipheriv,
|
|
2245
|
+
createDecipheriv,
|
|
2246
|
+
createHash as createHash3,
|
|
2247
|
+
randomBytes as randomBytes4,
|
|
2248
|
+
pbkdf2Sync
|
|
2249
|
+
} from "crypto";
|
|
2250
|
+
import { Entry as Entry3 } from "@napi-rs/keyring";
|
|
2039
2251
|
var MEMORY_FILE = "agent-memory.enc";
|
|
2040
2252
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
2041
2253
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
2042
2254
|
function getMemoryDir() {
|
|
2043
2255
|
const dir = join9(homedir6(), ".config", "q-ring");
|
|
2044
2256
|
if (!existsSync6(dir)) {
|
|
2045
|
-
mkdirSync6(dir, { recursive: true });
|
|
2257
|
+
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
2046
2258
|
}
|
|
2047
2259
|
return dir;
|
|
2048
2260
|
}
|
|
2261
|
+
function writeMemoryFile(path, data) {
|
|
2262
|
+
writeFileSync5(path, data, { mode: 384 });
|
|
2263
|
+
try {
|
|
2264
|
+
chmodSync2(path, 384);
|
|
2265
|
+
} catch {
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2049
2268
|
function getMemoryPath() {
|
|
2050
2269
|
return join9(getMemoryDir(), MEMORY_FILE);
|
|
2051
2270
|
}
|
|
2271
|
+
var PBKDF2_ITERATIONS = 21e4;
|
|
2272
|
+
var KEY_LENGTH = 32;
|
|
2273
|
+
var PASSPHRASE_ENV = "QRING_MEMORY_PASSPHRASE";
|
|
2274
|
+
var V2_PREFIX = "qmem2";
|
|
2275
|
+
var MemoryKeyUnavailableError = class extends Error {
|
|
2276
|
+
constructor(message) {
|
|
2277
|
+
super(message);
|
|
2278
|
+
this.name = "MemoryKeyUnavailableError";
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2052
2281
|
function deriveLegacyKey() {
|
|
2053
2282
|
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
2054
2283
|
return createHash3("sha256").update(fingerprint).digest();
|
|
2055
2284
|
}
|
|
2056
|
-
function
|
|
2285
|
+
function passphrase() {
|
|
2286
|
+
const p = process.env[PASSPHRASE_ENV];
|
|
2287
|
+
return p && p.length > 0 ? p : void 0;
|
|
2288
|
+
}
|
|
2289
|
+
function derivePassphraseKey(salt) {
|
|
2290
|
+
return pbkdf2Sync(passphrase(), salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
2291
|
+
}
|
|
2292
|
+
function keyringKey() {
|
|
2057
2293
|
try {
|
|
2058
|
-
const entry = new
|
|
2294
|
+
const entry = new Entry3(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
2059
2295
|
const stored = entry.getPassword();
|
|
2060
2296
|
if (stored) return Buffer.from(stored, "base64");
|
|
2061
|
-
const key =
|
|
2297
|
+
const key = randomBytes4(KEY_LENGTH);
|
|
2062
2298
|
entry.setPassword(key.toString("base64"));
|
|
2063
2299
|
return key;
|
|
2064
2300
|
} catch {
|
|
2065
|
-
|
|
2066
|
-
return deriveLegacyKey();
|
|
2301
|
+
return null;
|
|
2067
2302
|
}
|
|
2068
2303
|
}
|
|
2069
2304
|
function encryptWith(data, key) {
|
|
2070
|
-
const iv =
|
|
2305
|
+
const iv = randomBytes4(12);
|
|
2071
2306
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
2072
2307
|
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
2073
2308
|
const tag = cipher.getAuthTag();
|
|
@@ -2084,18 +2319,44 @@ function decryptWith(blob, key) {
|
|
|
2084
2319
|
return decipher.update(encrypted) + decipher.final("utf8");
|
|
2085
2320
|
}
|
|
2086
2321
|
function encrypt(data) {
|
|
2087
|
-
|
|
2322
|
+
const kk = keyringKey();
|
|
2323
|
+
if (kk) return encryptWith(data, kk);
|
|
2324
|
+
if (passphrase()) {
|
|
2325
|
+
const salt = randomBytes4(16);
|
|
2326
|
+
const key = derivePassphraseKey(salt);
|
|
2327
|
+
return `${V2_PREFIX}:${salt.toString("base64")}:${encryptWith(data, key)}`;
|
|
2328
|
+
}
|
|
2329
|
+
throw new MemoryKeyUnavailableError(
|
|
2330
|
+
`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.`
|
|
2331
|
+
);
|
|
2088
2332
|
}
|
|
2089
2333
|
function decrypt(blob) {
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2334
|
+
if (blob.startsWith(`${V2_PREFIX}:`)) {
|
|
2335
|
+
if (!passphrase()) {
|
|
2336
|
+
throw new MemoryKeyUnavailableError(
|
|
2337
|
+
`Agent memory was encrypted with ${PASSPHRASE_ENV} but it is not set \u2014 cannot decrypt.`
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
const rest = blob.slice(V2_PREFIX.length + 1);
|
|
2341
|
+
const sep = rest.indexOf(":");
|
|
2342
|
+
const salt = Buffer.from(rest.slice(0, sep), "base64");
|
|
2343
|
+
return decryptWith(rest.slice(sep + 1), derivePassphraseKey(salt));
|
|
2344
|
+
}
|
|
2345
|
+
const kk = keyringKey();
|
|
2346
|
+
if (kk) {
|
|
2347
|
+
try {
|
|
2348
|
+
return decryptWith(blob, kk);
|
|
2349
|
+
} catch {
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
const plain = decryptWith(blob, deriveLegacyKey());
|
|
2353
|
+
if (kk) {
|
|
2354
|
+
try {
|
|
2355
|
+
writeMemoryFile(getMemoryPath(), encryptWith(plain, kk));
|
|
2356
|
+
} catch {
|
|
2357
|
+
}
|
|
2098
2358
|
}
|
|
2359
|
+
return plain;
|
|
2099
2360
|
}
|
|
2100
2361
|
function loadStore() {
|
|
2101
2362
|
const path = getMemoryPath();
|
|
@@ -2106,14 +2367,17 @@ function loadStore() {
|
|
|
2106
2367
|
const raw = readFileSync7(path, "utf8");
|
|
2107
2368
|
const decrypted = decrypt(raw);
|
|
2108
2369
|
return JSON.parse(decrypted);
|
|
2109
|
-
} catch {
|
|
2370
|
+
} catch (err) {
|
|
2371
|
+
if (err instanceof MemoryKeyUnavailableError) {
|
|
2372
|
+
console.error(`q-ring: ${err.message}`);
|
|
2373
|
+
}
|
|
2110
2374
|
return { entries: {} };
|
|
2111
2375
|
}
|
|
2112
2376
|
}
|
|
2113
2377
|
function saveStore(store) {
|
|
2114
2378
|
const json = JSON.stringify(store);
|
|
2115
2379
|
const encrypted = encrypt(json);
|
|
2116
|
-
|
|
2380
|
+
writeMemoryFile(getMemoryPath(), encrypted);
|
|
2117
2381
|
}
|
|
2118
2382
|
function remember(key, value) {
|
|
2119
2383
|
const store = loadStore();
|
|
@@ -2149,6 +2413,7 @@ function clearMemory() {
|
|
|
2149
2413
|
|
|
2150
2414
|
export {
|
|
2151
2415
|
PACKAGE_VERSION,
|
|
2416
|
+
serviceForScope,
|
|
2152
2417
|
checkDecay,
|
|
2153
2418
|
readProjectConfig,
|
|
2154
2419
|
collapseEnvironment,
|
|
@@ -2168,6 +2433,7 @@ export {
|
|
|
2168
2433
|
fireHooks,
|
|
2169
2434
|
grantApproval,
|
|
2170
2435
|
revokeApproval,
|
|
2436
|
+
countLegacyApprovals,
|
|
2171
2437
|
listApprovals,
|
|
2172
2438
|
registry,
|
|
2173
2439
|
checkExecPolicy,
|
|
@@ -2192,4 +2458,4 @@ export {
|
|
|
2192
2458
|
forget,
|
|
2193
2459
|
clearMemory
|
|
2194
2460
|
};
|
|
2195
|
-
//# sourceMappingURL=chunk-
|
|
2461
|
+
//# sourceMappingURL=chunk-C2TFJ2EH.js.map
|