@i4ctime/q-ring 0.11.5 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -51
- package/dist/{chunk-A4RZVP3P.js → chunk-5TQNDJT3.js} +160 -90
- package/dist/chunk-5TQNDJT3.js.map +1 -0
- package/dist/{chunk-MQM4DLPI.js → chunk-YWDKAZWS.js} +165 -90
- package/dist/chunk-YWDKAZWS.js.map +1 -0
- package/dist/dashboard-6XKC5BL4.js +630 -0
- package/dist/dashboard-6XKC5BL4.js.map +1 -0
- package/dist/dashboard-HHJY73JX.js +630 -0
- package/dist/dashboard-HHJY73JX.js.map +1 -0
- package/dist/index.js +45 -30
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +516 -166
- package/dist/mcp.js.map +1 -1
- package/package.json +15 -4
- package/dist/chunk-A4RZVP3P.js.map +0 -1
- package/dist/chunk-MQM4DLPI.js.map +0 -1
- package/dist/dashboard-FSCJDLJX.js +0 -1313
- package/dist/dashboard-FSCJDLJX.js.map +0 -1
- package/dist/dashboard-OXUD62RV.js +0 -1313
- package/dist/dashboard-OXUD62RV.js.map +0 -1
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ qring tunnel list
|
|
|
182
182
|
|
|
183
183
|
### Teleportation — Encrypted Sharing
|
|
184
184
|
|
|
185
|
-
Pack secrets into AES-256-GCM encrypted bundles for secure transfer between machines.
|
|
185
|
+
Pack secrets into AES-256-GCM encrypted bundles for secure transfer between machines. Keys are derived with PBKDF2-HMAC-SHA512 (210 000 iterations) from your passphrase; each bundle records its iteration count, so bundles produced by older versions still unpack.
|
|
186
186
|
|
|
187
187
|
```bash
|
|
188
188
|
# Pack secrets (prompts for passphrase)
|
|
@@ -197,7 +197,7 @@ qring teleport unpack <bundle> --dry-run
|
|
|
197
197
|
|
|
198
198
|
### Import — Bulk Secret Ingestion
|
|
199
199
|
|
|
200
|
-
Import secrets from `.env` files directly into q-ring. Supports standard dotenv syntax including comments, quoted values, and escape sequences.
|
|
200
|
+
Import secrets from `.env` files directly into q-ring. Supports standard dotenv syntax including comments, quoted values, and escape sequences. The CLI accepts either a file path or raw content; the `import_dotenv` MCP tool only accepts raw content (it never reads files from disk) so an agent can't coerce it into reading arbitrary local files.
|
|
201
201
|
|
|
202
202
|
```bash
|
|
203
203
|
# Import all secrets from a .env file
|
|
@@ -335,7 +335,7 @@ qring hook test <id>
|
|
|
335
335
|
|
|
336
336
|
Hooks are fire-and-forget: a failing hook never blocks secret operations. The hook registry is stored at `~/.config/q-ring/hooks.json`.
|
|
337
337
|
|
|
338
|
-
**SSRF protection:** HTTP hook URLs targeting private/loopback IP ranges (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`) are blocked by default. DNS
|
|
338
|
+
**SSRF protection:** HTTP hook URLs targeting private/loopback IP ranges (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`) are blocked by default. DNS is checked up front *and* re-validated at connect time, so a hostname can't pass the check then rebind to a private address before the socket opens. To allow hooks targeting local services (e.g. during development), set the environment variable `Q_RING_ALLOW_PRIVATE_HOOKS=1`.
|
|
339
339
|
|
|
340
340
|
### Configurable Rotation
|
|
341
341
|
|
|
@@ -397,7 +397,7 @@ qring get DB_URL
|
|
|
397
397
|
|
|
398
398
|
### User Approvals (Zero-Trust Agent)
|
|
399
399
|
|
|
400
|
-
Protect sensitive production secrets from being read autonomously by the MCP server without explicit user approval. Each approval token is HMAC-verified, scoped, reasoned, and time-limited.
|
|
400
|
+
Protect sensitive production secrets from being read autonomously by the MCP server without explicit user approval. Each approval token is HMAC-verified, scoped, reasoned, and time-limited. The gate applies to bulk reads too — `export_secrets` and `teleport_pack` over MCP skip approval-protected keys that lack a valid grant.
|
|
401
401
|
|
|
402
402
|
```bash
|
|
403
403
|
# Mark a secret as requiring approval
|
|
@@ -508,6 +508,8 @@ qring wizard myservice --hook-exec "pm2 restart app"
|
|
|
508
508
|
|
|
509
509
|
Define project-level governance rules in `.q-ring.json` to control which MCP tools can be used, which keys are accessible, and which commands can be executed. Policy is enforced at both the MCP server and keyring level.
|
|
510
510
|
|
|
511
|
+
Over MCP, policy is resolved from the directory the server was **launched** in — not from the `projectPath` a caller passes — so an agent can't sidestep restrictions by pointing at a directory with no policy. Launch the MCP server from your project root (where `.q-ring.json` lives). Edits to `.q-ring.json` are picked up automatically (the policy cache invalidates on file change), so you don't need to restart the server.
|
|
512
|
+
|
|
511
513
|
```bash
|
|
512
514
|
# View the active policy
|
|
513
515
|
qring policy
|
|
@@ -626,7 +628,7 @@ qring agent --once
|
|
|
626
628
|
|
|
627
629
|
### Quantum Status Dashboard — Live Monitoring
|
|
628
630
|
|
|
629
|
-
Launch a real-time dashboard in your browser that turns the entire quantum subsystem into one glanceable page.
|
|
631
|
+
Launch a real-time dashboard in your browser that turns the entire quantum subsystem into one glanceable page. It's a single self-contained HTML page served locally — no cloud, no config, fully offline — built as a Preact + htm app (runtime bundled and inlined). It streams updates every 5 seconds via Server-Sent Events and diffs the DOM in place, so data refreshes without re-running entrance animations and your search input, caret, and scroll position are preserved across ticks.
|
|
630
632
|
|
|
631
633
|
What you get:
|
|
632
634
|
|
|
@@ -644,14 +646,16 @@ What you get:
|
|
|
644
646
|
|
|
645
647
|
Top-bar controls let you **pause** SSE updates (handy while reading the audit feed), **refresh** on demand, or jump to the raw JSON snapshot at `/api/status`. Keyboard shortcuts: `/` focus secrets search · `P` pause · `R` refresh.
|
|
646
648
|
|
|
649
|
+
The dashboard binds to `127.0.0.1` only and **never** exposes secret values, but it does surface key names, the audit log, and approval grants — so every route is gated by a random, per-launch token. `qring status` prints (and opens) the full URL including `?token=…`; requests without the token get a `403`. Stop the server to invalidate the token.
|
|
650
|
+
|
|
647
651
|
```bash
|
|
648
|
-
# Open the dashboard (auto-launches your browser)
|
|
652
|
+
# Open the dashboard (auto-launches your browser at http://127.0.0.1:9876/?token=…)
|
|
649
653
|
qring status
|
|
650
654
|
|
|
651
655
|
# Specify a custom port
|
|
652
656
|
qring status --port 4200
|
|
653
657
|
|
|
654
|
-
# Don't auto-open the browser
|
|
658
|
+
# Don't auto-open the browser (copy the printed tokenized URL yourself)
|
|
655
659
|
qring status --no-open
|
|
656
660
|
```
|
|
657
661
|
|
|
@@ -663,95 +667,95 @@ q-ring includes a full MCP server with 44 tools for AI agent integration.
|
|
|
663
667
|
|
|
664
668
|
| Tool | Description |
|
|
665
669
|
|------|-------------|
|
|
666
|
-
| `get_secret` |
|
|
667
|
-
| `list_secrets` | List keys
|
|
668
|
-
| `set_secret` |
|
|
669
|
-
| `delete_secret` |
|
|
670
|
-
| `has_secret` | Boolean check
|
|
671
|
-
| `export_secrets` |
|
|
672
|
-
| `import_dotenv` | Parse and
|
|
673
|
-
| `check_project` |
|
|
674
|
-
| `env_generate` |
|
|
670
|
+
| `get_secret` | Read a secret value (collapses superposition, audits the read) |
|
|
671
|
+
| `list_secrets` | List keys + metadata in scope (values never exposed); filter by tag, expiry, glob |
|
|
672
|
+
| `set_secret` | Create or overwrite a single secret with optional TTL, per-env state, tags, rotation format |
|
|
673
|
+
| `delete_secret` | Permanently remove a secret value (not undoable from q-ring) |
|
|
674
|
+
| `has_secret` | Boolean existence check that respects decay (no audit read) |
|
|
675
|
+
| `export_secrets` | Render multiple secrets as `.env` or JSON for one-off export (skips approval-protected keys without a grant) |
|
|
676
|
+
| `import_dotenv` | Parse `.env` text and bulk-store every key/value pair (accepts raw content only — never reads files) |
|
|
677
|
+
| `check_project` | Compare `.q-ring.json` manifest against the keyring for missing/expired/stale keys |
|
|
678
|
+
| `env_generate` | Render a complete `.env` body from the project manifest, with warnings for gaps |
|
|
675
679
|
|
|
676
680
|
### Quantum Tools
|
|
677
681
|
|
|
678
682
|
| Tool | Description |
|
|
679
683
|
|------|-------------|
|
|
680
|
-
| `inspect_secret` |
|
|
681
|
-
| `detect_environment` |
|
|
682
|
-
| `generate_secret` |
|
|
683
|
-
| `entangle_secrets` | Link two
|
|
684
|
-
| `disentangle_secrets` |
|
|
684
|
+
| `inspect_secret` | Show metadata for one key (states, decay, entanglement, access count) without revealing the value |
|
|
685
|
+
| `detect_environment` | Resolve which env slug should drive superposition collapse for the current context |
|
|
686
|
+
| `generate_secret` | Generate a CSPRNG-backed value in a chosen format and optionally store it |
|
|
687
|
+
| `entangle_secrets` | Link two keys so future writes/rotations propagate the same value |
|
|
688
|
+
| `disentangle_secrets` | Break the sync link between two keys (does not delete values) |
|
|
685
689
|
|
|
686
690
|
### Tunneling Tools
|
|
687
691
|
|
|
688
692
|
| Tool | Description |
|
|
689
693
|
|------|-------------|
|
|
690
|
-
| `tunnel_create` |
|
|
691
|
-
| `tunnel_read` |
|
|
692
|
-
| `tunnel_list` |
|
|
693
|
-
| `tunnel_destroy` | Immediately
|
|
694
|
+
| `tunnel_create` | Stash a value in process memory and return an opaque ID (never touches disk) |
|
|
695
|
+
| `tunnel_read` | Fetch a tunneled value by ID — may self-destruct on read |
|
|
696
|
+
| `tunnel_list` | Enumerate active tunnels with remaining read budget and TTL (IDs only) |
|
|
697
|
+
| `tunnel_destroy` | Immediately remove a tunnel from memory before its TTL/reads run out |
|
|
694
698
|
|
|
695
699
|
### Teleportation Tools
|
|
696
700
|
|
|
697
701
|
| Tool | Description |
|
|
698
702
|
|------|-------------|
|
|
699
|
-
| `teleport_pack` | Encrypt secrets into a
|
|
700
|
-
| `teleport_unpack` | Decrypt and import
|
|
703
|
+
| `teleport_pack` | Encrypt selected secrets into a passphrase-protected AES-256-GCM bundle |
|
|
704
|
+
| `teleport_unpack` | Decrypt a teleport bundle and import each secret (with optional dry-run) |
|
|
701
705
|
|
|
702
706
|
### Validation Tools
|
|
703
707
|
|
|
704
708
|
| Tool | Description |
|
|
705
709
|
|------|-------------|
|
|
706
|
-
| `validate_secret` |
|
|
707
|
-
| `list_providers` |
|
|
710
|
+
| `validate_secret` | Hit the upstream service (OpenAI/Stripe/GitHub/AWS/HTTP) to confirm a single key is still live |
|
|
711
|
+
| `list_providers` | Enumerate built-in validation providers and their auto-detect prefixes |
|
|
708
712
|
|
|
709
713
|
### Hook Tools
|
|
710
714
|
|
|
711
715
|
| Tool | Description |
|
|
712
716
|
|------|-------------|
|
|
713
|
-
| `register_hook` | Register a shell/HTTP/signal
|
|
714
|
-
| `list_hooks` |
|
|
715
|
-
| `remove_hook` |
|
|
717
|
+
| `register_hook` | Register a shell/HTTP/signal side-effect that fires on write/delete/rotate |
|
|
718
|
+
| `list_hooks` | Show every registered hook with match criteria, type, and enabled flag |
|
|
719
|
+
| `remove_hook` | Detach a single hook by ID without touching any secrets |
|
|
716
720
|
|
|
717
721
|
### Execution & Scanning Tools
|
|
718
722
|
|
|
719
723
|
| Tool | Description |
|
|
720
724
|
|------|-------------|
|
|
721
|
-
| `exec_with_secrets` | Run a
|
|
722
|
-
| `scan_codebase_for_secrets` |
|
|
723
|
-
| `lint_files` |
|
|
725
|
+
| `exec_with_secrets` | Run a child command with secrets injected as env vars and any leaked values redacted from output |
|
|
726
|
+
| `scan_codebase_for_secrets` | Walk a directory tree and flag hardcoded secrets via regex + entropy heuristics |
|
|
727
|
+
| `lint_files` | Inspect a specific file list for hardcoded secrets with optional auto-fix to `process.env.KEY` |
|
|
724
728
|
|
|
725
729
|
### AI Agent Tools
|
|
726
730
|
|
|
727
731
|
| Tool | Description |
|
|
728
732
|
|------|-------------|
|
|
729
|
-
| `get_project_context` |
|
|
730
|
-
| `agent_remember` |
|
|
731
|
-
| `agent_recall` |
|
|
732
|
-
| `agent_forget` |
|
|
733
|
-
| `analyze_secrets` | Usage
|
|
733
|
+
| `get_project_context` | Single redacted snapshot of secrets, env, manifest, hooks, and recent audit activity |
|
|
734
|
+
| `agent_remember` | Persist a non-secret note in encrypted agent memory across sessions |
|
|
735
|
+
| `agent_recall` | Read a memory value, or list every stored key when no key is supplied |
|
|
736
|
+
| `agent_forget` | Permanently delete a key from agent memory |
|
|
737
|
+
| `analyze_secrets` | Usage profile: most-accessed, stale, never-accessed, no-rotation candidates |
|
|
734
738
|
|
|
735
739
|
### Observer & Health Tools
|
|
736
740
|
|
|
737
741
|
| Tool | Description |
|
|
738
742
|
|------|-------------|
|
|
739
|
-
| `audit_log` | Query
|
|
740
|
-
| `detect_anomalies` |
|
|
741
|
-
| `verify_audit_chain` |
|
|
742
|
-
| `export_audit` | Export audit events
|
|
743
|
-
| `health_check` |
|
|
744
|
-
| `status_dashboard` |
|
|
745
|
-
| `agent_scan` |
|
|
743
|
+
| `audit_log` | Query the tamper-evident audit log filtered by key, action, and limit |
|
|
744
|
+
| `detect_anomalies` | Surface burst-read and off-hours findings from audit history |
|
|
745
|
+
| `verify_audit_chain` | Recompute the audit hash chain and report the first break point if tampered |
|
|
746
|
+
| `export_audit` | Export audit events as jsonl, json, or csv for archival/SIEM |
|
|
747
|
+
| `health_check` | Read-only scope sweep: decay/stale/expired counts plus current anomalies |
|
|
748
|
+
| `status_dashboard` | Start a local SSE dashboard with live KPIs, secrets, hooks, and audit feed (returns a token-gated `127.0.0.1` URL) |
|
|
749
|
+
| `agent_scan` | Multi-project health pass with optional `autoRotate` for expired secrets |
|
|
746
750
|
|
|
747
751
|
### Governance & Policy Tools
|
|
748
752
|
|
|
749
753
|
| Tool | Description |
|
|
750
754
|
|------|-------------|
|
|
751
|
-
| `check_policy` |
|
|
752
|
-
| `get_policy_summary` |
|
|
753
|
-
| `rotate_secret` |
|
|
754
|
-
| `ci_validate_secrets` |
|
|
755
|
+
| `check_policy` | Dry-run a tool/key/exec action against `.q-ring.json` policy without performing it |
|
|
756
|
+
| `get_policy_summary` | High-level overview of policy rule counts and approval/rotation requirements |
|
|
757
|
+
| `rotate_secret` | Ask the upstream provider to issue a new credential and store it back in the keyring |
|
|
758
|
+
| `ci_validate_secrets` | Batch-validate every accessible secret in scope and return a structured pass/fail report |
|
|
755
759
|
|
|
756
760
|
### Cursor / Kiro Configuration
|
|
757
761
|
|
|
@@ -555,87 +555,11 @@ import { randomUUID } from "crypto";
|
|
|
555
555
|
// src/utils/http-request.ts
|
|
556
556
|
import { request as httpsRequest } from "https";
|
|
557
557
|
import { request as httpRequestPlain } from "http";
|
|
558
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
559
|
-
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
560
|
-
function httpRequest(opts) {
|
|
561
|
-
const {
|
|
562
|
-
url,
|
|
563
|
-
method = "GET",
|
|
564
|
-
headers = {},
|
|
565
|
-
body,
|
|
566
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
567
|
-
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES
|
|
568
|
-
} = opts;
|
|
569
|
-
return new Promise((resolve, reject) => {
|
|
570
|
-
const parsed = new URL(url);
|
|
571
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
572
|
-
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
576
|
-
const reqHeaders = { ...headers };
|
|
577
|
-
if (body && !reqHeaders["Content-Length"]) {
|
|
578
|
-
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
579
|
-
}
|
|
580
|
-
const req = reqFn(
|
|
581
|
-
url,
|
|
582
|
-
{ method, headers: reqHeaders, timeout: timeoutMs },
|
|
583
|
-
(res) => {
|
|
584
|
-
const chunks = [];
|
|
585
|
-
let totalBytes = 0;
|
|
586
|
-
let truncated = false;
|
|
587
|
-
res.on("data", (chunk) => {
|
|
588
|
-
totalBytes += chunk.length;
|
|
589
|
-
if (totalBytes > maxResponseBytes) {
|
|
590
|
-
truncated = true;
|
|
591
|
-
res.destroy();
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
chunks.push(chunk);
|
|
595
|
-
});
|
|
596
|
-
let settled = false;
|
|
597
|
-
const settle = (result) => {
|
|
598
|
-
if (!settled) {
|
|
599
|
-
settled = true;
|
|
600
|
-
resolve(result);
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
const fail = (err) => {
|
|
604
|
-
if (!settled) {
|
|
605
|
-
settled = true;
|
|
606
|
-
reject(err);
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
res.on("error", (err) => fail(new Error(`Response error: ${err.message}`)));
|
|
610
|
-
res.on("end", () => {
|
|
611
|
-
settle({
|
|
612
|
-
statusCode: res.statusCode ?? 0,
|
|
613
|
-
body: Buffer.concat(chunks).toString("utf8"),
|
|
614
|
-
truncated
|
|
615
|
-
});
|
|
616
|
-
});
|
|
617
|
-
res.on("close", () => {
|
|
618
|
-
settle({
|
|
619
|
-
statusCode: res.statusCode ?? 0,
|
|
620
|
-
body: Buffer.concat(chunks).toString("utf8"),
|
|
621
|
-
truncated
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
);
|
|
626
|
-
req.on("error", (err) => reject(new Error(`Network error: ${err.message}`)));
|
|
627
|
-
req.on("timeout", () => {
|
|
628
|
-
req.destroy();
|
|
629
|
-
reject(new Error("Request timed out"));
|
|
630
|
-
});
|
|
631
|
-
if (body) req.write(body);
|
|
632
|
-
req.end();
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
558
|
|
|
636
559
|
// src/core/ssrf.ts
|
|
637
560
|
import { lookup } from "dns/promises";
|
|
638
561
|
import * as dns from "dns";
|
|
562
|
+
import { lookup as dnsLookup } from "dns";
|
|
639
563
|
import { isIPv4, isIPv6 } from "net";
|
|
640
564
|
function lookupAddressesSync(hostname2) {
|
|
641
565
|
const lookupSync2 = dns.lookupSync;
|
|
@@ -682,6 +606,36 @@ async function checkSSRF(url) {
|
|
|
682
606
|
}
|
|
683
607
|
return null;
|
|
684
608
|
}
|
|
609
|
+
function guardedLookup(hostname2, options, callback) {
|
|
610
|
+
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") {
|
|
611
|
+
dnsLookup(
|
|
612
|
+
hostname2,
|
|
613
|
+
options,
|
|
614
|
+
callback
|
|
615
|
+
);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
dnsLookup(
|
|
619
|
+
hostname2,
|
|
620
|
+
options,
|
|
621
|
+
(err, address, family) => {
|
|
622
|
+
if (err) return callback(err, address, family);
|
|
623
|
+
const list = Array.isArray(address) ? address : [{ address, family: family ?? 0 }];
|
|
624
|
+
for (const a of list) {
|
|
625
|
+
if (isPrivateIP(a.address)) {
|
|
626
|
+
const blocked = Object.assign(
|
|
627
|
+
new Error(
|
|
628
|
+
`Blocked: "${hostname2}" resolved to private address ${a.address} at connect time.`
|
|
629
|
+
),
|
|
630
|
+
{ code: "EQRINGSSRF" }
|
|
631
|
+
);
|
|
632
|
+
return callback(blocked, address, family);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
callback(null, address, family);
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
}
|
|
685
639
|
function checkJitHttpProvisionUrl(url) {
|
|
686
640
|
if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
|
|
687
641
|
try {
|
|
@@ -719,6 +673,91 @@ function checkJitHttpProvisionUrl(url) {
|
|
|
719
673
|
return null;
|
|
720
674
|
}
|
|
721
675
|
|
|
676
|
+
// src/utils/http-request.ts
|
|
677
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
678
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 65536;
|
|
679
|
+
function httpRequest(opts) {
|
|
680
|
+
const {
|
|
681
|
+
url,
|
|
682
|
+
method = "GET",
|
|
683
|
+
headers = {},
|
|
684
|
+
body,
|
|
685
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
686
|
+
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES
|
|
687
|
+
} = opts;
|
|
688
|
+
return new Promise((resolve, reject) => {
|
|
689
|
+
const parsed = new URL(url);
|
|
690
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
691
|
+
reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequestPlain;
|
|
695
|
+
const reqHeaders = { ...headers };
|
|
696
|
+
if (body && !reqHeaders["Content-Length"]) {
|
|
697
|
+
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
698
|
+
}
|
|
699
|
+
const req = reqFn(
|
|
700
|
+
url,
|
|
701
|
+
{
|
|
702
|
+
method,
|
|
703
|
+
headers: reqHeaders,
|
|
704
|
+
timeout: timeoutMs,
|
|
705
|
+
// Re-validate the resolved IP at connect time (DNS-rebinding guard).
|
|
706
|
+
lookup: guardedLookup
|
|
707
|
+
},
|
|
708
|
+
(res) => {
|
|
709
|
+
const chunks = [];
|
|
710
|
+
let totalBytes = 0;
|
|
711
|
+
let truncated = false;
|
|
712
|
+
res.on("data", (chunk) => {
|
|
713
|
+
totalBytes += chunk.length;
|
|
714
|
+
if (totalBytes > maxResponseBytes) {
|
|
715
|
+
truncated = true;
|
|
716
|
+
res.destroy();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
chunks.push(chunk);
|
|
720
|
+
});
|
|
721
|
+
let settled = false;
|
|
722
|
+
const settle = (result) => {
|
|
723
|
+
if (!settled) {
|
|
724
|
+
settled = true;
|
|
725
|
+
resolve(result);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
const fail = (err) => {
|
|
729
|
+
if (!settled) {
|
|
730
|
+
settled = true;
|
|
731
|
+
reject(err);
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
res.on("error", (err) => fail(new Error(`Response error: ${err.message}`)));
|
|
735
|
+
res.on("end", () => {
|
|
736
|
+
settle({
|
|
737
|
+
statusCode: res.statusCode ?? 0,
|
|
738
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
739
|
+
truncated
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
res.on("close", () => {
|
|
743
|
+
settle({
|
|
744
|
+
statusCode: res.statusCode ?? 0,
|
|
745
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
746
|
+
truncated
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
req.on("error", (err) => reject(new Error(`Network error: ${err.message}`)));
|
|
752
|
+
req.on("timeout", () => {
|
|
753
|
+
req.destroy();
|
|
754
|
+
reject(new Error("Request timed out"));
|
|
755
|
+
});
|
|
756
|
+
if (body) req.write(body);
|
|
757
|
+
req.end();
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
722
761
|
// src/core/hooks.ts
|
|
723
762
|
function getRegistryPath2() {
|
|
724
763
|
const dir = join5(homedir3(), ".config", "q-ring");
|
|
@@ -1128,15 +1167,29 @@ function listApprovals() {
|
|
|
1128
1167
|
}
|
|
1129
1168
|
|
|
1130
1169
|
// src/core/policy.ts
|
|
1170
|
+
import { statSync as statSync2 } from "fs";
|
|
1171
|
+
import { join as join7 } from "path";
|
|
1131
1172
|
var cachedPolicy = null;
|
|
1173
|
+
var policyRoot = null;
|
|
1174
|
+
function resolvePolicyPath(projectPath) {
|
|
1175
|
+
return policyRoot ?? projectPath ?? process.cwd();
|
|
1176
|
+
}
|
|
1177
|
+
function configMtime(pp) {
|
|
1178
|
+
try {
|
|
1179
|
+
return statSync2(join7(pp, ".q-ring.json")).mtimeMs;
|
|
1180
|
+
} catch {
|
|
1181
|
+
return 0;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1132
1184
|
function loadPolicy(projectPath) {
|
|
1133
|
-
const pp = projectPath
|
|
1134
|
-
|
|
1185
|
+
const pp = resolvePolicyPath(projectPath);
|
|
1186
|
+
const mtimeMs = configMtime(pp);
|
|
1187
|
+
if (cachedPolicy && cachedPolicy.path === pp && cachedPolicy.mtimeMs === mtimeMs) {
|
|
1135
1188
|
return cachedPolicy.policy;
|
|
1136
1189
|
}
|
|
1137
1190
|
const config = readProjectConfig(pp);
|
|
1138
1191
|
const policy = config?.policy ?? {};
|
|
1139
|
-
cachedPolicy = { path: pp, policy };
|
|
1192
|
+
cachedPolicy = { path: pp, mtimeMs, policy };
|
|
1140
1193
|
return policy;
|
|
1141
1194
|
}
|
|
1142
1195
|
function checkSecretLifecyclePolicy(input, projectPath) {
|
|
@@ -1205,7 +1258,13 @@ function checkExecPolicy(command, projectPath) {
|
|
|
1205
1258
|
const policy = loadPolicy(projectPath);
|
|
1206
1259
|
if (!policy.exec) return { allowed: true, policySource: "no-policy" };
|
|
1207
1260
|
if (policy.exec.denyCommands) {
|
|
1208
|
-
const denied = policy.exec.denyCommands.find((d) =>
|
|
1261
|
+
const denied = policy.exec.denyCommands.find((d) => {
|
|
1262
|
+
const pattern = new RegExp(
|
|
1263
|
+
`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`,
|
|
1264
|
+
"i"
|
|
1265
|
+
);
|
|
1266
|
+
return pattern.test(command);
|
|
1267
|
+
});
|
|
1209
1268
|
if (denied) {
|
|
1210
1269
|
return {
|
|
1211
1270
|
allowed: false,
|
|
@@ -1215,7 +1274,8 @@ function checkExecPolicy(command, projectPath) {
|
|
|
1215
1274
|
}
|
|
1216
1275
|
}
|
|
1217
1276
|
if (policy.exec.allowCommands) {
|
|
1218
|
-
const
|
|
1277
|
+
const normalized = command.trimStart();
|
|
1278
|
+
const allowed = policy.exec.allowCommands.some((a) => normalized.startsWith(a));
|
|
1219
1279
|
if (!allowed) {
|
|
1220
1280
|
return {
|
|
1221
1281
|
allowed: false,
|
|
@@ -1242,7 +1302,7 @@ function getPolicySummary(projectPath) {
|
|
|
1242
1302
|
// src/core/keyring.ts
|
|
1243
1303
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
1244
1304
|
import { homedir as homedir5 } from "os";
|
|
1245
|
-
import { join as
|
|
1305
|
+
import { join as join8 } from "path";
|
|
1246
1306
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
1247
1307
|
|
|
1248
1308
|
// src/utils/hash.ts
|
|
@@ -1446,10 +1506,10 @@ registry.register(httpProvider);
|
|
|
1446
1506
|
|
|
1447
1507
|
// src/core/keyring.ts
|
|
1448
1508
|
function withJitEnvelopeLock(service, key, fn) {
|
|
1449
|
-
const dir =
|
|
1509
|
+
const dir = join8(homedir5(), ".config", "q-ring", "jit-locks");
|
|
1450
1510
|
mkdirSync5(dir, { recursive: true });
|
|
1451
1511
|
const safe = Buffer.from(`${service}\0${key}`, "utf8").toString("base64url");
|
|
1452
|
-
const lockPath =
|
|
1512
|
+
const lockPath = join8(dir, `${safe}.lock`);
|
|
1453
1513
|
const deadline = Date.now() + 8e3;
|
|
1454
1514
|
while (Date.now() < deadline) {
|
|
1455
1515
|
try {
|
|
@@ -1833,6 +1893,16 @@ function exportSecrets(opts = {}) {
|
|
|
1833
1893
|
if (entry.envelope) {
|
|
1834
1894
|
const decay = checkDecay(entry.envelope);
|
|
1835
1895
|
if (decay.isExpired) continue;
|
|
1896
|
+
if (source === "mcp" && entry.envelope.meta.requiresApproval && !hasApproval(entry.key, entry.scope)) {
|
|
1897
|
+
logAudit({
|
|
1898
|
+
action: "read",
|
|
1899
|
+
key: entry.key,
|
|
1900
|
+
scope: entry.scope,
|
|
1901
|
+
source,
|
|
1902
|
+
detail: "blocked: requires user approval (export)"
|
|
1903
|
+
});
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1836
1906
|
const value = collapseValue(entry.envelope, env);
|
|
1837
1907
|
if (value !== null) {
|
|
1838
1908
|
rawValues.set(entry.key, value);
|
|
@@ -1964,7 +2034,7 @@ function tunnelList() {
|
|
|
1964
2034
|
|
|
1965
2035
|
// src/core/memory.ts
|
|
1966
2036
|
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1967
|
-
import { join as
|
|
2037
|
+
import { join as join9 } from "path";
|
|
1968
2038
|
import { homedir as homedir6, hostname, userInfo } from "os";
|
|
1969
2039
|
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes3 } from "crypto";
|
|
1970
2040
|
import { Entry as Entry2 } from "@napi-rs/keyring";
|
|
@@ -1972,14 +2042,14 @@ var MEMORY_FILE = "agent-memory.enc";
|
|
|
1972
2042
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
1973
2043
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
1974
2044
|
function getMemoryDir() {
|
|
1975
|
-
const dir =
|
|
2045
|
+
const dir = join9(homedir6(), ".config", "q-ring");
|
|
1976
2046
|
if (!existsSync6(dir)) {
|
|
1977
2047
|
mkdirSync6(dir, { recursive: true });
|
|
1978
2048
|
}
|
|
1979
2049
|
return dir;
|
|
1980
2050
|
}
|
|
1981
2051
|
function getMemoryPath() {
|
|
1982
|
-
return
|
|
2052
|
+
return join9(getMemoryDir(), MEMORY_FILE);
|
|
1983
2053
|
}
|
|
1984
2054
|
function deriveLegacyKey() {
|
|
1985
2055
|
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
@@ -2090,8 +2160,8 @@ export {
|
|
|
2090
2160
|
exportAudit,
|
|
2091
2161
|
detectAnomalies,
|
|
2092
2162
|
listEntanglements,
|
|
2093
|
-
httpRequest,
|
|
2094
2163
|
checkSSRF,
|
|
2164
|
+
httpRequest,
|
|
2095
2165
|
registerHook,
|
|
2096
2166
|
removeHook,
|
|
2097
2167
|
listHooks,
|
|
@@ -2124,4 +2194,4 @@ export {
|
|
|
2124
2194
|
forget,
|
|
2125
2195
|
clearMemory
|
|
2126
2196
|
};
|
|
2127
|
-
//# sourceMappingURL=chunk-
|
|
2197
|
+
//# sourceMappingURL=chunk-5TQNDJT3.js.map
|