@i4ctime/q-ring 0.3.2 → 0.4.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 +196 -6
- package/dist/{chunk-F4SPZ774.js → chunk-6IQ5SFLI.js} +298 -6
- package/dist/chunk-6IQ5SFLI.js.map +1 -0
- package/dist/{chunk-3WTTWJYU.js → chunk-IGNU622R.js} +337 -5
- package/dist/chunk-IGNU622R.js.map +1 -0
- package/dist/{dashboard-X3ONQFLV.js → dashboard-32PCZF7D.js} +2 -2
- package/dist/{dashboard-QQWKOOI5.js → dashboard-HVIQO6NT.js} +2 -2
- package/dist/index.js +675 -10
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +580 -9
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3WTTWJYU.js.map +0 -1
- package/dist/chunk-F4SPZ774.js.map +0 -1
- /package/dist/{dashboard-QQWKOOI5.js.map → dashboard-32PCZF7D.js.map} +0 -0
- /package/dist/{dashboard-X3ONQFLV.js.map → dashboard-HVIQO6NT.js.map} +0 -0
package/README.md
CHANGED
|
@@ -138,6 +138,9 @@ qring entangle API_KEY API_KEY_BACKUP
|
|
|
138
138
|
|
|
139
139
|
# Now updating API_KEY also updates API_KEY_BACKUP
|
|
140
140
|
qring set API_KEY "new-value"
|
|
141
|
+
|
|
142
|
+
# Unlink entangled secrets
|
|
143
|
+
qring disentangle API_KEY API_KEY_BACKUP
|
|
141
144
|
```
|
|
142
145
|
|
|
143
146
|
### Tunneling — Ephemeral Secrets
|
|
@@ -170,6 +173,158 @@ cat bundle.txt | qring teleport unpack
|
|
|
170
173
|
qring teleport unpack <bundle> --dry-run
|
|
171
174
|
```
|
|
172
175
|
|
|
176
|
+
### Import — Bulk Secret Ingestion
|
|
177
|
+
|
|
178
|
+
Import secrets from `.env` files directly into q-ring. Supports standard dotenv syntax including comments, quoted values, and escape sequences.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Import all secrets from a .env file
|
|
182
|
+
qring import .env
|
|
183
|
+
|
|
184
|
+
# Import to project scope, skipping existing keys
|
|
185
|
+
qring import .env --project --skip-existing
|
|
186
|
+
|
|
187
|
+
# Preview what would be imported
|
|
188
|
+
qring import .env --dry-run
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Selective Export
|
|
192
|
+
|
|
193
|
+
Export only the secrets you need using key names or tag filters.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Export specific keys
|
|
197
|
+
qring export --keys "API_KEY,DB_PASS,REDIS_URL"
|
|
198
|
+
|
|
199
|
+
# Export by tag
|
|
200
|
+
qring export --tags "backend"
|
|
201
|
+
|
|
202
|
+
# Combine with format
|
|
203
|
+
qring export --keys "API_KEY,DB_PASS" --format json
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Secret Search and Filtering
|
|
207
|
+
|
|
208
|
+
Filter `qring list` output by tag, expiry state, or key pattern.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Filter by tag
|
|
212
|
+
qring list --tag backend
|
|
213
|
+
|
|
214
|
+
# Show only expired secrets
|
|
215
|
+
qring list --expired
|
|
216
|
+
|
|
217
|
+
# Show only stale secrets (75%+ decay)
|
|
218
|
+
qring list --stale
|
|
219
|
+
|
|
220
|
+
# Glob pattern on key name
|
|
221
|
+
qring list --filter "API_*"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Project Secret Manifest
|
|
225
|
+
|
|
226
|
+
Declare required secrets in `.q-ring.json` and validate project readiness with a single command.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# Validate project secrets against the manifest
|
|
230
|
+
qring check
|
|
231
|
+
|
|
232
|
+
# See which secrets are present, missing, expired, or stale
|
|
233
|
+
qring check --project-path /path/to/project
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Env File Sync
|
|
237
|
+
|
|
238
|
+
Generate a `.env` file from the project manifest, resolving each key from q-ring with environment-aware superposition collapse.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# Generate to stdout
|
|
242
|
+
qring env:generate
|
|
243
|
+
|
|
244
|
+
# Write to a file
|
|
245
|
+
qring env:generate --output .env
|
|
246
|
+
|
|
247
|
+
# Force a specific environment
|
|
248
|
+
qring env:generate --env staging --output .env.staging
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Secret Liveness Validation
|
|
252
|
+
|
|
253
|
+
Test if a secret is actually valid with its target service. q-ring auto-detects the provider from key prefixes (`sk-` → OpenAI, `ghp_` → GitHub, etc.) or accepts an explicit provider name.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Validate a single secret
|
|
257
|
+
qring validate OPENAI_API_KEY
|
|
258
|
+
|
|
259
|
+
# Force a specific provider
|
|
260
|
+
qring validate SOME_KEY --provider stripe
|
|
261
|
+
|
|
262
|
+
# Validate all secrets with detectable providers
|
|
263
|
+
qring validate --all
|
|
264
|
+
|
|
265
|
+
# Only validate manifest-declared secrets
|
|
266
|
+
qring validate --all --manifest
|
|
267
|
+
|
|
268
|
+
# List available providers
|
|
269
|
+
qring validate --list-providers
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Built-in providers:** OpenAI, Stripe, GitHub, AWS (format check), Generic HTTP.
|
|
273
|
+
|
|
274
|
+
Output:
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
✓ OPENAI_API_KEY valid (openai, 342ms)
|
|
278
|
+
✗ STRIPE_KEY invalid (stripe, 128ms) — API key has been revoked
|
|
279
|
+
⚠ AWS_ACCESS_KEY error (aws, 10002ms) — network timeout
|
|
280
|
+
○ DATABASE_URL unknown — no provider detected
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Hooks — Callbacks on Secret Change
|
|
284
|
+
|
|
285
|
+
Register webhooks, shell commands, or process signals that fire when secrets are created, updated, or deleted. Supports key matching, glob patterns, tag filtering, and scope constraints.
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# Run a shell command when a secret changes
|
|
289
|
+
qring hook add --key DB_PASS --exec "docker restart app"
|
|
290
|
+
|
|
291
|
+
# POST to a webhook on any write/delete
|
|
292
|
+
qring hook add --key API_KEY --url "https://hooks.example.com/rotate"
|
|
293
|
+
|
|
294
|
+
# Trigger on all secrets tagged "backend"
|
|
295
|
+
qring hook add --tag backend --exec "pm2 restart all"
|
|
296
|
+
|
|
297
|
+
# Signal a process when DB secrets change
|
|
298
|
+
qring hook add --key-pattern "DB_*" --signal-target "node"
|
|
299
|
+
|
|
300
|
+
# List all hooks
|
|
301
|
+
qring hook list
|
|
302
|
+
|
|
303
|
+
# Remove a hook
|
|
304
|
+
qring hook remove <id>
|
|
305
|
+
|
|
306
|
+
# Enable/disable
|
|
307
|
+
qring hook enable <id>
|
|
308
|
+
qring hook disable <id>
|
|
309
|
+
|
|
310
|
+
# Dry-run test a hook
|
|
311
|
+
qring hook test <id>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Hooks are fire-and-forget: a failing hook never blocks secret operations. The hook registry is stored at `~/.config/q-ring/hooks.json`.
|
|
315
|
+
|
|
316
|
+
### Configurable Rotation
|
|
317
|
+
|
|
318
|
+
Set a rotation format per secret so the agent auto-rotates with the correct value shape.
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# Store a secret with rotation format metadata
|
|
322
|
+
qring set STRIPE_KEY "sk-..." --rotation-format api-key --rotation-prefix "sk-"
|
|
323
|
+
|
|
324
|
+
# Store a password with password rotation format
|
|
325
|
+
qring set DB_PASS "..." --rotation-format password
|
|
326
|
+
```
|
|
327
|
+
|
|
173
328
|
### Agent Mode — Autonomous Monitoring
|
|
174
329
|
|
|
175
330
|
A background daemon that continuously monitors secret health, detects anomalies, and optionally auto-rotates expired secrets.
|
|
@@ -204,17 +359,21 @@ qring status --no-open
|
|
|
204
359
|
|
|
205
360
|
## MCP Server
|
|
206
361
|
|
|
207
|
-
q-ring includes a full MCP server with
|
|
362
|
+
q-ring includes a full MCP server with 31 tools for AI agent integration.
|
|
208
363
|
|
|
209
364
|
### Core Tools
|
|
210
365
|
|
|
211
366
|
| Tool | Description |
|
|
212
367
|
|------|-------------|
|
|
213
368
|
| `get_secret` | Retrieve with superposition collapse + observer logging |
|
|
214
|
-
| `list_secrets` | List keys with quantum metadata
|
|
215
|
-
| `set_secret` | Store with optional TTL, env state, tags |
|
|
369
|
+
| `list_secrets` | List keys with quantum metadata, filterable by tag/expiry/pattern |
|
|
370
|
+
| `set_secret` | Store with optional TTL, env state, tags, rotation format |
|
|
216
371
|
| `delete_secret` | Remove a secret |
|
|
217
372
|
| `has_secret` | Boolean check (respects decay) |
|
|
373
|
+
| `export_secrets` | Export as .env/JSON with optional key and tag filters |
|
|
374
|
+
| `import_dotenv` | Parse and import secrets from .env content |
|
|
375
|
+
| `check_project` | Validate project secrets against `.q-ring.json` manifest |
|
|
376
|
+
| `env_generate` | Generate .env content from the project manifest |
|
|
218
377
|
|
|
219
378
|
### Quantum Tools
|
|
220
379
|
|
|
@@ -224,6 +383,7 @@ q-ring includes a full MCP server with 20 tools for AI agent integration.
|
|
|
224
383
|
| `detect_environment` | Wavefunction collapse — detect current env context |
|
|
225
384
|
| `generate_secret` | Quantum noise — generate and optionally save secrets |
|
|
226
385
|
| `entangle_secrets` | Link two secrets for synchronized rotation |
|
|
386
|
+
| `disentangle_secrets` | Remove entanglement between two secrets |
|
|
227
387
|
|
|
228
388
|
### Tunneling Tools
|
|
229
389
|
|
|
@@ -241,6 +401,21 @@ q-ring includes a full MCP server with 20 tools for AI agent integration.
|
|
|
241
401
|
| `teleport_pack` | Encrypt secrets into a portable bundle |
|
|
242
402
|
| `teleport_unpack` | Decrypt and import a bundle |
|
|
243
403
|
|
|
404
|
+
### Validation Tools
|
|
405
|
+
|
|
406
|
+
| Tool | Description |
|
|
407
|
+
|------|-------------|
|
|
408
|
+
| `validate_secret` | Test if a secret is valid with its target service (OpenAI, Stripe, GitHub, etc.) |
|
|
409
|
+
| `list_providers` | List all available validation providers |
|
|
410
|
+
|
|
411
|
+
### Hook Tools
|
|
412
|
+
|
|
413
|
+
| Tool | Description |
|
|
414
|
+
|------|-------------|
|
|
415
|
+
| `register_hook` | Register a shell/HTTP/signal callback on secret changes |
|
|
416
|
+
| `list_hooks` | List all registered hooks with match criteria and status |
|
|
417
|
+
| `remove_hook` | Remove a registered hook by ID |
|
|
418
|
+
|
|
244
419
|
### Observer & Health Tools
|
|
245
420
|
|
|
246
421
|
| Tool | Description |
|
|
@@ -316,13 +491,16 @@ qring CLI ─────┐
|
|
|
316
491
|
MCP Server ────┘ │
|
|
317
492
|
├── Envelope (quantum metadata)
|
|
318
493
|
├── Scope Resolver (global / project)
|
|
319
|
-
├── Collapse (env detection)
|
|
494
|
+
├── Collapse (env detection + branchMap globs)
|
|
320
495
|
├── Observer (audit log)
|
|
321
496
|
├── Noise (secret generation)
|
|
322
497
|
├── Entanglement (cross-secret linking)
|
|
498
|
+
├── Validate (provider-based liveness checks)
|
|
499
|
+
├── Hooks (shell/HTTP/signal callbacks)
|
|
500
|
+
├── Import (.env file ingestion)
|
|
323
501
|
├── Tunnel (ephemeral in-memory)
|
|
324
502
|
├── Teleport (encrypted sharing)
|
|
325
|
-
├── Agent (autonomous monitor)
|
|
503
|
+
├── Agent (autonomous monitor + rotation)
|
|
326
504
|
└── Dashboard (live status via SSE)
|
|
327
505
|
```
|
|
328
506
|
|
|
@@ -337,11 +515,23 @@ Optional per-project configuration:
|
|
|
337
515
|
"branchMap": {
|
|
338
516
|
"main": "prod",
|
|
339
517
|
"develop": "dev",
|
|
340
|
-
"staging": "staging"
|
|
518
|
+
"staging": "staging",
|
|
519
|
+
"release/*": "staging",
|
|
520
|
+
"feature/*": "dev"
|
|
521
|
+
},
|
|
522
|
+
"secrets": {
|
|
523
|
+
"OPENAI_API_KEY": { "required": true, "description": "OpenAI API key", "format": "api-key", "prefix": "sk-", "provider": "openai" },
|
|
524
|
+
"DATABASE_URL": { "required": true, "description": "Postgres connection string", "validationUrl": "https://api.example.com/health" },
|
|
525
|
+
"SENTRY_DSN": { "required": false, "description": "Sentry error tracking" }
|
|
341
526
|
}
|
|
342
527
|
}
|
|
343
528
|
```
|
|
344
529
|
|
|
530
|
+
- **`branchMap`** supports glob patterns with `*` wildcards (e.g., `release/*` matches `release/v1.0`)
|
|
531
|
+
- **`secrets`** declares the project's required secrets — use `qring check` to validate, `qring env:generate` to produce a `.env` file
|
|
532
|
+
- **`provider`** associates a liveness validation provider with a secret (e.g., `"openai"`, `"stripe"`, `"github"`) — use `qring validate` to test
|
|
533
|
+
- **`validationUrl`** configures the generic HTTP provider's endpoint for custom validation
|
|
534
|
+
|
|
345
535
|
## 📜 License
|
|
346
536
|
|
|
347
537
|
[AGPL-3.0](LICENSE) - Free to use, modify, and share. Any derivative work or hosted service must release its source code under the same license.
|
|
@@ -20,7 +20,10 @@ function createEnvelope(value, opts) {
|
|
|
20
20
|
description: opts?.description,
|
|
21
21
|
tags: opts?.tags,
|
|
22
22
|
entangled: opts?.entangled,
|
|
23
|
-
accessCount: 0
|
|
23
|
+
accessCount: 0,
|
|
24
|
+
rotationFormat: opts?.rotationFormat,
|
|
25
|
+
rotationPrefix: opts?.rotationPrefix,
|
|
26
|
+
provider: opts?.provider
|
|
24
27
|
}
|
|
25
28
|
};
|
|
26
29
|
}
|
|
@@ -173,7 +176,7 @@ function collapseEnvironment(ctx = {}) {
|
|
|
173
176
|
const branch = detectGitBranch(ctx.projectPath);
|
|
174
177
|
if (branch) {
|
|
175
178
|
const branchMap = { ...BRANCH_ENV_MAP, ...config?.branchMap };
|
|
176
|
-
const mapped = branchMap[branch];
|
|
179
|
+
const mapped = branchMap[branch] ?? matchGlob(branchMap, branch);
|
|
177
180
|
if (mapped) {
|
|
178
181
|
return { env: mapped, source: "git-branch" };
|
|
179
182
|
}
|
|
@@ -183,6 +186,16 @@ function collapseEnvironment(ctx = {}) {
|
|
|
183
186
|
}
|
|
184
187
|
return null;
|
|
185
188
|
}
|
|
189
|
+
function matchGlob(branchMap, branch) {
|
|
190
|
+
for (const [pattern, env] of Object.entries(branchMap)) {
|
|
191
|
+
if (!pattern.includes("*")) continue;
|
|
192
|
+
const regex = new RegExp(
|
|
193
|
+
"^" + pattern.replace(/\*/g, ".*") + "$"
|
|
194
|
+
);
|
|
195
|
+
if (regex.test(branch)) return env;
|
|
196
|
+
}
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
186
199
|
function mapEnvName(raw) {
|
|
187
200
|
const lower = raw.toLowerCase();
|
|
188
201
|
if (lower === "production") return "prod";
|
|
@@ -323,6 +336,13 @@ function entangle(source, target) {
|
|
|
323
336
|
saveRegistry(registry);
|
|
324
337
|
}
|
|
325
338
|
}
|
|
339
|
+
function disentangle(source, target) {
|
|
340
|
+
const registry = loadRegistry();
|
|
341
|
+
registry.pairs = registry.pairs.filter(
|
|
342
|
+
(p) => !(p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key || p.source.service === target.service && p.source.key === target.key && p.target.service === source.service && p.target.key === source.key)
|
|
343
|
+
);
|
|
344
|
+
saveRegistry(registry);
|
|
345
|
+
}
|
|
326
346
|
function findEntangled(source) {
|
|
327
347
|
const registry = loadRegistry();
|
|
328
348
|
return registry.pairs.filter(
|
|
@@ -373,6 +393,213 @@ function resolveScope(opts) {
|
|
|
373
393
|
return [{ scope: "global", service: globalService() }];
|
|
374
394
|
}
|
|
375
395
|
|
|
396
|
+
// src/core/hooks.ts
|
|
397
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
398
|
+
import { join as join4 } from "path";
|
|
399
|
+
import { homedir as homedir3 } from "os";
|
|
400
|
+
import { exec } from "child_process";
|
|
401
|
+
import { request as httpsRequest } from "https";
|
|
402
|
+
import { request as httpRequest } from "http";
|
|
403
|
+
import { randomUUID } from "crypto";
|
|
404
|
+
function getRegistryPath2() {
|
|
405
|
+
const dir = join4(homedir3(), ".config", "q-ring");
|
|
406
|
+
if (!existsSync4(dir)) {
|
|
407
|
+
mkdirSync3(dir, { recursive: true });
|
|
408
|
+
}
|
|
409
|
+
return join4(dir, "hooks.json");
|
|
410
|
+
}
|
|
411
|
+
function loadRegistry2() {
|
|
412
|
+
const path = getRegistryPath2();
|
|
413
|
+
if (!existsSync4(path)) {
|
|
414
|
+
return { hooks: [] };
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
418
|
+
} catch {
|
|
419
|
+
return { hooks: [] };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function saveRegistry2(registry) {
|
|
423
|
+
writeFileSync2(getRegistryPath2(), JSON.stringify(registry, null, 2));
|
|
424
|
+
}
|
|
425
|
+
function registerHook(entry) {
|
|
426
|
+
const registry = loadRegistry2();
|
|
427
|
+
const hook = {
|
|
428
|
+
...entry,
|
|
429
|
+
id: randomUUID().slice(0, 8),
|
|
430
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
431
|
+
};
|
|
432
|
+
registry.hooks.push(hook);
|
|
433
|
+
saveRegistry2(registry);
|
|
434
|
+
return hook;
|
|
435
|
+
}
|
|
436
|
+
function removeHook(id) {
|
|
437
|
+
const registry = loadRegistry2();
|
|
438
|
+
const before = registry.hooks.length;
|
|
439
|
+
registry.hooks = registry.hooks.filter((h) => h.id !== id);
|
|
440
|
+
if (registry.hooks.length < before) {
|
|
441
|
+
saveRegistry2(registry);
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
function listHooks() {
|
|
447
|
+
return loadRegistry2().hooks;
|
|
448
|
+
}
|
|
449
|
+
function matchesHook(hook, payload, tags) {
|
|
450
|
+
if (!hook.enabled) return false;
|
|
451
|
+
const m = hook.match;
|
|
452
|
+
if (m.action?.length && !m.action.includes(payload.action)) return false;
|
|
453
|
+
if (m.key && m.key !== payload.key) return false;
|
|
454
|
+
if (m.keyPattern) {
|
|
455
|
+
const regex = new RegExp(
|
|
456
|
+
"^" + m.keyPattern.replace(/\*/g, ".*") + "$",
|
|
457
|
+
"i"
|
|
458
|
+
);
|
|
459
|
+
if (!regex.test(payload.key)) return false;
|
|
460
|
+
}
|
|
461
|
+
if (m.tag && (!tags || !tags.includes(m.tag))) return false;
|
|
462
|
+
if (m.scope && m.scope !== payload.scope) return false;
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
function executeShell(command, payload) {
|
|
466
|
+
return new Promise((resolve) => {
|
|
467
|
+
const env = {
|
|
468
|
+
...process.env,
|
|
469
|
+
QRING_HOOK_KEY: payload.key,
|
|
470
|
+
QRING_HOOK_ACTION: payload.action,
|
|
471
|
+
QRING_HOOK_SCOPE: payload.scope
|
|
472
|
+
};
|
|
473
|
+
exec(command, { timeout: 3e4, env }, (err, stdout, stderr) => {
|
|
474
|
+
if (err) {
|
|
475
|
+
resolve({ hookId: "", success: false, message: `Shell error: ${err.message}` });
|
|
476
|
+
} else {
|
|
477
|
+
resolve({ hookId: "", success: true, message: stdout.trim() || "OK" });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function executeHttp(url, payload) {
|
|
483
|
+
return new Promise((resolve) => {
|
|
484
|
+
const body = JSON.stringify(payload);
|
|
485
|
+
const parsedUrl = new URL(url);
|
|
486
|
+
const reqFn = parsedUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
|
487
|
+
const req = reqFn(
|
|
488
|
+
url,
|
|
489
|
+
{
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: {
|
|
492
|
+
"Content-Type": "application/json",
|
|
493
|
+
"Content-Length": Buffer.byteLength(body),
|
|
494
|
+
"User-Agent": "q-ring-hooks/1.0"
|
|
495
|
+
},
|
|
496
|
+
timeout: 1e4
|
|
497
|
+
},
|
|
498
|
+
(res) => {
|
|
499
|
+
let data = "";
|
|
500
|
+
res.on("data", (chunk) => data += chunk);
|
|
501
|
+
res.on("end", () => {
|
|
502
|
+
resolve({
|
|
503
|
+
hookId: "",
|
|
504
|
+
success: (res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300,
|
|
505
|
+
message: `HTTP ${res.statusCode}`
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
req.on("error", (err) => {
|
|
511
|
+
resolve({ hookId: "", success: false, message: `HTTP error: ${err.message}` });
|
|
512
|
+
});
|
|
513
|
+
req.on("timeout", () => {
|
|
514
|
+
req.destroy();
|
|
515
|
+
resolve({ hookId: "", success: false, message: "HTTP timeout" });
|
|
516
|
+
});
|
|
517
|
+
req.write(body);
|
|
518
|
+
req.end();
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
function executeSignal(target, signal = "SIGHUP") {
|
|
522
|
+
return new Promise((resolve) => {
|
|
523
|
+
const pid = parseInt(target, 10);
|
|
524
|
+
if (!isNaN(pid)) {
|
|
525
|
+
try {
|
|
526
|
+
process.kill(pid, signal);
|
|
527
|
+
resolve({ hookId: "", success: true, message: `Signal ${signal} sent to PID ${pid}` });
|
|
528
|
+
} catch (err) {
|
|
529
|
+
resolve({ hookId: "", success: false, message: `Signal error: ${err instanceof Error ? err.message : String(err)}` });
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
exec(`pgrep -f "${target}"`, { timeout: 5e3 }, (err, stdout) => {
|
|
534
|
+
if (err || !stdout.trim()) {
|
|
535
|
+
resolve({ hookId: "", success: false, message: `Process "${target}" not found` });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
|
|
539
|
+
let sent = 0;
|
|
540
|
+
for (const p of pids) {
|
|
541
|
+
try {
|
|
542
|
+
process.kill(p, signal);
|
|
543
|
+
sent++;
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
resolve({ hookId: "", success: sent > 0, message: `Signal ${signal} sent to ${sent} process(es)` });
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
async function executeHook(hook, payload) {
|
|
552
|
+
let result;
|
|
553
|
+
switch (hook.type) {
|
|
554
|
+
case "shell":
|
|
555
|
+
result = hook.command ? await executeShell(hook.command, payload) : { hookId: hook.id, success: false, message: "No command specified" };
|
|
556
|
+
break;
|
|
557
|
+
case "http":
|
|
558
|
+
result = hook.url ? await executeHttp(hook.url, payload) : { hookId: hook.id, success: false, message: "No URL specified" };
|
|
559
|
+
break;
|
|
560
|
+
case "signal":
|
|
561
|
+
result = hook.signal ? await executeSignal(hook.signal.target, hook.signal.signal) : { hookId: hook.id, success: false, message: "No signal target specified" };
|
|
562
|
+
break;
|
|
563
|
+
default:
|
|
564
|
+
result = { hookId: hook.id, success: false, message: `Unknown hook type: ${hook.type}` };
|
|
565
|
+
}
|
|
566
|
+
result.hookId = hook.id;
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
async function fireHooks(payload, tags) {
|
|
570
|
+
const hooks = listHooks();
|
|
571
|
+
const matching = hooks.filter((h) => matchesHook(h, payload, tags));
|
|
572
|
+
if (matching.length === 0) return [];
|
|
573
|
+
const results = await Promise.allSettled(
|
|
574
|
+
matching.map((h) => executeHook(h, payload))
|
|
575
|
+
);
|
|
576
|
+
const hookResults = [];
|
|
577
|
+
for (const r of results) {
|
|
578
|
+
if (r.status === "fulfilled") {
|
|
579
|
+
hookResults.push(r.value);
|
|
580
|
+
} else {
|
|
581
|
+
hookResults.push({
|
|
582
|
+
hookId: "unknown",
|
|
583
|
+
success: false,
|
|
584
|
+
message: r.reason?.message ?? "Hook execution failed"
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
for (const r of hookResults) {
|
|
589
|
+
try {
|
|
590
|
+
logAudit({
|
|
591
|
+
action: "write",
|
|
592
|
+
key: payload.key,
|
|
593
|
+
scope: payload.scope,
|
|
594
|
+
source: payload.source,
|
|
595
|
+
detail: `hook:${r.hookId} ${r.success ? "ok" : "fail"} \u2014 ${r.message}`
|
|
596
|
+
});
|
|
597
|
+
} catch {
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return hookResults;
|
|
601
|
+
}
|
|
602
|
+
|
|
376
603
|
// src/core/keyring.ts
|
|
377
604
|
function readEnvelope(service, key) {
|
|
378
605
|
const entry = new Entry(service, key);
|
|
@@ -432,6 +659,9 @@ function setSecret(key, value, opts = {}) {
|
|
|
432
659
|
const source = opts.source ?? "cli";
|
|
433
660
|
const existing = readEnvelope(service, key);
|
|
434
661
|
let envelope;
|
|
662
|
+
const rotFmt = opts.rotationFormat ?? existing?.meta.rotationFormat;
|
|
663
|
+
const rotPfx = opts.rotationPrefix ?? existing?.meta.rotationPrefix;
|
|
664
|
+
const prov = opts.provider ?? existing?.meta.provider;
|
|
435
665
|
if (opts.states) {
|
|
436
666
|
envelope = createEnvelope("", {
|
|
437
667
|
states: opts.states,
|
|
@@ -440,7 +670,10 @@ function setSecret(key, value, opts = {}) {
|
|
|
440
670
|
tags: opts.tags,
|
|
441
671
|
ttlSeconds: opts.ttlSeconds,
|
|
442
672
|
expiresAt: opts.expiresAt,
|
|
443
|
-
entangled: existing?.meta.entangled
|
|
673
|
+
entangled: existing?.meta.entangled,
|
|
674
|
+
rotationFormat: rotFmt,
|
|
675
|
+
rotationPrefix: rotPfx,
|
|
676
|
+
provider: prov
|
|
444
677
|
});
|
|
445
678
|
} else {
|
|
446
679
|
envelope = createEnvelope(value, {
|
|
@@ -448,7 +681,10 @@ function setSecret(key, value, opts = {}) {
|
|
|
448
681
|
tags: opts.tags,
|
|
449
682
|
ttlSeconds: opts.ttlSeconds,
|
|
450
683
|
expiresAt: opts.expiresAt,
|
|
451
|
-
entangled: existing?.meta.entangled
|
|
684
|
+
entangled: existing?.meta.entangled,
|
|
685
|
+
rotationFormat: rotFmt,
|
|
686
|
+
rotationPrefix: rotPfx,
|
|
687
|
+
provider: prov
|
|
452
688
|
});
|
|
453
689
|
}
|
|
454
690
|
if (existing) {
|
|
@@ -480,6 +716,14 @@ function setSecret(key, value, opts = {}) {
|
|
|
480
716
|
} catch {
|
|
481
717
|
}
|
|
482
718
|
}
|
|
719
|
+
fireHooks({
|
|
720
|
+
action: "write",
|
|
721
|
+
key,
|
|
722
|
+
scope,
|
|
723
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
724
|
+
source
|
|
725
|
+
}, envelope.meta.tags).catch(() => {
|
|
726
|
+
});
|
|
483
727
|
}
|
|
484
728
|
function deleteSecret(key, opts = {}) {
|
|
485
729
|
const scopes = resolveScope(opts);
|
|
@@ -491,12 +735,31 @@ function deleteSecret(key, opts = {}) {
|
|
|
491
735
|
if (entry.deleteCredential()) {
|
|
492
736
|
deleted = true;
|
|
493
737
|
logAudit({ action: "delete", key, scope, source });
|
|
738
|
+
fireHooks({
|
|
739
|
+
action: "delete",
|
|
740
|
+
key,
|
|
741
|
+
scope,
|
|
742
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
743
|
+
source
|
|
744
|
+
}).catch(() => {
|
|
745
|
+
});
|
|
494
746
|
}
|
|
495
747
|
} catch {
|
|
496
748
|
}
|
|
497
749
|
}
|
|
498
750
|
return deleted;
|
|
499
751
|
}
|
|
752
|
+
function hasSecret(key, opts = {}) {
|
|
753
|
+
const scopes = resolveScope(opts);
|
|
754
|
+
for (const { service } of scopes) {
|
|
755
|
+
const envelope = readEnvelope(service, key);
|
|
756
|
+
if (envelope) {
|
|
757
|
+
const decay = checkDecay(envelope);
|
|
758
|
+
if (!decay.isExpired) return true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
500
763
|
function listSecrets(opts = {}) {
|
|
501
764
|
const source = opts.source ?? "cli";
|
|
502
765
|
const services = [];
|
|
@@ -538,8 +801,17 @@ function listSecrets(opts = {}) {
|
|
|
538
801
|
function exportSecrets(opts = {}) {
|
|
539
802
|
const format = opts.format ?? "env";
|
|
540
803
|
const env = resolveEnv(opts);
|
|
541
|
-
|
|
804
|
+
let entries = listSecrets(opts);
|
|
542
805
|
const source = opts.source ?? "cli";
|
|
806
|
+
if (opts.keys?.length) {
|
|
807
|
+
const keySet = new Set(opts.keys);
|
|
808
|
+
entries = entries.filter((e) => keySet.has(e.key));
|
|
809
|
+
}
|
|
810
|
+
if (opts.tags?.length) {
|
|
811
|
+
entries = entries.filter(
|
|
812
|
+
(e) => opts.tags.some((t) => e.envelope?.meta.tags?.includes(t))
|
|
813
|
+
);
|
|
814
|
+
}
|
|
543
815
|
const merged = /* @__PURE__ */ new Map();
|
|
544
816
|
const globalEntries = entries.filter((e) => e.scope === "global");
|
|
545
817
|
const projectEntries = entries.filter((e) => e.scope === "project");
|
|
@@ -581,6 +853,19 @@ function entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
|
581
853
|
detail: `entangled with ${targetKey}`
|
|
582
854
|
});
|
|
583
855
|
}
|
|
856
|
+
function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
|
|
857
|
+
const sourceScopes = resolveScope({ ...sourceOpts, scope: sourceOpts.scope ?? "global" });
|
|
858
|
+
const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
|
|
859
|
+
const source = { service: sourceScopes[0].service, key: sourceKey };
|
|
860
|
+
const target = { service: targetScopes[0].service, key: targetKey };
|
|
861
|
+
disentangle(source, target);
|
|
862
|
+
logAudit({
|
|
863
|
+
action: "entangle",
|
|
864
|
+
key: sourceKey,
|
|
865
|
+
source: sourceOpts.source ?? "cli",
|
|
866
|
+
detail: `disentangled from ${targetKey}`
|
|
867
|
+
});
|
|
868
|
+
}
|
|
584
869
|
|
|
585
870
|
// src/core/tunnel.ts
|
|
586
871
|
var tunnelStore = /* @__PURE__ */ new Map();
|
|
@@ -655,21 +940,28 @@ function tunnelList() {
|
|
|
655
940
|
|
|
656
941
|
export {
|
|
657
942
|
checkDecay,
|
|
943
|
+
readProjectConfig,
|
|
658
944
|
collapseEnvironment,
|
|
659
945
|
logAudit,
|
|
660
946
|
queryAudit,
|
|
661
947
|
detectAnomalies,
|
|
662
948
|
listEntanglements,
|
|
949
|
+
registerHook,
|
|
950
|
+
removeHook,
|
|
951
|
+
listHooks,
|
|
952
|
+
fireHooks,
|
|
663
953
|
getSecret,
|
|
664
954
|
getEnvelope,
|
|
665
955
|
setSecret,
|
|
666
956
|
deleteSecret,
|
|
957
|
+
hasSecret,
|
|
667
958
|
listSecrets,
|
|
668
959
|
exportSecrets,
|
|
669
960
|
entangleSecrets,
|
|
961
|
+
disentangleSecrets,
|
|
670
962
|
tunnelCreate,
|
|
671
963
|
tunnelRead,
|
|
672
964
|
tunnelDestroy,
|
|
673
965
|
tunnelList
|
|
674
966
|
};
|
|
675
|
-
//# sourceMappingURL=chunk-
|
|
967
|
+
//# sourceMappingURL=chunk-6IQ5SFLI.js.map
|