@i4ctime/q-ring 0.4.0 → 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.
@@ -1,967 +0,0 @@
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
- }
28
- };
29
- }
30
- function parseEnvelope(raw) {
31
- try {
32
- const parsed = JSON.parse(raw);
33
- if (parsed && typeof parsed === "object" && parsed.v === 1) {
34
- return parsed;
35
- }
36
- } catch {
37
- }
38
- return null;
39
- }
40
- function wrapLegacy(rawValue) {
41
- const now = (/* @__PURE__ */ new Date()).toISOString();
42
- return {
43
- v: 1,
44
- value: rawValue,
45
- meta: {
46
- createdAt: now,
47
- updatedAt: now,
48
- accessCount: 0
49
- }
50
- };
51
- }
52
- function serializeEnvelope(envelope) {
53
- return JSON.stringify(envelope);
54
- }
55
- function collapseValue(envelope, env) {
56
- if (envelope.states) {
57
- const targetEnv = env ?? envelope.defaultEnv;
58
- if (targetEnv && envelope.states[targetEnv]) {
59
- return envelope.states[targetEnv];
60
- }
61
- if (envelope.defaultEnv && envelope.states[envelope.defaultEnv]) {
62
- return envelope.states[envelope.defaultEnv];
63
- }
64
- const keys = Object.keys(envelope.states);
65
- if (keys.length > 0) {
66
- return envelope.states[keys[0]];
67
- }
68
- return null;
69
- }
70
- return envelope.value ?? null;
71
- }
72
- function checkDecay(envelope) {
73
- if (!envelope.meta.expiresAt) {
74
- return {
75
- isExpired: false,
76
- isStale: false,
77
- lifetimePercent: 0,
78
- secondsRemaining: null,
79
- timeRemaining: null
80
- };
81
- }
82
- const now = Date.now();
83
- const expires = new Date(envelope.meta.expiresAt).getTime();
84
- const created = new Date(envelope.meta.createdAt).getTime();
85
- const totalLifetime = expires - created;
86
- const elapsed = now - created;
87
- const remaining = expires - now;
88
- const lifetimePercent = totalLifetime > 0 ? Math.round(elapsed / totalLifetime * 100) : 100;
89
- const secondsRemaining = Math.floor(remaining / 1e3);
90
- let timeRemaining = null;
91
- if (remaining > 0) {
92
- const days = Math.floor(remaining / 864e5);
93
- const hours = Math.floor(remaining % 864e5 / 36e5);
94
- const minutes = Math.floor(remaining % 36e5 / 6e4);
95
- if (days > 0) timeRemaining = `${days}d ${hours}h`;
96
- else if (hours > 0) timeRemaining = `${hours}h ${minutes}m`;
97
- else timeRemaining = `${minutes}m`;
98
- } else {
99
- timeRemaining = "expired";
100
- }
101
- return {
102
- isExpired: remaining <= 0,
103
- isStale: lifetimePercent >= 75,
104
- lifetimePercent,
105
- secondsRemaining,
106
- timeRemaining
107
- };
108
- }
109
- function recordAccess(envelope) {
110
- return {
111
- ...envelope,
112
- meta: {
113
- ...envelope.meta,
114
- accessCount: envelope.meta.accessCount + 1,
115
- lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
116
- }
117
- };
118
- }
119
-
120
- // src/core/collapse.ts
121
- import { execSync } from "child_process";
122
- import { existsSync, readFileSync } from "fs";
123
- import { join } from "path";
124
- var BRANCH_ENV_MAP = {
125
- main: "prod",
126
- master: "prod",
127
- production: "prod",
128
- develop: "dev",
129
- development: "dev",
130
- dev: "dev",
131
- staging: "staging",
132
- stage: "staging",
133
- test: "test",
134
- testing: "test"
135
- };
136
- function detectGitBranch(cwd) {
137
- try {
138
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
139
- cwd: cwd ?? process.cwd(),
140
- stdio: ["pipe", "pipe", "pipe"],
141
- encoding: "utf8",
142
- timeout: 3e3
143
- }).trim();
144
- return branch || null;
145
- } catch {
146
- return null;
147
- }
148
- }
149
- function readProjectConfig(projectPath) {
150
- const configPath = join(projectPath ?? process.cwd(), ".q-ring.json");
151
- try {
152
- if (existsSync(configPath)) {
153
- return JSON.parse(readFileSync(configPath, "utf8"));
154
- }
155
- } catch {
156
- }
157
- return null;
158
- }
159
- function collapseEnvironment(ctx = {}) {
160
- if (ctx.explicit) {
161
- return { env: ctx.explicit, source: "explicit" };
162
- }
163
- const qringEnv = process.env.QRING_ENV;
164
- if (qringEnv) {
165
- return { env: qringEnv, source: "QRING_ENV" };
166
- }
167
- const nodeEnv = process.env.NODE_ENV;
168
- if (nodeEnv) {
169
- const mapped = mapEnvName(nodeEnv);
170
- return { env: mapped, source: "NODE_ENV" };
171
- }
172
- const config = readProjectConfig(ctx.projectPath);
173
- if (config?.env) {
174
- return { env: config.env, source: "project-config" };
175
- }
176
- const branch = detectGitBranch(ctx.projectPath);
177
- if (branch) {
178
- const branchMap = { ...BRANCH_ENV_MAP, ...config?.branchMap };
179
- const mapped = branchMap[branch] ?? matchGlob(branchMap, branch);
180
- if (mapped) {
181
- return { env: mapped, source: "git-branch" };
182
- }
183
- }
184
- if (config?.defaultEnv) {
185
- return { env: config.defaultEnv, source: "project-config" };
186
- }
187
- return null;
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
- }
199
- function mapEnvName(raw) {
200
- const lower = raw.toLowerCase();
201
- if (lower === "production") return "prod";
202
- if (lower === "development") return "dev";
203
- return lower;
204
- }
205
-
206
- // src/core/observer.ts
207
- import { existsSync as existsSync2, mkdirSync, appendFileSync, readFileSync as readFileSync2 } from "fs";
208
- import { join as join2 } from "path";
209
- import { homedir } from "os";
210
- function getAuditDir() {
211
- const dir = join2(homedir(), ".config", "q-ring");
212
- if (!existsSync2(dir)) {
213
- mkdirSync(dir, { recursive: true });
214
- }
215
- return dir;
216
- }
217
- function getAuditPath() {
218
- return join2(getAuditDir(), "audit.jsonl");
219
- }
220
- function logAudit(event) {
221
- const full = {
222
- ...event,
223
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
224
- pid: process.pid
225
- };
226
- try {
227
- appendFileSync(getAuditPath(), JSON.stringify(full) + "\n");
228
- } catch {
229
- }
230
- }
231
- function queryAudit(query = {}) {
232
- const path = getAuditPath();
233
- if (!existsSync2(path)) return [];
234
- try {
235
- const lines = readFileSync2(path, "utf8").split("\n").filter((l) => l.trim());
236
- let events = lines.map((line) => {
237
- try {
238
- return JSON.parse(line);
239
- } catch {
240
- return null;
241
- }
242
- }).filter((e) => e !== null);
243
- if (query.key) {
244
- events = events.filter((e) => e.key === query.key);
245
- }
246
- if (query.action) {
247
- events = events.filter((e) => e.action === query.action);
248
- }
249
- if (query.since) {
250
- const since = new Date(query.since).getTime();
251
- events = events.filter(
252
- (e) => new Date(e.timestamp).getTime() >= since
253
- );
254
- }
255
- events.sort(
256
- (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
257
- );
258
- if (query.limit) {
259
- events = events.slice(0, query.limit);
260
- }
261
- return events;
262
- } catch {
263
- return [];
264
- }
265
- }
266
- function detectAnomalies(key) {
267
- const recent = queryAudit({
268
- key,
269
- action: "read",
270
- since: new Date(Date.now() - 36e5).toISOString()
271
- // last hour
272
- });
273
- const anomalies = [];
274
- if (key && recent.length > 50) {
275
- anomalies.push({
276
- type: "burst",
277
- description: `${recent.length} reads of "${key}" in the last hour`,
278
- events: recent.slice(0, 10)
279
- });
280
- }
281
- const nightAccess = recent.filter((e) => {
282
- const hour = new Date(e.timestamp).getHours();
283
- return hour >= 1 && hour < 5;
284
- });
285
- if (nightAccess.length > 0) {
286
- anomalies.push({
287
- type: "unusual-hour",
288
- description: `${nightAccess.length} access(es) during unusual hours (1am-5am)`,
289
- events: nightAccess
290
- });
291
- }
292
- return anomalies;
293
- }
294
-
295
- // src/core/entanglement.ts
296
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
297
- import { join as join3 } from "path";
298
- import { homedir as homedir2 } from "os";
299
- function getRegistryPath() {
300
- const dir = join3(homedir2(), ".config", "q-ring");
301
- if (!existsSync3(dir)) {
302
- mkdirSync2(dir, { recursive: true });
303
- }
304
- return join3(dir, "entanglement.json");
305
- }
306
- function loadRegistry() {
307
- const path = getRegistryPath();
308
- if (!existsSync3(path)) {
309
- return { pairs: [] };
310
- }
311
- try {
312
- return JSON.parse(readFileSync3(path, "utf8"));
313
- } catch {
314
- return { pairs: [] };
315
- }
316
- }
317
- function saveRegistry(registry) {
318
- writeFileSync(getRegistryPath(), JSON.stringify(registry, null, 2));
319
- }
320
- function entangle(source, target) {
321
- const registry = loadRegistry();
322
- const exists = registry.pairs.some(
323
- (p) => p.source.service === source.service && p.source.key === source.key && p.target.service === target.service && p.target.key === target.key
324
- );
325
- if (!exists) {
326
- registry.pairs.push({
327
- source,
328
- target,
329
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
330
- });
331
- registry.pairs.push({
332
- source: target,
333
- target: source,
334
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
335
- });
336
- saveRegistry(registry);
337
- }
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
- }
346
- function findEntangled(source) {
347
- const registry = loadRegistry();
348
- return registry.pairs.filter(
349
- (p) => p.source.service === source.service && p.source.key === source.key
350
- ).map((p) => p.target);
351
- }
352
- function listEntanglements() {
353
- return loadRegistry().pairs;
354
- }
355
-
356
- // src/core/keyring.ts
357
- import { Entry, findCredentials } from "@napi-rs/keyring";
358
-
359
- // src/utils/hash.ts
360
- import { createHash } from "crypto";
361
- function hashProjectPath(projectPath) {
362
- return createHash("sha256").update(projectPath).digest("hex").slice(0, 12);
363
- }
364
-
365
- // src/core/scope.ts
366
- var SERVICE_PREFIX = "q-ring";
367
- function globalService() {
368
- return `${SERVICE_PREFIX}:global`;
369
- }
370
- function projectService(projectPath) {
371
- const hash = hashProjectPath(projectPath);
372
- return `${SERVICE_PREFIX}:project:${hash}`;
373
- }
374
- function resolveScope(opts) {
375
- const { scope, projectPath } = opts;
376
- if (scope === "global") {
377
- return [{ scope: "global", service: globalService() }];
378
- }
379
- if (scope === "project") {
380
- if (!projectPath) {
381
- throw new Error("Project path is required for project scope");
382
- }
383
- return [
384
- { scope: "project", service: projectService(projectPath), projectPath }
385
- ];
386
- }
387
- if (projectPath) {
388
- return [
389
- { scope: "project", service: projectService(projectPath), projectPath },
390
- { scope: "global", service: globalService() }
391
- ];
392
- }
393
- return [{ scope: "global", service: globalService() }];
394
- }
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
-
603
- // src/core/keyring.ts
604
- function readEnvelope(service, key) {
605
- const entry = new Entry(service, key);
606
- const raw = entry.getPassword();
607
- if (raw === null) return null;
608
- const envelope = parseEnvelope(raw);
609
- return envelope ?? wrapLegacy(raw);
610
- }
611
- function writeEnvelope(service, key, envelope) {
612
- const entry = new Entry(service, key);
613
- entry.setPassword(serializeEnvelope(envelope));
614
- }
615
- function resolveEnv(opts) {
616
- if (opts.env) return opts.env;
617
- const result = collapseEnvironment({ projectPath: opts.projectPath });
618
- return result?.env;
619
- }
620
- function getSecret(key, opts = {}) {
621
- const scopes = resolveScope(opts);
622
- const env = resolveEnv(opts);
623
- const source = opts.source ?? "cli";
624
- for (const { service, scope } of scopes) {
625
- const envelope = readEnvelope(service, key);
626
- if (!envelope) continue;
627
- const decay = checkDecay(envelope);
628
- if (decay.isExpired) {
629
- logAudit({
630
- action: "read",
631
- key,
632
- scope,
633
- source,
634
- detail: "blocked: secret expired (quantum decay)"
635
- });
636
- continue;
637
- }
638
- const value = collapseValue(envelope, env);
639
- if (value === null) continue;
640
- const updated = recordAccess(envelope);
641
- writeEnvelope(service, key, updated);
642
- logAudit({ action: "read", key, scope, env, source });
643
- return value;
644
- }
645
- return null;
646
- }
647
- function getEnvelope(key, opts = {}) {
648
- const scopes = resolveScope(opts);
649
- for (const { service, scope } of scopes) {
650
- const envelope = readEnvelope(service, key);
651
- if (envelope) return { envelope, scope };
652
- }
653
- return null;
654
- }
655
- function setSecret(key, value, opts = {}) {
656
- const scope = opts.scope ?? "global";
657
- const scopes = resolveScope({ ...opts, scope });
658
- const { service } = scopes[0];
659
- const source = opts.source ?? "cli";
660
- const existing = readEnvelope(service, key);
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;
665
- if (opts.states) {
666
- envelope = createEnvelope("", {
667
- states: opts.states,
668
- defaultEnv: opts.defaultEnv,
669
- description: opts.description,
670
- tags: opts.tags,
671
- ttlSeconds: opts.ttlSeconds,
672
- expiresAt: opts.expiresAt,
673
- entangled: existing?.meta.entangled,
674
- rotationFormat: rotFmt,
675
- rotationPrefix: rotPfx,
676
- provider: prov
677
- });
678
- } else {
679
- envelope = createEnvelope(value, {
680
- description: opts.description,
681
- tags: opts.tags,
682
- ttlSeconds: opts.ttlSeconds,
683
- expiresAt: opts.expiresAt,
684
- entangled: existing?.meta.entangled,
685
- rotationFormat: rotFmt,
686
- rotationPrefix: rotPfx,
687
- provider: prov
688
- });
689
- }
690
- if (existing) {
691
- envelope.meta.createdAt = existing.meta.createdAt;
692
- envelope.meta.accessCount = existing.meta.accessCount;
693
- }
694
- writeEnvelope(service, key, envelope);
695
- logAudit({ action: "write", key, scope, source });
696
- const entangled = findEntangled({ service, key });
697
- for (const target of entangled) {
698
- try {
699
- const targetEnvelope = readEnvelope(target.service, target.key);
700
- if (targetEnvelope) {
701
- if (opts.states) {
702
- targetEnvelope.states = opts.states;
703
- } else {
704
- targetEnvelope.value = value;
705
- }
706
- targetEnvelope.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
707
- writeEnvelope(target.service, target.key, targetEnvelope);
708
- logAudit({
709
- action: "entangle",
710
- key: target.key,
711
- scope: "global",
712
- source,
713
- detail: `propagated from ${key}`
714
- });
715
- }
716
- } catch {
717
- }
718
- }
719
- fireHooks({
720
- action: "write",
721
- key,
722
- scope,
723
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
724
- source
725
- }, envelope.meta.tags).catch(() => {
726
- });
727
- }
728
- function deleteSecret(key, opts = {}) {
729
- const scopes = resolveScope(opts);
730
- const source = opts.source ?? "cli";
731
- let deleted = false;
732
- for (const { service, scope } of scopes) {
733
- const entry = new Entry(service, key);
734
- try {
735
- if (entry.deleteCredential()) {
736
- deleted = true;
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
- });
746
- }
747
- } catch {
748
- }
749
- }
750
- return deleted;
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
- }
763
- function listSecrets(opts = {}) {
764
- const source = opts.source ?? "cli";
765
- const services = [];
766
- if (!opts.scope || opts.scope === "global") {
767
- services.push({ service: globalService(), scope: "global" });
768
- }
769
- if ((!opts.scope || opts.scope === "project") && opts.projectPath) {
770
- services.push({
771
- service: projectService(opts.projectPath),
772
- scope: "project"
773
- });
774
- }
775
- const results = [];
776
- const seen = /* @__PURE__ */ new Set();
777
- for (const { service, scope } of services) {
778
- try {
779
- const credentials = findCredentials(service);
780
- for (const cred of credentials) {
781
- const id = `${scope}:${cred.account}`;
782
- if (seen.has(id)) continue;
783
- seen.add(id);
784
- const envelope = parseEnvelope(cred.password) ?? wrapLegacy(cred.password);
785
- const decay = checkDecay(envelope);
786
- results.push({
787
- key: cred.account,
788
- scope,
789
- envelope,
790
- decay
791
- });
792
- }
793
- } catch {
794
- }
795
- }
796
- if (!opts.silent) {
797
- logAudit({ action: "list", source });
798
- }
799
- return results.sort((a, b) => a.key.localeCompare(b.key));
800
- }
801
- function exportSecrets(opts = {}) {
802
- const format = opts.format ?? "env";
803
- const env = resolveEnv(opts);
804
- let entries = listSecrets(opts);
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
- }
815
- const merged = /* @__PURE__ */ new Map();
816
- const globalEntries = entries.filter((e) => e.scope === "global");
817
- const projectEntries = entries.filter((e) => e.scope === "project");
818
- for (const entry of [...globalEntries, ...projectEntries]) {
819
- if (entry.envelope) {
820
- const decay = checkDecay(entry.envelope);
821
- if (decay.isExpired) continue;
822
- const value = collapseValue(entry.envelope, env);
823
- if (value !== null) {
824
- merged.set(entry.key, value);
825
- }
826
- }
827
- }
828
- logAudit({ action: "export", source, detail: `format=${format}` });
829
- if (format === "json") {
830
- const obj = {};
831
- for (const [key, value] of merged) {
832
- obj[key] = value;
833
- }
834
- return JSON.stringify(obj, null, 2);
835
- }
836
- const lines = [];
837
- for (const [key, value] of merged) {
838
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
839
- lines.push(`${key}="${escaped}"`);
840
- }
841
- return lines.join("\n");
842
- }
843
- function entangleSecrets(sourceKey, sourceOpts, targetKey, targetOpts) {
844
- const sourceScopes = resolveScope({ ...sourceOpts, scope: sourceOpts.scope ?? "global" });
845
- const targetScopes = resolveScope({ ...targetOpts, scope: targetOpts.scope ?? "global" });
846
- const source = { service: sourceScopes[0].service, key: sourceKey };
847
- const target = { service: targetScopes[0].service, key: targetKey };
848
- entangle(source, target);
849
- logAudit({
850
- action: "entangle",
851
- key: sourceKey,
852
- source: sourceOpts.source ?? "cli",
853
- detail: `entangled with ${targetKey}`
854
- });
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
- }
869
-
870
- // src/core/tunnel.ts
871
- var tunnelStore = /* @__PURE__ */ new Map();
872
- var cleanupInterval = null;
873
- function ensureCleanup() {
874
- if (cleanupInterval) return;
875
- cleanupInterval = setInterval(() => {
876
- const now = Date.now();
877
- for (const [id, entry] of tunnelStore) {
878
- if (entry.expiresAt && now >= entry.expiresAt) {
879
- tunnelStore.delete(id);
880
- }
881
- }
882
- if (tunnelStore.size === 0 && cleanupInterval) {
883
- clearInterval(cleanupInterval);
884
- cleanupInterval = null;
885
- }
886
- }, 5e3);
887
- if (cleanupInterval && typeof cleanupInterval === "object" && "unref" in cleanupInterval) {
888
- cleanupInterval.unref();
889
- }
890
- }
891
- function tunnelCreate(value, opts = {}) {
892
- const id = `tun_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
893
- const now = Date.now();
894
- tunnelStore.set(id, {
895
- value,
896
- createdAt: now,
897
- expiresAt: opts.ttlSeconds ? now + opts.ttlSeconds * 1e3 : void 0,
898
- accessCount: 0,
899
- maxReads: opts.maxReads
900
- });
901
- ensureCleanup();
902
- return id;
903
- }
904
- function tunnelRead(id) {
905
- const entry = tunnelStore.get(id);
906
- if (!entry) return null;
907
- if (entry.expiresAt && Date.now() >= entry.expiresAt) {
908
- tunnelStore.delete(id);
909
- return null;
910
- }
911
- entry.accessCount++;
912
- if (entry.maxReads && entry.accessCount >= entry.maxReads) {
913
- const value = entry.value;
914
- tunnelStore.delete(id);
915
- return value;
916
- }
917
- return entry.value;
918
- }
919
- function tunnelDestroy(id) {
920
- return tunnelStore.delete(id);
921
- }
922
- function tunnelList() {
923
- const now = Date.now();
924
- const result = [];
925
- for (const [id, entry] of tunnelStore) {
926
- if (entry.expiresAt && now >= entry.expiresAt) {
927
- tunnelStore.delete(id);
928
- continue;
929
- }
930
- result.push({
931
- id,
932
- createdAt: entry.createdAt,
933
- expiresAt: entry.expiresAt,
934
- accessCount: entry.accessCount,
935
- maxReads: entry.maxReads
936
- });
937
- }
938
- return result;
939
- }
940
-
941
- export {
942
- checkDecay,
943
- readProjectConfig,
944
- collapseEnvironment,
945
- logAudit,
946
- queryAudit,
947
- detectAnomalies,
948
- listEntanglements,
949
- registerHook,
950
- removeHook,
951
- listHooks,
952
- fireHooks,
953
- getSecret,
954
- getEnvelope,
955
- setSecret,
956
- deleteSecret,
957
- hasSecret,
958
- listSecrets,
959
- exportSecrets,
960
- entangleSecrets,
961
- disentangleSecrets,
962
- tunnelCreate,
963
- tunnelRead,
964
- tunnelDestroy,
965
- tunnelList
966
- };
967
- //# sourceMappingURL=chunk-6IQ5SFLI.js.map