@i4ctime/q-ring 0.17.5 → 0.18.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 +52 -10
- package/dist/{chunk-VMDD5QTJ.js → chunk-NTCTVLSK.js} +126 -38
- package/dist/chunk-NTCTVLSK.js.map +1 -0
- package/dist/{chunk-5ZCQSBVS.js → chunk-ZBPMEJRO.js} +142 -38
- package/dist/chunk-ZBPMEJRO.js.map +1 -0
- package/dist/dashboard-LICF6OZ5.js +699 -0
- package/dist/dashboard-LICF6OZ5.js.map +1 -0
- package/dist/dashboard-OEHI6OOD.js +699 -0
- package/dist/dashboard-OEHI6OOD.js.map +1 -0
- package/dist/index.js +833 -82
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +681 -172
- package/dist/mcp.js.map +1 -1
- package/package.json +10 -10
- package/dist/chunk-5ZCQSBVS.js.map +0 -1
- package/dist/chunk-VMDD5QTJ.js.map +0 -1
- package/dist/dashboard-K7LYTWSL.js +0 -685
- package/dist/dashboard-K7LYTWSL.js.map +0 -1
- package/dist/dashboard-ZN3FPR73.js +0 -685
- package/dist/dashboard-ZN3FPR73.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.npmjs.com/package/@i4ctime/q-ring)
|
|
11
11
|
[](https://www.npmjs.com/package/@i4ctime/q-ring)
|
|
12
12
|
[](https://qring.i4c.studio/docs)
|
|
13
|
-
[](https://glama.ai/mcp/servers/I4cTime/q-ring)
|
|
14
14
|
[](https://smithery.ai/servers/i4ctime/q-ring)
|
|
15
15
|
[](https://cursor.directory/plugins/q-ring)
|
|
16
16
|
[](https://www.pulsemcp.com/servers/i4ctime-q-ring)
|
|
@@ -33,7 +33,7 @@ Stop pasting API keys into plain-text `.env` files or wrestling with clunky secr
|
|
|
33
33
|
- **Entanglement:** Link keys across projects so rotating one automatically updates them all.
|
|
34
34
|
- **Tunneling:** Create ephemeral, in-memory secrets that self-destruct after a set time or read count.
|
|
35
35
|
- **Teleportation:** Securely pack and share AES-256-GCM encrypted secret bundles.
|
|
36
|
-
- **Seamless AI Integration:**
|
|
36
|
+
- **Seamless AI Integration:** 46 built-in MCP tools for native use in **Cursor**, **Kiro**, and **Claude Code**.
|
|
37
37
|
|
|
38
38
|
## 🚀 Installation
|
|
39
39
|
|
|
@@ -111,6 +111,23 @@ QRING_ENV=dev qring get API_KEY # → sk-dev-123
|
|
|
111
111
|
qring inspect API_KEY
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
### Environment Promotion — Diff, Then Promote
|
|
115
|
+
|
|
116
|
+
Once a secret carries per-environment states, promotion replaces copy-paste: compare two environments key by key (statuses only, never values), then copy a value from one state to another. `diff` exits 1 on drift, so it doubles as a CI gate; `promote` refuses to overwrite a differing target unless you say so.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# What differs between staging and prod? (same / different / missing on one side)
|
|
120
|
+
qring diff staging prod
|
|
121
|
+
|
|
122
|
+
# Make prod match staging for one key (asks before overwriting a different value)
|
|
123
|
+
qring promote DATABASE_URL --from staging --to prod
|
|
124
|
+
|
|
125
|
+
# Non-interactive, e.g. in a release script
|
|
126
|
+
qring promote DATABASE_URL --from staging --to prod --force --json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
MCP agents get the same two operations as `diff_environments` and `promote_secret`.
|
|
130
|
+
|
|
114
131
|
### Wavefunction Collapse — Smart Environment Detection
|
|
115
132
|
|
|
116
133
|
q-ring auto-detects your environment without explicit flags. Resolution order:
|
|
@@ -210,16 +227,27 @@ qring tunnel list
|
|
|
210
227
|
|
|
211
228
|
### Teleportation — Encrypted Sharing
|
|
212
229
|
|
|
213
|
-
Pack secrets into AES-256-GCM encrypted bundles for secure transfer between machines.
|
|
230
|
+
Pack secrets into AES-256-GCM encrypted bundles for secure transfer between machines. Two ways to lock a bundle:
|
|
231
|
+
|
|
232
|
+
- **Passphrase** (v1): keys derived with PBKDF2-HMAC-SHA512 (210 000 iterations); each bundle records its iteration count, so old bundles still unpack.
|
|
233
|
+
- **Recipients** (v2, 0.18): each teammate runs `qring teleport keygen` once and shares their recipient string (`qring1…`, an X25519 public key; the private half lives in their OS keyring). `pack --to` encrypts a fresh content key to every recipient — HKDF-SHA256 over an ephemeral X25519 agreement, AES-256-GCM throughout, `node:crypto` only — so nothing secret travels beside the bundle and nobody has to whisper a passphrase.
|
|
214
234
|
|
|
215
235
|
```bash
|
|
216
|
-
#
|
|
236
|
+
# Passphrase bundle (prompts)
|
|
217
237
|
qring teleport pack --keys "API_KEY,DB_PASS" > bundle.txt
|
|
238
|
+
cat bundle.txt | qring teleport unpack
|
|
239
|
+
|
|
240
|
+
# Recipient bundle: teammates publish their recipient once…
|
|
241
|
+
qring teleport keygen # → qring1a3F… (share this; keep the keyring)
|
|
242
|
+
qring teleport identity # print it again later
|
|
243
|
+
|
|
244
|
+
# …then you address the pack to them (repeatable or comma-separated)
|
|
245
|
+
qring teleport pack --keys "API_KEY,DB_PASS" --to qring1a3F… --to qring1Zz9… > bundle.txt
|
|
218
246
|
|
|
219
|
-
#
|
|
247
|
+
# They unpack with the identity in their keyring — no passphrase
|
|
220
248
|
cat bundle.txt | qring teleport unpack
|
|
221
249
|
|
|
222
|
-
# Preview
|
|
250
|
+
# Preview: who it's addressed to, whether that's you, and what's inside
|
|
223
251
|
qring teleport unpack <bundle> --dry-run
|
|
224
252
|
```
|
|
225
253
|
|
|
@@ -339,7 +367,7 @@ qring setup cursor --dry-run
|
|
|
339
367
|
|
|
340
368
|
### Push to Deployment Platforms
|
|
341
369
|
|
|
342
|
-
Push manifest secrets to GitHub Actions, Vercel,
|
|
370
|
+
Push manifest secrets to GitHub Actions, Vercel, Cloudflare Workers, fly.io, Railway, or Netlify through each platform's **own authenticated CLI** (`gh` / `vercel` / `wrangler` / `flyctl` / `railway` / `netlify`) — q-ring never holds platform tokens, and values travel over stdin (or, for Netlify's import-only CLI, a `0600` temp file that is removed immediately), never argv. Every push is recorded in the audit chain.
|
|
343
371
|
|
|
344
372
|
```bash
|
|
345
373
|
# Push the .q-ring.json manifest keys to GitHub Actions secrets
|
|
@@ -351,6 +379,11 @@ qring push vercel --vercel-env production,preview
|
|
|
351
379
|
# Push to Cloudflare Workers secrets
|
|
352
380
|
qring push cloudflare
|
|
353
381
|
|
|
382
|
+
# fly.io (flyctl secrets import over stdin — note flyctl deploys per import), Railway, Netlify
|
|
383
|
+
qring push fly --app my-app
|
|
384
|
+
qring push railway --service api --railway-env production
|
|
385
|
+
qring push netlify --site 1234-abcd
|
|
386
|
+
|
|
354
387
|
# Explicit keys, preview first
|
|
355
388
|
qring push github --keys DATABASE_URL,API_KEY --dry-run
|
|
356
389
|
```
|
|
@@ -426,7 +459,7 @@ Hooks are fire-and-forget: a failing hook never blocks secret operations. The ho
|
|
|
426
459
|
|
|
427
460
|
### Configurable Rotation
|
|
428
461
|
|
|
429
|
-
Set a rotation format per secret so the agent auto-rotates with the correct value shape.
|
|
462
|
+
Set a rotation format per secret so the agent auto-rotates with the correct value shape, and a rotation interval so q-ring reminds you before a credential goes stale. Every secret remembers when its value last changed (`rotatedAt`); with `--rotate-every` it becomes *due soon* inside the last 20% of the interval (or the last 7 days, whichever is shorter) and *overdue* past it — in `qring inspect`, `qring rotate:due`, the dashboard's "Rotate soon" card and the `inspect_secret` tool.
|
|
430
463
|
|
|
431
464
|
```bash
|
|
432
465
|
# Store a secret with rotation format metadata
|
|
@@ -434,6 +467,13 @@ qring set STRIPE_KEY "sk-..." --rotation-format api-key --rotation-prefix "sk-"
|
|
|
434
467
|
|
|
435
468
|
# Store a password with password rotation format
|
|
436
469
|
qring set DB_PASS "..." --rotation-format password
|
|
470
|
+
|
|
471
|
+
# Remind me every 90 days
|
|
472
|
+
qring set STRIPE_KEY "sk-..." --rotate-every 90
|
|
473
|
+
|
|
474
|
+
# What needs rotating? (most overdue first; --all lists every scheduled secret)
|
|
475
|
+
qring rotate:due
|
|
476
|
+
qring rotate:due --json
|
|
437
477
|
```
|
|
438
478
|
|
|
439
479
|
### Secure Execution & Auto-Redaction
|
|
@@ -883,7 +923,7 @@ qring status --no-open
|
|
|
883
923
|
|
|
884
924
|
## MCP Server
|
|
885
925
|
|
|
886
|
-
q-ring includes a full MCP server with
|
|
926
|
+
q-ring includes a full MCP server with 46 tools for AI agent integration.
|
|
887
927
|
|
|
888
928
|
### Core Tools
|
|
889
929
|
|
|
@@ -892,6 +932,8 @@ q-ring includes a full MCP server with 44 tools for AI agent integration.
|
|
|
892
932
|
| `get_secret` | Read a secret value (collapses superposition, audits the read) |
|
|
893
933
|
| `list_secrets` | List keys + metadata in scope (values never exposed); filter by tag, expiry, glob |
|
|
894
934
|
| `set_secret` | Create or overwrite a single secret with optional TTL, per-env state, tags, rotation format |
|
|
935
|
+
| `promote_secret` | Copy one secret's value from one environment state to another (no-op when equal; `force` to overwrite a differing target) |
|
|
936
|
+
| `diff_environments` | Compare two environments key by key — same / different / only-a / only-b / collapsed; statuses only, never values |
|
|
895
937
|
| `delete_secret` | Permanently remove a secret value (not undoable from q-ring) |
|
|
896
938
|
| `has_secret` | Boolean existence check that respects decay (no audit read) |
|
|
897
939
|
| `export_secrets` | Render multiple secrets as `.env` or JSON for one-off export (skips approval-protected keys without a grant) |
|
|
@@ -1074,7 +1116,7 @@ The **q-ring Cursor Plugin** brings quantum secret management directly into your
|
|
|
1074
1116
|
| **2 Agents** | `security-auditor` (proactive monitoring) and `secret-ops` (day-to-day assistant) |
|
|
1075
1117
|
| **8 Commands** | `/qring:scan-secrets`, `/qring:health-check`, `/qring:rotate-expired`, `/qring:setup-project`, `/qring:teleport-secrets`, `/qring:dashboard`, `/qring:exec-safe`, `/qring:analyze` |
|
|
1076
1118
|
| **3 Hooks** | `afterFileEdit` (lint scan), `sessionStart` (project context), `beforeShellExecution` (`.env` guard) |
|
|
1077
|
-
| **MCP Connector** | Auto-connects to `qring-mcp` via stdio — all
|
|
1119
|
+
| **MCP Connector** | Auto-connects to `qring-mcp` via stdio — all 46 tools available |
|
|
1078
1120
|
|
|
1079
1121
|
Install from the Cursor marketplace or see [`cursor-plugin/README.md`](cursor-plugin/README.md) for manual setup.
|
|
1080
1122
|
|
|
@@ -36,6 +36,10 @@ var SecretMetadataSchema = z.object({
|
|
|
36
36
|
ephemeral: z.boolean().optional(),
|
|
37
37
|
rotationFormat: z.string().optional(),
|
|
38
38
|
rotationPrefix: z.string().optional(),
|
|
39
|
+
rotatedAt: z.string().optional(),
|
|
40
|
+
// `.catch(undefined)`: a bad interval strips the reminder rather than
|
|
41
|
+
// rejecting the whole envelope (which would surface the JSON as the value).
|
|
42
|
+
rotateEveryDays: z.number().int().positive().max(3650).optional().catch(void 0),
|
|
39
43
|
provider: z.string().optional(),
|
|
40
44
|
validationUrl: z.string().optional(),
|
|
41
45
|
requiresApproval: z.boolean().optional(),
|
|
@@ -73,6 +77,8 @@ function createEnvelope(value, opts) {
|
|
|
73
77
|
accessCount: 0,
|
|
74
78
|
rotationFormat: opts?.rotationFormat,
|
|
75
79
|
rotationPrefix: opts?.rotationPrefix,
|
|
80
|
+
rotatedAt: opts?.rotatedAt ?? now,
|
|
81
|
+
rotateEveryDays: opts?.rotateEveryDays,
|
|
76
82
|
provider: opts?.provider,
|
|
77
83
|
requiresApproval: opts?.requiresApproval,
|
|
78
84
|
jitProvider: opts?.jitProvider,
|
|
@@ -302,14 +308,13 @@ import {
|
|
|
302
308
|
closeSync,
|
|
303
309
|
statSync as statSync3
|
|
304
310
|
} from "fs";
|
|
305
|
-
import { join as
|
|
306
|
-
import { homedir as homedir3 } from "os";
|
|
311
|
+
import { join as join6 } from "path";
|
|
307
312
|
import { createHash, createHmac, randomBytes as randomBytes2 } from "crypto";
|
|
308
313
|
|
|
309
314
|
// src/core/backend.ts
|
|
310
315
|
import { Entry as NapiEntry, findCredentials as napiFindCredentials } from "@napi-rs/keyring";
|
|
311
316
|
import { existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "fs";
|
|
312
|
-
import { dirname as dirname2, join as
|
|
317
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
313
318
|
import { homedir as homedir2 } from "os";
|
|
314
319
|
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from "crypto";
|
|
315
320
|
|
|
@@ -321,8 +326,19 @@ import {
|
|
|
321
326
|
readFileSync as readFileSync3,
|
|
322
327
|
statSync as statSync2
|
|
323
328
|
} from "fs";
|
|
324
|
-
import { join as
|
|
329
|
+
import { join as join4 } from "path";
|
|
330
|
+
|
|
331
|
+
// src/utils/config-dir.ts
|
|
325
332
|
import { homedir } from "os";
|
|
333
|
+
import { join as join3 } from "path";
|
|
334
|
+
var CONFIG_DIR_ENV = "QRING_CONFIG_DIR";
|
|
335
|
+
function configDir() {
|
|
336
|
+
const override = process.env[CONFIG_DIR_ENV];
|
|
337
|
+
if (override && override.trim()) return override;
|
|
338
|
+
return join3(homedir(), ".config", "q-ring");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/utils/file-lock.ts
|
|
326
342
|
function sleepSync(ms) {
|
|
327
343
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
328
344
|
}
|
|
@@ -335,11 +351,11 @@ function isProcessAlive(pid) {
|
|
|
335
351
|
}
|
|
336
352
|
}
|
|
337
353
|
function withFileLock(name, fn, opts = {}) {
|
|
338
|
-
const baseDir = process.env.QRING_LOCK_DIR ?? (process.env.QRING_AUDIT_DIR ?
|
|
339
|
-
const lockDir =
|
|
354
|
+
const baseDir = process.env.QRING_LOCK_DIR ?? (process.env.QRING_AUDIT_DIR ? join4(process.env.QRING_AUDIT_DIR, ".locks") : configDir());
|
|
355
|
+
const lockDir = join4(baseDir, opts.dir ?? "locks");
|
|
340
356
|
mkdirSync(lockDir, { recursive: true, mode: 448 });
|
|
341
357
|
const safe = Buffer.from(name, "utf8").toString("base64url");
|
|
342
|
-
const lockPath =
|
|
358
|
+
const lockPath = join4(lockDir, `${safe}.lock`);
|
|
343
359
|
const deadline = Date.now() + (opts.timeoutMs ?? 8e3);
|
|
344
360
|
const staleMs = opts.staleMs ?? 3e4;
|
|
345
361
|
while (Date.now() < deadline) {
|
|
@@ -382,6 +398,12 @@ var PATH_ENV = "QRING_FILE_BACKEND_PATH";
|
|
|
382
398
|
var PBKDF2_ITERATIONS = 21e4;
|
|
383
399
|
var KEY_LENGTH = 32;
|
|
384
400
|
var FILE_PREFIX = "qfile1";
|
|
401
|
+
function keyringFailure(op, err) {
|
|
402
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
403
|
+
return new BackendUnavailableError(
|
|
404
|
+
`OS keyring ${op} failed \u2014 the credential store is locked or unreachable (${detail}). Unlock your keyring (or set ${BACKEND_ENV}=file with ${PASSPHRASE_ENV} for a headless store) and retry.`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
385
407
|
var BackendUnavailableError = class extends Error {
|
|
386
408
|
constructor(message) {
|
|
387
409
|
super(message);
|
|
@@ -399,7 +421,7 @@ function activeBackend() {
|
|
|
399
421
|
return "keyring";
|
|
400
422
|
}
|
|
401
423
|
function storePath() {
|
|
402
|
-
return process.env[PATH_ENV] ??
|
|
424
|
+
return process.env[PATH_ENV] ?? join5(homedir2(), ".config", "q-ring", "file-backend.enc");
|
|
403
425
|
}
|
|
404
426
|
function passphrase() {
|
|
405
427
|
const p = process.env[PASSPHRASE_ENV];
|
|
@@ -492,7 +514,11 @@ var Entry = class {
|
|
|
492
514
|
if (activeBackend() === "file") {
|
|
493
515
|
return loadStore().map[this.storeKey()] ?? null;
|
|
494
516
|
}
|
|
495
|
-
|
|
517
|
+
try {
|
|
518
|
+
return this.delegate().getPassword();
|
|
519
|
+
} catch (err) {
|
|
520
|
+
throw keyringFailure("read", err);
|
|
521
|
+
}
|
|
496
522
|
}
|
|
497
523
|
setPassword(password) {
|
|
498
524
|
if (activeBackend() === "file") {
|
|
@@ -511,7 +537,11 @@ var Entry = class {
|
|
|
511
537
|
return existed;
|
|
512
538
|
});
|
|
513
539
|
}
|
|
514
|
-
|
|
540
|
+
try {
|
|
541
|
+
return this.delegate().deleteCredential();
|
|
542
|
+
} catch (err) {
|
|
543
|
+
throw keyringFailure("delete", err);
|
|
544
|
+
}
|
|
515
545
|
}
|
|
516
546
|
// @napi-rs/keyring's other delete alias, used by `qring doctor`.
|
|
517
547
|
deletePassword() {
|
|
@@ -592,14 +622,14 @@ function getAuditDir() {
|
|
|
592
622
|
}
|
|
593
623
|
return process.env.QRING_AUDIT_DIR;
|
|
594
624
|
}
|
|
595
|
-
const dir =
|
|
625
|
+
const dir = configDir();
|
|
596
626
|
if (!existsSync2(dir)) {
|
|
597
627
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
598
628
|
}
|
|
599
629
|
return dir;
|
|
600
630
|
}
|
|
601
631
|
function getAuditPath() {
|
|
602
|
-
return
|
|
632
|
+
return join6(getAuditDir(), "audit.jsonl");
|
|
603
633
|
}
|
|
604
634
|
function getLastLineHash() {
|
|
605
635
|
const path = getAuditPath();
|
|
@@ -849,8 +879,7 @@ function detectAnomalies(key) {
|
|
|
849
879
|
|
|
850
880
|
// src/core/entanglement.ts
|
|
851
881
|
import { existsSync as existsSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
852
|
-
import { join as
|
|
853
|
-
import { homedir as homedir4 } from "os";
|
|
882
|
+
import { join as join7 } from "path";
|
|
854
883
|
|
|
855
884
|
// src/utils/registry.ts
|
|
856
885
|
import { existsSync as existsSync3, readFileSync as readFileSync6, renameSync } from "fs";
|
|
@@ -880,11 +909,11 @@ function loadJsonRegistry(path, empty) {
|
|
|
880
909
|
// src/core/entanglement.ts
|
|
881
910
|
var REGISTRY_VERSION = 1;
|
|
882
911
|
function getRegistryPath() {
|
|
883
|
-
const dir =
|
|
912
|
+
const dir = configDir();
|
|
884
913
|
if (!existsSync4(dir)) {
|
|
885
914
|
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
886
915
|
}
|
|
887
|
-
return
|
|
916
|
+
return join7(dir, "entanglement.json");
|
|
888
917
|
}
|
|
889
918
|
function loadRegistry() {
|
|
890
919
|
return loadJsonRegistry(getRegistryPath(), { pairs: [] });
|
|
@@ -926,8 +955,7 @@ function listEntanglements() {
|
|
|
926
955
|
|
|
927
956
|
// src/core/hooks.ts
|
|
928
957
|
import { existsSync as existsSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
|
|
929
|
-
import { join as
|
|
930
|
-
import { homedir as homedir5 } from "os";
|
|
958
|
+
import { join as join8 } from "path";
|
|
931
959
|
import { execFile, spawn } from "child_process";
|
|
932
960
|
import { randomUUID } from "crypto";
|
|
933
961
|
|
|
@@ -1151,11 +1179,11 @@ function httpRequest(opts) {
|
|
|
1151
1179
|
|
|
1152
1180
|
// src/core/hooks.ts
|
|
1153
1181
|
function getRegistryPath2() {
|
|
1154
|
-
const dir =
|
|
1182
|
+
const dir = configDir();
|
|
1155
1183
|
if (!existsSync5(dir)) {
|
|
1156
1184
|
mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
1157
1185
|
}
|
|
1158
|
-
return
|
|
1186
|
+
return join8(dir, "hooks.json");
|
|
1159
1187
|
}
|
|
1160
1188
|
function loadRegistry2() {
|
|
1161
1189
|
return loadJsonRegistry(getRegistryPath2(), { hooks: [] });
|
|
@@ -1409,12 +1437,11 @@ async function fireHooks(payload, tags) {
|
|
|
1409
1437
|
|
|
1410
1438
|
// src/core/approval.ts
|
|
1411
1439
|
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
|
|
1412
|
-
import { join as
|
|
1413
|
-
import { homedir as homedir6 } from "os";
|
|
1440
|
+
import { join as join9 } from "path";
|
|
1414
1441
|
import { createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
1415
1442
|
function getHmacSecret() {
|
|
1416
|
-
const dir =
|
|
1417
|
-
const secretPath =
|
|
1443
|
+
const dir = configDir();
|
|
1444
|
+
const secretPath = join9(dir, ".approval-key");
|
|
1418
1445
|
if (!existsSync6(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
1419
1446
|
if (existsSync6(secretPath)) {
|
|
1420
1447
|
return readFileSync7(secretPath, "utf8").trim();
|
|
@@ -1450,11 +1477,11 @@ function verifyHmac(entry) {
|
|
|
1450
1477
|
}
|
|
1451
1478
|
}
|
|
1452
1479
|
function getRegistryPath3() {
|
|
1453
|
-
const dir =
|
|
1480
|
+
const dir = configDir();
|
|
1454
1481
|
if (!existsSync6(dir)) {
|
|
1455
1482
|
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
1456
1483
|
}
|
|
1457
|
-
return
|
|
1484
|
+
return join9(dir, "approvals.json");
|
|
1458
1485
|
}
|
|
1459
1486
|
function loadRegistry3() {
|
|
1460
1487
|
return loadJsonRegistry(getRegistryPath3(), { approvals: [] });
|
|
@@ -1481,7 +1508,7 @@ function listApprovals() {
|
|
|
1481
1508
|
|
|
1482
1509
|
// src/core/policy.ts
|
|
1483
1510
|
import { statSync as statSync4 } from "fs";
|
|
1484
|
-
import { join as
|
|
1511
|
+
import { join as join10 } from "path";
|
|
1485
1512
|
import { z as z2 } from "zod";
|
|
1486
1513
|
var stringArray = z2.array(z2.string());
|
|
1487
1514
|
var mcpPolicySchema = z2.object({
|
|
@@ -1540,7 +1567,7 @@ function resolvePolicyPath(projectPath) {
|
|
|
1540
1567
|
}
|
|
1541
1568
|
function configMtime(pp) {
|
|
1542
1569
|
try {
|
|
1543
|
-
return statSync4(
|
|
1570
|
+
return statSync4(join10(pp, ".q-ring.json")).mtimeMs;
|
|
1544
1571
|
} catch {
|
|
1545
1572
|
return 0;
|
|
1546
1573
|
}
|
|
@@ -1563,7 +1590,7 @@ function loadPolicy(projectPath) {
|
|
|
1563
1590
|
if (!parsed.success) {
|
|
1564
1591
|
const issues = parsed.error.issues.map((i) => `policy${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`).join("; ");
|
|
1565
1592
|
const error = new PolicyConfigError(
|
|
1566
|
-
`Invalid policy in ${
|
|
1593
|
+
`Invalid policy in ${join10(pp, ".q-ring.json")} \u2014 refusing to run under an unparseable security policy (fail closed). Fix these and retry: ${issues}`
|
|
1567
1594
|
);
|
|
1568
1595
|
console.error(`q-ring: ${error.message}`);
|
|
1569
1596
|
cachedPolicy = { path: pp, mtimeMs, error };
|
|
@@ -1801,15 +1828,14 @@ function notifyApprovalRequested(key, source) {
|
|
|
1801
1828
|
|
|
1802
1829
|
// src/core/canary-webhooks.ts
|
|
1803
1830
|
import { existsSync as existsSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
|
|
1804
|
-
import { join as
|
|
1805
|
-
import { homedir as homedir7 } from "os";
|
|
1831
|
+
import { join as join11 } from "path";
|
|
1806
1832
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1807
1833
|
function getRegistryPath4() {
|
|
1808
1834
|
const override = process.env.QRING_CANARY_ALERTS_PATH;
|
|
1809
1835
|
if (override) return override;
|
|
1810
|
-
const dir =
|
|
1836
|
+
const dir = configDir();
|
|
1811
1837
|
if (!existsSync7(dir)) mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
1812
|
-
return
|
|
1838
|
+
return join11(dir, "canary-alerts.json");
|
|
1813
1839
|
}
|
|
1814
1840
|
function loadRegistry4() {
|
|
1815
1841
|
return loadJsonRegistry(getRegistryPath4(), { channels: [] });
|
|
@@ -2109,6 +2135,10 @@ registry.register(httpProvider);
|
|
|
2109
2135
|
function withJitEnvelopeLock(service, key, fn) {
|
|
2110
2136
|
return withFileLock(`${service}\0${key}`, fn, { dir: "jit-locks" });
|
|
2111
2137
|
}
|
|
2138
|
+
var MAX_ROTATE_EVERY_DAYS = 3650;
|
|
2139
|
+
function isValidRotateEveryDays(days) {
|
|
2140
|
+
return Number.isInteger(days) && days >= 1 && days <= MAX_ROTATE_EVERY_DAYS;
|
|
2141
|
+
}
|
|
2112
2142
|
function readEnvelope(service, key) {
|
|
2113
2143
|
const entry = new Entry(service, key);
|
|
2114
2144
|
const raw = entry.getPassword();
|
|
@@ -2271,6 +2301,14 @@ function setSecret(key, value, opts = {}) {
|
|
|
2271
2301
|
let envelope;
|
|
2272
2302
|
const rotFmt = opts.rotationFormat ?? existing?.meta.rotationFormat;
|
|
2273
2303
|
const rotPfx = opts.rotationPrefix ?? existing?.meta.rotationPrefix;
|
|
2304
|
+
const rotEvery = opts.rotateEveryDays ?? existing?.meta.rotateEveryDays;
|
|
2305
|
+
if (rotEvery !== void 0 && !isValidRotateEveryDays(rotEvery)) {
|
|
2306
|
+
throw new Error(
|
|
2307
|
+
`rotateEveryDays must be a whole number of days between 1 and ${MAX_ROTATE_EVERY_DAYS}`
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
const valueChanged = !existing || (opts.states ? JSON.stringify(existing.states ?? null) !== JSON.stringify(opts.states) : existing.states !== void 0 || existing.value !== value);
|
|
2311
|
+
const rotatedAt = valueChanged ? (/* @__PURE__ */ new Date()).toISOString() : existing?.meta.rotatedAt ?? existing?.meta.updatedAt ?? existing?.meta.createdAt;
|
|
2274
2312
|
const prov = opts.provider ?? existing?.meta.provider;
|
|
2275
2313
|
const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
|
|
2276
2314
|
const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
|
|
@@ -2301,6 +2339,8 @@ function setSecret(key, value, opts = {}) {
|
|
|
2301
2339
|
entangled: existing?.meta.entangled,
|
|
2302
2340
|
rotationFormat: rotFmt,
|
|
2303
2341
|
rotationPrefix: rotPfx,
|
|
2342
|
+
rotatedAt,
|
|
2343
|
+
rotateEveryDays: rotEvery,
|
|
2304
2344
|
provider: prov,
|
|
2305
2345
|
requiresApproval: reqApp,
|
|
2306
2346
|
jitProvider: jitProv,
|
|
@@ -2316,6 +2356,8 @@ function setSecret(key, value, opts = {}) {
|
|
|
2316
2356
|
entangled: existing?.meta.entangled,
|
|
2317
2357
|
rotationFormat: rotFmt,
|
|
2318
2358
|
rotationPrefix: rotPfx,
|
|
2359
|
+
rotatedAt,
|
|
2360
|
+
rotateEveryDays: rotEvery,
|
|
2319
2361
|
provider: prov,
|
|
2320
2362
|
requiresApproval: reqApp,
|
|
2321
2363
|
jitProvider: jitProv,
|
|
@@ -2577,6 +2619,50 @@ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
2577
2619
|
});
|
|
2578
2620
|
}
|
|
2579
2621
|
|
|
2622
|
+
// src/core/rotation.ts
|
|
2623
|
+
var DAY_MS = 864e5;
|
|
2624
|
+
var DUE_SOON_FRACTION = 0.2;
|
|
2625
|
+
var DUE_SOON_MAX_DAYS = 7;
|
|
2626
|
+
function dueSoonWindowDays(rotateEveryDays) {
|
|
2627
|
+
return Math.min(rotateEveryDays * DUE_SOON_FRACTION, DUE_SOON_MAX_DAYS);
|
|
2628
|
+
}
|
|
2629
|
+
function parseMs(iso) {
|
|
2630
|
+
if (!iso) return null;
|
|
2631
|
+
const ms = new Date(iso).getTime();
|
|
2632
|
+
return Number.isFinite(ms) ? ms : null;
|
|
2633
|
+
}
|
|
2634
|
+
function rotationStatus(meta, now = Date.now()) {
|
|
2635
|
+
const nowMs = typeof now === "number" ? now : now.getTime();
|
|
2636
|
+
const lastRotatedAt = meta.rotatedAt ?? meta.updatedAt ?? meta.createdAt;
|
|
2637
|
+
const lastMs = parseMs(lastRotatedAt) ?? nowMs;
|
|
2638
|
+
const ageDays = Math.max(0, Math.floor((nowMs - lastMs) / DAY_MS));
|
|
2639
|
+
const every = meta.rotateEveryDays;
|
|
2640
|
+
if (!every || !Number.isFinite(every) || every <= 0) {
|
|
2641
|
+
return {
|
|
2642
|
+
lastRotatedAt,
|
|
2643
|
+
ageDays,
|
|
2644
|
+
rotateEveryDays: null,
|
|
2645
|
+
dueAt: null,
|
|
2646
|
+
daysUntilDue: null,
|
|
2647
|
+
state: "unscheduled"
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
const dueMs = lastMs + every * DAY_MS;
|
|
2651
|
+
const untilMs = dueMs - nowMs;
|
|
2652
|
+
const daysUntilDue = Math.ceil(untilMs / DAY_MS);
|
|
2653
|
+
let state = "ok";
|
|
2654
|
+
if (untilMs <= 0) state = "overdue";
|
|
2655
|
+
else if (untilMs <= dueSoonWindowDays(every) * DAY_MS) state = "due-soon";
|
|
2656
|
+
return {
|
|
2657
|
+
lastRotatedAt,
|
|
2658
|
+
ageDays,
|
|
2659
|
+
rotateEveryDays: every,
|
|
2660
|
+
dueAt: new Date(dueMs).toISOString(),
|
|
2661
|
+
daysUntilDue,
|
|
2662
|
+
state
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2580
2666
|
// src/core/tunnel.ts
|
|
2581
2667
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
2582
2668
|
var tunnelStore = /* @__PURE__ */ new Map();
|
|
@@ -2651,8 +2737,8 @@ function tunnelList() {
|
|
|
2651
2737
|
|
|
2652
2738
|
// src/core/memory.ts
|
|
2653
2739
|
import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8, chmodSync as chmodSync3 } from "fs";
|
|
2654
|
-
import { join as
|
|
2655
|
-
import {
|
|
2740
|
+
import { join as join12 } from "path";
|
|
2741
|
+
import { hostname, userInfo } from "os";
|
|
2656
2742
|
import {
|
|
2657
2743
|
createCipheriv as createCipheriv2,
|
|
2658
2744
|
createDecipheriv as createDecipheriv2,
|
|
@@ -2664,7 +2750,7 @@ var MEMORY_FILE = "agent-memory.enc";
|
|
|
2664
2750
|
var KEYRING_SERVICE = "qring-memory-key";
|
|
2665
2751
|
var KEYRING_ACCOUNT = "encryption-key";
|
|
2666
2752
|
function getMemoryDir() {
|
|
2667
|
-
const dir =
|
|
2753
|
+
const dir = configDir();
|
|
2668
2754
|
if (!existsSync8(dir)) {
|
|
2669
2755
|
mkdirSync8(dir, { recursive: true, mode: 448 });
|
|
2670
2756
|
}
|
|
@@ -2678,7 +2764,7 @@ function writeMemoryFile(path, data) {
|
|
|
2678
2764
|
}
|
|
2679
2765
|
}
|
|
2680
2766
|
function getMemoryPath() {
|
|
2681
|
-
return
|
|
2767
|
+
return join12(getMemoryDir(), MEMORY_FILE);
|
|
2682
2768
|
}
|
|
2683
2769
|
var PBKDF2_ITERATIONS2 = 21e4;
|
|
2684
2770
|
var KEY_LENGTH2 = 32;
|
|
@@ -2900,6 +2986,7 @@ function summariseSession(session) {
|
|
|
2900
2986
|
|
|
2901
2987
|
export {
|
|
2902
2988
|
PACKAGE_VERSION,
|
|
2989
|
+
Entry,
|
|
2903
2990
|
checkDecay,
|
|
2904
2991
|
readProjectConfig,
|
|
2905
2992
|
collapseEnvironment,
|
|
@@ -2933,6 +3020,7 @@ export {
|
|
|
2933
3020
|
exportSecrets,
|
|
2934
3021
|
entangleSecrets,
|
|
2935
3022
|
disentangleSecrets,
|
|
3023
|
+
rotationStatus,
|
|
2936
3024
|
tunnelCreate,
|
|
2937
3025
|
tunnelRead,
|
|
2938
3026
|
tunnelDestroy,
|
|
@@ -2945,4 +3033,4 @@ export {
|
|
|
2945
3033
|
listAgentSessionsForAgents,
|
|
2946
3034
|
summariseSession
|
|
2947
3035
|
};
|
|
2948
|
-
//# sourceMappingURL=chunk-
|
|
3036
|
+
//# sourceMappingURL=chunk-NTCTVLSK.js.map
|