@i4ctime/q-ring 0.9.7 → 0.10.1
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 +7 -0
- package/dist/{chunk-5JBU7TWN.js → chunk-E46HG5NM.js} +452 -97
- package/dist/chunk-E46HG5NM.js.map +1 -0
- package/dist/{chunk-WG4ZKN7Q.js → chunk-LLMO64FE.js} +453 -98
- package/dist/chunk-LLMO64FE.js.map +1 -0
- package/dist/{dashboard-JT5ZNLT5.js → dashboard-73QYM5FU.js} +3 -3
- package/dist/{dashboard-Q5OQRQCX.js.map → dashboard-73QYM5FU.js.map} +1 -1
- package/dist/{dashboard-Q5OQRQCX.js → dashboard-CMXRH4AQ.js} +3 -3
- package/dist/{dashboard-JT5ZNLT5.js.map → dashboard-CMXRH4AQ.js.map} +1 -1
- package/dist/index.js +2790 -2164
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1856 -1557
- package/dist/mcp.js.map +1 -1
- package/package.json +16 -3
- package/dist/chunk-5JBU7TWN.js.map +0 -1
- package/dist/chunk-WG4ZKN7Q.js.map +0 -1
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/core/envelope.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var EntanglementLinkSchema = z.object({
|
|
6
|
+
service: z.string(),
|
|
7
|
+
key: z.string()
|
|
8
|
+
});
|
|
9
|
+
var SecretMetadataSchema = z.object({
|
|
10
|
+
createdAt: z.string(),
|
|
11
|
+
updatedAt: z.string(),
|
|
12
|
+
expiresAt: z.string().optional(),
|
|
13
|
+
ttlSeconds: z.number().optional(),
|
|
14
|
+
description: z.string().optional(),
|
|
15
|
+
tags: z.array(z.string()).optional(),
|
|
16
|
+
entangled: z.array(EntanglementLinkSchema).optional(),
|
|
17
|
+
accessCount: z.number(),
|
|
18
|
+
lastAccessedAt: z.string().optional(),
|
|
19
|
+
ephemeral: z.boolean().optional(),
|
|
20
|
+
rotationFormat: z.string().optional(),
|
|
21
|
+
rotationPrefix: z.string().optional(),
|
|
22
|
+
provider: z.string().optional(),
|
|
23
|
+
validationUrl: z.string().optional(),
|
|
24
|
+
requiresApproval: z.boolean().optional(),
|
|
25
|
+
jitProvider: z.string().optional(),
|
|
26
|
+
jitExpiresAt: z.string().optional()
|
|
27
|
+
});
|
|
28
|
+
var QuantumEnvelopeSchema = z.object({
|
|
29
|
+
v: z.literal(1),
|
|
30
|
+
value: z.string().optional(),
|
|
31
|
+
states: z.record(z.string(), z.string()).optional(),
|
|
32
|
+
defaultEnv: z.string().optional(),
|
|
33
|
+
meta: SecretMetadataSchema
|
|
34
|
+
});
|
|
4
35
|
function createEnvelope(value, opts) {
|
|
5
36
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6
37
|
let expiresAt = opts?.expiresAt;
|
|
@@ -32,8 +63,9 @@ function createEnvelope(value, opts) {
|
|
|
32
63
|
function parseEnvelope(raw) {
|
|
33
64
|
try {
|
|
34
65
|
const parsed = JSON.parse(raw);
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
const r = QuantumEnvelopeSchema.safeParse(parsed);
|
|
67
|
+
if (r.success) {
|
|
68
|
+
return r.data;
|
|
37
69
|
}
|
|
38
70
|
} catch {
|
|
39
71
|
}
|
|
@@ -84,6 +116,15 @@ function checkDecay(envelope) {
|
|
|
84
116
|
const now = Date.now();
|
|
85
117
|
const expires = new Date(envelope.meta.expiresAt).getTime();
|
|
86
118
|
const created = new Date(envelope.meta.createdAt).getTime();
|
|
119
|
+
if (!Number.isFinite(expires) || !Number.isFinite(created)) {
|
|
120
|
+
return {
|
|
121
|
+
isExpired: true,
|
|
122
|
+
isStale: true,
|
|
123
|
+
lifetimePercent: 100,
|
|
124
|
+
secondsRemaining: null,
|
|
125
|
+
timeRemaining: "invalid date"
|
|
126
|
+
};
|
|
127
|
+
}
|
|
87
128
|
const totalLifetime = expires - created;
|
|
88
129
|
const elapsed = now - created;
|
|
89
130
|
const remaining = expires - now;
|
|
@@ -188,12 +229,15 @@ function collapseEnvironment(ctx = {}) {
|
|
|
188
229
|
}
|
|
189
230
|
return null;
|
|
190
231
|
}
|
|
232
|
+
var MAX_BRANCH_GLOB_PATTERN_LEN = 200;
|
|
233
|
+
var MAX_BRANCH_GLOB_REGEX_SOURCE = 400;
|
|
191
234
|
function matchGlob(branchMap, branch) {
|
|
192
235
|
for (const [pattern, env] of Object.entries(branchMap)) {
|
|
193
236
|
if (!pattern.includes("*")) continue;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
237
|
+
if (pattern.length > MAX_BRANCH_GLOB_PATTERN_LEN) continue;
|
|
238
|
+
const source = "^" + pattern.replace(/\*/g, ".*") + "$";
|
|
239
|
+
if (source.length > MAX_BRANCH_GLOB_REGEX_SOURCE) continue;
|
|
240
|
+
const regex = new RegExp(source);
|
|
197
241
|
if (regex.test(branch)) return env;
|
|
198
242
|
}
|
|
199
243
|
return void 0;
|
|
@@ -206,11 +250,27 @@ function mapEnvName(raw) {
|
|
|
206
250
|
}
|
|
207
251
|
|
|
208
252
|
// src/core/observer.ts
|
|
209
|
-
import {
|
|
253
|
+
import {
|
|
254
|
+
existsSync as existsSync2,
|
|
255
|
+
mkdirSync,
|
|
256
|
+
appendFileSync,
|
|
257
|
+
readFileSync as readFileSync2,
|
|
258
|
+
openSync,
|
|
259
|
+
fstatSync,
|
|
260
|
+
readSync,
|
|
261
|
+
closeSync,
|
|
262
|
+
statSync
|
|
263
|
+
} from "fs";
|
|
210
264
|
import { join as join2 } from "path";
|
|
211
265
|
import { homedir } from "os";
|
|
212
266
|
import { createHash } from "crypto";
|
|
213
267
|
function getAuditDir() {
|
|
268
|
+
if (process.env.QRING_AUDIT_DIR) {
|
|
269
|
+
if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
|
|
270
|
+
mkdirSync(process.env.QRING_AUDIT_DIR, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
return process.env.QRING_AUDIT_DIR;
|
|
273
|
+
}
|
|
214
274
|
const dir = join2(homedir(), ".config", "q-ring");
|
|
215
275
|
if (!existsSync2(dir)) {
|
|
216
276
|
mkdirSync(dir, { recursive: true });
|
|
@@ -256,11 +316,24 @@ function logAudit(event) {
|
|
|
256
316
|
} catch {
|
|
257
317
|
}
|
|
258
318
|
}
|
|
319
|
+
var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
|
|
259
320
|
function queryAudit(query = {}) {
|
|
260
321
|
const path = getAuditPath();
|
|
261
322
|
if (!existsSync2(path)) return [];
|
|
262
323
|
try {
|
|
263
|
-
const
|
|
324
|
+
const st = statSync(path);
|
|
325
|
+
const readStart = st.size > MAX_AUDIT_BYTES ? st.size - MAX_AUDIT_BYTES : 0;
|
|
326
|
+
const readLen = st.size > MAX_AUDIT_BYTES ? MAX_AUDIT_BYTES : st.size;
|
|
327
|
+
const buf = Buffer.alloc(readLen);
|
|
328
|
+
const fd = openSync(path, "r");
|
|
329
|
+
readSync(fd, buf, 0, readLen, readStart);
|
|
330
|
+
closeSync(fd);
|
|
331
|
+
let text = buf.toString("utf8");
|
|
332
|
+
if (readStart > 0) {
|
|
333
|
+
const firstNl = text.indexOf("\n");
|
|
334
|
+
if (firstNl !== -1) text = text.slice(firstNl + 1);
|
|
335
|
+
}
|
|
336
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
264
337
|
let events = lines.map((line) => {
|
|
265
338
|
try {
|
|
266
339
|
return JSON.parse(line);
|
|
@@ -393,7 +466,7 @@ function detectAnomalies(key) {
|
|
|
393
466
|
}
|
|
394
467
|
|
|
395
468
|
// src/core/entanglement.ts
|
|
396
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync
|
|
469
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
397
470
|
import { join as join3 } from "path";
|
|
398
471
|
import { homedir as homedir2 } from "os";
|
|
399
472
|
function getRegistryPath() {
|
|
@@ -415,7 +488,9 @@ function loadRegistry() {
|
|
|
415
488
|
}
|
|
416
489
|
}
|
|
417
490
|
function saveRegistry(registry2) {
|
|
418
|
-
|
|
491
|
+
writeFileSync(getRegistryPath(), JSON.stringify(registry2, null, 2), {
|
|
492
|
+
mode: 384
|
|
493
|
+
});
|
|
419
494
|
}
|
|
420
495
|
function entangle(source, target) {
|
|
421
496
|
const registry2 = loadRegistry();
|
|
@@ -454,6 +529,9 @@ function listEntanglements() {
|
|
|
454
529
|
}
|
|
455
530
|
|
|
456
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";
|
|
457
535
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
458
536
|
|
|
459
537
|
// src/utils/hash.ts
|
|
@@ -509,19 +587,18 @@ function resolveScope(opts) {
|
|
|
509
587
|
}
|
|
510
588
|
|
|
511
589
|
// src/core/hooks.ts
|
|
512
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as
|
|
590
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
513
591
|
import { join as join4 } from "path";
|
|
514
592
|
import { homedir as homedir3 } from "os";
|
|
515
|
-
import {
|
|
593
|
+
import { execFile, spawn } from "child_process";
|
|
516
594
|
import { randomUUID } from "crypto";
|
|
517
|
-
import { lookup } from "dns/promises";
|
|
518
595
|
|
|
519
596
|
// src/utils/http-request.ts
|
|
520
597
|
import { request as httpsRequest } from "https";
|
|
521
|
-
import { request as
|
|
598
|
+
import { request as httpRequestPlain } from "http";
|
|
522
599
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
523
600
|
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
524
|
-
function
|
|
601
|
+
function httpRequest(opts) {
|
|
525
602
|
const {
|
|
526
603
|
url,
|
|
527
604
|
method = "GET",
|
|
@@ -536,7 +613,7 @@ function httpRequest_(opts) {
|
|
|
536
613
|
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
537
614
|
return;
|
|
538
615
|
}
|
|
539
|
-
const reqFn = parsed.protocol === "https:" ? httpsRequest :
|
|
616
|
+
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
540
617
|
const reqHeaders = { ...headers };
|
|
541
618
|
if (body && !reqHeaders["Content-Length"]) {
|
|
542
619
|
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
@@ -597,6 +674,92 @@ function httpRequest_(opts) {
|
|
|
597
674
|
});
|
|
598
675
|
}
|
|
599
676
|
|
|
677
|
+
// src/core/ssrf.ts
|
|
678
|
+
import { lookup } from "dns/promises";
|
|
679
|
+
import * as dns from "dns";
|
|
680
|
+
import { isIPv4, isIPv6 } from "net";
|
|
681
|
+
function lookupAddressesSync(hostname) {
|
|
682
|
+
const lookupSync2 = dns.lookupSync;
|
|
683
|
+
return lookupSync2(hostname, { all: true });
|
|
684
|
+
}
|
|
685
|
+
function isHostnameIpLiteral(hostname) {
|
|
686
|
+
if (isIPv4(hostname)) return true;
|
|
687
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) {
|
|
688
|
+
return isIPv6(hostname.slice(1, -1));
|
|
689
|
+
}
|
|
690
|
+
return isIPv6(hostname);
|
|
691
|
+
}
|
|
692
|
+
function isPrivateIP(ip) {
|
|
693
|
+
const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
|
|
694
|
+
const ipv4Re = new RegExp(`^::ffff:(${octet}\\.${octet}\\.${octet}\\.${octet})$`, "i");
|
|
695
|
+
const ipv4Mapped = ip.match(ipv4Re);
|
|
696
|
+
if (ipv4Mapped) return isPrivateIP(ipv4Mapped[1]);
|
|
697
|
+
if (/^127\./.test(ip)) return true;
|
|
698
|
+
if (/^10\./.test(ip)) return true;
|
|
699
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
700
|
+
if (/^192\.168\./.test(ip)) return true;
|
|
701
|
+
if (/^169\.254\./.test(ip)) return true;
|
|
702
|
+
if (ip === "0.0.0.0") return true;
|
|
703
|
+
if (ip === "::1" || ip === "::") return true;
|
|
704
|
+
if (/^f[cd][0-9a-f]{2}:/i.test(ip)) return true;
|
|
705
|
+
if (/^fe80:/i.test(ip)) return true;
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
async function checkSSRF(url) {
|
|
709
|
+
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
710
|
+
try {
|
|
711
|
+
const parsed = new URL(url);
|
|
712
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
713
|
+
if (isPrivateIP(hostname)) {
|
|
714
|
+
return `Blocked: URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
715
|
+
}
|
|
716
|
+
const results = await lookup(hostname, { all: true });
|
|
717
|
+
for (const { address } of results) {
|
|
718
|
+
if (isPrivateIP(address)) {
|
|
719
|
+
return `Blocked: URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} catch {
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
function checkJitHttpProvisionUrl(url) {
|
|
727
|
+
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
728
|
+
try {
|
|
729
|
+
const parsed = new URL(url);
|
|
730
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
731
|
+
return "Blocked: JIT HTTP provider only allows http: or https: URLs.";
|
|
732
|
+
}
|
|
733
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
734
|
+
if (!hostname) {
|
|
735
|
+
return "Blocked: empty hostname in JIT URL.";
|
|
736
|
+
}
|
|
737
|
+
if (isPrivateIP(hostname)) {
|
|
738
|
+
return `Blocked: URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
739
|
+
}
|
|
740
|
+
if (isHostnameIpLiteral(hostname)) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
let results;
|
|
744
|
+
try {
|
|
745
|
+
results = lookupAddressesSync(hostname);
|
|
746
|
+
} catch {
|
|
747
|
+
return `Blocked: DNS resolution failed for "${hostname}" (JIT HTTP provisioning fails closed).`;
|
|
748
|
+
}
|
|
749
|
+
if (!results.length) {
|
|
750
|
+
return `Blocked: DNS returned no addresses for "${hostname}".`;
|
|
751
|
+
}
|
|
752
|
+
for (const { address } of results) {
|
|
753
|
+
if (isPrivateIP(address)) {
|
|
754
|
+
return `Blocked: URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
return "Blocked: malformed JIT HTTP URL.";
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
|
|
600
763
|
// src/core/hooks.ts
|
|
601
764
|
function getRegistryPath2() {
|
|
602
765
|
const dir = join4(homedir3(), ".config", "q-ring");
|
|
@@ -617,7 +780,9 @@ function loadRegistry2() {
|
|
|
617
780
|
}
|
|
618
781
|
}
|
|
619
782
|
function saveRegistry2(registry2) {
|
|
620
|
-
|
|
783
|
+
writeFileSync2(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
|
|
784
|
+
mode: 384
|
|
785
|
+
});
|
|
621
786
|
}
|
|
622
787
|
function registerHook(entry) {
|
|
623
788
|
const registry2 = loadRegistry2();
|
|
@@ -649,16 +814,22 @@ function matchesHook(hook, payload, tags) {
|
|
|
649
814
|
if (m.action?.length && !m.action.includes(payload.action)) return false;
|
|
650
815
|
if (m.key && m.key !== payload.key) return false;
|
|
651
816
|
if (m.keyPattern) {
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
"i"
|
|
655
|
-
);
|
|
817
|
+
const escaped = m.keyPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
818
|
+
const regex = new RegExp("^" + escaped + "$", "i");
|
|
656
819
|
if (!regex.test(payload.key)) return false;
|
|
657
820
|
}
|
|
658
821
|
if (m.tag && (!tags || !tags.includes(m.tag))) return false;
|
|
659
822
|
if (m.scope && m.scope !== payload.scope) return false;
|
|
660
823
|
return true;
|
|
661
824
|
}
|
|
825
|
+
var SAFE_PGREP_NAME = /^[a-zA-Z0-9._:/-]{1,256}$/;
|
|
826
|
+
function shellRunner() {
|
|
827
|
+
if (process.platform === "win32") {
|
|
828
|
+
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
829
|
+
return { file: comspec, args: ["/d", "/s", "/c"] };
|
|
830
|
+
}
|
|
831
|
+
return { file: "/bin/sh", args: ["-c"] };
|
|
832
|
+
}
|
|
662
833
|
function executeShell(command, payload) {
|
|
663
834
|
return new Promise((resolve) => {
|
|
664
835
|
const env = {
|
|
@@ -667,49 +838,35 @@ function executeShell(command, payload) {
|
|
|
667
838
|
QRING_HOOK_ACTION: payload.action,
|
|
668
839
|
QRING_HOOK_SCOPE: payload.scope
|
|
669
840
|
};
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
841
|
+
const { file, args } = shellRunner();
|
|
842
|
+
execFile(
|
|
843
|
+
file,
|
|
844
|
+
[...args, command],
|
|
845
|
+
{
|
|
846
|
+
timeout: 3e4,
|
|
847
|
+
env,
|
|
848
|
+
windowsHide: true,
|
|
849
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
850
|
+
encoding: "utf8"
|
|
851
|
+
},
|
|
852
|
+
(err, stdout) => {
|
|
853
|
+
if (err) {
|
|
854
|
+
resolve({
|
|
855
|
+
hookId: "",
|
|
856
|
+
success: false,
|
|
857
|
+
message: `Shell error: ${err.message}`
|
|
858
|
+
});
|
|
859
|
+
} else {
|
|
860
|
+
resolve({
|
|
861
|
+
hookId: "",
|
|
862
|
+
success: true,
|
|
863
|
+
message: (stdout ?? "").trim() || "OK"
|
|
864
|
+
});
|
|
865
|
+
}
|
|
675
866
|
}
|
|
676
|
-
|
|
867
|
+
);
|
|
677
868
|
});
|
|
678
869
|
}
|
|
679
|
-
function isPrivateIP(ip) {
|
|
680
|
-
const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
|
|
681
|
-
const ipv4Re = new RegExp(`^::ffff:(${octet}\\.${octet}\\.${octet}\\.${octet})$`, "i");
|
|
682
|
-
const ipv4Mapped = ip.match(ipv4Re);
|
|
683
|
-
if (ipv4Mapped) return isPrivateIP(ipv4Mapped[1]);
|
|
684
|
-
if (/^127\./.test(ip)) return true;
|
|
685
|
-
if (/^10\./.test(ip)) return true;
|
|
686
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
687
|
-
if (/^192\.168\./.test(ip)) return true;
|
|
688
|
-
if (/^169\.254\./.test(ip)) return true;
|
|
689
|
-
if (ip === "0.0.0.0") return true;
|
|
690
|
-
if (ip === "::1" || ip === "::") return true;
|
|
691
|
-
if (/^f[cd][0-9a-f]{2}:/i.test(ip)) return true;
|
|
692
|
-
if (/^fe80:/i.test(ip)) return true;
|
|
693
|
-
return false;
|
|
694
|
-
}
|
|
695
|
-
async function checkSSRF(url) {
|
|
696
|
-
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
697
|
-
try {
|
|
698
|
-
const parsed = new URL(url);
|
|
699
|
-
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
700
|
-
if (isPrivateIP(hostname)) {
|
|
701
|
-
return `Blocked: hook URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
702
|
-
}
|
|
703
|
-
const results = await lookup(hostname, { all: true });
|
|
704
|
-
for (const { address } of results) {
|
|
705
|
-
if (isPrivateIP(address)) {
|
|
706
|
-
return `Blocked: hook URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
} catch {
|
|
710
|
-
}
|
|
711
|
-
return null;
|
|
712
|
-
}
|
|
713
870
|
async function executeHttp(url, payload) {
|
|
714
871
|
const ssrfBlock = await checkSSRF(url);
|
|
715
872
|
if (ssrfBlock) {
|
|
@@ -724,7 +881,7 @@ async function executeHttp(url, payload) {
|
|
|
724
881
|
}
|
|
725
882
|
try {
|
|
726
883
|
const body = JSON.stringify(payload);
|
|
727
|
-
const res = await
|
|
884
|
+
const res = await httpRequest({
|
|
728
885
|
url,
|
|
729
886
|
method: "POST",
|
|
730
887
|
headers: {
|
|
@@ -749,19 +906,48 @@ async function executeHttp(url, payload) {
|
|
|
749
906
|
}
|
|
750
907
|
function executeSignal(target, signal = "SIGHUP") {
|
|
751
908
|
return new Promise((resolve) => {
|
|
752
|
-
const
|
|
753
|
-
if (
|
|
909
|
+
const trimmed = target.trim();
|
|
910
|
+
if (/^\d+$/.test(trimmed)) {
|
|
911
|
+
const pid = parseInt(trimmed, 10);
|
|
754
912
|
try {
|
|
755
913
|
process.kill(pid, signal);
|
|
756
|
-
resolve({
|
|
914
|
+
resolve({
|
|
915
|
+
hookId: "",
|
|
916
|
+
success: true,
|
|
917
|
+
message: `Signal ${signal} sent to PID ${pid}`
|
|
918
|
+
});
|
|
757
919
|
} catch (err) {
|
|
758
|
-
resolve({
|
|
920
|
+
resolve({
|
|
921
|
+
hookId: "",
|
|
922
|
+
success: false,
|
|
923
|
+
message: `Signal error: ${err instanceof Error ? err.message : String(err)}`
|
|
924
|
+
});
|
|
759
925
|
}
|
|
760
926
|
return;
|
|
761
927
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
928
|
+
if (!SAFE_PGREP_NAME.test(trimmed)) {
|
|
929
|
+
resolve({
|
|
930
|
+
hookId: "",
|
|
931
|
+
success: false,
|
|
932
|
+
message: "Signal target must be a numeric PID, or a process name matching /^[a-zA-Z0-9._:/-]{1,256}$/ (no spaces or shell metacharacters)."
|
|
933
|
+
});
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const child = spawn("pgrep", ["-f", trimmed], {
|
|
937
|
+
timeout: 5e3,
|
|
938
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
939
|
+
});
|
|
940
|
+
let stdout = "";
|
|
941
|
+
child.stdout.on("data", (d) => {
|
|
942
|
+
stdout += d.toString();
|
|
943
|
+
});
|
|
944
|
+
child.on("close", (code) => {
|
|
945
|
+
if (code !== 0 || !stdout.trim()) {
|
|
946
|
+
resolve({
|
|
947
|
+
hookId: "",
|
|
948
|
+
success: false,
|
|
949
|
+
message: `Process "${trimmed}" not found`
|
|
950
|
+
});
|
|
765
951
|
return;
|
|
766
952
|
}
|
|
767
953
|
const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
|
|
@@ -773,7 +959,18 @@ function executeSignal(target, signal = "SIGHUP") {
|
|
|
773
959
|
} catch {
|
|
774
960
|
}
|
|
775
961
|
}
|
|
776
|
-
resolve({
|
|
962
|
+
resolve({
|
|
963
|
+
hookId: "",
|
|
964
|
+
success: sent > 0,
|
|
965
|
+
message: `Signal ${signal} sent to ${sent} process(es)`
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
child.on("error", () => {
|
|
969
|
+
resolve({
|
|
970
|
+
hookId: "",
|
|
971
|
+
success: false,
|
|
972
|
+
message: `Process "${trimmed}" not found`
|
|
973
|
+
});
|
|
777
974
|
});
|
|
778
975
|
});
|
|
779
976
|
}
|
|
@@ -830,33 +1027,50 @@ async function fireHooks(payload, tags) {
|
|
|
830
1027
|
}
|
|
831
1028
|
|
|
832
1029
|
// src/core/approval.ts
|
|
833
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as
|
|
1030
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
834
1031
|
import { join as join5 } from "path";
|
|
835
1032
|
import { homedir as homedir4 } from "os";
|
|
836
|
-
import { createHmac, randomBytes } from "crypto";
|
|
1033
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
837
1034
|
function getHmacSecret() {
|
|
838
|
-
const secretPath = join5(homedir4(), ".config", "q-ring", ".approval-key");
|
|
839
1035
|
const dir = join5(homedir4(), ".config", "q-ring");
|
|
840
|
-
|
|
1036
|
+
const secretPath = join5(dir, ".approval-key");
|
|
1037
|
+
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
841
1038
|
if (existsSync5(secretPath)) {
|
|
842
1039
|
return readFileSync5(secretPath, "utf8").trim();
|
|
843
1040
|
}
|
|
844
1041
|
const secret = randomBytes(32).toString("hex");
|
|
845
|
-
|
|
1042
|
+
writeFileSync3(secretPath, secret, { mode: 384 });
|
|
846
1043
|
return secret;
|
|
847
1044
|
}
|
|
848
1045
|
function computeHmac(entry) {
|
|
849
|
-
const payload =
|
|
1046
|
+
const payload = [
|
|
1047
|
+
entry.id,
|
|
1048
|
+
entry.key,
|
|
1049
|
+
entry.scope,
|
|
1050
|
+
entry.reason,
|
|
1051
|
+
entry.grantedBy,
|
|
1052
|
+
entry.grantedAt,
|
|
1053
|
+
entry.expiresAt,
|
|
1054
|
+
entry.workspace ?? "",
|
|
1055
|
+
entry.sessionId ?? ""
|
|
1056
|
+
].join("|");
|
|
850
1057
|
return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
|
|
851
1058
|
}
|
|
852
1059
|
function verifyHmac(entry) {
|
|
853
1060
|
const expected = computeHmac(entry);
|
|
854
|
-
|
|
1061
|
+
const a = Buffer.from(expected, "utf8");
|
|
1062
|
+
const b = Buffer.from(entry.hmac, "utf8");
|
|
1063
|
+
if (a.length !== b.length) return false;
|
|
1064
|
+
try {
|
|
1065
|
+
return timingSafeEqual(a, b);
|
|
1066
|
+
} catch {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
855
1069
|
}
|
|
856
1070
|
function getRegistryPath3() {
|
|
857
1071
|
const dir = join5(homedir4(), ".config", "q-ring");
|
|
858
1072
|
if (!existsSync5(dir)) {
|
|
859
|
-
mkdirSync4(dir, { recursive: true });
|
|
1073
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
860
1074
|
}
|
|
861
1075
|
return join5(dir, "approvals.json");
|
|
862
1076
|
}
|
|
@@ -884,6 +1098,20 @@ function hasApproval(key, scope) {
|
|
|
884
1098
|
|
|
885
1099
|
// src/core/provision.ts
|
|
886
1100
|
import { execFileSync, spawnSync } from "child_process";
|
|
1101
|
+
import { z as z2 } from "zod";
|
|
1102
|
+
var AwsStsConfigSchema = z2.object({
|
|
1103
|
+
roleArn: z2.string(),
|
|
1104
|
+
sessionName: z2.string().optional(),
|
|
1105
|
+
durationSeconds: z2.number().optional()
|
|
1106
|
+
});
|
|
1107
|
+
var HttpJitConfigSchema = z2.object({
|
|
1108
|
+
url: z2.string(),
|
|
1109
|
+
method: z2.string().optional(),
|
|
1110
|
+
valuePath: z2.string().optional(),
|
|
1111
|
+
expiresInSeconds: z2.number().optional(),
|
|
1112
|
+
headers: z2.record(z2.string(), z2.string()).optional(),
|
|
1113
|
+
body: z2.unknown().optional()
|
|
1114
|
+
});
|
|
887
1115
|
var ProvisionRegistry = class {
|
|
888
1116
|
providers = /* @__PURE__ */ new Map();
|
|
889
1117
|
register(provider) {
|
|
@@ -900,16 +1128,20 @@ var awsStsProvider = {
|
|
|
900
1128
|
name: "aws-sts",
|
|
901
1129
|
description: "AWS STS AssumeRole (requires existing local AWS CLI credentials)",
|
|
902
1130
|
provision(configRaw) {
|
|
903
|
-
let
|
|
1131
|
+
let raw;
|
|
904
1132
|
try {
|
|
905
|
-
|
|
1133
|
+
raw = JSON.parse(configRaw);
|
|
906
1134
|
} catch {
|
|
907
1135
|
throw new Error('aws-sts requires valid JSON config (e.g. {"roleArn":"arn:aws:..."})');
|
|
908
1136
|
}
|
|
1137
|
+
const parsed = AwsStsConfigSchema.safeParse(raw);
|
|
1138
|
+
if (!parsed.success) {
|
|
1139
|
+
throw new Error(`aws-sts invalid config: ${parsed.error.message}`);
|
|
1140
|
+
}
|
|
1141
|
+
const config = parsed.data;
|
|
909
1142
|
const roleArn = config.roleArn;
|
|
910
1143
|
const sessionName = config.sessionName || "q-ring-agent";
|
|
911
1144
|
const duration = config.durationSeconds || 3600;
|
|
912
|
-
if (!roleArn) throw new Error("aws-sts requires roleArn in config");
|
|
913
1145
|
try {
|
|
914
1146
|
const output = execFileSync("aws", [
|
|
915
1147
|
"sts",
|
|
@@ -923,8 +1155,8 @@ var awsStsProvider = {
|
|
|
923
1155
|
"--output",
|
|
924
1156
|
"json"
|
|
925
1157
|
], { encoding: "utf8" });
|
|
926
|
-
const
|
|
927
|
-
const creds =
|
|
1158
|
+
const parsed2 = JSON.parse(output);
|
|
1159
|
+
const creds = parsed2.Credentials;
|
|
928
1160
|
const value = JSON.stringify({
|
|
929
1161
|
AWS_ACCESS_KEY_ID: creds.AccessKeyId,
|
|
930
1162
|
AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey,
|
|
@@ -943,17 +1175,24 @@ var httpProvider = {
|
|
|
943
1175
|
name: "http",
|
|
944
1176
|
description: "Generic HTTP token endpoint using Node.js http/https",
|
|
945
1177
|
provision(configRaw) {
|
|
946
|
-
let
|
|
1178
|
+
let raw;
|
|
947
1179
|
try {
|
|
948
|
-
|
|
1180
|
+
raw = JSON.parse(configRaw);
|
|
949
1181
|
} catch {
|
|
950
1182
|
throw new Error("http provider requires valid JSON config");
|
|
951
1183
|
}
|
|
1184
|
+
const parsed = HttpJitConfigSchema.safeParse(raw);
|
|
1185
|
+
if (!parsed.success) {
|
|
1186
|
+
throw new Error(`http provider invalid config: ${parsed.error.message}`);
|
|
1187
|
+
}
|
|
1188
|
+
const config = parsed.data;
|
|
952
1189
|
const url = config.url;
|
|
953
1190
|
const method = config.method || "POST";
|
|
954
1191
|
const valuePath = config.valuePath || "token";
|
|
955
1192
|
const expiresInSeconds = config.expiresInSeconds || 3600;
|
|
956
1193
|
if (!url) throw new Error("http provider requires url in config");
|
|
1194
|
+
const ssrfBlock = checkJitHttpProvisionUrl(url);
|
|
1195
|
+
if (ssrfBlock) throw new Error(`SSRF blocked: ${ssrfBlock}`);
|
|
957
1196
|
const headers = {
|
|
958
1197
|
"User-Agent": "q-ring-jit/1.0",
|
|
959
1198
|
...config.headers ?? {}
|
|
@@ -986,8 +1225,8 @@ req.end();
|
|
|
986
1225
|
if (result.status !== 0) {
|
|
987
1226
|
throw new Error(result.stderr || "HTTP request failed");
|
|
988
1227
|
}
|
|
989
|
-
const
|
|
990
|
-
let val =
|
|
1228
|
+
const parsed2 = JSON.parse(result.stdout);
|
|
1229
|
+
let val = parsed2;
|
|
991
1230
|
for (const key of valuePath.split(".")) {
|
|
992
1231
|
val = val[key];
|
|
993
1232
|
}
|
|
@@ -1035,6 +1274,39 @@ function checkToolPolicy(toolName, projectPath) {
|
|
|
1035
1274
|
}
|
|
1036
1275
|
return { allowed: true, policySource: ".q-ring.json" };
|
|
1037
1276
|
}
|
|
1277
|
+
function checkSecretLifecyclePolicy(input, projectPath) {
|
|
1278
|
+
const policy = loadPolicy(projectPath);
|
|
1279
|
+
if (!policy.secrets) return { allowed: true, policySource: "no-policy" };
|
|
1280
|
+
const s = policy.secrets;
|
|
1281
|
+
if (s.maxTtlSeconds != null && input.ttlSeconds != null && input.ttlSeconds > s.maxTtlSeconds) {
|
|
1282
|
+
return {
|
|
1283
|
+
allowed: false,
|
|
1284
|
+
reason: `TTL ${input.ttlSeconds}s exceeds policy maximum ${s.maxTtlSeconds}s`,
|
|
1285
|
+
policySource: ".q-ring.json policy.secrets.maxTtlSeconds"
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
if (s.requireApprovalForTags?.length && input.tags?.length) {
|
|
1289
|
+
const hit = input.tags.find((t) => s.requireApprovalForTags.includes(t));
|
|
1290
|
+
if (hit && !input.requiresApproval) {
|
|
1291
|
+
return {
|
|
1292
|
+
allowed: false,
|
|
1293
|
+
reason: `Tag "${hit}" requires explicit approval metadata (set requiresApproval / --requires-approval)`,
|
|
1294
|
+
policySource: ".q-ring.json policy.secrets.requireApprovalForTags"
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (s.requireRotationFormatForTags?.length && input.tags?.length) {
|
|
1299
|
+
const hit = input.tags.find((t) => s.requireRotationFormatForTags.includes(t));
|
|
1300
|
+
if (hit && !input.rotationFormat) {
|
|
1301
|
+
return {
|
|
1302
|
+
allowed: false,
|
|
1303
|
+
reason: `Tag "${hit}" requires a rotationFormat to be set`,
|
|
1304
|
+
policySource: ".q-ring.json policy.secrets.requireRotationFormatForTags"
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return { allowed: true, policySource: ".q-ring.json" };
|
|
1309
|
+
}
|
|
1038
1310
|
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1039
1311
|
const policy = loadPolicy(projectPath);
|
|
1040
1312
|
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
@@ -1103,6 +1375,32 @@ function getPolicySummary(projectPath) {
|
|
|
1103
1375
|
}
|
|
1104
1376
|
|
|
1105
1377
|
// src/core/keyring.ts
|
|
1378
|
+
function withJitEnvelopeLock(service, key, fn) {
|
|
1379
|
+
const dir = join6(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1380
|
+
mkdirSync5(dir, { recursive: true });
|
|
1381
|
+
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1382
|
+
const lockPath = join6(dir, `${safe}.lock`);
|
|
1383
|
+
const deadline = Date.now() + 8e3;
|
|
1384
|
+
while (Date.now() < deadline) {
|
|
1385
|
+
try {
|
|
1386
|
+
writeFileSync4(lockPath, `${process.pid}
|
|
1387
|
+
`, { flag: "wx", mode: 384 });
|
|
1388
|
+
try {
|
|
1389
|
+
return fn();
|
|
1390
|
+
} finally {
|
|
1391
|
+
try {
|
|
1392
|
+
unlinkSync(lockPath);
|
|
1393
|
+
} catch {
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
} catch {
|
|
1397
|
+
const start = Date.now();
|
|
1398
|
+
while (Date.now() - start < 15) {
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
throw new Error("Could not acquire JIT envelope lock (timeout)");
|
|
1403
|
+
}
|
|
1106
1404
|
function readEnvelope(service, key) {
|
|
1107
1405
|
const entry = new Entry(service, key);
|
|
1108
1406
|
const raw = entry.getPassword();
|
|
@@ -1121,7 +1419,7 @@ function resolveEnv(opts) {
|
|
|
1121
1419
|
}
|
|
1122
1420
|
function resolveTemplates(value, opts, seen) {
|
|
1123
1421
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1124
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1422
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1125
1423
|
const refKey = refKeyRaw.trim();
|
|
1126
1424
|
const refValue = getSecret(refKey, { ...opts, _seen: seen });
|
|
1127
1425
|
if (refValue === null) {
|
|
@@ -1132,7 +1430,7 @@ function resolveTemplates(value, opts, seen) {
|
|
|
1132
1430
|
}
|
|
1133
1431
|
function resolveTemplatesOffline(value, rawValues, seen) {
|
|
1134
1432
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1135
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1433
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1136
1434
|
const refKey = refKeyRaw.trim();
|
|
1137
1435
|
if (seen.has(refKey)) {
|
|
1138
1436
|
throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${refKey}`);
|
|
@@ -1202,11 +1500,27 @@ function getSecret(key, opts = {}) {
|
|
|
1202
1500
|
isExpired = new Date(envelope.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1203
1501
|
}
|
|
1204
1502
|
if (isExpired) {
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1503
|
+
const jitConfigSource = value;
|
|
1504
|
+
withJitEnvelopeLock(service, key, () => {
|
|
1505
|
+
const latest = readEnvelope(service, key);
|
|
1506
|
+
if (!latest?.meta.jitProvider) return;
|
|
1507
|
+
let innerExpired = true;
|
|
1508
|
+
if (latest.states && latest.states["jit"] && latest.meta.jitExpiresAt) {
|
|
1509
|
+
innerExpired = new Date(latest.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1510
|
+
}
|
|
1511
|
+
if (!innerExpired) {
|
|
1512
|
+
envelope.states = latest.states;
|
|
1513
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const result = provider.provision(jitConfigSource);
|
|
1517
|
+
latest.states = latest.states ?? {};
|
|
1518
|
+
latest.states["jit"] = result.value;
|
|
1519
|
+
latest.meta.jitExpiresAt = result.expiresAt;
|
|
1520
|
+
writeEnvelope(service, key, latest);
|
|
1521
|
+
envelope.states = latest.states;
|
|
1522
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1523
|
+
});
|
|
1210
1524
|
}
|
|
1211
1525
|
value = envelope.states["jit"];
|
|
1212
1526
|
}
|
|
@@ -1222,6 +1536,13 @@ function getSecret(key, opts = {}) {
|
|
|
1222
1536
|
return null;
|
|
1223
1537
|
}
|
|
1224
1538
|
function getEnvelope(key, opts = {}) {
|
|
1539
|
+
const source = opts.source ?? "cli";
|
|
1540
|
+
if (source === "mcp") {
|
|
1541
|
+
const policyDecision = checkKeyReadPolicy(key, void 0, opts.projectPath);
|
|
1542
|
+
if (!policyDecision.allowed) {
|
|
1543
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1225
1546
|
const scopes = resolveScope(opts);
|
|
1226
1547
|
for (const { service, scope } of scopes) {
|
|
1227
1548
|
const envelope = readEnvelope(service, key);
|
|
@@ -1241,6 +1562,20 @@ function setSecret(key, value, opts = {}) {
|
|
|
1241
1562
|
const prov = opts.provider ?? existing?.meta.provider;
|
|
1242
1563
|
const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
|
|
1243
1564
|
const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
|
|
1565
|
+
const mergedTags = opts.tags ?? existing?.meta.tags;
|
|
1566
|
+
const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
|
|
1567
|
+
const life = checkSecretLifecyclePolicy(
|
|
1568
|
+
{
|
|
1569
|
+
tags: mergedTags,
|
|
1570
|
+
ttlSeconds: ttlForPolicy,
|
|
1571
|
+
rotationFormat: rotFmt,
|
|
1572
|
+
requiresApproval: !!reqApp
|
|
1573
|
+
},
|
|
1574
|
+
opts.projectPath
|
|
1575
|
+
);
|
|
1576
|
+
if (!life.allowed) {
|
|
1577
|
+
throw new Error(life.reason ?? "Secret lifecycle policy denied");
|
|
1578
|
+
}
|
|
1244
1579
|
if (opts.states) {
|
|
1245
1580
|
envelope = createEnvelope("", {
|
|
1246
1581
|
states: opts.states,
|
|
@@ -1311,6 +1646,12 @@ function setSecret(key, value, opts = {}) {
|
|
|
1311
1646
|
function deleteSecret(key, opts = {}) {
|
|
1312
1647
|
const scopes = resolveScope(opts);
|
|
1313
1648
|
const source = opts.source ?? "cli";
|
|
1649
|
+
if (source === "mcp") {
|
|
1650
|
+
const policyDecision = checkKeyReadPolicy(key, void 0, opts.projectPath);
|
|
1651
|
+
if (!policyDecision.allowed) {
|
|
1652
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1314
1655
|
let deleted = false;
|
|
1315
1656
|
for (const { service, scope } of scopes) {
|
|
1316
1657
|
const entry = new Entry(service, key);
|
|
@@ -1333,6 +1674,11 @@ function deleteSecret(key, opts = {}) {
|
|
|
1333
1674
|
return deleted;
|
|
1334
1675
|
}
|
|
1335
1676
|
function hasSecret(key, opts = {}) {
|
|
1677
|
+
const source = opts.source ?? "cli";
|
|
1678
|
+
if (source === "mcp") {
|
|
1679
|
+
const policyDecision = checkKeyReadPolicy(key, void 0, opts.projectPath);
|
|
1680
|
+
if (!policyDecision.allowed) return false;
|
|
1681
|
+
}
|
|
1336
1682
|
const scopes = resolveScope(opts);
|
|
1337
1683
|
for (const { service } of scopes) {
|
|
1338
1684
|
const envelope = readEnvelope(service, key);
|
|
@@ -1385,7 +1731,14 @@ function listSecrets(opts = {}) {
|
|
|
1385
1731
|
if (!opts.silent) {
|
|
1386
1732
|
logAudit({ action: "list", source });
|
|
1387
1733
|
}
|
|
1388
|
-
|
|
1734
|
+
const sorted = results.sort((a, b) => a.key.localeCompare(b.key));
|
|
1735
|
+
if (source === "mcp") {
|
|
1736
|
+
return sorted.filter((e) => {
|
|
1737
|
+
const decision = checkKeyReadPolicy(e.key, void 0, opts.projectPath);
|
|
1738
|
+
return decision.allowed;
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
return sorted;
|
|
1389
1742
|
}
|
|
1390
1743
|
function exportSecrets(opts = {}) {
|
|
1391
1744
|
const format = opts.format ?? "env";
|
|
@@ -1468,6 +1821,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
1468
1821
|
}
|
|
1469
1822
|
|
|
1470
1823
|
// src/core/tunnel.ts
|
|
1824
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1471
1825
|
var tunnelStore = /* @__PURE__ */ new Map();
|
|
1472
1826
|
var cleanupInterval = null;
|
|
1473
1827
|
function ensureCleanup() {
|
|
@@ -1489,7 +1843,7 @@ function ensureCleanup() {
|
|
|
1489
1843
|
}
|
|
1490
1844
|
}
|
|
1491
1845
|
function tunnelCreate(value, opts = {}) {
|
|
1492
|
-
const id = `tun_${Date.now().toString(36)}_${
|
|
1846
|
+
const id = `tun_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url")}`;
|
|
1493
1847
|
const now = Date.now();
|
|
1494
1848
|
tunnelStore.set(id, {
|
|
1495
1849
|
value,
|
|
@@ -1548,7 +1902,8 @@ export {
|
|
|
1548
1902
|
exportAudit,
|
|
1549
1903
|
detectAnomalies,
|
|
1550
1904
|
listEntanglements,
|
|
1551
|
-
|
|
1905
|
+
httpRequest,
|
|
1906
|
+
checkSSRF,
|
|
1552
1907
|
registerHook,
|
|
1553
1908
|
removeHook,
|
|
1554
1909
|
listHooks,
|
|
@@ -1573,4 +1928,4 @@ export {
|
|
|
1573
1928
|
tunnelDestroy,
|
|
1574
1929
|
tunnelList
|
|
1575
1930
|
};
|
|
1576
|
-
//# sourceMappingURL=chunk-
|
|
1931
|
+
//# sourceMappingURL=chunk-E46HG5NM.js.map
|