@i4ctime/q-ring 0.10.1 → 0.11.5
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 +71 -8
- package/dist/{chunk-LLMO64FE.js → chunk-A4RZVP3P.js} +363 -223
- package/dist/chunk-A4RZVP3P.js.map +1 -0
- package/dist/{chunk-E46HG5NM.js → chunk-MQM4DLPI.js} +388 -242
- package/dist/chunk-MQM4DLPI.js.map +1 -0
- package/dist/dashboard-FSCJDLJX.js +1313 -0
- package/dist/dashboard-FSCJDLJX.js.map +1 -0
- package/dist/dashboard-OXUD62RV.js +1313 -0
- package/dist/dashboard-OXUD62RV.js.map +1 -0
- package/dist/index.js +26 -154
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +24 -152
- package/dist/mcp.js.map +1 -1
- package/package.json +4 -2
- package/dist/chunk-E46HG5NM.js.map +0 -1
- package/dist/chunk-LLMO64FE.js.map +0 -1
- package/dist/dashboard-73QYM5FU.js +0 -557
- package/dist/dashboard-73QYM5FU.js.map +0 -1
- package/dist/dashboard-CMXRH4AQ.js +0 -557
- package/dist/dashboard-CMXRH4AQ.js.map +0 -1
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/version.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
function readPackageVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkgPath = join(here, "..", "package.json");
|
|
11
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
12
|
+
const pkg = JSON.parse(raw);
|
|
13
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
14
|
+
} catch {
|
|
15
|
+
return "0.0.0";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var PACKAGE_VERSION = readPackageVersion();
|
|
19
|
+
|
|
3
20
|
// src/core/envelope.ts
|
|
4
21
|
import { z } from "zod";
|
|
5
22
|
var EntanglementLinkSchema = z.object({
|
|
@@ -162,8 +179,8 @@ function recordAccess(envelope) {
|
|
|
162
179
|
|
|
163
180
|
// src/core/collapse.ts
|
|
164
181
|
import { execSync } from "child_process";
|
|
165
|
-
import { existsSync, readFileSync } from "fs";
|
|
166
|
-
import { join } from "path";
|
|
182
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
183
|
+
import { join as join2 } from "path";
|
|
167
184
|
var BRANCH_ENV_MAP = {
|
|
168
185
|
main: "prod",
|
|
169
186
|
master: "prod",
|
|
@@ -190,10 +207,10 @@ function detectGitBranch(cwd) {
|
|
|
190
207
|
}
|
|
191
208
|
}
|
|
192
209
|
function readProjectConfig(projectPath) {
|
|
193
|
-
const configPath =
|
|
210
|
+
const configPath = join2(projectPath ?? process.cwd(), ".q-ring.json");
|
|
194
211
|
try {
|
|
195
212
|
if (existsSync(configPath)) {
|
|
196
|
-
return JSON.parse(
|
|
213
|
+
return JSON.parse(readFileSync2(configPath, "utf8"));
|
|
197
214
|
}
|
|
198
215
|
} catch {
|
|
199
216
|
}
|
|
@@ -254,14 +271,14 @@ import {
|
|
|
254
271
|
existsSync as existsSync2,
|
|
255
272
|
mkdirSync,
|
|
256
273
|
appendFileSync,
|
|
257
|
-
readFileSync as
|
|
274
|
+
readFileSync as readFileSync3,
|
|
258
275
|
openSync,
|
|
259
276
|
fstatSync,
|
|
260
277
|
readSync,
|
|
261
278
|
closeSync,
|
|
262
279
|
statSync
|
|
263
280
|
} from "fs";
|
|
264
|
-
import { join as
|
|
281
|
+
import { join as join3 } from "path";
|
|
265
282
|
import { homedir } from "os";
|
|
266
283
|
import { createHash } from "crypto";
|
|
267
284
|
function getAuditDir() {
|
|
@@ -271,14 +288,14 @@ function getAuditDir() {
|
|
|
271
288
|
}
|
|
272
289
|
return process.env.QRING_AUDIT_DIR;
|
|
273
290
|
}
|
|
274
|
-
const dir =
|
|
291
|
+
const dir = join3(homedir(), ".config", "q-ring");
|
|
275
292
|
if (!existsSync2(dir)) {
|
|
276
293
|
mkdirSync(dir, { recursive: true });
|
|
277
294
|
}
|
|
278
295
|
return dir;
|
|
279
296
|
}
|
|
280
297
|
function getAuditPath() {
|
|
281
|
-
return
|
|
298
|
+
return join3(getAuditDir(), "audit.jsonl");
|
|
282
299
|
}
|
|
283
300
|
function getLastLineHash() {
|
|
284
301
|
const path = getAuditPath();
|
|
@@ -363,7 +380,7 @@ function verifyAuditChain() {
|
|
|
363
380
|
if (!existsSync2(path)) {
|
|
364
381
|
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
365
382
|
}
|
|
366
|
-
const lines =
|
|
383
|
+
const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
|
|
367
384
|
if (lines.length === 0) {
|
|
368
385
|
return { totalEvents: 0, validEvents: 0, intact: true };
|
|
369
386
|
}
|
|
@@ -401,7 +418,7 @@ function verifyAuditChain() {
|
|
|
401
418
|
function exportAudit(opts = {}) {
|
|
402
419
|
const path = getAuditPath();
|
|
403
420
|
if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
|
|
404
|
-
const lines =
|
|
421
|
+
const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
|
|
405
422
|
let events = lines.map((l) => {
|
|
406
423
|
try {
|
|
407
424
|
return JSON.parse(l);
|
|
@@ -466,15 +483,15 @@ function detectAnomalies(key) {
|
|
|
466
483
|
}
|
|
467
484
|
|
|
468
485
|
// src/core/entanglement.ts
|
|
469
|
-
import { existsSync as existsSync3, readFileSync as
|
|
470
|
-
import { join as
|
|
486
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
487
|
+
import { join as join4 } from "path";
|
|
471
488
|
import { homedir as homedir2 } from "os";
|
|
472
489
|
function getRegistryPath() {
|
|
473
|
-
const dir =
|
|
490
|
+
const dir = join4(homedir2(), ".config", "q-ring");
|
|
474
491
|
if (!existsSync3(dir)) {
|
|
475
492
|
mkdirSync2(dir, { recursive: true });
|
|
476
493
|
}
|
|
477
|
-
return
|
|
494
|
+
return join4(dir, "entanglement.json");
|
|
478
495
|
}
|
|
479
496
|
function loadRegistry() {
|
|
480
497
|
const path = getRegistryPath();
|
|
@@ -482,7 +499,7 @@ function loadRegistry() {
|
|
|
482
499
|
return { pairs: [] };
|
|
483
500
|
}
|
|
484
501
|
try {
|
|
485
|
-
return JSON.parse(
|
|
502
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
486
503
|
} catch {
|
|
487
504
|
return { pairs: [] };
|
|
488
505
|
}
|
|
@@ -528,67 +545,9 @@ function listEntanglements() {
|
|
|
528
545
|
return loadRegistry().pairs;
|
|
529
546
|
}
|
|
530
547
|
|
|
531
|
-
// src/core/keyring.ts
|
|
532
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
533
|
-
import { homedir as homedir5 } from "os";
|
|
534
|
-
import { join as join6 } from "path";
|
|
535
|
-
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
536
|
-
|
|
537
|
-
// src/utils/hash.ts
|
|
538
|
-
import { createHash as createHash2 } from "crypto";
|
|
539
|
-
function hashProjectPath(projectPath) {
|
|
540
|
-
return createHash2("sha256").update(projectPath).digest("hex").slice(0, 12);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// src/core/scope.ts
|
|
544
|
-
var SERVICE_PREFIX = "q-ring";
|
|
545
|
-
function globalService() {
|
|
546
|
-
return `${SERVICE_PREFIX}:global`;
|
|
547
|
-
}
|
|
548
|
-
function projectService(projectPath) {
|
|
549
|
-
const hash = hashProjectPath(projectPath);
|
|
550
|
-
return `${SERVICE_PREFIX}:project:${hash}`;
|
|
551
|
-
}
|
|
552
|
-
function teamService(teamId) {
|
|
553
|
-
return `${SERVICE_PREFIX}:team:${teamId}`;
|
|
554
|
-
}
|
|
555
|
-
function orgService(orgId) {
|
|
556
|
-
return `${SERVICE_PREFIX}:org:${orgId}`;
|
|
557
|
-
}
|
|
558
|
-
function resolveScope(opts) {
|
|
559
|
-
const { scope, projectPath, teamId, orgId } = opts;
|
|
560
|
-
if (scope === "global") {
|
|
561
|
-
return [{ scope: "global", service: globalService() }];
|
|
562
|
-
}
|
|
563
|
-
if (scope === "project") {
|
|
564
|
-
if (!projectPath) throw new Error("Project path is required for project scope");
|
|
565
|
-
return [{ scope: "project", service: projectService(projectPath), projectPath }];
|
|
566
|
-
}
|
|
567
|
-
if (scope === "team") {
|
|
568
|
-
if (!teamId) throw new Error("Team ID is required for team scope");
|
|
569
|
-
return [{ scope: "team", service: teamService(teamId), teamId }];
|
|
570
|
-
}
|
|
571
|
-
if (scope === "org") {
|
|
572
|
-
if (!orgId) throw new Error("Org ID is required for org scope");
|
|
573
|
-
return [{ scope: "org", service: orgService(orgId), orgId }];
|
|
574
|
-
}
|
|
575
|
-
const chain = [];
|
|
576
|
-
if (projectPath) {
|
|
577
|
-
chain.push({ scope: "project", service: projectService(projectPath), projectPath });
|
|
578
|
-
}
|
|
579
|
-
if (teamId) {
|
|
580
|
-
chain.push({ scope: "team", service: teamService(teamId), teamId });
|
|
581
|
-
}
|
|
582
|
-
if (orgId) {
|
|
583
|
-
chain.push({ scope: "org", service: orgService(orgId), orgId });
|
|
584
|
-
}
|
|
585
|
-
chain.push({ scope: "global", service: globalService() });
|
|
586
|
-
return chain;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
548
|
// src/core/hooks.ts
|
|
590
|
-
import { existsSync as existsSync4, readFileSync as
|
|
591
|
-
import { join as
|
|
549
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
550
|
+
import { join as join5 } from "path";
|
|
592
551
|
import { homedir as homedir3 } from "os";
|
|
593
552
|
import { execFile, spawn } from "child_process";
|
|
594
553
|
import { randomUUID } from "crypto";
|
|
@@ -678,16 +637,16 @@ function httpRequest(opts) {
|
|
|
678
637
|
import { lookup } from "dns/promises";
|
|
679
638
|
import * as dns from "dns";
|
|
680
639
|
import { isIPv4, isIPv6 } from "net";
|
|
681
|
-
function lookupAddressesSync(
|
|
640
|
+
function lookupAddressesSync(hostname2) {
|
|
682
641
|
const lookupSync2 = dns.lookupSync;
|
|
683
|
-
return lookupSync2(
|
|
642
|
+
return lookupSync2(hostname2, { all: true });
|
|
684
643
|
}
|
|
685
|
-
function isHostnameIpLiteral(
|
|
686
|
-
if (isIPv4(
|
|
687
|
-
if (
|
|
688
|
-
return isIPv6(
|
|
644
|
+
function isHostnameIpLiteral(hostname2) {
|
|
645
|
+
if (isIPv4(hostname2)) return true;
|
|
646
|
+
if (hostname2.startsWith("[") && hostname2.endsWith("]")) {
|
|
647
|
+
return isIPv6(hostname2.slice(1, -1));
|
|
689
648
|
}
|
|
690
|
-
return isIPv6(
|
|
649
|
+
return isIPv6(hostname2);
|
|
691
650
|
}
|
|
692
651
|
function isPrivateIP(ip) {
|
|
693
652
|
const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
|
|
@@ -709,14 +668,14 @@ async function checkSSRF(url) {
|
|
|
709
668
|
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
710
669
|
try {
|
|
711
670
|
const parsed = new URL(url);
|
|
712
|
-
const
|
|
713
|
-
if (isPrivateIP(
|
|
714
|
-
return `Blocked: URL resolves to private address (${
|
|
671
|
+
const hostname2 = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
672
|
+
if (isPrivateIP(hostname2)) {
|
|
673
|
+
return `Blocked: URL resolves to private address (${hostname2}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
715
674
|
}
|
|
716
|
-
const results = await lookup(
|
|
675
|
+
const results = await lookup(hostname2, { all: true });
|
|
717
676
|
for (const { address } of results) {
|
|
718
677
|
if (isPrivateIP(address)) {
|
|
719
|
-
return `Blocked: URL "${
|
|
678
|
+
return `Blocked: URL "${hostname2}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
720
679
|
}
|
|
721
680
|
}
|
|
722
681
|
} catch {
|
|
@@ -730,28 +689,28 @@ function checkJitHttpProvisionUrl(url) {
|
|
|
730
689
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
731
690
|
return "Blocked: JIT HTTP provider only allows http: or https: URLs.";
|
|
732
691
|
}
|
|
733
|
-
const
|
|
734
|
-
if (!
|
|
692
|
+
const hostname2 = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
693
|
+
if (!hostname2) {
|
|
735
694
|
return "Blocked: empty hostname in JIT URL.";
|
|
736
695
|
}
|
|
737
|
-
if (isPrivateIP(
|
|
738
|
-
return `Blocked: URL resolves to private address (${
|
|
696
|
+
if (isPrivateIP(hostname2)) {
|
|
697
|
+
return `Blocked: URL resolves to private address (${hostname2}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
739
698
|
}
|
|
740
|
-
if (isHostnameIpLiteral(
|
|
699
|
+
if (isHostnameIpLiteral(hostname2)) {
|
|
741
700
|
return null;
|
|
742
701
|
}
|
|
743
702
|
let results;
|
|
744
703
|
try {
|
|
745
|
-
results = lookupAddressesSync(
|
|
704
|
+
results = lookupAddressesSync(hostname2);
|
|
746
705
|
} catch {
|
|
747
|
-
return `Blocked: DNS resolution failed for "${
|
|
706
|
+
return `Blocked: DNS resolution failed for "${hostname2}" (JIT HTTP provisioning fails closed).`;
|
|
748
707
|
}
|
|
749
708
|
if (!results.length) {
|
|
750
|
-
return `Blocked: DNS returned no addresses for "${
|
|
709
|
+
return `Blocked: DNS returned no addresses for "${hostname2}".`;
|
|
751
710
|
}
|
|
752
711
|
for (const { address } of results) {
|
|
753
712
|
if (isPrivateIP(address)) {
|
|
754
|
-
return `Blocked: URL "${
|
|
713
|
+
return `Blocked: URL "${hostname2}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
755
714
|
}
|
|
756
715
|
}
|
|
757
716
|
} catch {
|
|
@@ -762,11 +721,11 @@ function checkJitHttpProvisionUrl(url) {
|
|
|
762
721
|
|
|
763
722
|
// src/core/hooks.ts
|
|
764
723
|
function getRegistryPath2() {
|
|
765
|
-
const dir =
|
|
724
|
+
const dir = join5(homedir3(), ".config", "q-ring");
|
|
766
725
|
if (!existsSync4(dir)) {
|
|
767
726
|
mkdirSync3(dir, { recursive: true });
|
|
768
727
|
}
|
|
769
|
-
return
|
|
728
|
+
return join5(dir, "hooks.json");
|
|
770
729
|
}
|
|
771
730
|
function loadRegistry2() {
|
|
772
731
|
const path = getRegistryPath2();
|
|
@@ -774,7 +733,7 @@ function loadRegistry2() {
|
|
|
774
733
|
return { hooks: [] };
|
|
775
734
|
}
|
|
776
735
|
try {
|
|
777
|
-
return JSON.parse(
|
|
736
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
778
737
|
} catch {
|
|
779
738
|
return { hooks: [] };
|
|
780
739
|
}
|
|
@@ -1043,16 +1002,16 @@ async function fireHooks(payload, tags) {
|
|
|
1043
1002
|
}
|
|
1044
1003
|
|
|
1045
1004
|
// src/core/approval.ts
|
|
1046
|
-
import { existsSync as existsSync5, readFileSync as
|
|
1047
|
-
import { join as
|
|
1005
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
1006
|
+
import { join as join6 } from "path";
|
|
1048
1007
|
import { homedir as homedir4 } from "os";
|
|
1049
1008
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1050
1009
|
function getHmacSecret() {
|
|
1051
|
-
const dir =
|
|
1052
|
-
const secretPath =
|
|
1010
|
+
const dir = join6(homedir4(), ".config", "q-ring");
|
|
1011
|
+
const secretPath = join6(dir, ".approval-key");
|
|
1053
1012
|
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
1054
1013
|
if (existsSync5(secretPath)) {
|
|
1055
|
-
return
|
|
1014
|
+
return readFileSync6(secretPath, "utf8").trim();
|
|
1056
1015
|
}
|
|
1057
1016
|
const secret = randomBytes(32).toString("hex");
|
|
1058
1017
|
writeFileSync3(secretPath, secret, { mode: 384 });
|
|
@@ -1084,11 +1043,11 @@ function verifyHmac(entry) {
|
|
|
1084
1043
|
}
|
|
1085
1044
|
}
|
|
1086
1045
|
function getRegistryPath3() {
|
|
1087
|
-
const dir =
|
|
1046
|
+
const dir = join6(homedir4(), ".config", "q-ring");
|
|
1088
1047
|
if (!existsSync5(dir)) {
|
|
1089
1048
|
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
1090
1049
|
}
|
|
1091
|
-
return
|
|
1050
|
+
return join6(dir, "approvals.json");
|
|
1092
1051
|
}
|
|
1093
1052
|
function loadRegistry3() {
|
|
1094
1053
|
const path = getRegistryPath3();
|
|
@@ -1096,7 +1055,7 @@ function loadRegistry3() {
|
|
|
1096
1055
|
return { approvals: [] };
|
|
1097
1056
|
}
|
|
1098
1057
|
try {
|
|
1099
|
-
return JSON.parse(
|
|
1058
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1100
1059
|
} catch {
|
|
1101
1060
|
return { approvals: [] };
|
|
1102
1061
|
}
|
|
@@ -1168,6 +1127,176 @@ function listApprovals() {
|
|
|
1168
1127
|
}));
|
|
1169
1128
|
}
|
|
1170
1129
|
|
|
1130
|
+
// src/core/policy.ts
|
|
1131
|
+
var cachedPolicy = null;
|
|
1132
|
+
function loadPolicy(projectPath) {
|
|
1133
|
+
const pp = projectPath ?? process.cwd();
|
|
1134
|
+
if (cachedPolicy && cachedPolicy.path === pp) {
|
|
1135
|
+
return cachedPolicy.policy;
|
|
1136
|
+
}
|
|
1137
|
+
const config = readProjectConfig(pp);
|
|
1138
|
+
const policy = config?.policy ?? {};
|
|
1139
|
+
cachedPolicy = { path: pp, policy };
|
|
1140
|
+
return policy;
|
|
1141
|
+
}
|
|
1142
|
+
function checkSecretLifecyclePolicy(input, projectPath) {
|
|
1143
|
+
const policy = loadPolicy(projectPath);
|
|
1144
|
+
if (!policy.secrets) return { allowed: true, policySource: "no-policy" };
|
|
1145
|
+
const s = policy.secrets;
|
|
1146
|
+
if (s.maxTtlSeconds != null && input.ttlSeconds != null && input.ttlSeconds > s.maxTtlSeconds) {
|
|
1147
|
+
return {
|
|
1148
|
+
allowed: false,
|
|
1149
|
+
reason: `TTL ${input.ttlSeconds}s exceeds policy maximum ${s.maxTtlSeconds}s`,
|
|
1150
|
+
policySource: ".q-ring.json policy.secrets.maxTtlSeconds"
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
if (s.requireApprovalForTags?.length && input.tags?.length) {
|
|
1154
|
+
const hit = input.tags.find((t) => s.requireApprovalForTags.includes(t));
|
|
1155
|
+
if (hit && !input.requiresApproval) {
|
|
1156
|
+
return {
|
|
1157
|
+
allowed: false,
|
|
1158
|
+
reason: `Tag "${hit}" requires explicit approval metadata (set requiresApproval / --requires-approval)`,
|
|
1159
|
+
policySource: ".q-ring.json policy.secrets.requireApprovalForTags"
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (s.requireRotationFormatForTags?.length && input.tags?.length) {
|
|
1164
|
+
const hit = input.tags.find((t) => s.requireRotationFormatForTags.includes(t));
|
|
1165
|
+
if (hit && !input.rotationFormat) {
|
|
1166
|
+
return {
|
|
1167
|
+
allowed: false,
|
|
1168
|
+
reason: `Tag "${hit}" requires a rotationFormat to be set`,
|
|
1169
|
+
policySource: ".q-ring.json policy.secrets.requireRotationFormatForTags"
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1174
|
+
}
|
|
1175
|
+
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1176
|
+
const policy = loadPolicy(projectPath);
|
|
1177
|
+
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
1178
|
+
if (policy.mcp.deniedKeys?.includes(key)) {
|
|
1179
|
+
return {
|
|
1180
|
+
allowed: false,
|
|
1181
|
+
reason: `Key "${key}" is denied by project policy`,
|
|
1182
|
+
policySource: ".q-ring.json policy.mcp.deniedKeys"
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
|
|
1186
|
+
return {
|
|
1187
|
+
allowed: false,
|
|
1188
|
+
reason: `Key "${key}" is not in the readable keys allowlist`,
|
|
1189
|
+
policySource: ".q-ring.json policy.mcp.readableKeys"
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
if (tags && policy.mcp.deniedTags) {
|
|
1193
|
+
const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
|
|
1194
|
+
if (blocked) {
|
|
1195
|
+
return {
|
|
1196
|
+
allowed: false,
|
|
1197
|
+
reason: `Tag "${blocked}" is denied by project policy`,
|
|
1198
|
+
policySource: ".q-ring.json policy.mcp.deniedTags"
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1203
|
+
}
|
|
1204
|
+
function checkExecPolicy(command, projectPath) {
|
|
1205
|
+
const policy = loadPolicy(projectPath);
|
|
1206
|
+
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1207
|
+
if (policy.exec.denyCommands) {
|
|
1208
|
+
const denied = policy.exec.denyCommands.find((d) => command.includes(d));
|
|
1209
|
+
if (denied) {
|
|
1210
|
+
return {
|
|
1211
|
+
allowed: false,
|
|
1212
|
+
reason: `Command containing "${denied}" is denied by project policy`,
|
|
1213
|
+
policySource: ".q-ring.json policy.exec.denyCommands"
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (policy.exec.allowCommands) {
|
|
1218
|
+
const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
|
|
1219
|
+
if (!allowed) {
|
|
1220
|
+
return {
|
|
1221
|
+
allowed: false,
|
|
1222
|
+
reason: `Command "${command}" is not in the exec allowlist`,
|
|
1223
|
+
policySource: ".q-ring.json policy.exec.allowCommands"
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1228
|
+
}
|
|
1229
|
+
function getExecMaxRuntime(projectPath) {
|
|
1230
|
+
return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
|
|
1231
|
+
}
|
|
1232
|
+
function getPolicySummary(projectPath) {
|
|
1233
|
+
const policy = loadPolicy(projectPath);
|
|
1234
|
+
return {
|
|
1235
|
+
hasMcpPolicy: !!policy.mcp,
|
|
1236
|
+
hasExecPolicy: !!policy.exec,
|
|
1237
|
+
hasSecretPolicy: !!policy.secrets,
|
|
1238
|
+
details: policy
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// src/core/keyring.ts
|
|
1243
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
1244
|
+
import { homedir as homedir5 } from "os";
|
|
1245
|
+
import { join as join7 } from "path";
|
|
1246
|
+
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
1247
|
+
|
|
1248
|
+
// src/utils/hash.ts
|
|
1249
|
+
import { createHash as createHash2 } from "crypto";
|
|
1250
|
+
function hashProjectPath(projectPath) {
|
|
1251
|
+
return createHash2("sha256").update(projectPath).digest("hex").slice(0, 12);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/core/scope.ts
|
|
1255
|
+
var SERVICE_PREFIX = "q-ring";
|
|
1256
|
+
function globalService() {
|
|
1257
|
+
return `${SERVICE_PREFIX}:global`;
|
|
1258
|
+
}
|
|
1259
|
+
function projectService(projectPath) {
|
|
1260
|
+
const hash = hashProjectPath(projectPath);
|
|
1261
|
+
return `${SERVICE_PREFIX}:project:${hash}`;
|
|
1262
|
+
}
|
|
1263
|
+
function teamService(teamId) {
|
|
1264
|
+
return `${SERVICE_PREFIX}:team:${teamId}`;
|
|
1265
|
+
}
|
|
1266
|
+
function orgService(orgId) {
|
|
1267
|
+
return `${SERVICE_PREFIX}:org:${orgId}`;
|
|
1268
|
+
}
|
|
1269
|
+
function resolveScope(opts) {
|
|
1270
|
+
const { scope, projectPath, teamId, orgId } = opts;
|
|
1271
|
+
if (scope === "global") {
|
|
1272
|
+
return [{ scope: "global", service: globalService() }];
|
|
1273
|
+
}
|
|
1274
|
+
if (scope === "project") {
|
|
1275
|
+
if (!projectPath) throw new Error("Project path is required for project scope");
|
|
1276
|
+
return [{ scope: "project", service: projectService(projectPath), projectPath }];
|
|
1277
|
+
}
|
|
1278
|
+
if (scope === "team") {
|
|
1279
|
+
if (!teamId) throw new Error("Team ID is required for team scope");
|
|
1280
|
+
return [{ scope: "team", service: teamService(teamId), teamId }];
|
|
1281
|
+
}
|
|
1282
|
+
if (scope === "org") {
|
|
1283
|
+
if (!orgId) throw new Error("Org ID is required for org scope");
|
|
1284
|
+
return [{ scope: "org", service: orgService(orgId), orgId }];
|
|
1285
|
+
}
|
|
1286
|
+
const chain = [];
|
|
1287
|
+
if (projectPath) {
|
|
1288
|
+
chain.push({ scope: "project", service: projectService(projectPath), projectPath });
|
|
1289
|
+
}
|
|
1290
|
+
if (teamId) {
|
|
1291
|
+
chain.push({ scope: "team", service: teamService(teamId), teamId });
|
|
1292
|
+
}
|
|
1293
|
+
if (orgId) {
|
|
1294
|
+
chain.push({ scope: "org", service: orgService(orgId), orgId });
|
|
1295
|
+
}
|
|
1296
|
+
chain.push({ scope: "global", service: globalService() });
|
|
1297
|
+
return chain;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1171
1300
|
// src/core/provision.ts
|
|
1172
1301
|
import { execFileSync, spawnSync } from "child_process";
|
|
1173
1302
|
import { z as z2 } from "zod";
|
|
@@ -1315,124 +1444,12 @@ var registry = new ProvisionRegistry();
|
|
|
1315
1444
|
registry.register(awsStsProvider);
|
|
1316
1445
|
registry.register(httpProvider);
|
|
1317
1446
|
|
|
1318
|
-
// src/core/policy.ts
|
|
1319
|
-
var cachedPolicy = null;
|
|
1320
|
-
function loadPolicy(projectPath) {
|
|
1321
|
-
const pp = projectPath ?? process.cwd();
|
|
1322
|
-
if (cachedPolicy && cachedPolicy.path === pp) {
|
|
1323
|
-
return cachedPolicy.policy;
|
|
1324
|
-
}
|
|
1325
|
-
const config = readProjectConfig(pp);
|
|
1326
|
-
const policy = config?.policy ?? {};
|
|
1327
|
-
cachedPolicy = { path: pp, policy };
|
|
1328
|
-
return policy;
|
|
1329
|
-
}
|
|
1330
|
-
function checkSecretLifecyclePolicy(input, projectPath) {
|
|
1331
|
-
const policy = loadPolicy(projectPath);
|
|
1332
|
-
if (!policy.secrets) return { allowed: true, policySource: "no-policy" };
|
|
1333
|
-
const s = policy.secrets;
|
|
1334
|
-
if (s.maxTtlSeconds != null && input.ttlSeconds != null && input.ttlSeconds > s.maxTtlSeconds) {
|
|
1335
|
-
return {
|
|
1336
|
-
allowed: false,
|
|
1337
|
-
reason: `TTL ${input.ttlSeconds}s exceeds policy maximum ${s.maxTtlSeconds}s`,
|
|
1338
|
-
policySource: ".q-ring.json policy.secrets.maxTtlSeconds"
|
|
1339
|
-
};
|
|
1340
|
-
}
|
|
1341
|
-
if (s.requireApprovalForTags?.length && input.tags?.length) {
|
|
1342
|
-
const hit = input.tags.find((t) => s.requireApprovalForTags.includes(t));
|
|
1343
|
-
if (hit && !input.requiresApproval) {
|
|
1344
|
-
return {
|
|
1345
|
-
allowed: false,
|
|
1346
|
-
reason: `Tag "${hit}" requires explicit approval metadata (set requiresApproval / --requires-approval)`,
|
|
1347
|
-
policySource: ".q-ring.json policy.secrets.requireApprovalForTags"
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
if (s.requireRotationFormatForTags?.length && input.tags?.length) {
|
|
1352
|
-
const hit = input.tags.find((t) => s.requireRotationFormatForTags.includes(t));
|
|
1353
|
-
if (hit && !input.rotationFormat) {
|
|
1354
|
-
return {
|
|
1355
|
-
allowed: false,
|
|
1356
|
-
reason: `Tag "${hit}" requires a rotationFormat to be set`,
|
|
1357
|
-
policySource: ".q-ring.json policy.secrets.requireRotationFormatForTags"
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
return { allowed: true, policySource: ".q-ring.json" };
|
|
1362
|
-
}
|
|
1363
|
-
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1364
|
-
const policy = loadPolicy(projectPath);
|
|
1365
|
-
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
1366
|
-
if (policy.mcp.deniedKeys?.includes(key)) {
|
|
1367
|
-
return {
|
|
1368
|
-
allowed: false,
|
|
1369
|
-
reason: `Key "${key}" is denied by project policy`,
|
|
1370
|
-
policySource: ".q-ring.json policy.mcp.deniedKeys"
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
|
|
1374
|
-
return {
|
|
1375
|
-
allowed: false,
|
|
1376
|
-
reason: `Key "${key}" is not in the readable keys allowlist`,
|
|
1377
|
-
policySource: ".q-ring.json policy.mcp.readableKeys"
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
if (tags && policy.mcp.deniedTags) {
|
|
1381
|
-
const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
|
|
1382
|
-
if (blocked) {
|
|
1383
|
-
return {
|
|
1384
|
-
allowed: false,
|
|
1385
|
-
reason: `Tag "${blocked}" is denied by project policy`,
|
|
1386
|
-
policySource: ".q-ring.json policy.mcp.deniedTags"
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
return { allowed: true, policySource: ".q-ring.json" };
|
|
1391
|
-
}
|
|
1392
|
-
function checkExecPolicy(command, projectPath) {
|
|
1393
|
-
const policy = loadPolicy(projectPath);
|
|
1394
|
-
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1395
|
-
if (policy.exec.denyCommands) {
|
|
1396
|
-
const denied = policy.exec.denyCommands.find((d) => command.includes(d));
|
|
1397
|
-
if (denied) {
|
|
1398
|
-
return {
|
|
1399
|
-
allowed: false,
|
|
1400
|
-
reason: `Command containing "${denied}" is denied by project policy`,
|
|
1401
|
-
policySource: ".q-ring.json policy.exec.denyCommands"
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
if (policy.exec.allowCommands) {
|
|
1406
|
-
const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
|
|
1407
|
-
if (!allowed) {
|
|
1408
|
-
return {
|
|
1409
|
-
allowed: false,
|
|
1410
|
-
reason: `Command "${command}" is not in the exec allowlist`,
|
|
1411
|
-
policySource: ".q-ring.json policy.exec.allowCommands"
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
return { allowed: true, policySource: ".q-ring.json" };
|
|
1416
|
-
}
|
|
1417
|
-
function getExecMaxRuntime(projectPath) {
|
|
1418
|
-
return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
|
|
1419
|
-
}
|
|
1420
|
-
function getPolicySummary(projectPath) {
|
|
1421
|
-
const policy = loadPolicy(projectPath);
|
|
1422
|
-
return {
|
|
1423
|
-
hasMcpPolicy: !!policy.mcp,
|
|
1424
|
-
hasExecPolicy: !!policy.exec,
|
|
1425
|
-
hasSecretPolicy: !!policy.secrets,
|
|
1426
|
-
details: policy
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
1447
|
// src/core/keyring.ts
|
|
1431
1448
|
function withJitEnvelopeLock(service, key, fn) {
|
|
1432
|
-
const dir =
|
|
1449
|
+
const dir = join7(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1433
1450
|
mkdirSync5(dir, { recursive: true });
|
|
1434
1451
|
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1435
|
-
const lockPath =
|
|
1452
|
+
const lockPath = join7(dir, `${safe}.lock`);
|
|
1436
1453
|
const deadline = Date.now() + 8e3;
|
|
1437
1454
|
while (Date.now() < deadline) {
|
|
1438
1455
|
try {
|
|
@@ -1945,7 +1962,125 @@ function tunnelList() {
|
|
|
1945
1962
|
return result;
|
|
1946
1963
|
}
|
|
1947
1964
|
|
|
1965
|
+
// src/core/memory.ts
|
|
1966
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1967
|
+
import { join as join8 } from "path";
|
|
1968
|
+
import { homedir as homedir6, hostname, userInfo } from "os";
|
|
1969
|
+
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes3 } from "crypto";
|
|
1970
|
+
import { Entry as Entry2 } from "@napi-rs/keyring";
|
|
1971
|
+
var MEMORY_FILE = "agent-memory.enc";
|
|
1972
|
+
var KEYRING_SERVICE = "qring-memory-key";
|
|
1973
|
+
var KEYRING_ACCOUNT = "encryption-key";
|
|
1974
|
+
function getMemoryDir() {
|
|
1975
|
+
const dir = join8(homedir6(), ".config", "q-ring");
|
|
1976
|
+
if (!existsSync6(dir)) {
|
|
1977
|
+
mkdirSync6(dir, { recursive: true });
|
|
1978
|
+
}
|
|
1979
|
+
return dir;
|
|
1980
|
+
}
|
|
1981
|
+
function getMemoryPath() {
|
|
1982
|
+
return join8(getMemoryDir(), MEMORY_FILE);
|
|
1983
|
+
}
|
|
1984
|
+
function deriveLegacyKey() {
|
|
1985
|
+
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
1986
|
+
return createHash3("sha256").update(fingerprint).digest();
|
|
1987
|
+
}
|
|
1988
|
+
function getOrCreateKey() {
|
|
1989
|
+
try {
|
|
1990
|
+
const entry = new Entry2(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
1991
|
+
const stored = entry.getPassword();
|
|
1992
|
+
if (stored) return Buffer.from(stored, "base64");
|
|
1993
|
+
const key = randomBytes3(32);
|
|
1994
|
+
entry.setPassword(key.toString("base64"));
|
|
1995
|
+
return key;
|
|
1996
|
+
} catch {
|
|
1997
|
+
console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
|
|
1998
|
+
return deriveLegacyKey();
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
function encryptWith(data, key) {
|
|
2002
|
+
const iv = randomBytes3(12);
|
|
2003
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
2004
|
+
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
2005
|
+
const tag = cipher.getAuthTag();
|
|
2006
|
+
return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
|
|
2007
|
+
}
|
|
2008
|
+
function decryptWith(blob, key) {
|
|
2009
|
+
const parts = blob.split(":");
|
|
2010
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted format");
|
|
2011
|
+
const iv = Buffer.from(parts[0], "base64");
|
|
2012
|
+
const tag = Buffer.from(parts[1], "base64");
|
|
2013
|
+
const encrypted = Buffer.from(parts[2], "base64");
|
|
2014
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
2015
|
+
decipher.setAuthTag(tag);
|
|
2016
|
+
return decipher.update(encrypted) + decipher.final("utf8");
|
|
2017
|
+
}
|
|
2018
|
+
function encrypt(data) {
|
|
2019
|
+
return encryptWith(data, getOrCreateKey());
|
|
2020
|
+
}
|
|
2021
|
+
function decrypt(blob) {
|
|
2022
|
+
const key = getOrCreateKey();
|
|
2023
|
+
try {
|
|
2024
|
+
return decryptWith(blob, key);
|
|
2025
|
+
} catch {
|
|
2026
|
+
const legacy = deriveLegacyKey();
|
|
2027
|
+
const plain = decryptWith(blob, legacy);
|
|
2028
|
+
writeFileSync5(getMemoryPath(), encryptWith(plain, key), "utf8");
|
|
2029
|
+
return plain;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
function loadStore() {
|
|
2033
|
+
const path = getMemoryPath();
|
|
2034
|
+
if (!existsSync6(path)) {
|
|
2035
|
+
return { entries: {} };
|
|
2036
|
+
}
|
|
2037
|
+
try {
|
|
2038
|
+
const raw = readFileSync7(path, "utf8");
|
|
2039
|
+
const decrypted = decrypt(raw);
|
|
2040
|
+
return JSON.parse(decrypted);
|
|
2041
|
+
} catch {
|
|
2042
|
+
return { entries: {} };
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function saveStore(store) {
|
|
2046
|
+
const json = JSON.stringify(store);
|
|
2047
|
+
const encrypted = encrypt(json);
|
|
2048
|
+
writeFileSync5(getMemoryPath(), encrypted, "utf8");
|
|
2049
|
+
}
|
|
2050
|
+
function remember(key, value) {
|
|
2051
|
+
const store = loadStore();
|
|
2052
|
+
store.entries[key] = {
|
|
2053
|
+
value,
|
|
2054
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2055
|
+
};
|
|
2056
|
+
saveStore(store);
|
|
2057
|
+
}
|
|
2058
|
+
function recall(key) {
|
|
2059
|
+
const store = loadStore();
|
|
2060
|
+
return store.entries[key]?.value ?? null;
|
|
2061
|
+
}
|
|
2062
|
+
function listMemory() {
|
|
2063
|
+
const store = loadStore();
|
|
2064
|
+
return Object.entries(store.entries).map(([key, entry]) => ({
|
|
2065
|
+
key,
|
|
2066
|
+
updatedAt: entry.updatedAt
|
|
2067
|
+
}));
|
|
2068
|
+
}
|
|
2069
|
+
function forget(key) {
|
|
2070
|
+
const store = loadStore();
|
|
2071
|
+
if (key in store.entries) {
|
|
2072
|
+
delete store.entries[key];
|
|
2073
|
+
saveStore(store);
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
2076
|
+
return false;
|
|
2077
|
+
}
|
|
2078
|
+
function clearMemory() {
|
|
2079
|
+
saveStore({ entries: {} });
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1948
2082
|
export {
|
|
2083
|
+
PACKAGE_VERSION,
|
|
1949
2084
|
checkDecay,
|
|
1950
2085
|
readProjectConfig,
|
|
1951
2086
|
collapseEnvironment,
|
|
@@ -1982,6 +2117,11 @@ export {
|
|
|
1982
2117
|
tunnelCreate,
|
|
1983
2118
|
tunnelRead,
|
|
1984
2119
|
tunnelDestroy,
|
|
1985
|
-
tunnelList
|
|
2120
|
+
tunnelList,
|
|
2121
|
+
remember,
|
|
2122
|
+
recall,
|
|
2123
|
+
listMemory,
|
|
2124
|
+
forget,
|
|
2125
|
+
clearMemory
|
|
1986
2126
|
};
|
|
1987
|
-
//# sourceMappingURL=chunk-
|
|
2127
|
+
//# sourceMappingURL=chunk-A4RZVP3P.js.map
|