@i4ctime/q-ring 0.3.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1576 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/envelope.ts
4
+ function createEnvelope(value, opts) {
5
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6
+ let expiresAt = opts?.expiresAt;
7
+ if (!expiresAt && opts?.ttlSeconds) {
8
+ expiresAt = new Date(Date.now() + opts.ttlSeconds * 1e3).toISOString();
9
+ }
10
+ return {
11
+ v: 1,
12
+ value: opts?.states ? void 0 : value,
13
+ states: opts?.states,
14
+ defaultEnv: opts?.defaultEnv,
15
+ meta: {
16
+ createdAt: now,
17
+ updatedAt: now,
18
+ expiresAt,
19
+ ttlSeconds: opts?.ttlSeconds,
20
+ description: opts?.description,
21
+ tags: opts?.tags,
22
+ entangled: opts?.entangled,
23
+ accessCount: 0,
24
+ rotationFormat: opts?.rotationFormat,
25
+ rotationPrefix: opts?.rotationPrefix,
26
+ provider: opts?.provider,
27
+ requiresApproval: opts?.requiresApproval,
28
+ jitProvider: opts?.jitProvider
29
+ }
30
+ };
31
+ }
32
+ function parseEnvelope(raw) {
33
+ try {
34
+ const parsed = JSON.parse(raw);
35
+ if (parsed && typeof parsed === "object" && parsed.v === 1) {
36
+ return parsed;
37
+ }
38
+ } catch {
39
+ }
40
+ return null;
41
+ }
42
+ function wrapLegacy(rawValue) {
43
+ const now = (/* @__PURE__ */ new Date()).toISOString();
44
+ return {
45
+ v: 1,
46
+ value: rawValue,
47
+ meta: {
48
+ createdAt: now,
49
+ updatedAt: now,
50
+ accessCount: 0
51
+ }
52
+ };
53
+ }
54
+ function serializeEnvelope(envelope) {
55
+ return JSON.stringify(envelope);
56
+ }
57
+ function collapseValue(envelope, env) {
58
+ if (envelope.states) {
59
+ const targetEnv = env ?? envelope.defaultEnv;
60
+ if (targetEnv && envelope.states[targetEnv]) {
61
+ return envelope.states[targetEnv];
62
+ }
63
+ if (envelope.defaultEnv && envelope.states[envelope.defaultEnv]) {
64
+ return envelope.states[envelope.defaultEnv];
65
+ }
66
+ const keys = Object.keys(envelope.states);
67
+ if (keys.length > 0) {
68
+ return envelope.states[keys[0]];
69
+ }
70
+ return null;
71
+ }
72
+ return envelope.value ?? null;
73
+ }
74
+ function checkDecay(envelope) {
75
+ if (!envelope.meta.expiresAt) {
76
+ return {
77
+ isExpired: false,
78
+ isStale: false,
79
+ lifetimePercent: 0,
80
+ secondsRemaining: null,
81
+ timeRemaining: null
82
+ };
83
+ }
84
+ const now = Date.now();
85
+ const expires = new Date(envelope.meta.expiresAt).getTime();
86
+ const created = new Date(envelope.meta.createdAt).getTime();
87
+ const totalLifetime = expires - created;
88
+ const elapsed = now - created;
89
+ const remaining = expires - now;
90
+ const lifetimePercent = totalLifetime > 0 ? Math.round(elapsed / totalLifetime * 100) : 100;
91
+ const secondsRemaining = Math.floor(remaining / 1e3);
92
+ let timeRemaining = null;
93
+ if (remaining > 0) {
94
+ const days = Math.floor(remaining / 864e5);
95
+ const hours = Math.floor(remaining % 864e5 / 36e5);
96
+ const minutes = Math.floor(remaining % 36e5 / 6e4);
97
+ if (days > 0) timeRemaining = `${days}d ${hours}h`;
98
+ else if (hours > 0) timeRemaining = `${hours}h ${minutes}m`;
99
+ else timeRemaining = `${minutes}m`;
100
+ } else {
101
+ timeRemaining = "expired";
102
+ }
103
+ return {
104
+ isExpired: remaining <= 0,
105
+ isStale: lifetimePercent >= 75,
106
+ lifetimePercent,
107
+ secondsRemaining,
108
+ timeRemaining
109
+ };
110
+ }
111
+ function recordAccess(envelope) {
112
+ return {
113
+ ...envelope,
114
+ meta: {
115
+ ...envelope.meta,
116
+ accessCount: envelope.meta.accessCount + 1,
117
+ lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
118
+ }
119
+ };
120
+ }
121
+
122
+ // src/core/collapse.ts
123
+ import { execSync } from "child_process";
124
+ import { existsSync, readFileSync } from "fs";
125
+ import { join } from "path";
126
+ var BRANCH_ENV_MAP = {
127
+ main: "prod",
128
+ master: "prod",
129
+ production: "prod",
130
+ develop: "dev",
131
+ development: "dev",
132
+ dev: "dev",
133
+ staging: "staging",
134
+ stage: "staging",
135
+ test: "test",
136
+ testing: "test"
137
+ };
138
+ function detectGitBranch(cwd) {
139
+ try {
140
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
141
+ cwd: cwd ?? process.cwd(),
142
+ stdio: ["pipe", "pipe", "pipe"],
143
+ encoding: "utf8",
144
+ timeout: 3e3
145
+ }).trim();
146
+ return branch || null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+ function readProjectConfig(projectPath) {
152
+ const configPath = join(projectPath ?? process.cwd(), ".q-ring.json");
153
+ try {
154
+ if (existsSync(configPath)) {
155
+ return JSON.parse(readFileSync(configPath, "utf8"));
156
+ }
157
+ } catch {
158
+ }
159
+ return null;
160
+ }
161
+ function collapseEnvironment(ctx = {}) {
162
+ if (ctx.explicit) {
163
+ return { env: ctx.explicit, source: "explicit" };
164
+ }
165
+ const qringEnv = process.env.QRING_ENV;
166
+ if (qringEnv) {
167
+ return { env: qringEnv, source: "QRING_ENV" };
168
+ }
169
+ const nodeEnv = process.env.NODE_ENV;
170
+ if (nodeEnv) {
171
+ const mapped = mapEnvName(nodeEnv);
172
+ return { env: mapped, source: "NODE_ENV" };
173
+ }
174
+ const config = readProjectConfig(ctx.projectPath);
175
+ if (config?.env) {
176
+ return { env: config.env, source: "project-config" };
177
+ }
178
+ const branch = detectGitBranch(ctx.projectPath);
179
+ if (branch) {
180
+ const branchMap = { ...BRANCH_ENV_MAP, ...config?.branchMap };
181
+ const mapped = branchMap[branch] ?? matchGlob(branchMap, branch);
182
+ if (mapped) {
183
+ return { env: mapped, source: "git-branch" };
184
+ }
185
+ }
186
+ if (config?.defaultEnv) {
187
+ return { env: config.defaultEnv, source: "project-config" };
188
+ }
189
+ return null;
190
+ }
191
+ function matchGlob(branchMap, branch) {
192
+ for (const [pattern, env] of Object.entries(branchMap)) {
193
+ if (!pattern.includes("*")) continue;
194
+ const regex = new RegExp(
195
+ "^" + pattern.replace(/\*/g, ".*") + "$"
196
+ );
197
+ if (regex.test(branch)) return env;
198
+ }
199
+ return void 0;
200
+ }
201
+ function mapEnvName(raw) {
202
+ const lower = raw.toLowerCase();
203
+ if (lower === "production") return "prod";
204
+ if (lower === "development") return "dev";
205
+ return lower;
206
+ }
207
+
208
+ // 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";
211
+ import { homedir } from "os";
212
+ import { createHash } from "crypto";
213
+ function getAuditDir() {
214
+ const dir = join2(homedir(), ".config", "q-ring");
215
+ if (!existsSync2(dir)) {
216
+ mkdirSync(dir, { recursive: true });
217
+ }
218
+ return dir;
219
+ }
220
+ function getAuditPath() {
221
+ return join2(getAuditDir(), "audit.jsonl");
222
+ }
223
+ function getLastLineHash() {
224
+ const path = getAuditPath();
225
+ if (!existsSync2(path)) return void 0;
226
+ try {
227
+ const fd = openSync(path, "r");
228
+ const stat = fstatSync(fd);
229
+ if (stat.size === 0) {
230
+ closeSync(fd);
231
+ return void 0;
232
+ }
233
+ const tailSize = Math.min(stat.size, 8192);
234
+ const buf = Buffer.alloc(tailSize);
235
+ readSync(fd, buf, 0, tailSize, stat.size - tailSize);
236
+ closeSync(fd);
237
+ const tail = buf.toString("utf8");
238
+ const lines = tail.split("\n").filter((l) => l.trim());
239
+ if (lines.length === 0) return void 0;
240
+ const lastLine = lines[lines.length - 1];
241
+ return createHash("sha256").update(lastLine).digest("hex");
242
+ } catch {
243
+ return void 0;
244
+ }
245
+ }
246
+ function logAudit(event) {
247
+ const prevHash = getLastLineHash();
248
+ const full = {
249
+ ...event,
250
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
251
+ pid: process.pid,
252
+ prevHash
253
+ };
254
+ try {
255
+ appendFileSync(getAuditPath(), JSON.stringify(full) + "\n");
256
+ } catch {
257
+ }
258
+ }
259
+ function queryAudit(query = {}) {
260
+ const path = getAuditPath();
261
+ if (!existsSync2(path)) return [];
262
+ try {
263
+ const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
264
+ let events = lines.map((line) => {
265
+ try {
266
+ return JSON.parse(line);
267
+ } catch {
268
+ return null;
269
+ }
270
+ }).filter((e) => e !== null);
271
+ if (query.key) events = events.filter((e) => e.key === query.key);
272
+ if (query.action) events = events.filter((e) => e.action === query.action);
273
+ if (query.source) events = events.filter((e) => e.source === query.source);
274
+ if (query.correlationId) events = events.filter((e) => e.correlationId === query.correlationId);
275
+ if (query.since) {
276
+ const since = new Date(query.since).getTime();
277
+ events = events.filter((e) => new Date(e.timestamp).getTime() >= since);
278
+ }
279
+ events.sort(
280
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
281
+ );
282
+ if (query.limit) events = events.slice(0, query.limit);
283
+ return events;
284
+ } catch {
285
+ return [];
286
+ }
287
+ }
288
+ function verifyAuditChain() {
289
+ const path = getAuditPath();
290
+ if (!existsSync2(path)) {
291
+ return { totalEvents: 0, validEvents: 0, intact: true };
292
+ }
293
+ const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
294
+ if (lines.length === 0) {
295
+ return { totalEvents: 0, validEvents: 0, intact: true };
296
+ }
297
+ let validEvents = 0;
298
+ for (let i = 0; i < lines.length; i++) {
299
+ let event;
300
+ try {
301
+ event = JSON.parse(lines[i]);
302
+ } catch {
303
+ return {
304
+ totalEvents: lines.length,
305
+ validEvents,
306
+ brokenAt: i,
307
+ intact: false
308
+ };
309
+ }
310
+ if (i === 0) {
311
+ validEvents++;
312
+ continue;
313
+ }
314
+ const expectedHash = createHash("sha256").update(lines[i - 1]).digest("hex");
315
+ if (event.prevHash !== expectedHash) {
316
+ return {
317
+ totalEvents: lines.length,
318
+ validEvents,
319
+ brokenAt: i,
320
+ brokenEvent: event,
321
+ intact: false
322
+ };
323
+ }
324
+ validEvents++;
325
+ }
326
+ return { totalEvents: lines.length, validEvents, intact: true };
327
+ }
328
+ function exportAudit(opts = {}) {
329
+ const path = getAuditPath();
330
+ if (!existsSync2(path)) return opts.format === "json" ? "[]" : "";
331
+ const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
332
+ let events = lines.map((l) => {
333
+ try {
334
+ return JSON.parse(l);
335
+ } catch {
336
+ return null;
337
+ }
338
+ }).filter((e) => e !== null);
339
+ if (opts.since) {
340
+ const since = new Date(opts.since).getTime();
341
+ events = events.filter((e) => new Date(e.timestamp).getTime() >= since);
342
+ }
343
+ if (opts.until) {
344
+ const until = new Date(opts.until).getTime();
345
+ events = events.filter((e) => new Date(e.timestamp).getTime() <= until);
346
+ }
347
+ if (opts.format === "json") {
348
+ return JSON.stringify(events, null, 2);
349
+ }
350
+ if (opts.format === "csv") {
351
+ const header = "timestamp,action,key,scope,env,source,pid,correlationId,detail";
352
+ const rows = events.map(
353
+ (e) => `${e.timestamp},${e.action},${e.key ?? ""},${e.scope ?? ""},${e.env ?? ""},${e.source},${e.pid},${e.correlationId ?? ""},${(e.detail ?? "").replace(/,/g, ";")}`
354
+ );
355
+ return [header, ...rows].join("\n");
356
+ }
357
+ return events.map((e) => JSON.stringify(e)).join("\n");
358
+ }
359
+ function detectAnomalies(key) {
360
+ const recent = queryAudit({
361
+ key,
362
+ action: "read",
363
+ since: new Date(Date.now() - 36e5).toISOString()
364
+ });
365
+ const anomalies = [];
366
+ if (key && recent.length > 50) {
367
+ anomalies.push({
368
+ type: "burst",
369
+ description: `${recent.length} reads of "${key}" in the last hour`,
370
+ events: recent.slice(0, 10)
371
+ });
372
+ }
373
+ const nightAccess = recent.filter((e) => {
374
+ const hour = new Date(e.timestamp).getHours();
375
+ return hour >= 1 && hour < 5;
376
+ });
377
+ if (nightAccess.length > 0) {
378
+ anomalies.push({
379
+ type: "unusual-hour",
380
+ description: `${nightAccess.length} access(es) during unusual hours (1am-5am)`,
381
+ events: nightAccess
382
+ });
383
+ }
384
+ const verification = verifyAuditChain();
385
+ if (!verification.intact) {
386
+ anomalies.push({
387
+ type: "tampered",
388
+ description: `Audit chain broken at event #${verification.brokenAt}`,
389
+ events: verification.brokenEvent ? [verification.brokenEvent] : []
390
+ });
391
+ }
392
+ return anomalies;
393
+ }
394
+
395
+ // 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";
398
+ import { homedir as homedir2 } from "os";
399
+ function getRegistryPath() {
400
+ const dir = join3(homedir2(), ".config", "q-ring");
401
+ if (!existsSync3(dir)) {
402
+ mkdirSync2(dir, { recursive: true });
403
+ }
404
+ return join3(dir, "entanglement.json");
405
+ }
406
+ function loadRegistry() {
407
+ const path = getRegistryPath();
408
+ if (!existsSync3(path)) {
409
+ return { pairs: [] };
410
+ }
411
+ try {
412
+ return JSON.parse(readFileSync3(path, "utf8"));
413
+ } catch {
414
+ return { pairs: [] };
415
+ }
416
+ }
417
+ function saveRegistry(registry2) {
418
+ writeFileSync2(getRegistryPath(), JSON.stringify(registry2, null, 2));
419
+ }
420
+ function entangle(source, target) {
421
+ const registry2 = loadRegistry();
422
+ const exists = registry2.pairs.some(
423
+ (p) => p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key
424
+ );
425
+ if (!exists) {
426
+ registry2.pairs.push({
427
+ source,
428
+ target,
429
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
430
+ });
431
+ registry2.pairs.push({
432
+ source: target,
433
+ target: source,
434
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
435
+ });
436
+ saveRegistry(registry2);
437
+ }
438
+ }
439
+ function disentangle(source, target) {
440
+ const registry2 = loadRegistry();
441
+ registry2.pairs = registry2.pairs.filter(
442
+ (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)
443
+ );
444
+ saveRegistry(registry2);
445
+ }
446
+ function findEntangled(source) {
447
+ const registry2 = loadRegistry();
448
+ return registry2.pairs.filter(
449
+ (p) => p.source.service === source.service && p.source.key === source.key
450
+ ).map((p) => p.target);
451
+ }
452
+ function listEntanglements() {
453
+ return loadRegistry().pairs;
454
+ }
455
+
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
+ // 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";
514
+ import { homedir as homedir3 } from "os";
515
+ import { exec } from "child_process";
516
+ import { randomUUID } from "crypto";
517
+ import { lookup } from "dns/promises";
518
+
519
+ // src/utils/http-request.ts
520
+ import { request as httpsRequest } from "https";
521
+ import { request as httpRequest } from "http";
522
+ var DEFAULT_TIMEOUT_MS = 1e4;
523
+ var DEFAULT_MAX_RESPONSE_BYTES = 65536;
524
+ function httpRequest_(opts) {
525
+ const {
526
+ url,
527
+ method = "GET",
528
+ headers = {},
529
+ body,
530
+ timeoutMs = DEFAULT_TIMEOUT_MS,
531
+ maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES
532
+ } = opts;
533
+ return new Promise((resolve, reject) => {
534
+ const parsed = new URL(url);
535
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
536
+ reject(new Error(`Unsupported URL protocol: ${parsed.protocol}`));
537
+ return;
538
+ }
539
+ const reqFn = parsed.protocol === "https:" ? httpsRequest : httpRequest;
540
+ const reqHeaders = { ...headers };
541
+ if (body && !reqHeaders["Content-Length"]) {
542
+ reqHeaders["Content-Length"] = Buffer.byteLength(body);
543
+ }
544
+ const req = reqFn(
545
+ url,
546
+ { method, headers: reqHeaders, timeout: timeoutMs },
547
+ (res) => {
548
+ const chunks = [];
549
+ let totalBytes = 0;
550
+ let truncated = false;
551
+ res.on("data", (chunk) => {
552
+ totalBytes += chunk.length;
553
+ if (totalBytes > maxResponseBytes) {
554
+ truncated = true;
555
+ res.destroy();
556
+ return;
557
+ }
558
+ chunks.push(chunk);
559
+ });
560
+ let settled = false;
561
+ const settle = (result) => {
562
+ if (!settled) {
563
+ settled = true;
564
+ resolve(result);
565
+ }
566
+ };
567
+ const fail = (err) => {
568
+ if (!settled) {
569
+ settled = true;
570
+ reject(err);
571
+ }
572
+ };
573
+ res.on("error", (err) => fail(new Error(`Response error: ${err.message}`)));
574
+ res.on("end", () => {
575
+ settle({
576
+ statusCode: res.statusCode ?? 0,
577
+ body: Buffer.concat(chunks).toString("utf8"),
578
+ truncated
579
+ });
580
+ });
581
+ res.on("close", () => {
582
+ settle({
583
+ statusCode: res.statusCode ?? 0,
584
+ body: Buffer.concat(chunks).toString("utf8"),
585
+ truncated
586
+ });
587
+ });
588
+ }
589
+ );
590
+ req.on("error", (err) => reject(new Error(`Network error: ${err.message}`)));
591
+ req.on("timeout", () => {
592
+ req.destroy();
593
+ reject(new Error("Request timed out"));
594
+ });
595
+ if (body) req.write(body);
596
+ req.end();
597
+ });
598
+ }
599
+
600
+ // src/core/hooks.ts
601
+ function getRegistryPath2() {
602
+ const dir = join4(homedir3(), ".config", "q-ring");
603
+ if (!existsSync4(dir)) {
604
+ mkdirSync3(dir, { recursive: true });
605
+ }
606
+ return join4(dir, "hooks.json");
607
+ }
608
+ function loadRegistry2() {
609
+ const path = getRegistryPath2();
610
+ if (!existsSync4(path)) {
611
+ return { hooks: [] };
612
+ }
613
+ try {
614
+ return JSON.parse(readFileSync4(path, "utf8"));
615
+ } catch {
616
+ return { hooks: [] };
617
+ }
618
+ }
619
+ function saveRegistry2(registry2) {
620
+ writeFileSync3(getRegistryPath2(), JSON.stringify(registry2, null, 2));
621
+ }
622
+ function registerHook(entry) {
623
+ const registry2 = loadRegistry2();
624
+ const hook = {
625
+ ...entry,
626
+ id: randomUUID().slice(0, 8),
627
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
628
+ };
629
+ registry2.hooks.push(hook);
630
+ saveRegistry2(registry2);
631
+ return hook;
632
+ }
633
+ function removeHook(id) {
634
+ const registry2 = loadRegistry2();
635
+ const before = registry2.hooks.length;
636
+ registry2.hooks = registry2.hooks.filter((h) => h.id !== id);
637
+ if (registry2.hooks.length < before) {
638
+ saveRegistry2(registry2);
639
+ return true;
640
+ }
641
+ return false;
642
+ }
643
+ function listHooks() {
644
+ return loadRegistry2().hooks;
645
+ }
646
+ function matchesHook(hook, payload, tags) {
647
+ if (!hook.enabled) return false;
648
+ const m = hook.match;
649
+ if (m.action?.length && !m.action.includes(payload.action)) return false;
650
+ if (m.key && m.key !== payload.key) return false;
651
+ if (m.keyPattern) {
652
+ const regex = new RegExp(
653
+ "^" + m.keyPattern.replace(/\*/g, ".*") + "$",
654
+ "i"
655
+ );
656
+ if (!regex.test(payload.key)) return false;
657
+ }
658
+ if (m.tag && (!tags || !tags.includes(m.tag))) return false;
659
+ if (m.scope && m.scope !== payload.scope) return false;
660
+ return true;
661
+ }
662
+ function executeShell(command, payload) {
663
+ return new Promise((resolve) => {
664
+ const env = {
665
+ ...process.env,
666
+ QRING_HOOK_KEY: payload.key,
667
+ QRING_HOOK_ACTION: payload.action,
668
+ QRING_HOOK_SCOPE: payload.scope
669
+ };
670
+ exec(command, { timeout: 3e4, env }, (err, stdout, stderr) => {
671
+ if (err) {
672
+ resolve({ hookId: "", success: false, message: `Shell error: ${err.message}` });
673
+ } else {
674
+ resolve({ hookId: "", success: true, message: stdout.trim() || "OK" });
675
+ }
676
+ });
677
+ });
678
+ }
679
+ function isPrivateIP(ip) {
680
+ const octet = "(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})";
681
+ const ipv4Re = new RegExp(`^::ffff:(${octet}\\.${octet}\\.${octet}\\.${octet})$`, "i");
682
+ const ipv4Mapped = ip.match(ipv4Re);
683
+ if (ipv4Mapped) return isPrivateIP(ipv4Mapped[1]);
684
+ if (/^127\./.test(ip)) return true;
685
+ if (/^10\./.test(ip)) return true;
686
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
687
+ if (/^192\.168\./.test(ip)) return true;
688
+ if (/^169\.254\./.test(ip)) return true;
689
+ if (ip === "0.0.0.0") return true;
690
+ if (ip === "::1" || ip === "::") return true;
691
+ if (/^f[cd][0-9a-f]{2}:/i.test(ip)) return true;
692
+ if (/^fe80:/i.test(ip)) return true;
693
+ return false;
694
+ }
695
+ async function checkSSRF(url) {
696
+ if (process.env.Q_RING_ALLOW_PRIVATE_HOOKS === "1") return null;
697
+ try {
698
+ const parsed = new URL(url);
699
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
700
+ if (isPrivateIP(hostname)) {
701
+ return `Blocked: hook URL resolves to private address (${hostname}). Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
702
+ }
703
+ const results = await lookup(hostname, { all: true });
704
+ for (const { address } of results) {
705
+ if (isPrivateIP(address)) {
706
+ return `Blocked: hook URL "${hostname}" resolves to private address ${address}. Set Q_RING_ALLOW_PRIVATE_HOOKS=1 to override.`;
707
+ }
708
+ }
709
+ } catch {
710
+ }
711
+ return null;
712
+ }
713
+ async function executeHttp(url, payload) {
714
+ const ssrfBlock = await checkSSRF(url);
715
+ if (ssrfBlock) {
716
+ logAudit({
717
+ action: "policy_deny",
718
+ key: payload.key,
719
+ scope: payload.scope,
720
+ source: payload.source,
721
+ detail: `hook SSRF blocked: ${url}`
722
+ });
723
+ return { hookId: "", success: false, message: ssrfBlock };
724
+ }
725
+ try {
726
+ const body = JSON.stringify(payload);
727
+ const res = await httpRequest_({
728
+ url,
729
+ method: "POST",
730
+ headers: {
731
+ "Content-Type": "application/json",
732
+ "User-Agent": "q-ring-hooks/1.0"
733
+ },
734
+ body,
735
+ timeoutMs: 1e4
736
+ });
737
+ return {
738
+ hookId: "",
739
+ success: res.statusCode >= 200 && res.statusCode < 300,
740
+ message: `HTTP ${res.statusCode}`
741
+ };
742
+ } catch (err) {
743
+ return {
744
+ hookId: "",
745
+ success: false,
746
+ message: err instanceof Error ? err.message : "HTTP error"
747
+ };
748
+ }
749
+ }
750
+ function executeSignal(target, signal = "SIGHUP") {
751
+ return new Promise((resolve) => {
752
+ const pid = parseInt(target, 10);
753
+ if (!isNaN(pid)) {
754
+ try {
755
+ process.kill(pid, signal);
756
+ resolve({ hookId: "", success: true, message: `Signal ${signal} sent to PID ${pid}` });
757
+ } catch (err) {
758
+ resolve({ hookId: "", success: false, message: `Signal error: ${err instanceof Error ? err.message : String(err)}` });
759
+ }
760
+ return;
761
+ }
762
+ exec(`pgrep -f "${target}"`, { timeout: 5e3 }, (err, stdout) => {
763
+ if (err || !stdout.trim()) {
764
+ resolve({ hookId: "", success: false, message: `Process "${target}" not found` });
765
+ return;
766
+ }
767
+ const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
768
+ let sent = 0;
769
+ for (const p of pids) {
770
+ try {
771
+ process.kill(p, signal);
772
+ sent++;
773
+ } catch {
774
+ }
775
+ }
776
+ resolve({ hookId: "", success: sent > 0, message: `Signal ${signal} sent to ${sent} process(es)` });
777
+ });
778
+ });
779
+ }
780
+ async function executeHook(hook, payload) {
781
+ let result;
782
+ switch (hook.type) {
783
+ case "shell":
784
+ result = hook.command ? await executeShell(hook.command, payload) : { hookId: hook.id, success: false, message: "No command specified" };
785
+ break;
786
+ case "http":
787
+ result = hook.url ? await executeHttp(hook.url, payload) : { hookId: hook.id, success: false, message: "No URL specified" };
788
+ break;
789
+ case "signal":
790
+ result = hook.signal ? await executeSignal(hook.signal.target, hook.signal.signal) : { hookId: hook.id, success: false, message: "No signal target specified" };
791
+ break;
792
+ default:
793
+ result = { hookId: hook.id, success: false, message: `Unknown hook type: ${hook.type}` };
794
+ }
795
+ result.hookId = hook.id;
796
+ return result;
797
+ }
798
+ async function fireHooks(payload, tags) {
799
+ const hooks = listHooks();
800
+ const matching = hooks.filter((h) => matchesHook(h, payload, tags));
801
+ if (matching.length === 0) return [];
802
+ const results = await Promise.allSettled(
803
+ matching.map((h) => executeHook(h, payload))
804
+ );
805
+ const hookResults = [];
806
+ for (const r of results) {
807
+ if (r.status === "fulfilled") {
808
+ hookResults.push(r.value);
809
+ } else {
810
+ hookResults.push({
811
+ hookId: "unknown",
812
+ success: false,
813
+ message: r.reason?.message ?? "Hook execution failed"
814
+ });
815
+ }
816
+ }
817
+ for (const r of hookResults) {
818
+ try {
819
+ logAudit({
820
+ action: "write",
821
+ key: payload.key,
822
+ scope: payload.scope,
823
+ source: payload.source,
824
+ detail: `hook:${r.hookId} ${r.success ? "ok" : "fail"} \u2014 ${r.message}`
825
+ });
826
+ } catch {
827
+ }
828
+ }
829
+ return hookResults;
830
+ }
831
+
832
+ // src/core/approval.ts
833
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
834
+ import { join as join5 } from "path";
835
+ import { homedir as homedir4 } from "os";
836
+ import { createHmac, randomBytes } from "crypto";
837
+ function getHmacSecret() {
838
+ const secretPath = join5(homedir4(), ".config", "q-ring", ".approval-key");
839
+ const dir = join5(homedir4(), ".config", "q-ring");
840
+ if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
841
+ if (existsSync5(secretPath)) {
842
+ return readFileSync5(secretPath, "utf8").trim();
843
+ }
844
+ const secret = randomBytes(32).toString("hex");
845
+ writeFileSync4(secretPath, secret, { mode: 384 });
846
+ return secret;
847
+ }
848
+ function computeHmac(entry) {
849
+ const payload = `${entry.id}|${entry.key}|${entry.scope}|${entry.reason}|${entry.grantedBy}|${entry.grantedAt}|${entry.expiresAt}`;
850
+ return createHmac("sha256", getHmacSecret()).update(payload).digest("hex");
851
+ }
852
+ function verifyHmac(entry) {
853
+ const expected = computeHmac(entry);
854
+ return expected === entry.hmac;
855
+ }
856
+ function getRegistryPath3() {
857
+ const dir = join5(homedir4(), ".config", "q-ring");
858
+ if (!existsSync5(dir)) {
859
+ mkdirSync4(dir, { recursive: true });
860
+ }
861
+ return join5(dir, "approvals.json");
862
+ }
863
+ function loadRegistry3() {
864
+ const path = getRegistryPath3();
865
+ if (!existsSync5(path)) {
866
+ return { approvals: [] };
867
+ }
868
+ try {
869
+ return JSON.parse(readFileSync5(path, "utf8"));
870
+ } catch {
871
+ return { approvals: [] };
872
+ }
873
+ }
874
+ function hasApproval(key, scope) {
875
+ const registry2 = loadRegistry3();
876
+ const entry = registry2.approvals.find(
877
+ (a) => a.key === key && a.scope === scope
878
+ );
879
+ if (!entry) return false;
880
+ if (new Date(entry.expiresAt).getTime() < Date.now()) return false;
881
+ if (!verifyHmac(entry)) return false;
882
+ return true;
883
+ }
884
+
885
+ // src/core/provision.ts
886
+ import { execFileSync, spawnSync } from "child_process";
887
+ var ProvisionRegistry = class {
888
+ providers = /* @__PURE__ */ new Map();
889
+ register(provider) {
890
+ this.providers.set(provider.name, provider);
891
+ }
892
+ get(name) {
893
+ return this.providers.get(name);
894
+ }
895
+ listProviders() {
896
+ return [...this.providers.values()];
897
+ }
898
+ };
899
+ var awsStsProvider = {
900
+ name: "aws-sts",
901
+ description: "AWS STS AssumeRole (requires existing local AWS CLI credentials)",
902
+ provision(configRaw) {
903
+ let config;
904
+ try {
905
+ config = JSON.parse(configRaw);
906
+ } catch {
907
+ throw new Error('aws-sts requires valid JSON config (e.g. {"roleArn":"arn:aws:..."})');
908
+ }
909
+ const roleArn = config.roleArn;
910
+ const sessionName = config.sessionName || "q-ring-agent";
911
+ const duration = config.durationSeconds || 3600;
912
+ if (!roleArn) throw new Error("aws-sts requires roleArn in config");
913
+ try {
914
+ const output = execFileSync("aws", [
915
+ "sts",
916
+ "assume-role",
917
+ "--role-arn",
918
+ roleArn,
919
+ "--role-session-name",
920
+ sessionName,
921
+ "--duration-seconds",
922
+ String(duration),
923
+ "--output",
924
+ "json"
925
+ ], { encoding: "utf8" });
926
+ const parsed = JSON.parse(output);
927
+ const creds = parsed.Credentials;
928
+ const value = JSON.stringify({
929
+ AWS_ACCESS_KEY_ID: creds.AccessKeyId,
930
+ AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey,
931
+ AWS_SESSION_TOKEN: creds.SessionToken
932
+ });
933
+ return {
934
+ value,
935
+ expiresAt: creds.Expiration
936
+ };
937
+ } catch (err) {
938
+ throw new Error(`AWS STS provision failed: ${err instanceof Error ? err.message : String(err)}`);
939
+ }
940
+ }
941
+ };
942
+ var httpProvider = {
943
+ name: "http",
944
+ description: "Generic HTTP token endpoint using Node.js http/https",
945
+ provision(configRaw) {
946
+ let config;
947
+ try {
948
+ config = JSON.parse(configRaw);
949
+ } catch {
950
+ throw new Error("http provider requires valid JSON config");
951
+ }
952
+ const url = config.url;
953
+ const method = config.method || "POST";
954
+ const valuePath = config.valuePath || "token";
955
+ const expiresInSeconds = config.expiresInSeconds || 3600;
956
+ if (!url) throw new Error("http provider requires url in config");
957
+ const headers = {
958
+ "User-Agent": "q-ring-jit/1.0",
959
+ ...config.headers ?? {}
960
+ };
961
+ let bodyStr;
962
+ if (config.body) {
963
+ headers["Content-Type"] = "application/json";
964
+ bodyStr = JSON.stringify(config.body);
965
+ }
966
+ const scriptConfig = JSON.stringify({ url, method, headers, bodyStr });
967
+ const script = `
968
+ const cfg = JSON.parse(process.env.__QRING_HTTP_CFG);
969
+ const parsedUrl = new URL(cfg.url);
970
+ const http = require(parsedUrl.protocol === "https:" ? "node:https" : "node:http");
971
+ const req = http.request(cfg.url, { method: cfg.method, headers: cfg.headers, timeout: 30000 }, (res) => {
972
+ let body = "";
973
+ res.on("data", (chunk) => body += chunk);
974
+ res.on("end", () => process.stdout.write(body));
975
+ });
976
+ req.on("error", (e) => { process.stderr.write(e.message); process.exit(1); });
977
+ if (cfg.bodyStr) req.write(cfg.bodyStr);
978
+ req.end();
979
+ `;
980
+ try {
981
+ const result = spawnSync("node", ["-e", script], {
982
+ encoding: "utf8",
983
+ timeout: 35e3,
984
+ env: { ...process.env, __QRING_HTTP_CFG: scriptConfig }
985
+ });
986
+ if (result.status !== 0) {
987
+ throw new Error(result.stderr || "HTTP request failed");
988
+ }
989
+ const parsed = JSON.parse(result.stdout);
990
+ let val = parsed;
991
+ for (const key of valuePath.split(".")) {
992
+ val = val[key];
993
+ }
994
+ return {
995
+ value: String(val),
996
+ expiresAt: new Date(Date.now() + expiresInSeconds * 1e3).toISOString()
997
+ };
998
+ } catch (err) {
999
+ throw new Error(`HTTP provision failed: ${err instanceof Error ? err.message : String(err)}`);
1000
+ }
1001
+ }
1002
+ };
1003
+ var registry = new ProvisionRegistry();
1004
+ registry.register(awsStsProvider);
1005
+ registry.register(httpProvider);
1006
+
1007
+ // src/core/policy.ts
1008
+ var cachedPolicy = null;
1009
+ function loadPolicy(projectPath) {
1010
+ const pp = projectPath ?? process.cwd();
1011
+ if (cachedPolicy && cachedPolicy.path === pp) {
1012
+ return cachedPolicy.policy;
1013
+ }
1014
+ const config = readProjectConfig(pp);
1015
+ const policy = config?.policy ?? {};
1016
+ cachedPolicy = { path: pp, policy };
1017
+ return policy;
1018
+ }
1019
+ function checkToolPolicy(toolName, projectPath) {
1020
+ const policy = loadPolicy(projectPath);
1021
+ if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
1022
+ if (policy.mcp.denyTools?.includes(toolName)) {
1023
+ return {
1024
+ allowed: false,
1025
+ reason: `Tool "${toolName}" is denied by project policy`,
1026
+ policySource: ".q-ring.json policy.mcp.denyTools"
1027
+ };
1028
+ }
1029
+ if (policy.mcp.allowTools && !policy.mcp.allowTools.includes(toolName)) {
1030
+ return {
1031
+ allowed: false,
1032
+ reason: `Tool "${toolName}" is not in the allowlist`,
1033
+ policySource: ".q-ring.json policy.mcp.allowTools"
1034
+ };
1035
+ }
1036
+ return { allowed: true, policySource: ".q-ring.json" };
1037
+ }
1038
+ function checkKeyReadPolicy(key, tags, projectPath) {
1039
+ const policy = loadPolicy(projectPath);
1040
+ if (!policy.mcp) return { allowed: true, policySource: "no-policy" };
1041
+ if (policy.mcp.deniedKeys?.includes(key)) {
1042
+ return {
1043
+ allowed: false,
1044
+ reason: `Key "${key}" is denied by project policy`,
1045
+ policySource: ".q-ring.json policy.mcp.deniedKeys"
1046
+ };
1047
+ }
1048
+ if (policy.mcp.readableKeys && !policy.mcp.readableKeys.includes(key)) {
1049
+ return {
1050
+ allowed: false,
1051
+ reason: `Key "${key}" is not in the readable keys allowlist`,
1052
+ policySource: ".q-ring.json policy.mcp.readableKeys"
1053
+ };
1054
+ }
1055
+ if (tags && policy.mcp.deniedTags) {
1056
+ const blocked = tags.find((t) => policy.mcp.deniedTags.includes(t));
1057
+ if (blocked) {
1058
+ return {
1059
+ allowed: false,
1060
+ reason: `Tag "${blocked}" is denied by project policy`,
1061
+ policySource: ".q-ring.json policy.mcp.deniedTags"
1062
+ };
1063
+ }
1064
+ }
1065
+ return { allowed: true, policySource: ".q-ring.json" };
1066
+ }
1067
+ function checkExecPolicy(command, projectPath) {
1068
+ const policy = loadPolicy(projectPath);
1069
+ if (!policy.exec) return { allowed: true, policySource: "no-policy" };
1070
+ if (policy.exec.denyCommands) {
1071
+ const denied = policy.exec.denyCommands.find((d) => command.includes(d));
1072
+ if (denied) {
1073
+ return {
1074
+ allowed: false,
1075
+ reason: `Command containing "${denied}" is denied by project policy`,
1076
+ policySource: ".q-ring.json policy.exec.denyCommands"
1077
+ };
1078
+ }
1079
+ }
1080
+ if (policy.exec.allowCommands) {
1081
+ const allowed = policy.exec.allowCommands.some((a) => command.startsWith(a));
1082
+ if (!allowed) {
1083
+ return {
1084
+ allowed: false,
1085
+ reason: `Command "${command}" is not in the exec allowlist`,
1086
+ policySource: ".q-ring.json policy.exec.allowCommands"
1087
+ };
1088
+ }
1089
+ }
1090
+ return { allowed: true, policySource: ".q-ring.json" };
1091
+ }
1092
+ function getExecMaxRuntime(projectPath) {
1093
+ return loadPolicy(projectPath).exec?.maxRuntimeSeconds;
1094
+ }
1095
+ function getPolicySummary(projectPath) {
1096
+ const policy = loadPolicy(projectPath);
1097
+ return {
1098
+ hasMcpPolicy: !!policy.mcp,
1099
+ hasExecPolicy: !!policy.exec,
1100
+ hasSecretPolicy: !!policy.secrets,
1101
+ details: policy
1102
+ };
1103
+ }
1104
+
1105
+ // src/core/keyring.ts
1106
+ function readEnvelope(service, key) {
1107
+ const entry = new Entry(service, key);
1108
+ const raw = entry.getPassword();
1109
+ if (raw === null) return null;
1110
+ const envelope = parseEnvelope(raw);
1111
+ return envelope ?? wrapLegacy(raw);
1112
+ }
1113
+ function writeEnvelope(service, key, envelope) {
1114
+ const entry = new Entry(service, key);
1115
+ entry.setPassword(serializeEnvelope(envelope));
1116
+ }
1117
+ function resolveEnv(opts) {
1118
+ if (opts.env) return opts.env;
1119
+ const result = collapseEnvironment({ projectPath: opts.projectPath });
1120
+ return result?.env;
1121
+ }
1122
+ function resolveTemplates(value, opts, seen) {
1123
+ if (!value.includes("{{") || !value.includes("}}")) return value;
1124
+ return value.replace(/\{\{([^}]+)\}\}/g, (match, refKeyRaw) => {
1125
+ const refKey = refKeyRaw.trim();
1126
+ const refValue = getSecret(refKey, { ...opts, _seen: seen });
1127
+ if (refValue === null) {
1128
+ throw new Error(`Template resolution failed: referenced secret "${refKey}" not found`);
1129
+ }
1130
+ return refValue;
1131
+ });
1132
+ }
1133
+ function resolveTemplatesOffline(value, rawValues, seen) {
1134
+ if (!value.includes("{{") || !value.includes("}}")) return value;
1135
+ return value.replace(/\{\{([^}]+)\}\}/g, (match, refKeyRaw) => {
1136
+ const refKey = refKeyRaw.trim();
1137
+ if (seen.has(refKey)) {
1138
+ throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${refKey}`);
1139
+ }
1140
+ const rawRef = rawValues.get(refKey);
1141
+ if (rawRef === void 0) {
1142
+ throw new Error(`Template resolution failed: referenced secret "${refKey}" not found`);
1143
+ }
1144
+ const nextSeen = new Set(seen);
1145
+ nextSeen.add(refKey);
1146
+ return resolveTemplatesOffline(rawRef, rawValues, nextSeen);
1147
+ });
1148
+ }
1149
+ function getSecret(key, opts = {}) {
1150
+ const scopes = resolveScope(opts);
1151
+ const env = resolveEnv(opts);
1152
+ const source = opts.source ?? "cli";
1153
+ const seen = opts._seen ?? /* @__PURE__ */ new Set();
1154
+ if (source === "mcp") {
1155
+ const policyDecision = checkKeyReadPolicy(key, void 0, opts.projectPath);
1156
+ if (!policyDecision.allowed) {
1157
+ throw new Error(`Policy Denied: ${policyDecision.reason}`);
1158
+ }
1159
+ }
1160
+ if (seen.has(key)) {
1161
+ throw new Error(`Circular dependency detected: ${[...seen].join(" -> ")} -> ${key}`);
1162
+ }
1163
+ const nextSeen = new Set(seen);
1164
+ nextSeen.add(key);
1165
+ for (const { service, scope } of scopes) {
1166
+ const envelope = readEnvelope(service, key);
1167
+ if (!envelope) continue;
1168
+ const decay = checkDecay(envelope);
1169
+ if (decay.isExpired) {
1170
+ if (!opts.silent) {
1171
+ logAudit({
1172
+ action: "read",
1173
+ key,
1174
+ scope,
1175
+ source,
1176
+ detail: "blocked: secret expired (quantum decay)"
1177
+ });
1178
+ }
1179
+ continue;
1180
+ }
1181
+ if (envelope.meta.requiresApproval && source === "mcp") {
1182
+ if (!hasApproval(key, scope)) {
1183
+ if (!opts.silent) {
1184
+ logAudit({
1185
+ action: "read",
1186
+ key,
1187
+ scope,
1188
+ source,
1189
+ detail: "blocked: requires user approval"
1190
+ });
1191
+ }
1192
+ throw new Error(`Access Denied: This secret requires user approval. Please ask the user to run 'qring approve ${key}'`);
1193
+ }
1194
+ }
1195
+ let value = collapseValue(envelope, env);
1196
+ if (value === null) continue;
1197
+ if (envelope.meta.jitProvider) {
1198
+ const provider = registry.get(envelope.meta.jitProvider);
1199
+ if (provider) {
1200
+ let isExpired = true;
1201
+ if (envelope.states && envelope.states["jit"] && envelope.meta.jitExpiresAt) {
1202
+ isExpired = new Date(envelope.meta.jitExpiresAt).getTime() <= Date.now();
1203
+ }
1204
+ if (isExpired) {
1205
+ const result = provider.provision(value);
1206
+ envelope.states = envelope.states ?? {};
1207
+ envelope.states["jit"] = result.value;
1208
+ envelope.meta.jitExpiresAt = result.expiresAt;
1209
+ writeEnvelope(service, key, envelope);
1210
+ }
1211
+ value = envelope.states["jit"];
1212
+ }
1213
+ }
1214
+ value = resolveTemplates(value, { ...opts, _seen: nextSeen }, nextSeen);
1215
+ if (!opts.silent) {
1216
+ const updated = recordAccess(envelope);
1217
+ writeEnvelope(service, key, updated);
1218
+ logAudit({ action: "read", key, scope, env, source });
1219
+ }
1220
+ return value;
1221
+ }
1222
+ return null;
1223
+ }
1224
+ function getEnvelope(key, opts = {}) {
1225
+ const scopes = resolveScope(opts);
1226
+ for (const { service, scope } of scopes) {
1227
+ const envelope = readEnvelope(service, key);
1228
+ if (envelope) return { envelope, scope };
1229
+ }
1230
+ return null;
1231
+ }
1232
+ function setSecret(key, value, opts = {}) {
1233
+ const scope = opts.scope ?? "global";
1234
+ const scopes = resolveScope({ ...opts, scope });
1235
+ const { service } = scopes[0];
1236
+ const source = opts.source ?? "cli";
1237
+ const existing = readEnvelope(service, key);
1238
+ let envelope;
1239
+ const rotFmt = opts.rotationFormat ?? existing?.meta.rotationFormat;
1240
+ const rotPfx = opts.rotationPrefix ?? existing?.meta.rotationPrefix;
1241
+ const prov = opts.provider ?? existing?.meta.provider;
1242
+ const reqApp = opts.requiresApproval ?? existing?.meta.requiresApproval;
1243
+ const jitProv = opts.jitProvider ?? existing?.meta.jitProvider;
1244
+ if (opts.states) {
1245
+ envelope = createEnvelope("", {
1246
+ states: opts.states,
1247
+ defaultEnv: opts.defaultEnv,
1248
+ description: opts.description,
1249
+ tags: opts.tags,
1250
+ ttlSeconds: opts.ttlSeconds,
1251
+ expiresAt: opts.expiresAt,
1252
+ entangled: existing?.meta.entangled,
1253
+ rotationFormat: rotFmt,
1254
+ rotationPrefix: rotPfx,
1255
+ provider: prov,
1256
+ requiresApproval: reqApp,
1257
+ jitProvider: jitProv
1258
+ });
1259
+ } else {
1260
+ envelope = createEnvelope(value, {
1261
+ description: opts.description,
1262
+ tags: opts.tags,
1263
+ ttlSeconds: opts.ttlSeconds,
1264
+ expiresAt: opts.expiresAt,
1265
+ entangled: existing?.meta.entangled,
1266
+ rotationFormat: rotFmt,
1267
+ rotationPrefix: rotPfx,
1268
+ provider: prov,
1269
+ requiresApproval: reqApp,
1270
+ jitProvider: jitProv
1271
+ });
1272
+ }
1273
+ if (existing) {
1274
+ envelope.meta.createdAt = existing.meta.createdAt;
1275
+ envelope.meta.accessCount = existing.meta.accessCount;
1276
+ }
1277
+ writeEnvelope(service, key, envelope);
1278
+ logAudit({ action: "write", key, scope, source });
1279
+ const entangled = findEntangled({ service, key });
1280
+ for (const target of entangled) {
1281
+ try {
1282
+ const targetEnvelope = readEnvelope(target.service, target.key);
1283
+ if (targetEnvelope) {
1284
+ if (opts.states) {
1285
+ targetEnvelope.states = opts.states;
1286
+ } else {
1287
+ targetEnvelope.value = value;
1288
+ }
1289
+ targetEnvelope.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1290
+ writeEnvelope(target.service, target.key, targetEnvelope);
1291
+ logAudit({
1292
+ action: "entangle",
1293
+ key: target.key,
1294
+ scope: "global",
1295
+ source,
1296
+ detail: `propagated from ${key}`
1297
+ });
1298
+ }
1299
+ } catch {
1300
+ }
1301
+ }
1302
+ fireHooks({
1303
+ action: "write",
1304
+ key,
1305
+ scope,
1306
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1307
+ source
1308
+ }, envelope.meta.tags).catch(() => {
1309
+ });
1310
+ }
1311
+ function deleteSecret(key, opts = {}) {
1312
+ const scopes = resolveScope(opts);
1313
+ const source = opts.source ?? "cli";
1314
+ let deleted = false;
1315
+ for (const { service, scope } of scopes) {
1316
+ const entry = new Entry(service, key);
1317
+ try {
1318
+ if (entry.deleteCredential()) {
1319
+ deleted = true;
1320
+ logAudit({ action: "delete", key, scope, source });
1321
+ fireHooks({
1322
+ action: "delete",
1323
+ key,
1324
+ scope,
1325
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1326
+ source
1327
+ }).catch(() => {
1328
+ });
1329
+ }
1330
+ } catch {
1331
+ }
1332
+ }
1333
+ return deleted;
1334
+ }
1335
+ function hasSecret(key, opts = {}) {
1336
+ const scopes = resolveScope(opts);
1337
+ for (const { service } of scopes) {
1338
+ const envelope = readEnvelope(service, key);
1339
+ if (envelope) {
1340
+ const decay = checkDecay(envelope);
1341
+ if (!decay.isExpired) return true;
1342
+ }
1343
+ }
1344
+ return false;
1345
+ }
1346
+ function listSecrets(opts = {}) {
1347
+ const source = opts.source ?? "cli";
1348
+ const services = [];
1349
+ if (!opts.scope || opts.scope === "global") {
1350
+ services.push({ service: globalService(), scope: "global" });
1351
+ }
1352
+ if ((!opts.scope || opts.scope === "project") && opts.projectPath) {
1353
+ services.push({
1354
+ service: projectService(opts.projectPath),
1355
+ scope: "project"
1356
+ });
1357
+ }
1358
+ if ((!opts.scope || opts.scope === "team") && opts.teamId) {
1359
+ services.push({ service: teamService(opts.teamId), scope: "team" });
1360
+ }
1361
+ if ((!opts.scope || opts.scope === "org") && opts.orgId) {
1362
+ services.push({ service: orgService(opts.orgId), scope: "org" });
1363
+ }
1364
+ const results = [];
1365
+ const seen = /* @__PURE__ */ new Set();
1366
+ for (const { service, scope } of services) {
1367
+ try {
1368
+ const credentials = findCredentials(service);
1369
+ for (const cred of credentials) {
1370
+ const id = `${scope}:${cred.account}`;
1371
+ if (seen.has(id)) continue;
1372
+ seen.add(id);
1373
+ const envelope = parseEnvelope(cred.password) ?? wrapLegacy(cred.password);
1374
+ const decay = checkDecay(envelope);
1375
+ results.push({
1376
+ key: cred.account,
1377
+ scope,
1378
+ envelope,
1379
+ decay
1380
+ });
1381
+ }
1382
+ } catch {
1383
+ }
1384
+ }
1385
+ if (!opts.silent) {
1386
+ logAudit({ action: "list", source });
1387
+ }
1388
+ return results.sort((a, b) => a.key.localeCompare(b.key));
1389
+ }
1390
+ function exportSecrets(opts = {}) {
1391
+ const format = opts.format ?? "env";
1392
+ const env = resolveEnv(opts);
1393
+ let entries = listSecrets(opts);
1394
+ const source = opts.source ?? "cli";
1395
+ if (opts.keys?.length) {
1396
+ const keySet = new Set(opts.keys);
1397
+ entries = entries.filter((e) => keySet.has(e.key));
1398
+ }
1399
+ if (opts.tags?.length) {
1400
+ entries = entries.filter(
1401
+ (e) => opts.tags.some((t) => e.envelope?.meta.tags?.includes(t))
1402
+ );
1403
+ }
1404
+ const rawValues = /* @__PURE__ */ new Map();
1405
+ const globalEntries = entries.filter((e) => e.scope === "global");
1406
+ const orgEntries = entries.filter((e) => e.scope === "org");
1407
+ const teamEntries = entries.filter((e) => e.scope === "team");
1408
+ const projectEntries = entries.filter((e) => e.scope === "project");
1409
+ for (const entry of [...globalEntries, ...orgEntries, ...teamEntries, ...projectEntries]) {
1410
+ if (entry.envelope) {
1411
+ const decay = checkDecay(entry.envelope);
1412
+ if (decay.isExpired) continue;
1413
+ const value = collapseValue(entry.envelope, env);
1414
+ if (value !== null) {
1415
+ rawValues.set(entry.key, value);
1416
+ }
1417
+ }
1418
+ }
1419
+ const merged = /* @__PURE__ */ new Map();
1420
+ for (const [key, value] of rawValues) {
1421
+ try {
1422
+ const resolved = resolveTemplatesOffline(value, rawValues, /* @__PURE__ */ new Set([key]));
1423
+ merged.set(key, resolved);
1424
+ } catch (err) {
1425
+ console.warn(`Warning: skipped exporting ${key} due to template error: ${err instanceof Error ? err.message : String(err)}`);
1426
+ }
1427
+ }
1428
+ logAudit({ action: "export", source, detail: `format=${format}` });
1429
+ if (format === "json") {
1430
+ const obj = {};
1431
+ for (const [key, value] of merged) {
1432
+ obj[key] = value;
1433
+ }
1434
+ return JSON.stringify(obj, null, 2);
1435
+ }
1436
+ const lines = [];
1437
+ for (const [key, value] of merged) {
1438
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
1439
+ lines.push(`${key}="${escaped}"`);
1440
+ }
1441
+ return lines.join("\n");
1442
+ }
1443
+ function entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
1444
+ const sourceScopes = resolveScope({ ...sourceOpts, scope: sourceOpts.scope ?? "global" });
1445
+ const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
1446
+ const source = { service: sourceScopes[0].service, key: sourceKey };
1447
+ const target = { service: targetScopes[0].service, key: targetKey };
1448
+ entangle(source, target);
1449
+ logAudit({
1450
+ action: "entangle",
1451
+ key: sourceKey,
1452
+ source: sourceOpts.source ?? "cli",
1453
+ detail: `entangled with ${targetKey}`
1454
+ });
1455
+ }
1456
+ function disentangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
1457
+ const sourceScopes = resolveScope({ ...sourceOpts, scope: sourceOpts.scope ?? "global" });
1458
+ const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
1459
+ const source = { service: sourceScopes[0].service, key: sourceKey };
1460
+ const target = { service: targetScopes[0].service, key: targetKey };
1461
+ disentangle(source, target);
1462
+ logAudit({
1463
+ action: "entangle",
1464
+ key: sourceKey,
1465
+ source: sourceOpts.source ?? "cli",
1466
+ detail: `disentangled from ${targetKey}`
1467
+ });
1468
+ }
1469
+
1470
+ // src/core/tunnel.ts
1471
+ var tunnelStore = /* @__PURE__ */ new Map();
1472
+ var cleanupInterval = null;
1473
+ function ensureCleanup() {
1474
+ if (cleanupInterval) return;
1475
+ cleanupInterval = setInterval(() => {
1476
+ const now = Date.now();
1477
+ for (const [id, entry] of tunnelStore) {
1478
+ if (entry.expiresAt && now >= entry.expiresAt) {
1479
+ tunnelStore.delete(id);
1480
+ }
1481
+ }
1482
+ if (tunnelStore.size === 0 && cleanupInterval) {
1483
+ clearInterval(cleanupInterval);
1484
+ cleanupInterval = null;
1485
+ }
1486
+ }, 5e3);
1487
+ if (cleanupInterval && typeof cleanupInterval === "object" && "unref" in cleanupInterval) {
1488
+ cleanupInterval.unref();
1489
+ }
1490
+ }
1491
+ function tunnelCreate(value, opts = {}) {
1492
+ const id = `tun_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1493
+ const now = Date.now();
1494
+ tunnelStore.set(id, {
1495
+ value,
1496
+ createdAt: now,
1497
+ expiresAt: opts.ttlSeconds ? now + opts.ttlSeconds * 1e3 : void 0,
1498
+ accessCount: 0,
1499
+ maxReads: opts.maxReads
1500
+ });
1501
+ ensureCleanup();
1502
+ return id;
1503
+ }
1504
+ function tunnelRead(id) {
1505
+ const entry = tunnelStore.get(id);
1506
+ if (!entry) return null;
1507
+ if (entry.expiresAt && Date.now() >= entry.expiresAt) {
1508
+ tunnelStore.delete(id);
1509
+ return null;
1510
+ }
1511
+ entry.accessCount++;
1512
+ if (entry.maxReads && entry.accessCount >= entry.maxReads) {
1513
+ const value = entry.value;
1514
+ tunnelStore.delete(id);
1515
+ return value;
1516
+ }
1517
+ return entry.value;
1518
+ }
1519
+ function tunnelDestroy(id) {
1520
+ return tunnelStore.delete(id);
1521
+ }
1522
+ function tunnelList() {
1523
+ const now = Date.now();
1524
+ const result = [];
1525
+ for (const [id, entry] of tunnelStore) {
1526
+ if (entry.expiresAt && now >= entry.expiresAt) {
1527
+ tunnelStore.delete(id);
1528
+ continue;
1529
+ }
1530
+ result.push({
1531
+ id,
1532
+ createdAt: entry.createdAt,
1533
+ expiresAt: entry.expiresAt,
1534
+ accessCount: entry.accessCount,
1535
+ maxReads: entry.maxReads
1536
+ });
1537
+ }
1538
+ return result;
1539
+ }
1540
+
1541
+ export {
1542
+ checkDecay,
1543
+ readProjectConfig,
1544
+ collapseEnvironment,
1545
+ logAudit,
1546
+ queryAudit,
1547
+ verifyAuditChain,
1548
+ exportAudit,
1549
+ detectAnomalies,
1550
+ listEntanglements,
1551
+ httpRequest_,
1552
+ registerHook,
1553
+ removeHook,
1554
+ listHooks,
1555
+ fireHooks,
1556
+ registry,
1557
+ checkToolPolicy,
1558
+ checkKeyReadPolicy,
1559
+ checkExecPolicy,
1560
+ getExecMaxRuntime,
1561
+ getPolicySummary,
1562
+ getSecret,
1563
+ getEnvelope,
1564
+ setSecret,
1565
+ deleteSecret,
1566
+ hasSecret,
1567
+ listSecrets,
1568
+ exportSecrets,
1569
+ entangleSecrets,
1570
+ disentangleSecrets,
1571
+ tunnelCreate,
1572
+ tunnelRead,
1573
+ tunnelDestroy,
1574
+ tunnelList
1575
+ };
1576
+ //# sourceMappingURL=chunk-5JBU7TWN.js.map