@zincapp/znvault-cli 4.7.0 → 4.8.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/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/client/http.d.ts +19 -4
- package/dist/lib/client/http.d.ts.map +1 -1
- package/dist/lib/client/http.js +44 -10
- package/dist/lib/client/http.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,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":"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"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the absolute path to the `mysql` binary on the current PATH.
|
|
3
|
+
*
|
|
4
|
+
* Scans each directory in `process.env.PATH` for an executable named `mysql`.
|
|
5
|
+
* Logs the resolved path to stderr for auditability (spec F11).
|
|
6
|
+
*
|
|
7
|
+
* @returns Absolute path to the `mysql` binary.
|
|
8
|
+
* @throws If `mysql` is not found on PATH, with an actionable installation hint.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertMysqlOnPath(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Reject any passthrough token that is (or starts with `=`-form of) a forbidden
|
|
13
|
+
* connection/credential/defaults override flag.
|
|
14
|
+
*
|
|
15
|
+
* Recognised forms for a forbidden flag `--host`:
|
|
16
|
+
* - exact: `--host` (its value is the next token)
|
|
17
|
+
* - inline: `--host=value`
|
|
18
|
+
* - short: `-h` (value is next token) / `-P`, `-p`
|
|
19
|
+
*
|
|
20
|
+
* Tokens that merely START with a forbidden name but are a different flag
|
|
21
|
+
* (e.g. `--hostgroup`, `--port-something`) are NOT rejected.
|
|
22
|
+
*
|
|
23
|
+
* @throws Error naming the offending flag and citing F2, on the first match.
|
|
24
|
+
*/
|
|
25
|
+
export declare function assertPassthroughAllowed(passthrough: readonly string[]): void;
|
|
26
|
+
/**
|
|
27
|
+
* Options for the pure argv/env builder.
|
|
28
|
+
* `mode` is intentionally absent — stdin wiring is exec/connect-specific and
|
|
29
|
+
* is handled by runMysql, not by this pure builder.
|
|
30
|
+
*/
|
|
31
|
+
export interface BuildMysqlInvocationOpts {
|
|
32
|
+
/**
|
|
33
|
+
* Value for `--defaults-extra-file`. This is the `/dev/fd/<fd>` path returned
|
|
34
|
+
* by createMyCnf (B2) — mysql re-opens the inherited fd through it.
|
|
35
|
+
*/
|
|
36
|
+
fdPath: string;
|
|
37
|
+
/** Optional default schema to select (positional arg — spec F8). */
|
|
38
|
+
database?: string;
|
|
39
|
+
/** Extra arguments appended verbatim to the mysql argv. */
|
|
40
|
+
passthrough?: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the mysql argv and child environment.
|
|
44
|
+
*
|
|
45
|
+
* This is a PURE function — no spawning, no file I/O, no side effects.
|
|
46
|
+
* Export it so tests can assert security-critical argv/env invariants
|
|
47
|
+
* without spawning real mysql.
|
|
48
|
+
*
|
|
49
|
+
* Security invariants:
|
|
50
|
+
* - `--defaults-extra-file=<fdPath>` is args[0].
|
|
51
|
+
* - No --user / -u / --password / -p / MYSQL_PWD in args or env.
|
|
52
|
+
* - No --host / --port / -h / -P flags (connection coords from cnf).
|
|
53
|
+
* - Forbidden override flags in `passthrough` are rejected (spec F2).
|
|
54
|
+
* - MYSQL_HISTFILE=/dev/null overrides any pre-existing value.
|
|
55
|
+
*
|
|
56
|
+
* @throws If `passthrough` contains a forbidden connection/credential/defaults
|
|
57
|
+
* override flag (see assertPassthroughAllowed).
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildMysqlInvocation(opts: BuildMysqlInvocationOpts): {
|
|
60
|
+
args: string[];
|
|
61
|
+
env: NodeJS.ProcessEnv;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* A single entry of a child_process `stdio` array. A NUMBER means "inherit THIS
|
|
65
|
+
* parent fd at this child index"; the strings are the usual stdio dispositions.
|
|
66
|
+
*/
|
|
67
|
+
type StdioEntry = 'pipe' | 'inherit' | 'ignore' | number;
|
|
68
|
+
/**
|
|
69
|
+
* Build the `stdio` array for spawning mysql so that the cnf fd is inherited by
|
|
70
|
+
* the child at the SAME numeric index `fd`. This is what makes
|
|
71
|
+
* `--defaults-extra-file=/dev/fd/<fd>` resolve correctly in the child: the
|
|
72
|
+
* child must have an fd open at exactly `fd`.
|
|
73
|
+
*
|
|
74
|
+
* Layout:
|
|
75
|
+
* - index 0 (stdin): `stdin0` — 'pipe' for exec (we feed SQL), 'inherit' for
|
|
76
|
+
* connect (interactive terminal).
|
|
77
|
+
* - index 1 (stdout): 'inherit'.
|
|
78
|
+
* - index 2 (stderr): 'inherit'.
|
|
79
|
+
* - indices 3 .. fd-1: 'ignore' (gaps the array must fill; the child does not
|
|
80
|
+
* use them).
|
|
81
|
+
* - index fd: `fd` (a number → inherit the parent fd at this index).
|
|
82
|
+
*
|
|
83
|
+
* `fd` is always >= 3 in practice (0/1/2 are taken by the std streams of the
|
|
84
|
+
* Node process), so it never collides with stdin/stdout/stderr. We assert this
|
|
85
|
+
* defensively.
|
|
86
|
+
*
|
|
87
|
+
* @param fd The parent fd to inherit at the same number in the child.
|
|
88
|
+
* @param stdin0 Disposition for the child's stdin (index 0).
|
|
89
|
+
* @returns A stdio array of length max(3, fd+1).
|
|
90
|
+
* @throws If `fd` is < 3 (would collide with std streams — never expected).
|
|
91
|
+
*/
|
|
92
|
+
export declare function buildChildStdio(fd: number, stdin0: 'pipe' | 'inherit'): StdioEntry[];
|
|
93
|
+
/**
|
|
94
|
+
* Options for runMysql.
|
|
95
|
+
*/
|
|
96
|
+
export interface RunMysqlOpts {
|
|
97
|
+
/**
|
|
98
|
+
* The `/dev/fd/<fd>` path returned by createMyCnf — passed to mysql as
|
|
99
|
+
* --defaults-extra-file. This module never re-opens it; mysql reads the
|
|
100
|
+
* inherited fd through it.
|
|
101
|
+
*/
|
|
102
|
+
fdPath: string;
|
|
103
|
+
/**
|
|
104
|
+
* The numeric fd backing `fdPath`. The child INHERITS this fd at the SAME
|
|
105
|
+
* number so `/dev/fd/<fd>` resolves inside it. The parent does NOT close it
|
|
106
|
+
* here — createMyCnf's cleanup() owns the close (after the child exits).
|
|
107
|
+
*/
|
|
108
|
+
fd: number;
|
|
109
|
+
/** Default schema to select (positional arg — spec F8). */
|
|
110
|
+
database?: string;
|
|
111
|
+
/** 'connect' → interactive (stdio: inherit); 'exec' → non-interactive. */
|
|
112
|
+
mode: 'exec' | 'connect';
|
|
113
|
+
/**
|
|
114
|
+
* (exec mode) SQL files to read and concatenate as stdin.
|
|
115
|
+
* Precedence: files → sql → parent stdin (spec F-input).
|
|
116
|
+
*/
|
|
117
|
+
files?: string[];
|
|
118
|
+
/**
|
|
119
|
+
* (exec mode) Inline SQL string to feed as stdin.
|
|
120
|
+
* Precedence: files → sql → parent stdin (spec F-input).
|
|
121
|
+
*/
|
|
122
|
+
sql?: string;
|
|
123
|
+
/** Extra arguments appended verbatim to the mysql argv. */
|
|
124
|
+
passthrough?: string[];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Spawn the system `mysql` client and wait for it to exit.
|
|
128
|
+
*
|
|
129
|
+
* - Resolves with the child's numeric exit code (caller/CI relies on non-zero).
|
|
130
|
+
* - The cnf fd (`opts.fd`) is inherited by the child at the SAME number so
|
|
131
|
+
* `--defaults-extra-file=/dev/fd/<fd>` resolves in the child (spec F1). The
|
|
132
|
+
* directory entry was already unlinked in createMyCnf — there is NOTHING to
|
|
133
|
+
* unlink here, and NO post-spawn unlink race.
|
|
134
|
+
* - 'connect' mode: stdin/stdout/stderr are inherited (interactive terminal);
|
|
135
|
+
* the cnf fd is additionally inherited at index `fd`.
|
|
136
|
+
* - 'exec' mode: stdin (index 0) is wired as:
|
|
137
|
+
* 1. files (concatenated, in order) → stdin pipe
|
|
138
|
+
* 2. sql (string) → stdin pipe
|
|
139
|
+
* 3. parent stdin (if not a TTY) → piped through (inherit)
|
|
140
|
+
* 4. parent stdin is a TTY and neither files nor sql provided → Error
|
|
141
|
+
* stdout/stderr are inherited; the cnf fd is inherited at index `fd`.
|
|
142
|
+
*
|
|
143
|
+
* The parent's own copy of `opts.fd` is intentionally NOT closed here — the
|
|
144
|
+
* child holds its own dup (created by spawn's file actions), and createMyCnf's
|
|
145
|
+
* cleanup() closes the parent's copy after this resolves. (Closing here would be
|
|
146
|
+
* safe too, since the child already has its dup, but leaving the single owner —
|
|
147
|
+
* cleanup() — avoids double-close races.)
|
|
148
|
+
*
|
|
149
|
+
* @throws If the `mysql` binary is not on PATH (via assertMysqlOnPath).
|
|
150
|
+
* @throws If `passthrough` contains a forbidden override flag (via the builder).
|
|
151
|
+
* @throws If the cnf fd collides with a standard stream (via buildChildStdio).
|
|
152
|
+
* @throws If exec mode is requested but no SQL source is available.
|
|
153
|
+
*/
|
|
154
|
+
export declare function runMysql(opts: RunMysqlOpts): Promise<number>;
|
|
155
|
+
export {};
|
|
156
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/commands/mysql/run.ts"],"names":[],"mappings":"AAwCA;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAsB1C;AAoCD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAa7E;AAMD;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,wBAAwB,GAAG;IACpE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB,CA+BA;AAMD;;;GAGG;AACH,KAAK,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,EAAE,CAoBpF;AAMD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAuElE"}
|