@phnx-labs/agents-cli 1.20.18 → 1.20.20
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/CHANGELOG.md +10 -0
- package/dist/commands/secrets.js +137 -25
- package/dist/commands/versions.js +7 -1
- package/dist/lib/secrets/agent.d.ts +37 -6
- package/dist/lib/secrets/agent.js +197 -63
- package/dist/lib/secrets/bundles.d.ts +37 -7
- package/dist/lib/secrets/bundles.js +226 -80
- package/dist/lib/secrets/filestore.d.ts +82 -0
- package/dist/lib/secrets/filestore.js +295 -0
- package/dist/lib/secrets/linux.d.ts +6 -24
- package/dist/lib/secrets/linux.js +22 -265
- package/dist/lib/versions.d.ts +20 -0
- package/dist/lib/versions.js +48 -0
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import * as net from 'net';
|
|
26
26
|
import * as fs from 'fs';
|
|
27
|
+
import * as os from 'os';
|
|
27
28
|
import * as path from 'path';
|
|
28
29
|
import { spawn, spawnSync, execFileSync } from 'child_process';
|
|
29
30
|
import { getHelpersDir, readMeta } from '../state.js';
|
|
@@ -61,24 +62,134 @@ function pidPath() {
|
|
|
61
62
|
return path.join(agentDir(), 'agent.pid');
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
|
-
* Argv for re-invoking THIS cli
|
|
65
|
-
* spawns its own
|
|
66
|
-
* through `process.execPath` (the node binary) with the JS entrypoint as the
|
|
67
|
-
* first arg — the entrypoint isn't reliably executable in dev builds (invoked
|
|
68
|
-
*
|
|
65
|
+
* Argv for re-invoking THIS cli with a hidden subcommand, so a side-by-side dev
|
|
66
|
+
* build spawns its own helpers rather than the registry-installed one. We always
|
|
67
|
+
* go through `process.execPath` (the node binary) with the JS entrypoint as the
|
|
68
|
+
* first arg — the entrypoint isn't reliably executable in dev builds (invoked as
|
|
69
|
+
* `node dist/index.js`, no +x), so spawning it directly EACCES'd.
|
|
69
70
|
*/
|
|
70
|
-
function
|
|
71
|
+
function cliSpawn(sub) {
|
|
71
72
|
const argv1 = process.argv[1];
|
|
72
73
|
const entry = argv1 && fs.existsSync(argv1) ? argv1 : null;
|
|
73
74
|
if (entry)
|
|
74
|
-
return { cmd: process.execPath, args: [entry,
|
|
75
|
+
return { cmd: process.execPath, args: [entry, ...sub] };
|
|
75
76
|
// No resolvable entrypoint (unusual) — fall back to the PATH shim.
|
|
76
77
|
let bin = 'agents';
|
|
77
78
|
try {
|
|
78
79
|
bin = execFileSync('which', ['agents'], { encoding: 'utf-8' }).trim();
|
|
79
80
|
}
|
|
80
81
|
catch { /* default */ }
|
|
81
|
-
return { cmd: bin, args:
|
|
82
|
+
return { cmd: bin, args: sub };
|
|
83
|
+
}
|
|
84
|
+
function brokerSpawn() {
|
|
85
|
+
return cliSpawn(['secrets', '_agent-run']);
|
|
86
|
+
}
|
|
87
|
+
// ─── Persistent launchd service ──────────────────────────────────────────────
|
|
88
|
+
// On a heavily-loaded machine a freshly-spawned broker (a full CLI cold start)
|
|
89
|
+
// can't get scheduled enough CPU to finish booting and bind its socket — so the
|
|
90
|
+
// on-demand model fails exactly when there are many agents (the case we care
|
|
91
|
+
// about). The fix is to run the broker as a launchd user service: started once
|
|
92
|
+
// with RunAtLoad + KeepAlive, it stays up, and every read just connects. The
|
|
93
|
+
// cold start happens once (and launchd retries until it wins), never per-read.
|
|
94
|
+
const SERVICE_LABEL = 'com.phnx-labs.agents-secrets-agent';
|
|
95
|
+
function servicePlistPath() {
|
|
96
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
|
|
97
|
+
}
|
|
98
|
+
/** True if the launchd plist for the persistent broker is installed. */
|
|
99
|
+
export function secretsAgentServiceInstalled() {
|
|
100
|
+
return onDarwin() && fs.existsSync(servicePlistPath());
|
|
101
|
+
}
|
|
102
|
+
function generateServicePlist() {
|
|
103
|
+
const { cmd, args } = cliSpawn(['secrets', '_agent-run', '--service']);
|
|
104
|
+
const progArgs = [cmd, ...args]
|
|
105
|
+
.map((a) => ` <string>${a.replace(/&/g, '&').replace(/</g, '<')}</string>`)
|
|
106
|
+
.join('\n');
|
|
107
|
+
const logPath = path.join(agentDir(), 'service.log');
|
|
108
|
+
const home = os.homedir();
|
|
109
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
110
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
111
|
+
<plist version="1.0">
|
|
112
|
+
<dict>
|
|
113
|
+
<key>Label</key>
|
|
114
|
+
<string>${SERVICE_LABEL}</string>
|
|
115
|
+
<key>ProgramArguments</key>
|
|
116
|
+
<array>
|
|
117
|
+
${progArgs}
|
|
118
|
+
</array>
|
|
119
|
+
<key>RunAtLoad</key>
|
|
120
|
+
<true/>
|
|
121
|
+
<key>KeepAlive</key>
|
|
122
|
+
<true/>
|
|
123
|
+
<key>ProcessType</key>
|
|
124
|
+
<string>Interactive</string>
|
|
125
|
+
<key>StandardOutPath</key>
|
|
126
|
+
<string>${logPath}</string>
|
|
127
|
+
<key>StandardErrorPath</key>
|
|
128
|
+
<string>${logPath}</string>
|
|
129
|
+
<key>EnvironmentVariables</key>
|
|
130
|
+
<dict>
|
|
131
|
+
<key>PATH</key>
|
|
132
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${home}/.bun/bin</string>
|
|
133
|
+
</dict>
|
|
134
|
+
</dict>
|
|
135
|
+
</plist>`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Install + start the persistent broker as a launchd user service (idempotent).
|
|
139
|
+
* Writes the plist, bootstraps it into the GUI domain, and waits for the socket.
|
|
140
|
+
* `ProcessType: Interactive` asks launchd to schedule it at foreground priority
|
|
141
|
+
* so it can boot even when the machine is loaded. Returns true once reachable.
|
|
142
|
+
*/
|
|
143
|
+
export async function installSecretsAgentService(timeoutMs = 30000) {
|
|
144
|
+
if (!onDarwin())
|
|
145
|
+
return false;
|
|
146
|
+
const plist = servicePlistPath();
|
|
147
|
+
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
148
|
+
fs.writeFileSync(plist, generateServicePlist());
|
|
149
|
+
const uid = process.getuid?.() ?? 0;
|
|
150
|
+
// bootstrap is the modern API; fall back to legacy load. Both idempotent-ish.
|
|
151
|
+
try {
|
|
152
|
+
execFileSync('launchctl', ['bootstrap', `gui/${uid}`, plist], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
try {
|
|
156
|
+
execFileSync('launchctl', ['load', '-w', plist], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
157
|
+
}
|
|
158
|
+
catch { /* may already be loaded */ }
|
|
159
|
+
}
|
|
160
|
+
// kickstart to force an immediate start even if already bootstrapped.
|
|
161
|
+
try {
|
|
162
|
+
execFileSync('launchctl', ['kickstart', '-k', `gui/${uid}/${SERVICE_LABEL}`], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
163
|
+
}
|
|
164
|
+
catch { /* best effort */ }
|
|
165
|
+
const deadline = Date.now() + timeoutMs;
|
|
166
|
+
while (Date.now() < deadline) {
|
|
167
|
+
if (await agentPing())
|
|
168
|
+
return true;
|
|
169
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
/** Stop + remove the persistent broker service, and wipe whatever it held. */
|
|
174
|
+
export async function uninstallSecretsAgentService() {
|
|
175
|
+
if (!onDarwin())
|
|
176
|
+
return;
|
|
177
|
+
await agentLock(); // wipe the in-memory store before tearing down
|
|
178
|
+
const plist = servicePlistPath();
|
|
179
|
+
const uid = process.getuid?.() ?? 0;
|
|
180
|
+
try {
|
|
181
|
+
execFileSync('launchctl', ['bootout', `gui/${uid}/${SERVICE_LABEL}`], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
try {
|
|
185
|
+
execFileSync('launchctl', ['unload', '-w', plist], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
186
|
+
}
|
|
187
|
+
catch { /* not loaded */ }
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
fs.unlinkSync(plist);
|
|
191
|
+
}
|
|
192
|
+
catch { /* already gone */ }
|
|
82
193
|
}
|
|
83
194
|
// ─── Broker server (runs in the detached `secrets _agent-run` process) ───────
|
|
84
195
|
/**
|
|
@@ -127,9 +238,13 @@ export function handleAgentRequest(store, req, now = Date.now()) {
|
|
|
127
238
|
* `agents secrets _agent-run`. Holds the store in memory, serves the socket,
|
|
128
239
|
* sweeps expired entries, wipes on screen-lock/sleep, and self-exits when idle.
|
|
129
240
|
*/
|
|
130
|
-
export async function runSecretsAgent() {
|
|
241
|
+
export async function runSecretsAgent(opts = {}) {
|
|
131
242
|
if (!onDarwin())
|
|
132
243
|
return; // nothing to broker without biometry prompts
|
|
244
|
+
// When launchd keeps us alive as a persistent service, never idle-exit:
|
|
245
|
+
// exiting would just make launchd cold-start us again, reintroducing the
|
|
246
|
+
// startup-under-load fragility the service exists to avoid.
|
|
247
|
+
const persistent = opts.service === true;
|
|
133
248
|
// Single-instance guard: O_EXCL pid file. If a live broker already holds it,
|
|
134
249
|
// exit quietly — the existing one keeps serving.
|
|
135
250
|
const pidFile = pidPath();
|
|
@@ -169,7 +284,7 @@ export async function runSecretsAgent() {
|
|
|
169
284
|
if (now >= e.expiresAt)
|
|
170
285
|
store.delete(name);
|
|
171
286
|
if (store.size === 0) {
|
|
172
|
-
if (now - emptySince >= IDLE_EXIT_MS)
|
|
287
|
+
if (!persistent && now - emptySince >= IDLE_EXIT_MS)
|
|
173
288
|
shutdown(0);
|
|
174
289
|
}
|
|
175
290
|
else {
|
|
@@ -371,64 +486,60 @@ export function secretsAgentAutoEnabled() {
|
|
|
371
486
|
return false;
|
|
372
487
|
}
|
|
373
488
|
}
|
|
374
|
-
/**
|
|
375
|
-
* Inline node program that loads one bundle into the broker, started detached
|
|
376
|
-
* from the hot path. Reads the JSON payload from stdin (so secret values never
|
|
377
|
-
* appear in argv / `ps`), retries the socket for a few seconds to absorb a
|
|
378
|
-
* cold-started agent, sends the load, and exits. argv after -e: [execPath, <socket>].
|
|
379
|
-
*/
|
|
380
|
-
const DETACHED_LOAD_PROGRAM = `
|
|
381
|
-
const net = require('net');
|
|
382
|
-
const sock = process.argv[1];
|
|
383
|
-
let input = '';
|
|
384
|
-
process.stdin.setEncoding('utf-8');
|
|
385
|
-
process.stdin.on('data', (d) => { input += d; });
|
|
386
|
-
process.stdin.on('end', () => {
|
|
387
|
-
let payload; try { payload = JSON.parse(input); } catch (e) { process.exit(1); }
|
|
388
|
-
let attempts = 0;
|
|
389
|
-
const tryConnect = () => {
|
|
390
|
-
const c = net.createConnection(sock);
|
|
391
|
-
c.on('connect', () => {
|
|
392
|
-
c.write(JSON.stringify({ cmd: 'load', name: payload.name, bundle: payload.bundle, env: payload.env, ttlMs: payload.ttlMs }) + '\\n');
|
|
393
|
-
});
|
|
394
|
-
c.setEncoding('utf-8');
|
|
395
|
-
c.on('data', () => { try { c.destroy(); } catch (e) {} process.exit(0); });
|
|
396
|
-
c.on('error', () => {
|
|
397
|
-
try { c.destroy(); } catch (e) {}
|
|
398
|
-
if (++attempts >= 30) process.exit(1);
|
|
399
|
-
setTimeout(tryConnect, 100);
|
|
400
|
-
});
|
|
401
|
-
};
|
|
402
|
-
tryConnect();
|
|
403
|
-
});
|
|
404
|
-
`;
|
|
405
489
|
/**
|
|
406
490
|
* Fire-and-forget: populate the broker with a freshly-resolved bundle so the
|
|
407
491
|
* NEXT process reads it without a prompt. Used by the auto-cache path after a
|
|
408
492
|
* real keychain read of a `session`-tier bundle. Adds no latency to the caller
|
|
409
|
-
* — it spawns
|
|
410
|
-
*
|
|
493
|
+
* — it spawns a detached `secrets _agent-load` worker (passing the resolved env
|
|
494
|
+
* over stdin, never argv) and returns immediately.
|
|
495
|
+
*
|
|
496
|
+
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping with a
|
|
497
|
+
* generous budget) rather than a tight inline retry loop: under heavy load the
|
|
498
|
+
* broker is itself a cold-starting full CLI and can take several seconds to bind
|
|
499
|
+
* the socket, so a short fixed budget would give up before it's ready and the
|
|
500
|
+
* cache would silently never populate. Best-effort; never throws. macOS only.
|
|
411
501
|
*/
|
|
412
502
|
export function agentAutoLoadSync(name, bundle, env, ttlMs) {
|
|
413
503
|
if (!onDarwin())
|
|
414
504
|
return;
|
|
415
505
|
try {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
stdio: ['pipe', 'ignore', 'ignore'],
|
|
422
|
-
detached: true,
|
|
423
|
-
});
|
|
424
|
-
loader.stdin?.write(JSON.stringify({ name, bundle, env, ttlMs }));
|
|
425
|
-
loader.stdin?.end();
|
|
426
|
-
loader.unref();
|
|
506
|
+
const { cmd, args } = cliSpawn(['secrets', '_agent-load']);
|
|
507
|
+
const worker = spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'], detached: true });
|
|
508
|
+
worker.stdin?.write(JSON.stringify({ name, bundle, env, ttlMs }));
|
|
509
|
+
worker.stdin?.end();
|
|
510
|
+
worker.unref();
|
|
427
511
|
}
|
|
428
512
|
catch {
|
|
429
513
|
// best-effort: the next read just pops Touch ID as it would today
|
|
430
514
|
}
|
|
431
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Body of the hidden `secrets _agent-load` worker. Reads one `{name, bundle,
|
|
518
|
+
* env, ttlMs}` payload from stdin, ensures the broker is up (robust, generous
|
|
519
|
+
* budget), and loads the bundle into it. Detached from the originating read, so
|
|
520
|
+
* its latency is invisible — which is why it can afford a long ensure budget.
|
|
521
|
+
*/
|
|
522
|
+
export async function runAgentLoadFromStdin() {
|
|
523
|
+
if (!onDarwin())
|
|
524
|
+
return;
|
|
525
|
+
const chunks = [];
|
|
526
|
+
for await (const chunk of process.stdin)
|
|
527
|
+
chunks.push(chunk);
|
|
528
|
+
let payload;
|
|
529
|
+
try {
|
|
530
|
+
payload = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
return; // malformed payload — nothing to load
|
|
534
|
+
}
|
|
535
|
+
if (!payload || !payload.name || !payload.bundle || !payload.env)
|
|
536
|
+
return;
|
|
537
|
+
// Generous budget: the broker is a cold-starting full CLI; under load it can
|
|
538
|
+
// take several seconds to bind. We're detached, so waiting costs nothing.
|
|
539
|
+
if (!(await ensureAgentRunning(20000)))
|
|
540
|
+
return;
|
|
541
|
+
await agentLoad(payload.name, payload.bundle, payload.env, payload.ttlMs ?? DEFAULT_TTL_MS);
|
|
542
|
+
}
|
|
432
543
|
/** Store a resolved bundle in the broker. Returns false on transport failure. */
|
|
433
544
|
export async function agentLoad(name, bundle, env, ttlMs) {
|
|
434
545
|
const r = await request({ cmd: 'load', name, bundle, env, ttlMs });
|
|
@@ -453,16 +564,43 @@ async function agentPing() {
|
|
|
453
564
|
return r?.ok === true && r.cmd === 'ping' && r.version === PROTOCOL_VERSION;
|
|
454
565
|
}
|
|
455
566
|
/**
|
|
456
|
-
* Ensure a broker is running and reachable
|
|
457
|
-
*
|
|
458
|
-
*
|
|
567
|
+
* Ensure a broker is running and reachable. Returns true once the socket answers
|
|
568
|
+
* a ping. macOS only.
|
|
569
|
+
*
|
|
570
|
+
* Prefers the persistent launchd service: if it isn't installed we install it
|
|
571
|
+
* (which makes the broker survive for the whole login session, so subsequent
|
|
572
|
+
* reads never cold-start); if it's installed but unreachable we kickstart it.
|
|
573
|
+
* Only when the service path can't be used do we fall back to a one-off detached
|
|
574
|
+
* broker — that's the model that gets starved under heavy load, so it's last.
|
|
459
575
|
*/
|
|
460
576
|
export async function ensureAgentRunning(timeoutMs = 5000) {
|
|
461
577
|
if (!onDarwin())
|
|
462
578
|
return false;
|
|
463
579
|
if (await agentPing())
|
|
464
580
|
return true;
|
|
465
|
-
//
|
|
581
|
+
// Path 1: the persistent service. installSecretsAgentService is idempotent and
|
|
582
|
+
// waits for the socket; for an already-installed service we kickstart and wait.
|
|
583
|
+
try {
|
|
584
|
+
if (!secretsAgentServiceInstalled()) {
|
|
585
|
+
if (await installSecretsAgentService(Math.max(timeoutMs, 20000)))
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
const uid = process.getuid?.() ?? 0;
|
|
590
|
+
try {
|
|
591
|
+
execFileSync('launchctl', ['kickstart', '-k', `gui/${uid}/${SERVICE_LABEL}`], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
592
|
+
}
|
|
593
|
+
catch { /* may already be running */ }
|
|
594
|
+
const d = Date.now() + timeoutMs;
|
|
595
|
+
while (Date.now() < d) {
|
|
596
|
+
if (await agentPing())
|
|
597
|
+
return true;
|
|
598
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch { /* fall through to the one-off spawn */ }
|
|
603
|
+
// Path 2 (fallback): one-off detached broker. Clear a stale socket/pid first.
|
|
466
604
|
const stalePid = (() => {
|
|
467
605
|
try {
|
|
468
606
|
return parseInt(fs.readFileSync(pidPath(), 'utf-8').trim(), 10);
|
|
@@ -486,11 +624,7 @@ export async function ensureAgentRunning(timeoutMs = 5000) {
|
|
|
486
624
|
}
|
|
487
625
|
catch { /* gone */ }
|
|
488
626
|
const { cmd, args } = brokerSpawn();
|
|
489
|
-
|
|
490
|
-
stdio: 'ignore',
|
|
491
|
-
detached: true,
|
|
492
|
-
});
|
|
493
|
-
child.unref();
|
|
627
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
494
628
|
const deadline = Date.now() + timeoutMs;
|
|
495
629
|
while (Date.now() < deadline) {
|
|
496
630
|
if (await agentPing())
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secret bundles — named sets of
|
|
2
|
+
* Secret bundles — named sets of environment variables backed by a secret store.
|
|
3
3
|
*
|
|
4
|
-
* Bundle metadata (name, description, vars map) is stored
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Bundle metadata (name, description, vars map) is stored as a JSON blob under
|
|
5
|
+
* `agents-cli.bundles.<name>`; secret values live one per item under
|
|
6
|
+
* `agents-cli.secrets.<bundle>.<key>`. Two backends carry those items:
|
|
7
|
+
*
|
|
8
|
+
* - `keychain` (default): the macOS Keychain (device-local, Touch ID / device
|
|
9
|
+
* passcode gated) or Linux libsecret — see src/lib/secrets/index.ts.
|
|
10
|
+
* - `file`: an AES-256-GCM encrypted-file store keyed by a passphrase
|
|
11
|
+
* (src/lib/secrets/filestore.ts). Opt-in, for headless / remote runs where
|
|
12
|
+
* no biometry prompt can be satisfied (e.g. a release on a remote Mac over
|
|
13
|
+
* SSH). The item-name scheme is identical, so the only difference is where
|
|
14
|
+
* bytes land. A file-backed bundle is discovered by the presence of its
|
|
15
|
+
* metadata item in the file store.
|
|
10
16
|
*
|
|
11
17
|
* Cross-machine sync is handled by src/lib/secrets/sync.ts via an explicit
|
|
12
18
|
* encrypted export/import flow; the bundle layer is sync-agnostic.
|
|
13
19
|
*/
|
|
14
20
|
import { type BundleValue, type SecretRef } from './index.js';
|
|
21
|
+
/** Which store carries a bundle's items. */
|
|
22
|
+
export type SecretsBackend = 'keychain' | 'file';
|
|
23
|
+
/**
|
|
24
|
+
* Discover a bundle's backend by location: a file-backed bundle's metadata
|
|
25
|
+
* item exists in the encrypted-file store. This is a plain file-existence
|
|
26
|
+
* check — no passphrase, no Touch ID — so it sidesteps the chicken-and-egg of
|
|
27
|
+
* "read metadata to learn where metadata lives." Absent ⇒ keychain.
|
|
28
|
+
*/
|
|
29
|
+
export declare function bundleBackend(name: string): SecretsBackend;
|
|
15
30
|
/** Allowed values for a secret's `type` metadata field. */
|
|
16
31
|
export declare const SECRET_TYPES: readonly ["api-key", "token", "password", "url", "database-url", "ssh-key", "certificate", "webhook", "note"];
|
|
17
32
|
export type SecretType = typeof SECRET_TYPES[number];
|
|
@@ -38,6 +53,8 @@ export interface SecretsBundle {
|
|
|
38
53
|
name: string;
|
|
39
54
|
description?: string;
|
|
40
55
|
allow_exec?: boolean;
|
|
56
|
+
/** Which store carries this bundle's items. Absent ⇒ `keychain` (the default). */
|
|
57
|
+
backend?: SecretsBackend;
|
|
41
58
|
/** Secrets-agent interaction tier. Absent ⇒ `biometry` (the safe default). */
|
|
42
59
|
tier?: SecretsTier;
|
|
43
60
|
/** ISO 8601 UTC timestamp. Set once on the first writeBundle() for a bundle. */
|
|
@@ -156,6 +173,19 @@ export interface RenameOptions {
|
|
|
156
173
|
* a safe no-op for the source items.
|
|
157
174
|
*/
|
|
158
175
|
export declare function renameBundle(oldName: string, newName: string, opts?: RenameOptions): void;
|
|
176
|
+
/**
|
|
177
|
+
* The store (keychain or encrypted file) that carries a bundle's items. The
|
|
178
|
+
* CLI uses this to read/write/delete per-key items (built with
|
|
179
|
+
* secretsKeychainItem) in the same store as the bundle's metadata, for `add` /
|
|
180
|
+
* `import` / `remove` / `delete`. Pass the bundle's resolved backend
|
|
181
|
+
* (`bundle.backend ?? 'keychain'`).
|
|
182
|
+
*/
|
|
183
|
+
export declare function bundleItemStore(backend: SecretsBackend | undefined): {
|
|
184
|
+
set(item: string, value: string): void;
|
|
185
|
+
delete(item: string): boolean;
|
|
186
|
+
get(item: string): string;
|
|
187
|
+
has(item: string): boolean;
|
|
188
|
+
};
|
|
159
189
|
export declare function keychainItemsForBundle(bundle: SecretsBundle): Array<{
|
|
160
190
|
key: string;
|
|
161
191
|
item: string;
|