@i4ctime/q-ring 0.9.8 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,54 @@
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
21
+ import { z } from "zod";
22
+ var EntanglementLinkSchema = z.object({
23
+ service: z.string(),
24
+ key: z.string()
25
+ });
26
+ var SecretMetadataSchema = z.object({
27
+ createdAt: z.string(),
28
+ updatedAt: z.string(),
29
+ expiresAt: z.string().optional(),
30
+ ttlSeconds: z.number().optional(),
31
+ description: z.string().optional(),
32
+ tags: z.array(z.string()).optional(),
33
+ entangled: z.array(EntanglementLinkSchema).optional(),
34
+ accessCount: z.number(),
35
+ lastAccessedAt: z.string().optional(),
36
+ ephemeral: z.boolean().optional(),
37
+ rotationFormat: z.string().optional(),
38
+ rotationPrefix: z.string().optional(),
39
+ provider: z.string().optional(),
40
+ validationUrl: z.string().optional(),
41
+ requiresApproval: z.boolean().optional(),
42
+ jitProvider: z.string().optional(),
43
+ jitExpiresAt: z.string().optional()
44
+ });
45
+ var QuantumEnvelopeSchema = z.object({
46
+ v: z.literal(1),
47
+ value: z.string().optional(),
48
+ states: z.record(z.string(), z.string()).optional(),
49
+ defaultEnv: z.string().optional(),
50
+ meta: SecretMetadataSchema
51
+ });
4
52
  function createEnvelope(value, opts) {
5
53
  const now = (/* @__PURE__ */ new Date()).toISOString();
6
54
  let expiresAt = opts?.expiresAt;
@@ -32,8 +80,9 @@ function createEnvelope(value, opts) {
32
80
  function parseEnvelope(raw) {
33
81
  try {
34
82
  const parsed = JSON.parse(raw);
35
- if (parsed && typeof parsed === "object" && parsed.v === 1) {
36
- return parsed;
83
+ const r = QuantumEnvelopeSchema.safeParse(parsed);
84
+ if (r.success) {
85
+ return r.data;
37
86
  }
38
87
  } catch {
39
88
  }
@@ -84,6 +133,15 @@ function checkDecay(envelope) {
84
133
  const now = Date.now();
85
134
  const expires = new Date(envelope.meta.expiresAt).getTime();
86
135
  const created = new Date(envelope.meta.createdAt).getTime();
136
+ if (!Number.isFinite(expires) || !Number.isFinite(created)) {
137
+ return {
138
+ isExpired: true,
139
+ isStale: true,
140
+ lifetimePercent: 100,
141
+ secondsRemaining: null,
142
+ timeRemaining: "invalid date"
143
+ };
144
+ }
87
145
  const totalLifetime = expires - created;
88
146
  const elapsed = now - created;
89
147
  const remaining = expires - now;
@@ -121,8 +179,8 @@ function recordAccess(envelope) {
121
179
 
122
180
  // src/core/collapse.ts
123
181
  import { execSync } from "child_process";
124
- import { existsSync, readFileSync } from "fs";
125
- import { join } from "path";
182
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
183
+ import { join as join2 } from "path";
126
184
  var BRANCH_ENV_MAP = {
127
185
  main: "prod",
128
186
  master: "prod",
@@ -149,10 +207,10 @@ function detectGitBranch(cwd) {
149
207
  }
150
208
  }
151
209
  function readProjectConfig(projectPath) {
152
- const configPath = join(projectPath ?? process.cwd(), ".q-ring.json");
210
+ const configPath = join2(projectPath ?? process.cwd(), ".q-ring.json");
153
211
  try {
154
212
  if (existsSync(configPath)) {
155
- return JSON.parse(readFileSync(configPath, "utf8"));
213
+ return JSON.parse(readFileSync2(configPath, "utf8"));
156
214
  }
157
215
  } catch {
158
216
  }
@@ -188,12 +246,15 @@ function collapseEnvironment(ctx = {}) {
188
246
  }
189
247
  return null;
190
248
  }
249
+ var MAX_BRANCH_GLOB_PATTERN_LEN = 200;
250
+ var MAX_BRANCH_GLOB_REGEX_SOURCE = 400;
191
251
  function matchGlob(branchMap, branch) {
192
252
  for (const [pattern, env] of Object.entries(branchMap)) {
193
253
  if (!pattern.includes("*")) continue;
194
- const regex = new RegExp(
195
- "^" + pattern.replace(/\*/g, ".*") + "$"
196
- );
254
+ if (pattern.length > MAX_BRANCH_GLOB_PATTERN_LEN) continue;
255
+ const source = "^" + pattern.replace(/\*/g, ".*") + "$";
256
+ if (source.length > MAX_BRANCH_GLOB_REGEX_SOURCE) continue;
257
+ const regex = new RegExp(source);
197
258
  if (regex.test(branch)) return env;
198
259
  }
199
260
  return void 0;
@@ -206,19 +267,35 @@ function mapEnvName(raw) {
206
267
  }
207
268
 
208
269
  // src/core/observer.ts
209
- import { existsSync as existsSync2, mkdirSync, appendFileSync, readFileSync as readFileSync2, openSync, fstatSync, readSync, closeSync } from "fs";
210
- import { join as join2 } from "path";
270
+ import {
271
+ existsSync as existsSync2,
272
+ mkdirSync,
273
+ appendFileSync,
274
+ readFileSync as readFileSync3,
275
+ openSync,
276
+ fstatSync,
277
+ readSync,
278
+ closeSync,
279
+ statSync
280
+ } from "fs";
281
+ import { join as join3 } from "path";
211
282
  import { homedir } from "os";
212
283
  import { createHash } from "crypto";
213
284
  function getAuditDir() {
214
- const dir = join2(homedir(), ".config", "q-ring");
285
+ if (process.env.QRING_AUDIT_DIR) {
286
+ if (!existsSync2(process.env.QRING_AUDIT_DIR)) {
287
+ mkdirSync(process.env.QRING_AUDIT_DIR, { recursive: true });
288
+ }
289
+ return process.env.QRING_AUDIT_DIR;
290
+ }
291
+ const dir = join3(homedir(), ".config", "q-ring");
215
292
  if (!existsSync2(dir)) {
216
293
  mkdirSync(dir, { recursive: true });
217
294
  }
218
295
  return dir;
219
296
  }
220
297
  function getAuditPath() {
221
- return join2(getAuditDir(), "audit.jsonl");
298
+ return join3(getAuditDir(), "audit.jsonl");
222
299
  }
223
300
  function getLastLineHash() {
224
301
  const path = getAuditPath();
@@ -256,11 +333,24 @@ function logAudit(event) {
256
333
  } catch {
257
334
  }
258
335
  }
336
+ var MAX_AUDIT_BYTES = 12 * 1024 * 1024;
259
337
  function queryAudit(query = {}) {
260
338
  const path = getAuditPath();
261
339
  if (!existsSync2(path)) return [];
262
340
  try {
263
- const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
341
+ const st = statSync(path);
342
+ const readStart = st.size > MAX_AUDIT_BYTES ? st.size - MAX_AUDIT_BYTES : 0;
343
+ const readLen = st.size > MAX_AUDIT_BYTES ? MAX_AUDIT_BYTES : st.size;
344
+ const buf = Buffer.alloc(readLen);
345
+ const fd = openSync(path, "r");
346
+ readSync(fd, buf, 0, readLen, readStart);
347
+ closeSync(fd);
348
+ let text = buf.toString("utf8");
349
+ if (readStart > 0) {
350
+ const firstNl = text.indexOf("\n");
351
+ if (firstNl !== -1) text = text.slice(firstNl + 1);
352
+ }
353
+ const lines = text.split("\n").filter((l) => l.trim());
264
354
  let events = lines.map((line) => {
265
355
  try {
266
356
  return JSON.parse(line);
@@ -290,7 +380,7 @@ function verifyAuditChain() {
290
380
  if (!existsSync2(path)) {
291
381
  return { totalEvents: 0, validEvents: 0, intact: true };
292
382
  }
293
- const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
383
+ const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
294
384
  if (lines.length === 0) {
295
385
  return { totalEvents: 0, validEvents: 0, intact: true };
296
386
  }
@@ -328,7 +418,7 @@ function verifyAuditChain() {
328
418
  function exportAudit(opts = {}) {
329
419
  const path = getAuditPath();
330
420
  if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
331
- const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
421
+ const lines = readFileSync3(path, "utf8").split("\n").filter((l) => l.trim());
332
422
  let events = lines.map((l) => {
333
423
  try {
334
424
  return JSON.parse(l);
@@ -393,15 +483,15 @@ function detectAnomalies(key) {
393
483
  }
394
484
 
395
485
  // src/core/entanglement.ts
396
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
397
- import { join as join3 } from "path";
486
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
487
+ import { join as join4 } from "path";
398
488
  import { homedir as homedir2 } from "os";
399
489
  function getRegistryPath() {
400
- const dir = join3(homedir2(), ".config", "q-ring");
490
+ const dir = join4(homedir2(), ".config", "q-ring");
401
491
  if (!existsSync3(dir)) {
402
492
  mkdirSync2(dir, { recursive: true });
403
493
  }
404
- return join3(dir, "entanglement.json");
494
+ return join4(dir, "entanglement.json");
405
495
  }
406
496
  function loadRegistry() {
407
497
  const path = getRegistryPath();
@@ -409,13 +499,15 @@ function loadRegistry() {
409
499
  return { pairs: [] };
410
500
  }
411
501
  try {
412
- return JSON.parse(readFileSync3(path, "utf8"));
502
+ return JSON.parse(readFileSync4(path, "utf8"));
413
503
  } catch {
414
504
  return { pairs: [] };
415
505
  }
416
506
  }
417
507
  function saveRegistry(registry2) {
418
- writeFileSync2(getRegistryPath(), JSON.stringify(registry2, null, 2));
508
+ writeFileSync(getRegistryPath(), JSON.stringify(registry2, null, 2), {
509
+ mode: 384
510
+ });
419
511
  }
420
512
  function entangle(source, target) {
421
513
  const registry2 = loadRegistry();
@@ -453,74 +545,19 @@ function listEntanglements() {
453
545
  return loadRegistry().pairs;
454
546
  }
455
547
 
456
- // src/core/keyring.ts
457
- import { Entry, findCredentials } from "@napi-rs/keyring";
458
-
459
- // src/utils/hash.ts
460
- import { createHash as createHash2 } from "crypto";
461
- function hashProjectPath(projectPath) {
462
- return createHash2("sha256").update(projectPath).digest("hex").slice(0, 12);
463
- }
464
-
465
- // src/core/scope.ts
466
- var SERVICE_PREFIX = "q-ring";
467
- function globalService() {
468
- return `${SERVICE_PREFIX}:global`;
469
- }
470
- function projectService(projectPath) {
471
- const hash = hashProjectPath(projectPath);
472
- return `${SERVICE_PREFIX}:project:${hash}`;
473
- }
474
- function teamService(teamId) {
475
- return `${SERVICE_PREFIX}:team:${teamId}`;
476
- }
477
- function orgService(orgId) {
478
- return `${SERVICE_PREFIX}:org:${orgId}`;
479
- }
480
- function resolveScope(opts) {
481
- const { scope, projectPath, teamId, orgId } = opts;
482
- if (scope === "global") {
483
- return [{ scope: "global", service: globalService() }];
484
- }
485
- if (scope === "project") {
486
- if (!projectPath) throw new Error("Project path is required for project scope");
487
- return [{ scope: "project", service: projectService(projectPath), projectPath }];
488
- }
489
- if (scope === "team") {
490
- if (!teamId) throw new Error("Team ID is required for team scope");
491
- return [{ scope: "team", service: teamService(teamId), teamId }];
492
- }
493
- if (scope === "org") {
494
- if (!orgId) throw new Error("Org ID is required for org scope");
495
- return [{ scope: "org", service: orgService(orgId), orgId }];
496
- }
497
- const chain = [];
498
- if (projectPath) {
499
- chain.push({ scope: "project", service: projectService(projectPath), projectPath });
500
- }
501
- if (teamId) {
502
- chain.push({ scope: "team", service: teamService(teamId), teamId });
503
- }
504
- if (orgId) {
505
- chain.push({ scope: "org", service: orgService(orgId), orgId });
506
- }
507
- chain.push({ scope: "global", service: globalService() });
508
- return chain;
509
- }
510
-
511
548
  // src/core/hooks.ts
512
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
513
- import { join as join4 } from "path";
549
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
550
+ import { join as join5 } from "path";
514
551
  import { homedir as homedir3 } from "os";
515
- import { exec, spawn } from "child_process";
552
+ import { execFile, spawn } from "child_process";
516
553
  import { randomUUID } from "crypto";
517
554
 
518
555
  // src/utils/http-request.ts
519
556
  import { request as httpsRequest } from "https";
520
- import { request as httpRequest } from "http";
557
+ import { request as httpRequestPlain } from "http";
521
558
  var DEFAULT_TIMEOUT_MS = 1e4;
522
559
  var DEFAULT_MAX_RESPONSE_BYTES = 65536;
523
- function httpRequest_(opts) {
560
+ function httpRequest(opts) {
524
561
  const {
525
562
  url,
526
563
  method = "GET",
@@ -535,7 +572,7 @@ function httpRequest_(opts) {
535
572
  reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
536
573
  return;
537
574
  }
538
- const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequest;
575
+ const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
539
576
  const reqHeaders = { ...headers };
540
577
  if (body && !reqHeaders["Content-Length"]) {
541
578
  reqHeaders["Content-Length"] = Buffer.byteLength(body);
@@ -598,6 +635,19 @@ function httpRequest_(opts) {
598
635
 
599
636
  // src/core/ssrf.ts
600
637
  import { lookup } from "dns/promises";
638
+ import * as dns from "dns";
639
+ import { isIPv4, isIPv6 } from "net";
640
+ function lookupAddressesSync(hostname2) {
641
+ const lookupSync2 = dns.lookupSync;
642
+ return lookupSync2(hostname2, { all: true });
643
+ }
644
+ function isHostnameIpLiteral(hostname2) {
645
+ if (isIPv4(hostname2)) return true;
646
+ if (hostname2.startsWith("[") && hostname2.endsWith("]")) {
647
+ return isIPv6(hostname2.slice(1, -1));
648
+ }
649
+ return isIPv6(hostname2);
650
+ }
601
651
  function isPrivateIP(ip) {
602
652
  const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
603
653
  const ipv4Re = new RegExp(`^::ffff:(${octet}\\.${octet}\\.${octet}\\.${octet})$`, "i");
@@ -618,40 +668,64 @@ async function checkSSRF(url) {
618
668
  if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
619
669
  try {
620
670
  const parsed = new URL(url);
621
- const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
622
- if (isPrivateIP(hostname)) {
623
- return `Blocked: URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
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.`;
624
674
  }
625
- const results = await lookup(hostname, { all: true });
675
+ const results = await lookup(hostname2, { all: true });
626
676
  for (const { address } of results) {
627
677
  if (isPrivateIP(address)) {
628
- return `Blocked: URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
678
+ return `Blocked: URL "${hostname2}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
629
679
  }
630
680
  }
631
681
  } catch {
632
682
  }
633
683
  return null;
634
684
  }
635
- function checkSSRFSync(url) {
685
+ function checkJitHttpProvisionUrl(url) {
636
686
  if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
637
687
  try {
638
688
  const parsed = new URL(url);
639
- const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
640
- if (isPrivateIP(hostname)) {
641
- return `Blocked: URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
689
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
690
+ return "Blocked: JIT HTTP provider only allows http: or https: URLs.";
691
+ }
692
+ const hostname2 = parsed.hostname.replace(/^\[|\]$/g, "");
693
+ if (!hostname2) {
694
+ return "Blocked: empty hostname in JIT URL.";
695
+ }
696
+ if (isPrivateIP(hostname2)) {
697
+ return `Blocked: URL resolves to private address (${hostname2}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
698
+ }
699
+ if (isHostnameIpLiteral(hostname2)) {
700
+ return null;
701
+ }
702
+ let results;
703
+ try {
704
+ results = lookupAddressesSync(hostname2);
705
+ } catch {
706
+ return `Blocked: DNS resolution failed for "${hostname2}" (JIT HTTP provisioning fails closed).`;
707
+ }
708
+ if (!results.length) {
709
+ return `Blocked: DNS returned no addresses for "${hostname2}".`;
710
+ }
711
+ for (const { address } of results) {
712
+ if (isPrivateIP(address)) {
713
+ return `Blocked: URL "${hostname2}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
714
+ }
642
715
  }
643
716
  } catch {
717
+ return "Blocked: malformed JIT HTTP URL.";
644
718
  }
645
719
  return null;
646
720
  }
647
721
 
648
722
  // src/core/hooks.ts
649
723
  function getRegistryPath2() {
650
- const dir = join4(homedir3(), ".config", "q-ring");
724
+ const dir = join5(homedir3(), ".config", "q-ring");
651
725
  if (!existsSync4(dir)) {
652
726
  mkdirSync3(dir, { recursive: true });
653
727
  }
654
- return join4(dir, "hooks.json");
728
+ return join5(dir, "hooks.json");
655
729
  }
656
730
  function loadRegistry2() {
657
731
  const path = getRegistryPath2();
@@ -659,13 +733,15 @@ function loadRegistry2() {
659
733
  return { hooks: [] };
660
734
  }
661
735
  try {
662
- return JSON.parse(readFileSync4(path, "utf8"));
736
+ return JSON.parse(readFileSync5(path, "utf8"));
663
737
  } catch {
664
738
  return { hooks: [] };
665
739
  }
666
740
  }
667
741
  function saveRegistry2(registry2) {
668
- writeFileSync3(getRegistryPath2(), JSON.stringify(registry2, null, 2));
742
+ writeFileSync2(getRegistryPath2(), JSON.stringify(registry2, null, 2), {
743
+ mode: 384
744
+ });
669
745
  }
670
746
  function registerHook(entry) {
671
747
  const registry2 = loadRegistry2();
@@ -705,6 +781,14 @@ function matchesHook(hook, payload, tags) {
705
781
  if (m.scope && m.scope !== payload.scope) return false;
706
782
  return true;
707
783
  }
784
+ var SAFE_PGREP_NAME = /^[a-zA-Z0-9._:/-]{1,256}$/;
785
+ function shellRunner() {
786
+ if (process.platform === "win32") {
787
+ const comspec = process.env.ComSpec ?? "cmd.exe";
788
+ return { file: comspec, args: ["/d", "/s", "/c"] };
789
+ }
790
+ return { file: "/bin/sh", args: ["-c"] };
791
+ }
708
792
  function executeShell(command, payload) {
709
793
  return new Promise((resolve) => {
710
794
  const env = {
@@ -713,13 +797,33 @@ function executeShell(command, payload) {
713
797
  QRING_HOOK_ACTION: payload.action,
714
798
  QRING_HOOK_SCOPE: payload.scope
715
799
  };
716
- exec(command, { timeout: 3e4, env }, (err, stdout, stderr) => {
717
- if (err) {
718
- resolve({ hookId: "", success: false, message: `Shell error: ${err.message}` });
719
- } else {
720
- resolve({ hookId: "", success: true, message: stdout.trim() || "OK" });
800
+ const { file, args } = shellRunner();
801
+ execFile(
802
+ file,
803
+ [...args, command],
804
+ {
805
+ timeout: 3e4,
806
+ env,
807
+ windowsHide: true,
808
+ maxBuffer: 4 * 1024 * 1024,
809
+ encoding: "utf8"
810
+ },
811
+ (err, stdout) => {
812
+ if (err) {
813
+ resolve({
814
+ hookId: "",
815
+ success: false,
816
+ message: `Shell error: ${err.message}`
817
+ });
818
+ } else {
819
+ resolve({
820
+ hookId: "",
821
+ success: true,
822
+ message: (stdout ?? "").trim() || "OK"
823
+ });
824
+ }
721
825
  }
722
- });
826
+ );
723
827
  });
724
828
  }
725
829
  async function executeHttp(url, payload) {
@@ -736,7 +840,7 @@ async function executeHttp(url, payload) {
736
840
  }
737
841
  try {
738
842
  const body = JSON.stringify(payload);
739
- const res = await httpRequest_({
843
+ const res = await httpRequest({
740
844
  url,
741
845
  method: "POST",
742
846
  headers: {
@@ -761,24 +865,48 @@ async function executeHttp(url, payload) {
761
865
  }
762
866
  function executeSignal(target, signal = "SIGHUP") {
763
867
  return new Promise((resolve) => {
764
- const pid = parseInt(target, 10);
765
- if (!isNaN(pid)) {
868
+ const trimmed = target.trim();
869
+ if (/^\d+$/.test(trimmed)) {
870
+ const pid = parseInt(trimmed, 10);
766
871
  try {
767
872
  process.kill(pid, signal);
768
- resolve({ hookId: "", success: true, message: `Signal ${signal} sent to PID ${pid}` });
873
+ resolve({
874
+ hookId: "",
875
+ success: true,
876
+ message: `Signal ${signal} sent to PID ${pid}`
877
+ });
769
878
  } catch (err) {
770
- resolve({ hookId: "", success: false, message: `Signal error: ${err instanceof Error ? err.message : String(err)}` });
879
+ resolve({
880
+ hookId: "",
881
+ success: false,
882
+ message: `Signal error: ${err instanceof Error ? err.message : String(err)}`
883
+ });
771
884
  }
772
885
  return;
773
886
  }
774
- const child = spawn("pgrep", ["-f", target], { timeout: 5e3, stdio: ["ignore", "pipe", "pipe"] });
887
+ if (!SAFE_PGREP_NAME.test(trimmed)) {
888
+ resolve({
889
+ hookId: "",
890
+ success: false,
891
+ message: "Signal target must be a numeric PID, or a process name matching /^[a-zA-Z0-9._:/-]{1,256}$/ (no spaces or shell metacharacters)."
892
+ });
893
+ return;
894
+ }
895
+ const child = spawn("pgrep", ["-f", trimmed], {
896
+ timeout: 5e3,
897
+ stdio: ["ignore", "pipe", "pipe"]
898
+ });
775
899
  let stdout = "";
776
900
  child.stdout.on("data", (d) => {
777
901
  stdout += d.toString();
778
902
  });
779
903
  child.on("close", (code) => {
780
904
  if (code !== 0 || !stdout.trim()) {
781
- resolve({ hookId: "", success: false, message: `Process "${target}" not found` });
905
+ resolve({
906
+ hookId: "",
907
+ success: false,
908
+ message: `Process "${trimmed}" not found`
909
+ });
782
910
  return;
783
911
  }
784
912
  const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
@@ -790,10 +918,18 @@ function executeSignal(target, signal = "SIGHUP") {
790
918
  } catch {
791
919
  }
792
920
  }
793
- resolve({ hookId: "", success: sent > 0, message: `Signal ${signal} sent to ${sent} process(es)` });
921
+ resolve({
922
+ hookId: "",
923
+ success: sent > 0,
924
+ message: `Signal ${signal} sent to ${sent} process(es)`
925
+ });
794
926
  });
795
927
  child.on("error", () => {
796
- resolve({ hookId: "", success: false, message: `Process "${target}" not found` });
928
+ resolve({
929
+ hookId: "",
930
+ success: false,
931
+ message: `Process "${trimmed}" not found`
932
+ });
797
933
  });
798
934
  });
799
935
  }
@@ -850,35 +986,52 @@ async function fireHooks(payload, tags) {
850
986
  }
851
987
 
852
988
  // src/core/approval.ts
853
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
854
- import { join as join5 } from "path";
989
+ import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
990
+ import { join as join6 } from "path";
855
991
  import { homedir as homedir4 } from "os";
856
- import { createHmac, randomBytes } from "crypto";
992
+ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
857
993
  function getHmacSecret() {
858
- const secretPath = join5(homedir4(), ".config", "q-ring", ".approval-key");
859
- const dir = join5(homedir4(), ".config", "q-ring");
860
- if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
994
+ const dir = join6(homedir4(), ".config", "q-ring");
995
+ const secretPath = join6(dir, ".approval-key");
996
+ if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
861
997
  if (existsSync5(secretPath)) {
862
- return readFileSync5(secretPath, "utf8").trim();
998
+ return readFileSync6(secretPath, "utf8").trim();
863
999
  }
864
1000
  const secret = randomBytes(32).toString("hex");
865
- writeFileSync4(secretPath, secret, { mode: 384 });
1001
+ writeFileSync3(secretPath, secret, { mode: 384 });
866
1002
  return secret;
867
1003
  }
868
1004
  function computeHmac(entry) {
869
- const payload = `${entry.id}|${entry.key}|${entry.scope}|${entry.reason}|${entry.grantedBy}|${entry.grantedAt}|${entry.expiresAt}`;
1005
+ const payload = [
1006
+ entry.id,
1007
+ entry.key,
1008
+ entry.scope,
1009
+ entry.reason,
1010
+ entry.grantedBy,
1011
+ entry.grantedAt,
1012
+ entry.expiresAt,
1013
+ entry.workspace ?? "",
1014
+ entry.sessionId ?? ""
1015
+ ].join("|");
870
1016
  return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
871
1017
  }
872
1018
  function verifyHmac(entry) {
873
1019
  const expected = computeHmac(entry);
874
- return expected === entry.hmac;
1020
+ const a = Buffer.from(expected, "utf8");
1021
+ const b = Buffer.from(entry.hmac, "utf8");
1022
+ if (a.length !== b.length) return false;
1023
+ try {
1024
+ return timingSafeEqual(a, b);
1025
+ } catch {
1026
+ return false;
1027
+ }
875
1028
  }
876
1029
  function getRegistryPath3() {
877
- const dir = join5(homedir4(), ".config", "q-ring");
1030
+ const dir = join6(homedir4(), ".config", "q-ring");
878
1031
  if (!existsSync5(dir)) {
879
- mkdirSync4(dir, { recursive: true });
1032
+ mkdirSync4(dir, { recursive: true, mode: 448 });
880
1033
  }
881
- return join5(dir, "approvals.json");
1034
+ return join6(dir, "approvals.json");
882
1035
  }
883
1036
  function loadRegistry3() {
884
1037
  const path = getRegistryPath3();
@@ -886,7 +1039,7 @@ function loadRegistry3() {
886
1039
  return { approvals: [] };
887
1040
  }
888
1041
  try {
889
- return JSON.parse(readFileSync5(path, "utf8"));
1042
+ return JSON.parse(readFileSync6(path, "utf8"));
890
1043
  } catch {
891
1044
  return { approvals: [] };
892
1045
  }
@@ -901,9 +1054,221 @@ function hasApproval(key, scope) {
901
1054
  if (!verifyHmac(entry)) return false;
902
1055
  return true;
903
1056
  }
1057
+ function listApprovals() {
1058
+ const registry2 = loadRegistry3();
1059
+ const now = Date.now();
1060
+ return registry2.approvals.map((a) => ({
1061
+ ...a,
1062
+ valid: new Date(a.expiresAt).getTime() > now,
1063
+ tampered: !verifyHmac(a)
1064
+ }));
1065
+ }
1066
+
1067
+ // src/core/policy.ts
1068
+ var cachedPolicy = null;
1069
+ function loadPolicy(projectPath) {
1070
+ const pp = projectPath ?? process.cwd();
1071
+ if (cachedPolicy && cachedPolicy.path === pp) {
1072
+ return cachedPolicy.policy;
1073
+ }
1074
+ const config = readProjectConfig(pp);
1075
+ const policy = config?.policy ?? {};
1076
+ cachedPolicy = { path: pp, policy };
1077
+ return policy;
1078
+ }
1079
+ function checkToolPolicy(toolName, projectPath) {
1080
+ const policy = loadPolicy(projectPath);
1081
+ if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
1082
+ if (policy.mcp.denyTools?.includes(toolName)) {
1083
+ return {
1084
+ allowed: false,
1085
+ reason: `Tool "${toolName}" is denied by project policy`,
1086
+ policySource: ".q-ring.json policy.mcp.denyTools"
1087
+ };
1088
+ }
1089
+ if (policy.mcp.allowTools && !policy.mcp.allowTools.includes(toolName)) {
1090
+ return {
1091
+ allowed: false,
1092
+ reason: `Tool "${toolName}" is not in the allowlist`,
1093
+ policySource: ".q-ring.json policy.mcp.allowTools"
1094
+ };
1095
+ }
1096
+ return { allowed: true, policySource: ".q-ring.json" };
1097
+ }
1098
+ function checkSecretLifecyclePolicy(input, projectPath) {
1099
+ const policy = loadPolicy(projectPath);
1100
+ if (!policy.secrets) return { allowed: true, policySource: "no-policy" };
1101
+ const s = policy.secrets;
1102
+ if (s.maxTtlSeconds != null && input.ttlSeconds != null && input.ttlSeconds > s.maxTtlSeconds) {
1103
+ return {
1104
+ allowed: false,
1105
+ reason: `TTL ${input.ttlSeconds}s exceeds policy maximum ${s.maxTtlSeconds}s`,
1106
+ policySource: ".q-ring.json policy.secrets.maxTtlSeconds"
1107
+ };
1108
+ }
1109
+ if (s.requireApprovalForTags?.length && input.tags?.length) {
1110
+ const hit = input.tags.find((t) => s.requireApprovalForTags.includes(t));
1111
+ if (hit && !input.requiresApproval) {
1112
+ return {
1113
+ allowed: false,
1114
+ reason: `Tag "${hit}" requires explicit approval metadata (set requiresApproval / --requires-approval)`,
1115
+ policySource: ".q-ring.json policy.secrets.requireApprovalForTags"
1116
+ };
1117
+ }
1118
+ }
1119
+ if (s.requireRotationFormatForTags?.length && input.tags?.length) {
1120
+ const hit = input.tags.find((t) => s.requireRotationFormatForTags.includes(t));
1121
+ if (hit && !input.rotationFormat) {
1122
+ return {
1123
+ allowed: false,
1124
+ reason: `Tag "${hit}" requires a rotationFormat to be set`,
1125
+ policySource: ".q-ring.json policy.secrets.requireRotationFormatForTags"
1126
+ };
1127
+ }
1128
+ }
1129
+ return { allowed: true, policySource: ".q-ring.json" };
1130
+ }
1131
+ function checkKeyReadPolicy(key, tags, projectPath) {
1132
+ const policy = loadPolicy(projectPath);
1133
+ if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
1134
+ if (policy.mcp.deniedKeys?.includes(key)) {
1135
+ return {
1136
+ allowed: false,
1137
+ reason: `Key "${key}" is denied by project policy`,
1138
+ policySource: ".q-ring.json policy.mcp.deniedKeys"
1139
+ };
1140
+ }
1141
+ if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
1142
+ return {
1143
+ allowed: false,
1144
+ reason: `Key "${key}" is not in the readable keys allowlist`,
1145
+ policySource: ".q-ring.json policy.mcp.readableKeys"
1146
+ };
1147
+ }
1148
+ if (tags && policy.mcp.deniedTags) {
1149
+ const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
1150
+ if (blocked) {
1151
+ return {
1152
+ allowed: false,
1153
+ reason: `Tag "${blocked}" is denied by project policy`,
1154
+ policySource: ".q-ring.json policy.mcp.deniedTags"
1155
+ };
1156
+ }
1157
+ }
1158
+ return { allowed: true, policySource: ".q-ring.json" };
1159
+ }
1160
+ function checkExecPolicy(command, projectPath) {
1161
+ const policy = loadPolicy(projectPath);
1162
+ if (!policy.exec) return { allowed: true, policySource: "no-policy" };
1163
+ if (policy.exec.denyCommands) {
1164
+ const denied = policy.exec.denyCommands.find((d) => command.includes(d));
1165
+ if (denied) {
1166
+ return {
1167
+ allowed: false,
1168
+ reason: `Command containing "${denied}" is denied by project policy`,
1169
+ policySource: ".q-ring.json policy.exec.denyCommands"
1170
+ };
1171
+ }
1172
+ }
1173
+ if (policy.exec.allowCommands) {
1174
+ const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
1175
+ if (!allowed) {
1176
+ return {
1177
+ allowed: false,
1178
+ reason: `Command "${command}" is not in the exec allowlist`,
1179
+ policySource: ".q-ring.json policy.exec.allowCommands"
1180
+ };
1181
+ }
1182
+ }
1183
+ return { allowed: true, policySource: ".q-ring.json" };
1184
+ }
1185
+ function getExecMaxRuntime(projectPath) {
1186
+ return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
1187
+ }
1188
+ function getPolicySummary(projectPath) {
1189
+ const policy = loadPolicy(projectPath);
1190
+ return {
1191
+ hasMcpPolicy: !!policy.mcp,
1192
+ hasExecPolicy: !!policy.exec,
1193
+ hasSecretPolicy: !!policy.secrets,
1194
+ details: policy
1195
+ };
1196
+ }
1197
+
1198
+ // src/core/keyring.ts
1199
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
1200
+ import { homedir as homedir5 } from "os";
1201
+ import { join as join7 } from "path";
1202
+ import { Entry, findCredentials } from "@napi-rs/keyring";
1203
+
1204
+ // src/utils/hash.ts
1205
+ import { createHash as createHash2 } from "crypto";
1206
+ function hashProjectPath(projectPath) {
1207
+ return createHash2("sha256").update(projectPath).digest("hex").slice(0, 12);
1208
+ }
1209
+
1210
+ // src/core/scope.ts
1211
+ var SERVICE_PREFIX = "q-ring";
1212
+ function globalService() {
1213
+ return `${SERVICE_PREFIX}:global`;
1214
+ }
1215
+ function projectService(projectPath) {
1216
+ const hash = hashProjectPath(projectPath);
1217
+ return `${SERVICE_PREFIX}:project:${hash}`;
1218
+ }
1219
+ function teamService(teamId) {
1220
+ return `${SERVICE_PREFIX}:team:${teamId}`;
1221
+ }
1222
+ function orgService(orgId) {
1223
+ return `${SERVICE_PREFIX}:org:${orgId}`;
1224
+ }
1225
+ function resolveScope(opts) {
1226
+ const { scope, projectPath, teamId, orgId } = opts;
1227
+ if (scope === "global") {
1228
+ return [{ scope: "global", service: globalService() }];
1229
+ }
1230
+ if (scope === "project") {
1231
+ if (!projectPath) throw new Error("Project path is required for project scope");
1232
+ return [{ scope: "project", service: projectService(projectPath), projectPath }];
1233
+ }
1234
+ if (scope === "team") {
1235
+ if (!teamId) throw new Error("Team ID is required for team scope");
1236
+ return [{ scope: "team", service: teamService(teamId), teamId }];
1237
+ }
1238
+ if (scope === "org") {
1239
+ if (!orgId) throw new Error("Org ID is required for org scope");
1240
+ return [{ scope: "org", service: orgService(orgId), orgId }];
1241
+ }
1242
+ const chain = [];
1243
+ if (projectPath) {
1244
+ chain.push({ scope: "project", service: projectService(projectPath), projectPath });
1245
+ }
1246
+ if (teamId) {
1247
+ chain.push({ scope: "team", service: teamService(teamId), teamId });
1248
+ }
1249
+ if (orgId) {
1250
+ chain.push({ scope: "org", service: orgService(orgId), orgId });
1251
+ }
1252
+ chain.push({ scope: "global", service: globalService() });
1253
+ return chain;
1254
+ }
904
1255
 
905
1256
  // src/core/provision.ts
906
1257
  import { execFileSync, spawnSync } from "child_process";
1258
+ import { z as z2 } from "zod";
1259
+ var AwsStsConfigSchema = z2.object({
1260
+ roleArn: z2.string(),
1261
+ sessionName: z2.string().optional(),
1262
+ durationSeconds: z2.number().optional()
1263
+ });
1264
+ var HttpJitConfigSchema = z2.object({
1265
+ url: z2.string(),
1266
+ method: z2.string().optional(),
1267
+ valuePath: z2.string().optional(),
1268
+ expiresInSeconds: z2.number().optional(),
1269
+ headers: z2.record(z2.string(), z2.string()).optional(),
1270
+ body: z2.unknown().optional()
1271
+ });
907
1272
  var ProvisionRegistry = class {
908
1273
  providers = /* @__PURE__ */ new Map();
909
1274
  register(provider) {
@@ -920,16 +1285,20 @@ var awsStsProvider = {
920
1285
  name: "aws-sts",
921
1286
  description: "AWS STS AssumeRole (requires existing local AWS CLI credentials)",
922
1287
  provision(configRaw) {
923
- let config;
1288
+ let raw;
924
1289
  try {
925
- config = JSON.parse(configRaw);
1290
+ raw = JSON.parse(configRaw);
926
1291
  } catch {
927
1292
  throw new Error('aws-sts requires valid JSON config (e.g. {"roleArn":"arn:aws:..."})');
928
1293
  }
1294
+ const parsed = AwsStsConfigSchema.safeParse(raw);
1295
+ if (!parsed.success) {
1296
+ throw new Error(`aws-sts invalid config: ${parsed.error.message}`);
1297
+ }
1298
+ const config = parsed.data;
929
1299
  const roleArn = config.roleArn;
930
1300
  const sessionName = config.sessionName || "q-ring-agent";
931
1301
  const duration = config.durationSeconds || 3600;
932
- if (!roleArn) throw new Error("aws-sts requires roleArn in config");
933
1302
  try {
934
1303
  const output = execFileSync("aws", [
935
1304
  "sts",
@@ -943,8 +1312,8 @@ var awsStsProvider = {
943
1312
  "--output",
944
1313
  "json"
945
1314
  ], { encoding: "utf8" });
946
- const parsed = JSON.parse(output);
947
- const creds = parsed.Credentials;
1315
+ const parsed2 = JSON.parse(output);
1316
+ const creds = parsed2.Credentials;
948
1317
  const value = JSON.stringify({
949
1318
  AWS_ACCESS_KEY_ID: creds.AccessKeyId,
950
1319
  AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey,
@@ -963,18 +1332,23 @@ var httpProvider = {
963
1332
  name: "http",
964
1333
  description: "Generic HTTP token endpoint using Node.js http/https",
965
1334
  provision(configRaw) {
966
- let config;
1335
+ let raw;
967
1336
  try {
968
- config = JSON.parse(configRaw);
1337
+ raw = JSON.parse(configRaw);
969
1338
  } catch {
970
1339
  throw new Error("http provider requires valid JSON config");
971
1340
  }
1341
+ const parsed = HttpJitConfigSchema.safeParse(raw);
1342
+ if (!parsed.success) {
1343
+ throw new Error(`http provider invalid config: ${parsed.error.message}`);
1344
+ }
1345
+ const config = parsed.data;
972
1346
  const url = config.url;
973
1347
  const method = config.method || "POST";
974
1348
  const valuePath = config.valuePath || "token";
975
1349
  const expiresInSeconds = config.expiresInSeconds || 3600;
976
1350
  if (!url) throw new Error("http provider requires url in config");
977
- const ssrfBlock = checkSSRFSync(url);
1351
+ const ssrfBlock = checkJitHttpProvisionUrl(url);
978
1352
  if (ssrfBlock) throw new Error(`SSRF blocked: ${ssrfBlock}`);
979
1353
  const headers = {
980
1354
  "User-Agent": "q-ring-jit/1.0",
@@ -1008,8 +1382,8 @@ req.end();
1008
1382
  if (result.status !== 0) {
1009
1383
  throw new Error(result.stderr || "HTTP request failed");
1010
1384
  }
1011
- const parsed = JSON.parse(result.stdout);
1012
- let val = parsed;
1385
+ const parsed2 = JSON.parse(result.stdout);
1386
+ let val = parsed2;
1013
1387
  for (const key of valuePath.split(".")) {
1014
1388
  val = val[key];
1015
1389
  }
@@ -1026,105 +1400,33 @@ var registry = new ProvisionRegistry();
1026
1400
  registry.register(awsStsProvider);
1027
1401
  registry.register(httpProvider);
1028
1402
 
1029
- // src/core/policy.ts
1030
- var cachedPolicy = null;
1031
- function loadPolicy(projectPath) {
1032
- const pp = projectPath ?? process.cwd();
1033
- if (cachedPolicy && cachedPolicy.path === pp) {
1034
- return cachedPolicy.policy;
1035
- }
1036
- const config = readProjectConfig(pp);
1037
- const policy = config?.policy ?? {};
1038
- cachedPolicy = { path: pp, policy };
1039
- return policy;
1040
- }
1041
- function checkToolPolicy(toolName, projectPath) {
1042
- const policy = loadPolicy(projectPath);
1043
- if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
1044
- if (policy.mcp.denyTools?.includes(toolName)) {
1045
- return {
1046
- allowed: false,
1047
- reason: `Tool "${toolName}" is denied by project policy`,
1048
- policySource: ".q-ring.json policy.mcp.denyTools"
1049
- };
1050
- }
1051
- if (policy.mcp.allowTools && !policy.mcp.allowTools.includes(toolName)) {
1052
- return {
1053
- allowed: false,
1054
- reason: `Tool "${toolName}" is not in the allowlist`,
1055
- policySource: ".q-ring.json policy.mcp.allowTools"
1056
- };
1057
- }
1058
- return { allowed: true, policySource: ".q-ring.json" };
1059
- }
1060
- function checkKeyReadPolicy(key, tags, projectPath) {
1061
- const policy = loadPolicy(projectPath);
1062
- if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
1063
- if (policy.mcp.deniedKeys?.includes(key)) {
1064
- return {
1065
- allowed: false,
1066
- reason: `Key "${key}" is denied by project policy`,
1067
- policySource: ".q-ring.json policy.mcp.deniedKeys"
1068
- };
1069
- }
1070
- if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
1071
- return {
1072
- allowed: false,
1073
- reason: `Key "${key}" is not in the readable keys allowlist`,
1074
- policySource: ".q-ring.json policy.mcp.readableKeys"
1075
- };
1076
- }
1077
- if (tags && policy.mcp.deniedTags) {
1078
- const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
1079
- if (blocked) {
1080
- return {
1081
- allowed: false,
1082
- reason: `Tag "${blocked}" is denied by project policy`,
1083
- policySource: ".q-ring.json policy.mcp.deniedTags"
1084
- };
1085
- }
1086
- }
1087
- return { allowed: true, policySource: ".q-ring.json" };
1088
- }
1089
- function checkExecPolicy(command, projectPath) {
1090
- const policy = loadPolicy(projectPath);
1091
- if (!policy.exec) return { allowed: true, policySource: "no-policy" };
1092
- if (policy.exec.denyCommands) {
1093
- const denied = policy.exec.denyCommands.find((d) => command.includes(d));
1094
- if (denied) {
1095
- return {
1096
- allowed: false,
1097
- reason: `Command containing "${denied}" is denied by project policy`,
1098
- policySource: ".q-ring.json policy.exec.denyCommands"
1099
- };
1100
- }
1101
- }
1102
- if (policy.exec.allowCommands) {
1103
- const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
1104
- if (!allowed) {
1105
- return {
1106
- allowed: false,
1107
- reason: `Command "${command}" is not in the exec allowlist`,
1108
- policySource: ".q-ring.json policy.exec.allowCommands"
1109
- };
1403
+ // src/core/keyring.ts
1404
+ function withJitEnvelopeLock(service, key, fn) {
1405
+ const dir = join7(homedir5(), ".config", "q-ring", "jit-locks");
1406
+ mkdirSync5(dir, { recursive: true });
1407
+ const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
1408
+ const lockPath = join7(dir, `${safe}.lock`);
1409
+ const deadline = Date.now() + 8e3;
1410
+ while (Date.now() < deadline) {
1411
+ try {
1412
+ writeFileSync4(lockPath, `${process.pid}
1413
+ `, { flag: "wx", mode: 384 });
1414
+ try {
1415
+ return fn();
1416
+ } finally {
1417
+ try {
1418
+ unlinkSync(lockPath);
1419
+ } catch {
1420
+ }
1421
+ }
1422
+ } catch {
1423
+ const start = Date.now();
1424
+ while (Date.now() - start < 15) {
1425
+ }
1110
1426
  }
1111
1427
  }
1112
- return { allowed: true, policySource: ".q-ring.json" };
1428
+ throw new Error("Could not acquire JIT envelope lock (timeout)");
1113
1429
  }
1114
- function getExecMaxRuntime(projectPath) {
1115
- return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
1116
- }
1117
- function getPolicySummary(projectPath) {
1118
- const policy = loadPolicy(projectPath);
1119
- return {
1120
- hasMcpPolicy: !!policy.mcp,
1121
- hasExecPolicy: !!policy.exec,
1122
- hasSecretPolicy: !!policy.secrets,
1123
- details: policy
1124
- };
1125
- }
1126
-
1127
- // src/core/keyring.ts
1128
1430
  function readEnvelope(service, key) {
1129
1431
  const entry = new Entry(service, key);
1130
1432
  const raw = entry.getPassword();
@@ -1143,7 +1445,7 @@ function resolveEnv(opts) {
1143
1445
  }
1144
1446
  function resolveTemplates(value, opts, seen) {
1145
1447
  if (!value.includes("{{") || !value.includes("}}")) return value;
1146
- return value.replace(/\{\{([^}]+)\}\}/g, (match, refKeyRaw) => {
1448
+ return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
1147
1449
  const refKey = refKeyRaw.trim();
1148
1450
  const refValue = getSecret(refKey, { ...opts, _seen: seen });
1149
1451
  if (refValue === null) {
@@ -1154,7 +1456,7 @@ function resolveTemplates(value, opts, seen) {
1154
1456
  }
1155
1457
  function resolveTemplatesOffline(value, rawValues, seen) {
1156
1458
  if (!value.includes("{{") || !value.includes("}}")) return value;
1157
- return value.replace(/\{\{([^}]+)\}\}/g, (match, refKeyRaw) => {
1459
+ return value.replace(/\{\{([^}]+)\}\}/g, (_match, refKeyRaw) => {
1158
1460
  const refKey = refKeyRaw.trim();
1159
1461
  if (seen.has(refKey)) {
1160
1462
  throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${refKey}`);
@@ -1224,11 +1526,27 @@ function getSecret(key, opts = {}) {
1224
1526
  isExpired = new Date(envelope.meta.jitExpiresAt).getTime() <= Date.now();
1225
1527
  }
1226
1528
  if (isExpired) {
1227
- const result = provider.provision(value);
1228
- envelope.states = envelope.states ?? {};
1229
- envelope.states["jit"] = result.value;
1230
- envelope.meta.jitExpiresAt = result.expiresAt;
1231
- writeEnvelope(service, key, envelope);
1529
+ const jitConfigSource = value;
1530
+ withJitEnvelopeLock(service, key, () => {
1531
+ const latest = readEnvelope(service, key);
1532
+ if (!latest?.meta.jitProvider) return;
1533
+ let innerExpired = true;
1534
+ if (latest.states && latest.states["jit"] && latest.meta.jitExpiresAt) {
1535
+ innerExpired = new Date(latest.meta.jitExpiresAt).getTime() <= Date.now();
1536
+ }
1537
+ if (!innerExpired) {
1538
+ envelope.states = latest.states;
1539
+ envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
1540
+ return;
1541
+ }
1542
+ const result = provider.provision(jitConfigSource);
1543
+ latest.states = latest.states ?? {};
1544
+ latest.states["jit"] = result.value;
1545
+ latest.meta.jitExpiresAt = result.expiresAt;
1546
+ writeEnvelope(service, key, latest);
1547
+ envelope.states = latest.states;
1548
+ envelope.meta.jitExpiresAt = latest.meta.jitExpiresAt;
1549
+ });
1232
1550
  }
1233
1551
  value = envelope.states["jit"];
1234
1552
  }
@@ -1270,6 +1588,20 @@ function setSecret(key, value, opts = {}) {
1270
1588
  const prov = opts.provider ?? existing?.meta.provider;
1271
1589
  const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
1272
1590
  const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
1591
+ const mergedTags = opts.tags ?? existing?.meta.tags;
1592
+ const ttlForPolicy = opts.ttlSeconds ?? existing?.meta.ttlSeconds;
1593
+ const life = checkSecretLifecyclePolicy(
1594
+ {
1595
+ tags: mergedTags,
1596
+ ttlSeconds: ttlForPolicy,
1597
+ rotationFormat: rotFmt,
1598
+ requiresApproval: !!reqApp
1599
+ },
1600
+ opts.projectPath
1601
+ );
1602
+ if (!life.allowed) {
1603
+ throw new Error(life.reason ?? "Secret lifecycle policy denied");
1604
+ }
1273
1605
  if (opts.states) {
1274
1606
  envelope = createEnvelope("", {
1275
1607
  states: opts.states,
@@ -1586,7 +1918,122 @@ function tunnelList() {
1586
1918
  return result;
1587
1919
  }
1588
1920
 
1921
+ // src/core/memory.ts
1922
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
1923
+ import { join as join8 } from "path";
1924
+ import { homedir as homedir6, hostname, userInfo } from "os";
1925
+ import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes3 } from "crypto";
1926
+ import { Entry as Entry2 } from "@napi-rs/keyring";
1927
+ var MEMORY_FILE = "agent-memory.enc";
1928
+ var KEYRING_SERVICE = "qring-memory-key";
1929
+ var KEYRING_ACCOUNT = "encryption-key";
1930
+ function getMemoryDir() {
1931
+ const dir = join8(homedir6(), ".config", "q-ring");
1932
+ if (!existsSync6(dir)) {
1933
+ mkdirSync6(dir, { recursive: true });
1934
+ }
1935
+ return dir;
1936
+ }
1937
+ function getMemoryPath() {
1938
+ return join8(getMemoryDir(), MEMORY_FILE);
1939
+ }
1940
+ function deriveLegacyKey() {
1941
+ const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
1942
+ return createHash3("sha256").update(fingerprint).digest();
1943
+ }
1944
+ function getOrCreateKey() {
1945
+ try {
1946
+ const entry = new Entry2(KEYRING_SERVICE, KEYRING_ACCOUNT);
1947
+ const stored = entry.getPassword();
1948
+ if (stored) return Buffer.from(stored, "base64");
1949
+ const key = randomBytes3(32);
1950
+ entry.setPassword(key.toString("base64"));
1951
+ return key;
1952
+ } catch {
1953
+ console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
1954
+ return deriveLegacyKey();
1955
+ }
1956
+ }
1957
+ function encryptWith(data, key) {
1958
+ const iv = randomBytes3(12);
1959
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
1960
+ const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
1961
+ const tag = cipher.getAuthTag();
1962
+ return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
1963
+ }
1964
+ function decryptWith(blob, key) {
1965
+ const parts = blob.split(":");
1966
+ if (parts.length !== 3) throw new Error("Invalid encrypted format");
1967
+ const iv = Buffer.from(parts[0], "base64");
1968
+ const tag = Buffer.from(parts[1], "base64");
1969
+ const encrypted = Buffer.from(parts[2], "base64");
1970
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
1971
+ decipher.setAuthTag(tag);
1972
+ return decipher.update(encrypted) + decipher.final("utf8");
1973
+ }
1974
+ function encrypt(data) {
1975
+ return encryptWith(data, getOrCreateKey());
1976
+ }
1977
+ function decrypt(blob) {
1978
+ const key = getOrCreateKey();
1979
+ try {
1980
+ return decryptWith(blob, key);
1981
+ } catch {
1982
+ const legacy = deriveLegacyKey();
1983
+ const plain = decryptWith(blob, legacy);
1984
+ writeFileSync5(getMemoryPath(), encryptWith(plain, key), "utf8");
1985
+ return plain;
1986
+ }
1987
+ }
1988
+ function loadStore() {
1989
+ const path = getMemoryPath();
1990
+ if (!existsSync6(path)) {
1991
+ return { entries: {} };
1992
+ }
1993
+ try {
1994
+ const raw = readFileSync7(path, "utf8");
1995
+ const decrypted = decrypt(raw);
1996
+ return JSON.parse(decrypted);
1997
+ } catch {
1998
+ return { entries: {} };
1999
+ }
2000
+ }
2001
+ function saveStore(store) {
2002
+ const json = JSON.stringify(store);
2003
+ const encrypted = encrypt(json);
2004
+ writeFileSync5(getMemoryPath(), encrypted, "utf8");
2005
+ }
2006
+ function remember(key, value) {
2007
+ const store = loadStore();
2008
+ store.entries[key] = {
2009
+ value,
2010
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2011
+ };
2012
+ saveStore(store);
2013
+ }
2014
+ function recall(key) {
2015
+ const store = loadStore();
2016
+ return store.entries[key]?.value ?? null;
2017
+ }
2018
+ function listMemory() {
2019
+ const store = loadStore();
2020
+ return Object.entries(store.entries).map(([key, entry]) => ({
2021
+ key,
2022
+ updatedAt: entry.updatedAt
2023
+ }));
2024
+ }
2025
+ function forget(key) {
2026
+ const store = loadStore();
2027
+ if (key in store.entries) {
2028
+ delete store.entries[key];
2029
+ saveStore(store);
2030
+ return true;
2031
+ }
2032
+ return false;
2033
+ }
2034
+
1589
2035
  export {
2036
+ PACKAGE_VERSION,
1590
2037
  checkDecay,
1591
2038
  readProjectConfig,
1592
2039
  collapseEnvironment,
@@ -1596,12 +2043,13 @@ export {
1596
2043
  exportAudit,
1597
2044
  detectAnomalies,
1598
2045
  listEntanglements,
1599
- httpRequest_,
2046
+ httpRequest,
1600
2047
  checkSSRF,
1601
2048
  registerHook,
1602
2049
  removeHook,
1603
2050
  listHooks,
1604
2051
  fireHooks,
2052
+ listApprovals,
1605
2053
  registry,
1606
2054
  checkToolPolicy,
1607
2055
  checkKeyReadPolicy,
@@ -1620,6 +2068,10 @@ export {
1620
2068
  tunnelCreate,
1621
2069
  tunnelRead,
1622
2070
  tunnelDestroy,
1623
- tunnelList
2071
+ tunnelList,
2072
+ remember,
2073
+ recall,
2074
+ listMemory,
2075
+ forget
1624
2076
  };
1625
- //# sourceMappingURL=chunk-ZV7LRKBA.js.map
2077
+ //# sourceMappingURL=chunk-MQM4DLPI.js.map