@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();
|
|
@@ -665,16 +830,22 @@ function matchesHook(hook, payload, tags) {
|
|
|
665
830
|
if (m.action?.length && !m.action.includes(payload.action)) return false;
|
|
666
831
|
if (m.key && m.key !== payload.key) return false;
|
|
667
832
|
if (m.keyPattern) {
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
"i"
|
|
671
|
-
);
|
|
833
|
+
const escaped = m.keyPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
834
|
+
const regex = new RegExp("^" + escaped + "$", "i");
|
|
672
835
|
if (!regex.test(payload.key)) return false;
|
|
673
836
|
}
|
|
674
837
|
if (m.tag && (!tags || !tags.includes(m.tag))) return false;
|
|
675
838
|
if (m.scope && m.scope !== payload.scope) return false;
|
|
676
839
|
return true;
|
|
677
840
|
}
|
|
841
|
+
var SAFE_PGREP_NAME = /^[a-zA-Z0-9._:/-]{1,256}$/;
|
|
842
|
+
function shellRunner() {
|
|
843
|
+
if (process.platform === "win32") {
|
|
844
|
+
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
845
|
+
return { file: comspec, args: ["/d", "/s", "/c"] };
|
|
846
|
+
}
|
|
847
|
+
return { file: "/bin/sh", args: ["-c"] };
|
|
848
|
+
}
|
|
678
849
|
function executeShell(command, payload) {
|
|
679
850
|
return new Promise((resolve) => {
|
|
680
851
|
const env = {
|
|
@@ -683,49 +854,35 @@ function executeShell(command, payload) {
|
|
|
683
854
|
QRING_HOOK_ACTION: payload.action,
|
|
684
855
|
QRING_HOOK_SCOPE: payload.scope
|
|
685
856
|
};
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
857
|
+
const { file, args } = shellRunner();
|
|
858
|
+
execFile(
|
|
859
|
+
file,
|
|
860
|
+
[...args, command],
|
|
861
|
+
{
|
|
862
|
+
timeout: 3e4,
|
|
863
|
+
env,
|
|
864
|
+
windowsHide: true,
|
|
865
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
866
|
+
encoding: "utf8"
|
|
867
|
+
},
|
|
868
|
+
(err, stdout) => {
|
|
869
|
+
if (err) {
|
|
870
|
+
resolve({
|
|
871
|
+
hookId: "",
|
|
872
|
+
success: false,
|
|
873
|
+
message: `Shell error: ${err.message}`
|
|
874
|
+
});
|
|
875
|
+
} else {
|
|
876
|
+
resolve({
|
|
877
|
+
hookId: "",
|
|
878
|
+
success: true,
|
|
879
|
+
message: (stdout ?? "").trim() || "OK"
|
|
880
|
+
});
|
|
881
|
+
}
|
|
691
882
|
}
|
|
692
|
-
|
|
883
|
+
);
|
|
693
884
|
});
|
|
694
885
|
}
|
|
695
|
-
function isPrivateIP(ip) {
|
|
696
|
-
const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
|
|
697
|
-
const ipv4Re = new RegExp(`^::ffff:(${octet}\\.${octet}\\.${octet}\\.${octet})$`, "i");
|
|
698
|
-
const ipv4Mapped = ip.match(ipv4Re);
|
|
699
|
-
if (ipv4Mapped) return isPrivateIP(ipv4Mapped[1]);
|
|
700
|
-
if (/^127\./.test(ip)) return true;
|
|
701
|
-
if (/^10\./.test(ip)) return true;
|
|
702
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
703
|
-
if (/^192\.168\./.test(ip)) return true;
|
|
704
|
-
if (/^169\.254\./.test(ip)) return true;
|
|
705
|
-
if (ip === "0.0.0.0") return true;
|
|
706
|
-
if (ip === "::1" || ip === "::") return true;
|
|
707
|
-
if (/^f[cd][0-9a-f]{2}:/i.test(ip)) return true;
|
|
708
|
-
if (/^fe80:/i.test(ip)) return true;
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
async function checkSSRF(url) {
|
|
712
|
-
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
713
|
-
try {
|
|
714
|
-
const parsed = new URL(url);
|
|
715
|
-
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
716
|
-
if (isPrivateIP(hostname)) {
|
|
717
|
-
return `Blocked: hook URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
718
|
-
}
|
|
719
|
-
const results = await lookup(hostname, { all: true });
|
|
720
|
-
for (const { address } of results) {
|
|
721
|
-
if (isPrivateIP(address)) {
|
|
722
|
-
return `Blocked: hook URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
} catch {
|
|
726
|
-
}
|
|
727
|
-
return null;
|
|
728
|
-
}
|
|
729
886
|
async function executeHttp(url, payload) {
|
|
730
887
|
const ssrfBlock = await checkSSRF(url);
|
|
731
888
|
if (ssrfBlock) {
|
|
@@ -740,7 +897,7 @@ async function executeHttp(url, payload) {
|
|
|
740
897
|
}
|
|
741
898
|
try {
|
|
742
899
|
const body = JSON.stringify(payload);
|
|
743
|
-
const res = await
|
|
900
|
+
const res = await httpRequest({
|
|
744
901
|
url,
|
|
745
902
|
method: "POST",
|
|
746
903
|
headers: {
|
|
@@ -765,19 +922,48 @@ async function executeHttp(url, payload) {
|
|
|
765
922
|
}
|
|
766
923
|
function executeSignal(target, signal = "SIGHUP") {
|
|
767
924
|
return new Promise((resolve) => {
|
|
768
|
-
const
|
|
769
|
-
if (
|
|
925
|
+
const trimmed = target.trim();
|
|
926
|
+
if (/^\d+$/.test(trimmed)) {
|
|
927
|
+
const pid = parseInt(trimmed, 10);
|
|
770
928
|
try {
|
|
771
929
|
process.kill(pid, signal);
|
|
772
|
-
resolve({
|
|
930
|
+
resolve({
|
|
931
|
+
hookId: "",
|
|
932
|
+
success: true,
|
|
933
|
+
message: `Signal ${signal} sent to PID ${pid}`
|
|
934
|
+
});
|
|
773
935
|
} catch (err) {
|
|
774
|
-
resolve({
|
|
936
|
+
resolve({
|
|
937
|
+
hookId: "",
|
|
938
|
+
success: false,
|
|
939
|
+
message: `Signal error: ${err instanceof Error ? err.message : String(err)}`
|
|
940
|
+
});
|
|
775
941
|
}
|
|
776
942
|
return;
|
|
777
943
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
944
|
+
if (!SAFE_PGREP_NAME.test(trimmed)) {
|
|
945
|
+
resolve({
|
|
946
|
+
hookId: "",
|
|
947
|
+
success: false,
|
|
948
|
+
message: "Signal target must be a numeric PID, or a process name matching /^[a-zA-Z0-9._:/-]{1,256}$/ (no spaces or shell metacharacters)."
|
|
949
|
+
});
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const child = spawn("pgrep", ["-f", trimmed], {
|
|
953
|
+
timeout: 5e3,
|
|
954
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
955
|
+
});
|
|
956
|
+
let stdout = "";
|
|
957
|
+
child.stdout.on("data", (d) => {
|
|
958
|
+
stdout += d.toString();
|
|
959
|
+
});
|
|
960
|
+
child.on("close", (code) => {
|
|
961
|
+
if (code !== 0 || !stdout.trim()) {
|
|
962
|
+
resolve({
|
|
963
|
+
hookId: "",
|
|
964
|
+
success: false,
|
|
965
|
+
message: `Process "${trimmed}" not found`
|
|
966
|
+
});
|
|
781
967
|
return;
|
|
782
968
|
}
|
|
783
969
|
const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
|
|
@@ -789,7 +975,18 @@ function executeSignal(target, signal = "SIGHUP") {
|
|
|
789
975
|
} catch {
|
|
790
976
|
}
|
|
791
977
|
}
|
|
792
|
-
resolve({
|
|
978
|
+
resolve({
|
|
979
|
+
hookId: "",
|
|
980
|
+
success: sent > 0,
|
|
981
|
+
message: `Signal ${signal} sent to ${sent} process(es)`
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
child.on("error", () => {
|
|
985
|
+
resolve({
|
|
986
|
+
hookId: "",
|
|
987
|
+
success: false,
|
|
988
|
+
message: `Process "${trimmed}" not found`
|
|
989
|
+
});
|
|
793
990
|
});
|
|
794
991
|
});
|
|
795
992
|
}
|
|
@@ -846,33 +1043,50 @@ async function fireHooks(payload, tags) {
|
|
|
846
1043
|
}
|
|
847
1044
|
|
|
848
1045
|
// src/core/approval.ts
|
|
849
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as
|
|
1046
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
850
1047
|
import { join as join5 } from "path";
|
|
851
1048
|
import { homedir as homedir4 } from "os";
|
|
852
|
-
import { createHmac, randomBytes } from "crypto";
|
|
1049
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
853
1050
|
function getHmacSecret() {
|
|
854
|
-
const secretPath = join5(homedir4(), ".config", "q-ring", ".approval-key");
|
|
855
1051
|
const dir = join5(homedir4(), ".config", "q-ring");
|
|
856
|
-
|
|
1052
|
+
const secretPath = join5(dir, ".approval-key");
|
|
1053
|
+
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
857
1054
|
if (existsSync5(secretPath)) {
|
|
858
1055
|
return readFileSync5(secretPath, "utf8").trim();
|
|
859
1056
|
}
|
|
860
1057
|
const secret = randomBytes(32).toString("hex");
|
|
861
|
-
|
|
1058
|
+
writeFileSync3(secretPath, secret, { mode: 384 });
|
|
862
1059
|
return secret;
|
|
863
1060
|
}
|
|
864
1061
|
function computeHmac(entry) {
|
|
865
|
-
const payload =
|
|
1062
|
+
const payload = [
|
|
1063
|
+
entry.id,
|
|
1064
|
+
entry.key,
|
|
1065
|
+
entry.scope,
|
|
1066
|
+
entry.reason,
|
|
1067
|
+
entry.grantedBy,
|
|
1068
|
+
entry.grantedAt,
|
|
1069
|
+
entry.expiresAt,
|
|
1070
|
+
entry.workspace ?? "",
|
|
1071
|
+
entry.sessionId ?? ""
|
|
1072
|
+
].join("|");
|
|
866
1073
|
return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
|
|
867
1074
|
}
|
|
868
1075
|
function verifyHmac(entry) {
|
|
869
1076
|
const expected = computeHmac(entry);
|
|
870
|
-
|
|
1077
|
+
const a = Buffer.from(expected, "utf8");
|
|
1078
|
+
const b = Buffer.from(entry.hmac, "utf8");
|
|
1079
|
+
if (a.length !== b.length) return false;
|
|
1080
|
+
try {
|
|
1081
|
+
return timingSafeEqual(a, b);
|
|
1082
|
+
} catch {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
871
1085
|
}
|
|
872
1086
|
function getRegistryPath3() {
|
|
873
1087
|
const dir = join5(homedir4(), ".config", "q-ring");
|
|
874
1088
|
if (!existsSync5(dir)) {
|
|
875
|
-
mkdirSync4(dir, { recursive: true });
|
|
1089
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
876
1090
|
}
|
|
877
1091
|
return join5(dir, "approvals.json");
|
|
878
1092
|
}
|
|
@@ -888,7 +1102,7 @@ function loadRegistry3() {
|
|
|
888
1102
|
}
|
|
889
1103
|
}
|
|
890
1104
|
function saveRegistry3(registry2) {
|
|
891
|
-
|
|
1105
|
+
writeFileSync3(getRegistryPath3(), JSON.stringify(registry2, null, 2), { mode: 384 });
|
|
892
1106
|
}
|
|
893
1107
|
function cleanup(registry2) {
|
|
894
1108
|
const now = Date.now();
|
|
@@ -956,6 +1170,20 @@ function listApprovals() {
|
|
|
956
1170
|
|
|
957
1171
|
// src/core/provision.ts
|
|
958
1172
|
import { execFileSync, spawnSync } from "child_process";
|
|
1173
|
+
import { z as z2 } from "zod";
|
|
1174
|
+
var AwsStsConfigSchema = z2.object({
|
|
1175
|
+
roleArn: z2.string(),
|
|
1176
|
+
sessionName: z2.string().optional(),
|
|
1177
|
+
durationSeconds: z2.number().optional()
|
|
1178
|
+
});
|
|
1179
|
+
var HttpJitConfigSchema = z2.object({
|
|
1180
|
+
url: z2.string(),
|
|
1181
|
+
method: z2.string().optional(),
|
|
1182
|
+
valuePath: z2.string().optional(),
|
|
1183
|
+
expiresInSeconds: z2.number().optional(),
|
|
1184
|
+
headers: z2.record(z2.string(), z2.string()).optional(),
|
|
1185
|
+
body: z2.unknown().optional()
|
|
1186
|
+
});
|
|
959
1187
|
var ProvisionRegistry = class {
|
|
960
1188
|
providers = /* @__PURE__ */ new Map();
|
|
961
1189
|
register(provider) {
|
|
@@ -972,16 +1200,20 @@ var awsStsProvider = {
|
|
|
972
1200
|
name: "aws-sts",
|
|
973
1201
|
description: "AWS STS AssumeRole (requires existing local AWS CLI credentials)",
|
|
974
1202
|
provision(configRaw) {
|
|
975
|
-
let
|
|
1203
|
+
let raw;
|
|
976
1204
|
try {
|
|
977
|
-
|
|
1205
|
+
raw = JSON.parse(configRaw);
|
|
978
1206
|
} catch {
|
|
979
1207
|
throw new Error('aws-sts requires valid JSON config (e.g. {"roleArn":"arn:aws:..."})');
|
|
980
1208
|
}
|
|
1209
|
+
const parsed = AwsStsConfigSchema.safeParse(raw);
|
|
1210
|
+
if (!parsed.success) {
|
|
1211
|
+
throw new Error(`aws-sts invalid config: ${parsed.error.message}`);
|
|
1212
|
+
}
|
|
1213
|
+
const config = parsed.data;
|
|
981
1214
|
const roleArn = config.roleArn;
|
|
982
1215
|
const sessionName = config.sessionName || "q-ring-agent";
|
|
983
1216
|
const duration = config.durationSeconds || 3600;
|
|
984
|
-
if (!roleArn) throw new Error("aws-sts requires roleArn in config");
|
|
985
1217
|
try {
|
|
986
1218
|
const output = execFileSync("aws", [
|
|
987
1219
|
"sts",
|
|
@@ -995,8 +1227,8 @@ var awsStsProvider = {
|
|
|
995
1227
|
"--output",
|
|
996
1228
|
"json"
|
|
997
1229
|
], { encoding: "utf8" });
|
|
998
|
-
const
|
|
999
|
-
const creds =
|
|
1230
|
+
const parsed2 = JSON.parse(output);
|
|
1231
|
+
const creds = parsed2.Credentials;
|
|
1000
1232
|
const value = JSON.stringify({
|
|
1001
1233
|
AWS_ACCESS_KEY_ID: creds.AccessKeyId,
|
|
1002
1234
|
AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey,
|
|
@@ -1015,17 +1247,24 @@ var httpProvider = {
|
|
|
1015
1247
|
name: "http",
|
|
1016
1248
|
description: "Generic HTTP token endpoint using Node.js http/https",
|
|
1017
1249
|
provision(configRaw) {
|
|
1018
|
-
let
|
|
1250
|
+
let raw;
|
|
1019
1251
|
try {
|
|
1020
|
-
|
|
1252
|
+
raw = JSON.parse(configRaw);
|
|
1021
1253
|
} catch {
|
|
1022
1254
|
throw new Error("http provider requires valid JSON config");
|
|
1023
1255
|
}
|
|
1256
|
+
const parsed = HttpJitConfigSchema.safeParse(raw);
|
|
1257
|
+
if (!parsed.success) {
|
|
1258
|
+
throw new Error(`http provider invalid config: ${parsed.error.message}`);
|
|
1259
|
+
}
|
|
1260
|
+
const config = parsed.data;
|
|
1024
1261
|
const url = config.url;
|
|
1025
1262
|
const method = config.method || "POST";
|
|
1026
1263
|
const valuePath = config.valuePath || "token";
|
|
1027
1264
|
const expiresInSeconds = config.expiresInSeconds || 3600;
|
|
1028
1265
|
if (!url) throw new Error("http provider requires url in config");
|
|
1266
|
+
const ssrfBlock = checkJitHttpProvisionUrl(url);
|
|
1267
|
+
if (ssrfBlock) throw new Error(`SSRF blocked: ${ssrfBlock}`);
|
|
1029
1268
|
const headers = {
|
|
1030
1269
|
"User-Agent": "q-ring-jit/1.0",
|
|
1031
1270
|
...config.headers ?? {}
|
|
@@ -1058,8 +1297,8 @@ req.end();
|
|
|
1058
1297
|
if (result.status !== 0) {
|
|
1059
1298
|
throw new Error(result.stderr || "HTTP request failed");
|
|
1060
1299
|
}
|
|
1061
|
-
const
|
|
1062
|
-
let val =
|
|
1300
|
+
const parsed2 = JSON.parse(result.stdout);
|
|
1301
|
+
let val = parsed2;
|
|
1063
1302
|
for (const key of valuePath.split(".")) {
|
|
1064
1303
|
val = val[key];
|
|
1065
1304
|
}
|
|
@@ -1088,6 +1327,39 @@ function loadPolicy(projectPath) {
|
|
|
1088
1327
|
cachedPolicy = { path: pp, policy };
|
|
1089
1328
|
return policy;
|
|
1090
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
|
+
}
|
|
1091
1363
|
function checkKeyReadPolicy(key, tags, projectPath) {
|
|
1092
1364
|
const policy = loadPolicy(projectPath);
|
|
1093
1365
|
if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
|
|
@@ -1156,6 +1428,32 @@ function getPolicySummary(projectPath) {
|
|
|
1156
1428
|
}
|
|
1157
1429
|
|
|
1158
1430
|
// src/core/keyring.ts
|
|
1431
|
+
function withJitEnvelopeLock(service, key, fn) {
|
|
1432
|
+
const dir = join6(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1433
|
+
mkdirSync5(dir, { recursive: true });
|
|
1434
|
+
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1435
|
+
const lockPath = join6(dir, `${safe}.lock`);
|
|
1436
|
+
const deadline = Date.now() + 8e3;
|
|
1437
|
+
while (Date.now() < deadline) {
|
|
1438
|
+
try {
|
|
1439
|
+
writeFileSync4(lockPath, `${process.pid}
|
|
1440
|
+
`, { flag: "wx", mode: 384 });
|
|
1441
|
+
try {
|
|
1442
|
+
return fn();
|
|
1443
|
+
} finally {
|
|
1444
|
+
try {
|
|
1445
|
+
unlinkSync(lockPath);
|
|
1446
|
+
} catch {
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
} catch {
|
|
1450
|
+
const start = Date.now();
|
|
1451
|
+
while (Date.now() - start < 15) {
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
throw new Error("Could not acquire JIT envelope lock (timeout)");
|
|
1456
|
+
}
|
|
1159
1457
|
function readEnvelope(service, key) {
|
|
1160
1458
|
const entry = new Entry(service, key);
|
|
1161
1459
|
const raw = entry.getPassword();
|
|
@@ -1174,7 +1472,7 @@ function resolveEnv(opts) {
|
|
|
1174
1472
|
}
|
|
1175
1473
|
function resolveTemplates(value, opts, seen) {
|
|
1176
1474
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1177
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1475
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1178
1476
|
const refKey = refKeyRaw.trim();
|
|
1179
1477
|
const refValue = getSecret(refKey, { ...opts, _seen: seen });
|
|
1180
1478
|
if (refValue === null) {
|
|
@@ -1185,7 +1483,7 @@ function resolveTemplates(value, opts, seen) {
|
|
|
1185
1483
|
}
|
|
1186
1484
|
function resolveTemplatesOffline(value, rawValues, seen) {
|
|
1187
1485
|
if (!value.includes("{{") || !value.includes("}}")) return value;
|
|
1188
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (
|
|
1486
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
|
|
1189
1487
|
const refKey = refKeyRaw.trim();
|
|
1190
1488
|
if (seen.has(refKey)) {
|
|
1191
1489
|
throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${refKey}`);
|
|
@@ -1255,11 +1553,27 @@ function getSecret(key, opts = {}) {
|
|
|
1255
1553
|
isExpired = new Date(envelope.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1256
1554
|
}
|
|
1257
1555
|
if (isExpired) {
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1556
|
+
const jitConfigSource = value;
|
|
1557
|
+
withJitEnvelopeLock(service, key, () => {
|
|
1558
|
+
const latest = readEnvelope(service, key);
|
|
1559
|
+
if (!latest?.meta.jitProvider) return;
|
|
1560
|
+
let innerExpired = true;
|
|
1561
|
+
if (latest.states && latest.states["jit"] && latest.meta.jitExpiresAt) {
|
|
1562
|
+
innerExpired = new Date(latest.meta.jitExpiresAt).getTime() <= Date.now();
|
|
1563
|
+
}
|
|
1564
|
+
if (!innerExpired) {
|
|
1565
|
+
envelope.states = latest.states;
|
|
1566
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const result = provider.provision(jitConfigSource);
|
|
1570
|
+
latest.states = latest.states ?? {};
|
|
1571
|
+
latest.states["jit"] = result.value;
|
|
1572
|
+
latest.meta.jitExpiresAt = result.expiresAt;
|
|
1573
|
+
writeEnvelope(service, key, latest);
|
|
1574
|
+
envelope.states = latest.states;
|
|
1575
|
+
envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
|
|
1576
|
+
});
|
|
1263
1577
|
}
|
|
1264
1578
|
value = envelope.states["jit"];
|
|
1265
1579
|
}
|
|
@@ -1275,6 +1589,13 @@ function getSecret(key, opts = {}) {
|
|
|
1275
1589
|
return null;
|
|
1276
1590
|
}
|
|
1277
1591
|
function getEnvelope(key, opts = {}) {
|
|
1592
|
+
const source = opts.source ?? "cli";
|
|
1593
|
+
if (source === "mcp") {
|
|
1594
|
+
const policyDecision = checkKeyReadPolicy(key, void 0, opts.projectPath);
|
|
1595
|
+
if (!policyDecision.allowed) {
|
|
1596
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1278
1599
|
const scopes = resolveScope(opts);
|
|
1279
1600
|
for (const { service, scope } of scopes) {
|
|
1280
1601
|
const envelope = readEnvelope(service, key);
|
|
@@ -1294,6 +1615,20 @@ function setSecret(key, value, opts = {}) {
|
|
|
1294
1615
|
const prov = opts.provider ?? existing?.meta.provider;
|
|
1295
1616
|
const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
|
|
1296
1617
|
const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
|
|
1618
|
+
const mergedTags = opts.tags ?? existing?.meta.tags;
|
|
1619
|
+
const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
|
|
1620
|
+
const life = checkSecretLifecyclePolicy(
|
|
1621
|
+
{
|
|
1622
|
+
tags: mergedTags,
|
|
1623
|
+
ttlSeconds: ttlForPolicy,
|
|
1624
|
+
rotationFormat: rotFmt,
|
|
1625
|
+
requiresApproval: !!reqApp
|
|
1626
|
+
},
|
|
1627
|
+
opts.projectPath
|
|
1628
|
+
);
|
|
1629
|
+
if (!life.allowed) {
|
|
1630
|
+
throw new Error(life.reason ?? "Secret lifecycle policy denied");
|
|
1631
|
+
}
|
|
1297
1632
|
if (opts.states) {
|
|
1298
1633
|
envelope = createEnvelope("", {
|
|
1299
1634
|
states: opts.states,
|
|
@@ -1364,6 +1699,12 @@ function setSecret(key, value, opts = {}) {
|
|
|
1364
1699
|
function deleteSecret(key, opts = {}) {
|
|
1365
1700
|
const scopes = resolveScope(opts);
|
|
1366
1701
|
const source = opts.source ?? "cli";
|
|
1702
|
+
if (source === "mcp") {
|
|
1703
|
+
const policyDecision = checkKeyReadPolicy(key, void 0, opts.projectPath);
|
|
1704
|
+
if (!policyDecision.allowed) {
|
|
1705
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1367
1708
|
let deleted = false;
|
|
1368
1709
|
for (const { service, scope } of scopes) {
|
|
1369
1710
|
const entry = new Entry(service, key);
|
|
@@ -1386,6 +1727,11 @@ function deleteSecret(key, opts = {}) {
|
|
|
1386
1727
|
return deleted;
|
|
1387
1728
|
}
|
|
1388
1729
|
function hasSecret(key, opts = {}) {
|
|
1730
|
+
const source = opts.source ?? "cli";
|
|
1731
|
+
if (source === "mcp") {
|
|
1732
|
+
const policyDecision = checkKeyReadPolicy(key, void 0, opts.projectPath);
|
|
1733
|
+
if (!policyDecision.allowed) return false;
|
|
1734
|
+
}
|
|
1389
1735
|
const scopes = resolveScope(opts);
|
|
1390
1736
|
for (const { service } of scopes) {
|
|
1391
1737
|
const envelope = readEnvelope(service, key);
|
|
@@ -1438,7 +1784,14 @@ function listSecrets(opts = {}) {
|
|
|
1438
1784
|
if (!opts.silent) {
|
|
1439
1785
|
logAudit({ action: "list", source });
|
|
1440
1786
|
}
|
|
1441
|
-
|
|
1787
|
+
const sorted = results.sort((a, b) => a.key.localeCompare(b.key));
|
|
1788
|
+
if (source === "mcp") {
|
|
1789
|
+
return sorted.filter((e) => {
|
|
1790
|
+
const decision = checkKeyReadPolicy(e.key, void 0, opts.projectPath);
|
|
1791
|
+
return decision.allowed;
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
return sorted;
|
|
1442
1795
|
}
|
|
1443
1796
|
function exportSecrets(opts = {}) {
|
|
1444
1797
|
const format = opts.format ?? "env";
|
|
@@ -1521,6 +1874,7 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
1521
1874
|
}
|
|
1522
1875
|
|
|
1523
1876
|
// src/core/tunnel.ts
|
|
1877
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1524
1878
|
var tunnelStore = /* @__PURE__ */ new Map();
|
|
1525
1879
|
var cleanupInterval = null;
|
|
1526
1880
|
function ensureCleanup() {
|
|
@@ -1542,7 +1896,7 @@ function ensureCleanup() {
|
|
|
1542
1896
|
}
|
|
1543
1897
|
}
|
|
1544
1898
|
function tunnelCreate(value, opts = {}) {
|
|
1545
|
-
const id = `tun_${Date.now().toString(36)}_${
|
|
1899
|
+
const id = `tun_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url")}`;
|
|
1546
1900
|
const now = Date.now();
|
|
1547
1901
|
tunnelStore.set(id, {
|
|
1548
1902
|
value,
|
|
@@ -1601,7 +1955,8 @@ export {
|
|
|
1601
1955
|
exportAudit,
|
|
1602
1956
|
detectAnomalies,
|
|
1603
1957
|
listEntanglements,
|
|
1604
|
-
|
|
1958
|
+
httpRequest,
|
|
1959
|
+
checkSSRF,
|
|
1605
1960
|
registerHook,
|
|
1606
1961
|
removeHook,
|
|
1607
1962
|
listHooks,
|
|
@@ -1629,4 +1984,4 @@ export {
|
|
|
1629
1984
|
tunnelDestroy,
|
|
1630
1985
|
tunnelList
|
|
1631
1986
|
};
|
|
1632
|
-
//# sourceMappingURL=chunk-
|
|
1987
|
+
//# sourceMappingURL=chunk-LLMO64FE.js.map
|