@zincapp/znvault-cli 4.7.1 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/apikey/conditions.d.ts.map +1 -1
- package/dist/commands/apikey/conditions.js +1 -0
- package/dist/commands/apikey/conditions.js.map +1 -1
- package/dist/commands/apikey/create.d.ts.map +1 -1
- package/dist/commands/apikey/create.js +1 -0
- package/dist/commands/apikey/create.js.map +1 -1
- package/dist/commands/apikey/helpers.d.ts +1 -0
- package/dist/commands/apikey/helpers.d.ts.map +1 -1
- package/dist/commands/apikey/helpers.js +8 -0
- package/dist/commands/apikey/helpers.js.map +1 -1
- package/dist/commands/apikey/types.d.ts +3 -0
- package/dist/commands/apikey/types.d.ts.map +1 -1
- package/dist/commands/dynamic-secrets/types.d.ts +3 -0
- package/dist/commands/dynamic-secrets/types.d.ts.map +1 -1
- package/dist/commands/mysql/alias.d.ts +26 -0
- package/dist/commands/mysql/alias.d.ts.map +1 -0
- package/dist/commands/mysql/alias.js +56 -0
- package/dist/commands/mysql/alias.js.map +1 -0
- package/dist/commands/mysql/broker.d.ts +38 -0
- package/dist/commands/mysql/broker.d.ts.map +1 -0
- package/dist/commands/mysql/broker.js +171 -0
- package/dist/commands/mysql/broker.js.map +1 -0
- package/dist/commands/mysql/index.d.ts +33 -0
- package/dist/commands/mysql/index.d.ts.map +1 -0
- package/dist/commands/mysql/index.js +207 -0
- package/dist/commands/mysql/index.js.map +1 -0
- package/dist/commands/mysql/mycnf.d.ts +40 -0
- package/dist/commands/mysql/mycnf.d.ts.map +1 -0
- package/dist/commands/mysql/mycnf.js +136 -0
- package/dist/commands/mysql/mycnf.js.map +1 -0
- package/dist/commands/mysql/resolve.d.ts +18 -0
- package/dist/commands/mysql/resolve.d.ts.map +1 -0
- package/dist/commands/mysql/resolve.js +108 -0
- package/dist/commands/mysql/resolve.js.map +1 -0
- package/dist/commands/mysql/run.d.ts +156 -0
- package/dist/commands/mysql/run.d.ts.map +1 -0
- package/dist/commands/mysql/run.js +300 -0
- package/dist/commands/mysql/run.js.map +1 -0
- package/dist/commands/mysql/types.d.ts +29 -0
- package/dist/commands/mysql/types.d.ts.map +1 -0
- package/dist/commands/mysql/types.js +7 -0
- package/dist/commands/mysql/types.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/config/types.d.ts +4 -0
- package/dist/lib/config/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// src/commands/mysql/index.ts
|
|
2
|
+
import * as output from '../../lib/output.js';
|
|
3
|
+
import { resolveTarget } from './resolve.js';
|
|
4
|
+
import { runBrokered } from './broker.js';
|
|
5
|
+
import { assertMysqlOnPath, runMysql } from './run.js';
|
|
6
|
+
import { addAlias, listAliases, removeAlias } from './alias.js';
|
|
7
|
+
/**
|
|
8
|
+
* Collect helper for Commander repeatable options (--file <path> ... --file <path>).
|
|
9
|
+
* Each invocation appends the new value to the accumulator array.
|
|
10
|
+
*/
|
|
11
|
+
function collect(value, previous) {
|
|
12
|
+
return [...previous, value];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse and validate the `--ttl <seconds>` option (M-2).
|
|
16
|
+
*
|
|
17
|
+
* `parseInt('abc', 10)` is `NaN`, and `NaN ?? 600` stays `NaN` — which would be
|
|
18
|
+
* sent to the server as the requested TTL and produce a confusing bad request.
|
|
19
|
+
* Validate locally and fail fast with a clear message BEFORE any lease is
|
|
20
|
+
* generated.
|
|
21
|
+
*
|
|
22
|
+
* @param raw The raw `--ttl` option value, or undefined when not provided.
|
|
23
|
+
* @returns The parsed positive integer, or undefined when `--ttl` is absent
|
|
24
|
+
* (so the server/role default applies).
|
|
25
|
+
* @throws If `--ttl` is provided but is not a positive integer.
|
|
26
|
+
*/
|
|
27
|
+
export function parseTtlSeconds(raw) {
|
|
28
|
+
if (raw === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
const ttl = Number(raw);
|
|
31
|
+
if (!Number.isInteger(ttl) || ttl <= 0) {
|
|
32
|
+
throw new Error(`Invalid --ttl '${raw}': must be a positive integer number of seconds.`);
|
|
33
|
+
}
|
|
34
|
+
return ttl;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Register the `mysql` command group and all subcommands on `program`.
|
|
38
|
+
*/
|
|
39
|
+
export function registerMysqlCommands(program) {
|
|
40
|
+
const mysql = program
|
|
41
|
+
.command('mysql')
|
|
42
|
+
// enablePositionalOptions() lets the variadic [mysqlArgs...] coexist cleanly
|
|
43
|
+
// with named options — keeps '--' separator forwarding well-defined.
|
|
44
|
+
.enablePositionalOptions()
|
|
45
|
+
.description('Connect to MySQL databases via short-lived dynamic-secret credentials')
|
|
46
|
+
.addHelpText('after', `
|
|
47
|
+
Examples:
|
|
48
|
+
# Interactive shell (connect mode)
|
|
49
|
+
znvault mysql connect my-connection
|
|
50
|
+
|
|
51
|
+
# Execute SQL from a file
|
|
52
|
+
znvault mysql exec my-connection --file schema.sql
|
|
53
|
+
|
|
54
|
+
# Execute inline SQL
|
|
55
|
+
znvault mysql exec my-connection --sql "SELECT 1"
|
|
56
|
+
|
|
57
|
+
# Pipe SQL from stdin
|
|
58
|
+
echo "SELECT version()" | znvault mysql exec my-connection
|
|
59
|
+
|
|
60
|
+
# Use an alias (see: znvault mysql alias add)
|
|
61
|
+
znvault mysql connect staging-rw
|
|
62
|
+
|
|
63
|
+
# Save an alias for quick access
|
|
64
|
+
znvault mysql alias add staging-rw --connection staging-mysql --role app-rw
|
|
65
|
+
|
|
66
|
+
# Pass extra mysql flags after --
|
|
67
|
+
znvault mysql connect my-connection -- --table
|
|
68
|
+
`);
|
|
69
|
+
// ── exec ─────────────────────────────────────────────────────────────────────
|
|
70
|
+
//
|
|
71
|
+
// Using `exec <target> [mysqlArgs...]` (variadic) instead of passThroughOptions()
|
|
72
|
+
// so that named options (--role, --sql, etc.) can appear AFTER the positional
|
|
73
|
+
// <target> argument without being misread as excess positional args.
|
|
74
|
+
//
|
|
75
|
+
// With passThroughOptions(), Commander stops option-parsing at the first
|
|
76
|
+
// non-option token (the target), so `exec staging-mysql --role app-rw` was
|
|
77
|
+
// rejected as "too many arguments" — the target consumed the stop-point and
|
|
78
|
+
// --role/--sql were treated as excess positional args.
|
|
79
|
+
//
|
|
80
|
+
// The variadic approach: Commander continues parsing named options across the
|
|
81
|
+
// whole argv, and any unrecognised tokens (or tokens after `--`) land in the
|
|
82
|
+
// mysqlArgs array instead of causing an error. The `--` separator itself is
|
|
83
|
+
// NOT included in mysqlArgs — Commander strips it automatically.
|
|
84
|
+
mysql
|
|
85
|
+
.command('exec <target> [mysqlArgs...]')
|
|
86
|
+
.description('Execute SQL against a MySQL database via a short-lived credential')
|
|
87
|
+
.option('--role <name>', 'Dynamic-secrets role name or ID')
|
|
88
|
+
.option('--file <path>', 'SQL file to execute (repeatable; concatenated in order)', collect, [])
|
|
89
|
+
.option('--sql <sql>', 'Inline SQL to execute')
|
|
90
|
+
.option('--ttl <seconds>', `Requested lease TTL in seconds (default: 600; capped by role maxTtl)`)
|
|
91
|
+
.option('--database <db>', 'Database/schema to select (overrides the credential default)')
|
|
92
|
+
.action(async (target, mysqlArgs, opts) => {
|
|
93
|
+
// mysqlArgs contains any extra tokens (e.g. post-`--` flags like --batch).
|
|
94
|
+
// Commander excludes the `--` separator itself from this array.
|
|
95
|
+
const passthrough = mysqlArgs;
|
|
96
|
+
try {
|
|
97
|
+
// Fail fast: check mysql binary before generating any lease.
|
|
98
|
+
assertMysqlOnPath();
|
|
99
|
+
// Validate --ttl locally before generating any lease (M-2).
|
|
100
|
+
const ttlSeconds = parseTtlSeconds(opts.ttl);
|
|
101
|
+
const { roleId } = await resolveTarget(target, opts.role);
|
|
102
|
+
const code = await runBrokered({
|
|
103
|
+
roleId,
|
|
104
|
+
ttlSeconds,
|
|
105
|
+
run: ({ credential, fd, fdPath }) => runMysql({
|
|
106
|
+
fd,
|
|
107
|
+
fdPath,
|
|
108
|
+
database: opts.database ?? credential.database,
|
|
109
|
+
mode: 'exec',
|
|
110
|
+
files: opts.file,
|
|
111
|
+
sql: opts.sql,
|
|
112
|
+
passthrough,
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
process.exit(code);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// ── connect ───────────────────────────────────────────────────────────────────
|
|
123
|
+
// Same variadic idiom as exec — see exec comment above.
|
|
124
|
+
mysql
|
|
125
|
+
.command('connect <target> [mysqlArgs...]')
|
|
126
|
+
.description('Open an interactive MySQL shell via a short-lived credential')
|
|
127
|
+
.option('--role <name>', 'Dynamic-secrets role name or ID')
|
|
128
|
+
.option('--ttl <seconds>', `Requested lease TTL in seconds (default: 600; capped by role maxTtl)`)
|
|
129
|
+
.option('--database <db>', 'Database/schema to select (overrides the credential default)')
|
|
130
|
+
.action(async (target, mysqlArgs, opts) => {
|
|
131
|
+
const passthrough = mysqlArgs;
|
|
132
|
+
try {
|
|
133
|
+
// Fail fast: check mysql binary before generating any lease.
|
|
134
|
+
assertMysqlOnPath();
|
|
135
|
+
// Validate --ttl locally before generating any lease (M-2).
|
|
136
|
+
const ttlSeconds = parseTtlSeconds(opts.ttl);
|
|
137
|
+
const { roleId } = await resolveTarget(target, opts.role);
|
|
138
|
+
const code = await runBrokered({
|
|
139
|
+
roleId,
|
|
140
|
+
ttlSeconds,
|
|
141
|
+
run: ({ credential, fd, fdPath }) => runMysql({
|
|
142
|
+
fd,
|
|
143
|
+
fdPath,
|
|
144
|
+
database: opts.database ?? credential.database,
|
|
145
|
+
mode: 'connect',
|
|
146
|
+
passthrough,
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
process.exit(code);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// ── alias ─────────────────────────────────────────────────────────────────────
|
|
157
|
+
const alias = mysql
|
|
158
|
+
.command('alias')
|
|
159
|
+
.description('Manage MySQL connection aliases for quick access');
|
|
160
|
+
alias
|
|
161
|
+
.command('add <name>')
|
|
162
|
+
.description('Add or overwrite a MySQL connection alias')
|
|
163
|
+
.requiredOption('--connection <connection>', 'Connection name or ID to bind')
|
|
164
|
+
.requiredOption('--role <role>', 'Role name or ID to bind')
|
|
165
|
+
.action((name, opts) => {
|
|
166
|
+
try {
|
|
167
|
+
addAlias(name, opts.connection, opts.role);
|
|
168
|
+
output.success(`Alias '${name}' saved → ${opts.connection} / ${opts.role}`);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
alias
|
|
176
|
+
.command('list')
|
|
177
|
+
.alias('ls')
|
|
178
|
+
.description('List all MySQL aliases in the active profile')
|
|
179
|
+
.action(() => {
|
|
180
|
+
try {
|
|
181
|
+
const aliases = listAliases();
|
|
182
|
+
if (aliases.length === 0) {
|
|
183
|
+
output.info('No aliases defined. Use: znvault mysql alias add <name> --connection <c> --role <r>');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
output.table(['Name', 'Connection', 'Role'], aliases.map(({ name, connection, role }) => [name, connection, role]));
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
alias
|
|
194
|
+
.command('rm <name>')
|
|
195
|
+
.description('Remove a MySQL alias from the active profile')
|
|
196
|
+
.action((name) => {
|
|
197
|
+
try {
|
|
198
|
+
removeAlias(name);
|
|
199
|
+
output.success(`Alias '${name}' removed`);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/mysql/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result of createMyCnf.
|
|
3
|
+
*
|
|
4
|
+
* - `fd`: the open file descriptor backing the (now unlinked) my.cnf. The
|
|
5
|
+
* child mysql inherits this fd at the SAME number and re-opens it
|
|
6
|
+
* via `fdPath`.
|
|
7
|
+
* - `fdPath`: `/dev/fd/<fd>` — pass this verbatim as the value of
|
|
8
|
+
* `--defaults-extra-file`. Works on macOS (/dev/fd) and Linux
|
|
9
|
+
* (/dev/fd → /proc/self/fd).
|
|
10
|
+
* - `cleanup`: idempotent, best-effort. Closes `fd` (releasing the inode so its
|
|
11
|
+
* bytes are reclaimed) and rmdir's the now-empty mem-fs dir.
|
|
12
|
+
*/
|
|
13
|
+
export interface MyCnfHandle {
|
|
14
|
+
/** Open fd backing the unlinked cnf inode. Child inherits it at this number. */
|
|
15
|
+
fd: number;
|
|
16
|
+
/** `/dev/fd/<fd>` — the value to pass to --defaults-extra-file. */
|
|
17
|
+
fdPath: string;
|
|
18
|
+
/** Idempotent best-effort teardown: closeSync(fd) + rmdir(dir). */
|
|
19
|
+
cleanup: () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create the temp my.cnf, unlink its directory entry immediately, and return an
|
|
23
|
+
* open fd plus an idempotent cleanup().
|
|
24
|
+
*
|
|
25
|
+
* The body is fully synchronous (all fs operations are *Sync), but the function
|
|
26
|
+
* returns a Promise to preserve the broker's `await createMyCnf(...)` contract
|
|
27
|
+
* (and so it can become genuinely async later without touching callers).
|
|
28
|
+
* Declared non-`async` + returning Promise.resolve avoids the require-await
|
|
29
|
+
* lint warning while keeping the Promise return type (M-5).
|
|
30
|
+
*
|
|
31
|
+
* @throws If the exclusive create fails (e.g. EEXIST on a suffix collision,
|
|
32
|
+
* which is astronomically unlikely with 8 random bytes, or ENOSPC).
|
|
33
|
+
*/
|
|
34
|
+
export declare function createMyCnf(opts: {
|
|
35
|
+
user: string;
|
|
36
|
+
password: string;
|
|
37
|
+
host: string;
|
|
38
|
+
port: number;
|
|
39
|
+
}): Promise<MyCnfHandle>;
|
|
40
|
+
//# sourceMappingURL=mycnf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mycnf.d.ts","sourceRoot":"","sources":["../../../src/commands/mysql/mycnf.ts"],"names":[],"mappings":"AA2CA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAC;IACX,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAC5D,GAAG,OAAO,CAAC,WAAW,CAAC,CA8DvB"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/commands/mysql/mycnf.ts
|
|
2
|
+
//
|
|
3
|
+
// Writes the short-lived 0600 my.cnf that carries the leased MySQL credentials
|
|
4
|
+
// (spec B2) and hands it to the child mysql as an OPEN FILE DESCRIPTOR rather
|
|
5
|
+
// than a path on disk.
|
|
6
|
+
//
|
|
7
|
+
// F1 (no plaintext directory entry survives the run) is achieved by the
|
|
8
|
+
// open → write → unlink-immediately pattern:
|
|
9
|
+
// 1. openSync(path, 'wx+', 0600) → exclusive create, returns fd.
|
|
10
|
+
// 2. writeSync(fd, body) → credentials written through the fd.
|
|
11
|
+
// 3. unlinkSync(path) → directory entry removed AT ONCE. The
|
|
12
|
+
// inode (and its plaintext bytes) stays alive ONLY because the open fd
|
|
13
|
+
// still references it; there is NO name in the filesystem from this point
|
|
14
|
+
// on, so a `kill -9` or crash leaves nothing on disk to recover.
|
|
15
|
+
// 4. runMysql passes `/dev/fd/<fd>` as --defaults-extra-file and inherits the
|
|
16
|
+
// fd into the child at the SAME number, so mysql re-opens the still-alive
|
|
17
|
+
// inode through /dev/fd. (On macOS /dev/fd/N re-opens the inode — which is
|
|
18
|
+
// why the fd MUST be readable; see the 'wx+' note below. On Linux /dev/fd
|
|
19
|
+
// is /proc/self/fd and resolves the same inode.)
|
|
20
|
+
// 5. cleanup() closes the fd → last reference gone → kernel reclaims the
|
|
21
|
+
// inode + its bytes. cleanup() is idempotent and best-effort.
|
|
22
|
+
//
|
|
23
|
+
// This replaces the old "spawn-then-unlink" approach, which raced: spawn()
|
|
24
|
+
// returns when the child is forked but BEFORE it has exec'd mysql and read the
|
|
25
|
+
// defaults file, so the unlink could win the race and mysql would die with
|
|
26
|
+
// "Failed to open required defaults file". Keeping the inode alive via an open
|
|
27
|
+
// fd removes the race entirely — the name is already gone before spawn, and the
|
|
28
|
+
// fd guarantees the bytes survive until cleanup().
|
|
29
|
+
import * as fs from 'node:fs';
|
|
30
|
+
import * as os from 'node:os';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import { randomBytes } from 'node:crypto';
|
|
33
|
+
function memBackedTmpBase() {
|
|
34
|
+
// Prefer a memory-backed fs so the plaintext never hits spinning disk (spec F1).
|
|
35
|
+
try {
|
|
36
|
+
fs.accessSync('/dev/shm', fs.constants.W_OK);
|
|
37
|
+
return '/dev/shm';
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return os.tmpdir();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create the temp my.cnf, unlink its directory entry immediately, and return an
|
|
45
|
+
* open fd plus an idempotent cleanup().
|
|
46
|
+
*
|
|
47
|
+
* The body is fully synchronous (all fs operations are *Sync), but the function
|
|
48
|
+
* returns a Promise to preserve the broker's `await createMyCnf(...)` contract
|
|
49
|
+
* (and so it can become genuinely async later without touching callers).
|
|
50
|
+
* Declared non-`async` + returning Promise.resolve avoids the require-await
|
|
51
|
+
* lint warning while keeping the Promise return type (M-5).
|
|
52
|
+
*
|
|
53
|
+
* @throws If the exclusive create fails (e.g. EEXIST on a suffix collision,
|
|
54
|
+
* which is astronomically unlikely with 8 random bytes, or ENOSPC).
|
|
55
|
+
*/
|
|
56
|
+
export function createMyCnf(opts) {
|
|
57
|
+
const suffix = randomBytes(8).toString('hex');
|
|
58
|
+
const dir = path.join(memBackedTmpBase(), `znvault-my-${suffix}`);
|
|
59
|
+
// 0700 dir on a memory-backed fs (spec F1). Created before the file so the
|
|
60
|
+
// file's parent is owner-only even for the brief moment the name exists.
|
|
61
|
+
fs.mkdirSync(dir, { mode: 0o700 });
|
|
62
|
+
const file = path.join(dir, 'my.cnf');
|
|
63
|
+
const body = `[client]\nuser=${opts.user}\npassword=${opts.password}\nhost=${opts.host}\nport=${opts.port}\n`;
|
|
64
|
+
// 'wx+' = O_RDWR | O_CREAT | O_EXCL, mode 0600.
|
|
65
|
+
// - O_EXCL : fail if the file somehow already exists (no clobber / no
|
|
66
|
+
// following an attacker-planted symlink).
|
|
67
|
+
// - O_RDWR : the fd MUST be READABLE. On macOS, `/dev/fd/N` RE-OPENS the
|
|
68
|
+
// underlying inode (it is not a plain dup of the open file
|
|
69
|
+
// description), so a write-only ('wx') fd would make mysql fail
|
|
70
|
+
// with EBADF/permission when it tries to read /dev/fd/N. O_RDWR
|
|
71
|
+
// keeps the inode re-openable for read by the child. Verified
|
|
72
|
+
// against real mysql 9.4 on macOS.
|
|
73
|
+
let fd;
|
|
74
|
+
try {
|
|
75
|
+
fd = fs.openSync(file, 'wx+', 0o600);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// mkdir succeeded but open failed — drop the empty dir so we don't leak it.
|
|
79
|
+
try {
|
|
80
|
+
fs.rmdirSync(dir);
|
|
81
|
+
}
|
|
82
|
+
catch { /* ignore */ }
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
let closed = false;
|
|
86
|
+
try {
|
|
87
|
+
// CRITICAL: write at an EXPLICIT position 0 (the 5-arg overload) so the fd's
|
|
88
|
+
// current file OFFSET stays at 0. On macOS, `/dev/fd/N` does NOT give the
|
|
89
|
+
// child a fresh offset-0 description — it shares the original fd's offset.
|
|
90
|
+
// A plain `writeSync(fd, body)` advances the offset to EOF, so mysql reading
|
|
91
|
+
// /dev/fd/N would start at EOF and parse an EMPTY defaults file (verified:
|
|
92
|
+
// `mysql --print-defaults` shows zero args). Positioned writes do not move
|
|
93
|
+
// the file pointer, so the offset remains 0 and mysql reads the whole body.
|
|
94
|
+
const bodyBuf = Buffer.from(body, 'utf8');
|
|
95
|
+
fs.writeSync(fd, bodyBuf, 0, bodyBuf.length, 0);
|
|
96
|
+
// F1: remove the directory entry IMMEDIATELY. The inode stays alive purely
|
|
97
|
+
// because `fd` is still open; there is no name on disk from here on.
|
|
98
|
+
fs.unlinkSync(file);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
// Writing or unlinking failed — don't leak the fd or the dir.
|
|
102
|
+
try {
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
closed = true;
|
|
105
|
+
}
|
|
106
|
+
catch { /* ignore */ }
|
|
107
|
+
try {
|
|
108
|
+
fs.unlinkSync(file);
|
|
109
|
+
}
|
|
110
|
+
catch { /* ignore */ }
|
|
111
|
+
try {
|
|
112
|
+
fs.rmdirSync(dir);
|
|
113
|
+
}
|
|
114
|
+
catch { /* ignore */ }
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
const cleanup = () => {
|
|
118
|
+
// Closing the last fd referencing the (already unlinked) inode releases it,
|
|
119
|
+
// so the kernel reclaims the plaintext bytes. Idempotent via `closed`.
|
|
120
|
+
if (!closed) {
|
|
121
|
+
try {
|
|
122
|
+
fs.closeSync(fd);
|
|
123
|
+
}
|
|
124
|
+
catch { /* ignore */ }
|
|
125
|
+
closed = true;
|
|
126
|
+
}
|
|
127
|
+
// The file name is already gone; the dir should be empty. rmdir is
|
|
128
|
+
// best-effort (it may already be gone if cleanup ran twice).
|
|
129
|
+
try {
|
|
130
|
+
fs.rmdirSync(dir);
|
|
131
|
+
}
|
|
132
|
+
catch { /* ignore */ }
|
|
133
|
+
};
|
|
134
|
+
return Promise.resolve({ fd, fdPath: `/dev/fd/${fd.toString()}`, cleanup });
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=mycnf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mycnf.js","sourceRoot":"","sources":["../../../src/commands/mysql/mycnf.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `target` (a connection name/id OR an alias) plus an optional role
|
|
3
|
+
* name/id into concrete IDs.
|
|
4
|
+
*
|
|
5
|
+
* Resolution rules:
|
|
6
|
+
* 1. If `target` matches a saved alias, expand to { connection, role }.
|
|
7
|
+
* Validate both still exist; if not, throw a "dangling alias" error (F13).
|
|
8
|
+
* 2. Otherwise treat `target` as a connection name/id and fetch it.
|
|
9
|
+
* 3. Resolve the role:
|
|
10
|
+
* - If a role name/id is given, find it in the connection's role list.
|
|
11
|
+
* - If no role given and the connection has exactly one role, use it.
|
|
12
|
+
* - Otherwise throw an error instructing the user to pass --role.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveTarget(target: string, roleOpt?: string): Promise<{
|
|
15
|
+
connectionId: string;
|
|
16
|
+
roleId: string;
|
|
17
|
+
}>;
|
|
18
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../src/commands/mysql/resolve.ts"],"names":[],"mappings":"AA8DA;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA+DnD"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/commands/mysql/resolve.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a target (connection name/id or alias) + optional role to concrete IDs.
|
|
4
|
+
*
|
|
5
|
+
* This is used by `znvault mysql exec/connect` to turn the user-supplied target
|
|
6
|
+
* and --role option into the { connectionId, roleId } pair needed by the broker.
|
|
7
|
+
*/
|
|
8
|
+
import { client } from '../../lib/client.js';
|
|
9
|
+
import { getAlias } from './alias.js';
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a connection name or id to a concrete connection id.
|
|
12
|
+
*
|
|
13
|
+
* Strategy (avoids a guaranteed 404 round-trip on the common name case):
|
|
14
|
+
* - If `target` looks like a connection id (starts with "dbc_"), try GET by id
|
|
15
|
+
* first; if that 404s, fall back to listing and matching by name.
|
|
16
|
+
* - Otherwise (target is a friendly name), list all connections and match by
|
|
17
|
+
* name first; if not found, try GET by id as a last resort.
|
|
18
|
+
* - If neither resolves, throw a clear "not found (by id or name)" error.
|
|
19
|
+
*
|
|
20
|
+
* Connection names are unique per tenant, so a name match is unambiguous.
|
|
21
|
+
* If somehow multiple entries share a name, the first match is used.
|
|
22
|
+
*/
|
|
23
|
+
async function resolveConnectionId(target) {
|
|
24
|
+
const looksLikeId = target.startsWith('dbc_');
|
|
25
|
+
if (looksLikeId) {
|
|
26
|
+
// Try direct GET by id first.
|
|
27
|
+
try {
|
|
28
|
+
const conn = await client.get(`/v1/dynamic-secrets/connections/${target}`);
|
|
29
|
+
return conn.id;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Fall through to list-by-name.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// List all connections and match by name.
|
|
36
|
+
const connections = await client.get('/v1/dynamic-secrets/connections');
|
|
37
|
+
const byName = connections.find((c) => c.name === target);
|
|
38
|
+
if (byName !== undefined) {
|
|
39
|
+
return byName.id;
|
|
40
|
+
}
|
|
41
|
+
if (!looksLikeId) {
|
|
42
|
+
// Not found by name; try GET by id as a last resort.
|
|
43
|
+
try {
|
|
44
|
+
const conn = await client.get(`/v1/dynamic-secrets/connections/${target}`);
|
|
45
|
+
return conn.id;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Fall through to error.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Connection '${target}' not found (by id or name). ` +
|
|
52
|
+
`Run 'znvault dynamic-secrets connections list' to see available connections.`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve `target` (a connection name/id OR an alias) plus an optional role
|
|
56
|
+
* name/id into concrete IDs.
|
|
57
|
+
*
|
|
58
|
+
* Resolution rules:
|
|
59
|
+
* 1. If `target` matches a saved alias, expand to { connection, role }.
|
|
60
|
+
* Validate both still exist; if not, throw a "dangling alias" error (F13).
|
|
61
|
+
* 2. Otherwise treat `target` as a connection name/id and fetch it.
|
|
62
|
+
* 3. Resolve the role:
|
|
63
|
+
* - If a role name/id is given, find it in the connection's role list.
|
|
64
|
+
* - If no role given and the connection has exactly one role, use it.
|
|
65
|
+
* - Otherwise throw an error instructing the user to pass --role.
|
|
66
|
+
*/
|
|
67
|
+
export async function resolveTarget(target, roleOpt) {
|
|
68
|
+
const alias = getAlias(target);
|
|
69
|
+
if (alias !== undefined) {
|
|
70
|
+
// Alias path — validate that connection and role still exist.
|
|
71
|
+
const connectionTarget = alias.connection;
|
|
72
|
+
const roleTarget = alias.role;
|
|
73
|
+
let connectionId;
|
|
74
|
+
try {
|
|
75
|
+
connectionId = await resolveConnectionId(connectionTarget);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error(`Dangling alias '${target}': connection '${connectionTarget}' no longer exists`);
|
|
79
|
+
}
|
|
80
|
+
const roles = await client.get(`/v1/dynamic-secrets/connections/${connectionId}/roles`);
|
|
81
|
+
const role = roles.find((r) => r.name === roleTarget || r.id === roleTarget);
|
|
82
|
+
if (role === undefined) {
|
|
83
|
+
throw new Error(`Dangling alias '${target}': role '${roleTarget}' no longer exists on connection '${connectionTarget}'`);
|
|
84
|
+
}
|
|
85
|
+
return { connectionId, roleId: role.id };
|
|
86
|
+
}
|
|
87
|
+
// Direct connection path.
|
|
88
|
+
const connectionId = await resolveConnectionId(target);
|
|
89
|
+
const roles = await client.get(`/v1/dynamic-secrets/connections/${connectionId}/roles`);
|
|
90
|
+
if (roleOpt !== undefined) {
|
|
91
|
+
const role = roles.find((r) => r.name === roleOpt || r.id === roleOpt);
|
|
92
|
+
if (role === undefined) {
|
|
93
|
+
throw new Error(`Role '${roleOpt}' not found on connection '${target}'. ` +
|
|
94
|
+
`Available: ${roles.map((r) => r.name).join(', ') || '(none)'}`);
|
|
95
|
+
}
|
|
96
|
+
return { connectionId, roleId: role.id };
|
|
97
|
+
}
|
|
98
|
+
// No role given — require exactly one.
|
|
99
|
+
if (roles.length === 1) {
|
|
100
|
+
return { connectionId, roleId: roles[0].id };
|
|
101
|
+
}
|
|
102
|
+
if (roles.length === 0) {
|
|
103
|
+
throw new Error(`Connection '${target}' has no roles. Create one first, then pass --role <name>.`);
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Connection '${target}' has ${roles.length.toString()} roles. ` +
|
|
106
|
+
`Pass --role <name> to select one: ${roles.map((r) => r.name).join(', ')}`);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/commands/mysql/resolve.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAEhC;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC;;;;;;;;;;;;GAYG;AACH,KAAK,UAAU,mBAAmB,CAAC,MAAc;IAC/C,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAE9C,IAAI,WAAW,EAAE,CAAC;QAChB,8BAA8B;QAC9B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,CAAe,mCAAmC,MAAM,EAAE,CAAC,CAAC;YACzF,OAAO,IAAI,CAAC,EAAE,CAAC;QACjB,CAAC;QAAC,MAAM,CAAC;YACP,gCAAgC;QAClC,CAAC;IACH,CAAC;IAED,0CAA0C;IAC1C,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,GAAG,CAAiB,iCAAiC,CAAC,CAAC;IACxF,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;IAC1D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC,EAAE,CAAC;IACnB,CAAC;IAED,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,qDAAqD;QACrD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,CAAe,mCAAmC,MAAM,EAAE,CAAC,CAAC;YACzF,OAAO,IAAI,CAAC,EAAE,CAAC;QACjB,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CACb,eAAe,MAAM,+BAA+B;QAClD,8EAA8E,CACjF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,OAAgB;IAEhB,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE/B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,8DAA8D;QAC9D,MAAM,gBAAgB,GAAG,KAAK,CAAC,UAAU,CAAC;QAC1C,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC;QAE9B,IAAI,YAAoB,CAAC;QACzB,IAAI,CAAC;YACH,YAAY,GAAG,MAAM,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;QAC7D,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,mBAAmB,MAAM,kBAAkB,gBAAgB,oBAAoB,CAChF,CAAC;QACJ,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,GAAG,CAC5B,mCAAmC,YAAY,QAAQ,CACxD,CAAC;QACF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,KAAK,UAAU,CAAC,CAAC;QAC7E,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACb,mBAAmB,MAAM,YAAY,UAAU,qCAAqC,gBAAgB,GAAG,CACxG,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;IAC3C,CAAC;IAED,0BAA0B;IAC1B,MAAM,YAAY,GAAG,MAAM,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAEvD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,GAAG,CAC5B,mCAAmC,YAAY,QAAQ,CACxD,CAAC;IAEF,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC;QACvE,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACb,SAAS,OAAO,8BAA8B,MAAM,KAAK;gBACvD,cAAc,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,EAAE,CAClE,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;IAC3C,CAAC;IAED,uCAAuC;IACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IAC/C,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,eAAe,MAAM,4DAA4D,CAClF,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,KAAK,CACb,eAAe,MAAM,SAAS,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,UAAU;QAC7D,qCAAqC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC7E,CAAC;AACJ,CAAC"}
|