hikoutei 0.9.6 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Env-file writer and atomic private-file write for `hikoutei setup`.
3
+ *
4
+ * The secure, atomic `.env` output-write machinery of the setup flow,
5
+ * extracted verbatim: update only the two managed env keys while preserving
6
+ * unrelated lines, refuse to read or write through any symlink/hardlink
7
+ * alias of a reserved path, and always land the result via a unique private
8
+ * temp file plus rename (never pathname-chmodded). Also carries
9
+ * `revalidateSetupPaths`, the fail-closed re-run of the reserved-path
10
+ * collision check performed immediately before every checkpoint and `.env`
11
+ * write, so an alias planted mid-run can never redirect those writes.
12
+ */
13
+ import { type SetupStateWriteFs } from "./checkpoint.js";
14
+ import { type SetupErrorResult } from "./flowResult.js";
15
+ import type { RunSetupOptions } from "./setupFlow.js";
16
+ /** The two .env keys the setup CLI manages. */
17
+ export declare const SETUP_ENV_KEYS: {
18
+ readonly CREDENTIALS: "GOOGLE_APPLICATION_CREDENTIALS";
19
+ readonly SPREADSHEET_URL: "HIKOUTEI_SYNC_SPREADSHEET_URL";
20
+ };
21
+ /** Result of writing the .env output file. */
22
+ export interface EnvFileWriteResult {
23
+ /** True when the file did not exist before this write. */
24
+ readonly created: boolean;
25
+ /** True when the file content changed (created or keys added/updated). */
26
+ readonly modified: boolean;
27
+ }
28
+ /**
29
+ * Writes or updates the .env output file securely and atomically.
30
+ *
31
+ * An existing output must be a regular file at the lstat boundary (a
32
+ * symlink is rejected outright, and directories/FIFOs/devices/sockets are
33
+ * refused before any open so a FIFO can never block), the file is opened
34
+ * WITHOUT following symlinks (`O_NOFOLLOW` where supported) and
35
+ * non-blocking (`O_NONBLOCK` where supported), the descriptor is
36
+ * fstat-verified as a regular file BEFORE a single byte is read (covering
37
+ * a non-regular replacement between the lstat check and the open), and any
38
+ * alias of the credentials file or other reserved paths (hardlink/symlink
39
+ * alias — the key contents are never read through it) is refused before a
40
+ * single byte is read. The preserved env content is built in memory and written to a unique private sibling temp
41
+ * file (`O_CREAT|O_EXCL|O_WRONLY` plus `O_NOFOLLOW`, mode 0600, fsync +
42
+ * close), then atomically renamed over the output: rename replaces the
43
+ * directory entry and never follows a symlink or hardlink planted after
44
+ * validation, so it cannot overwrite the key inode. Cleanup removes only
45
+ * the temp inode this invocation created. Missing file is created; a file
46
+ * whose content is unchanged is not rewritten. An existing file whose
47
+ * owner bits are not exactly 0600 counts as modified and is atomically
48
+ * replaced by a fresh verified-0600 file — never pathname-chmodded — so
49
+ * hardlinks sharing the old inode keep their own mode and content. Throws on filesystem
50
+ * failure or an unsafe existing entry; the caller maps the throw to
51
+ * `output_write_failed`.
52
+ */
53
+ export declare function writeSetupEnvFile(outputPath: string, credentialsPath: string, spreadsheetUrl: string, reservedPaths?: readonly string[]): EnvFileWriteResult;
54
+ /**
55
+ * Writes content to the output path atomically via a unique private temp.
56
+ *
57
+ * The temp file (PID + UUID sibling name) is created with exclusive
58
+ * no-follow flags and mode 0600, owner-only mode is applied and verified
59
+ * THROUGH the still-open descriptor (`fchmod` + `fstat`) before the content
60
+ * is written and fsynced, the path is re-verified against the created
61
+ * inode, and the file is renamed over the output. No pathname `chmod` is
62
+ * ever performed. Rename replaces the directory entry rather than
63
+ * following a symlink/hardlink planted after validation. Cleanup removes
64
+ * only the temp inode this invocation created. Throws on failure. `fs` is
65
+ * injectable so tests can prove the descriptor-mode behavior.
66
+ *
67
+ * Exported for the injected descriptor-mode regression; the flow calls it
68
+ * through `writeSetupEnvFile` with the default filesystem.
69
+ */
70
+ export declare function atomicWritePrivateFile(outputPath: string, content: string, fs?: SetupStateWriteFs): void;
71
+ /** `O_NOFOLLOW` where the platform defines it, else 0. */
72
+ export declare function noFollowFlag(): number;
73
+ /** `O_NONBLOCK` where the platform defines it, else 0. */
74
+ export declare function nonBlockFlag(): number;
75
+ /**
76
+ * Re-runs the reserved-path collision check and fails closed when aliases
77
+ * changed after the initial preflight.
78
+ *
79
+ * The reserved paths (key, output, checkpoint, temp, lock) are re-resolved
80
+ * immediately before every checkpoint write and the .env write: a symlink
81
+ * or hardlink planted after the preflight must never redirect a write to a
82
+ * reserved file. Returns an error result on collision, `null` when safe.
83
+ */
84
+ export declare function revalidateSetupPaths(options: RunSetupOptions): SetupErrorResult | null;
85
+ //# sourceMappingURL=envFileWriter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envFileWriter.d.ts","sourceRoot":"","sources":["../../src/cli/envFileWriter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAaH,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAErE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,+CAA+C;AAC/C,eAAO,MAAM,cAAc;;;CAGjB,CAAC;AAEX,8CAA8C;AAC9C,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAeD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,SAAS,MAAM,EAAO,GACpC,kBAAkB,CA0BpB;AAuGD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,EAAE,GAAE,iBAA4C,GAC/C,IAAI,CA0EN;AAED,0DAA0D;AAC1D,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,0DAA0D;AAC1D,wBAAgB,YAAY,IAAI,MAAM,CAErC;AA+BD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,CAatF"}
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Env-file writer and atomic private-file write for `hikoutei setup`.
3
+ *
4
+ * The secure, atomic `.env` output-write machinery of the setup flow,
5
+ * extracted verbatim: update only the two managed env keys while preserving
6
+ * unrelated lines, refuse to read or write through any symlink/hardlink
7
+ * alias of a reserved path, and always land the result via a unique private
8
+ * temp file plus rename (never pathname-chmodded). Also carries
9
+ * `revalidateSetupPaths`, the fail-closed re-run of the reserved-path
10
+ * collision check performed immediately before every checkpoint and `.env`
11
+ * write, so an alias planted mid-run can never redirect those writes.
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, statSync, } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { defaultSetupStateWriteFs, fsyncParentDirectory, writeAllSync, } from "./checkpoint.js";
17
+ import { SETUP_ERROR_CODES } from "./errors.js";
18
+ import { errorResult } from "./flowResult.js";
19
+ import { findSetupPathCollision } from "./setupPathCollision.js";
20
+ /** The two .env keys the setup CLI manages. */
21
+ export const SETUP_ENV_KEYS = {
22
+ CREDENTIALS: "GOOGLE_APPLICATION_CREDENTIALS",
23
+ SPREADSHEET_URL: "HIKOUTEI_SYNC_SPREADSHEET_URL",
24
+ };
25
+ function isManagedEnvLine(line) {
26
+ return (line.startsWith(`${SETUP_ENV_KEYS.CREDENTIALS}=`) ||
27
+ line.startsWith(`${SETUP_ENV_KEYS.SPREADSHEET_URL}=`));
28
+ }
29
+ /** Prefix of the unique per-run private temp file for the .env write. */
30
+ const ENV_TEMP_PREFIX = ".hikoutei-env-";
31
+ /** .env file permission (owner read/write only). */
32
+ const ENV_FILE_MODE = 0o600;
33
+ /**
34
+ * Writes or updates the .env output file securely and atomically.
35
+ *
36
+ * An existing output must be a regular file at the lstat boundary (a
37
+ * symlink is rejected outright, and directories/FIFOs/devices/sockets are
38
+ * refused before any open so a FIFO can never block), the file is opened
39
+ * WITHOUT following symlinks (`O_NOFOLLOW` where supported) and
40
+ * non-blocking (`O_NONBLOCK` where supported), the descriptor is
41
+ * fstat-verified as a regular file BEFORE a single byte is read (covering
42
+ * a non-regular replacement between the lstat check and the open), and any
43
+ * alias of the credentials file or other reserved paths (hardlink/symlink
44
+ * alias — the key contents are never read through it) is refused before a
45
+ * single byte is read. The preserved env content is built in memory and written to a unique private sibling temp
46
+ * file (`O_CREAT|O_EXCL|O_WRONLY` plus `O_NOFOLLOW`, mode 0600, fsync +
47
+ * close), then atomically renamed over the output: rename replaces the
48
+ * directory entry and never follows a symlink or hardlink planted after
49
+ * validation, so it cannot overwrite the key inode. Cleanup removes only
50
+ * the temp inode this invocation created. Missing file is created; a file
51
+ * whose content is unchanged is not rewritten. An existing file whose
52
+ * owner bits are not exactly 0600 counts as modified and is atomically
53
+ * replaced by a fresh verified-0600 file — never pathname-chmodded — so
54
+ * hardlinks sharing the old inode keep their own mode and content. Throws on filesystem
55
+ * failure or an unsafe existing entry; the caller maps the throw to
56
+ * `output_write_failed`.
57
+ */
58
+ export function writeSetupEnvFile(outputPath, credentialsPath, spreadsheetUrl, reservedPaths = []) {
59
+ const read = readExistingEnvFile(outputPath, [credentialsPath, ...reservedPaths]);
60
+ const existing = read.existing;
61
+ const created = read.created;
62
+ const rawLines = existing === "" ? [] : existing.split("\n");
63
+ // A trailing empty element comes from the final newline; drop it so
64
+ // unrelated content is preserved without a spurious blank line.
65
+ const lines = rawLines.length > 0 && rawLines[rawLines.length - 1] === "" ? rawLines.slice(0, -1) : rawLines;
66
+ const next = [
67
+ ...lines.filter((line) => !isManagedEnvLine(line)),
68
+ `${SETUP_ENV_KEYS.CREDENTIALS}=${credentialsPath}`,
69
+ `${SETUP_ENV_KEYS.SPREADSHEET_URL}=${spreadsheetUrl}`,
70
+ ];
71
+ const content = `${next.join("\n")}\n`;
72
+ // A mode repair is a modification: an existing file whose owner bits are
73
+ // not exactly 0600 must be atomically replaced by a fresh verified-0600
74
+ // file (never pathname-chmodded, so hardlinks sharing the old inode keep
75
+ // their own mode and content).
76
+ const modeNeedsRepair = !created && read.mode !== ENV_FILE_MODE;
77
+ const modified = created || content !== existing || modeNeedsRepair;
78
+ if (modified) {
79
+ atomicWritePrivateFile(outputPath, content);
80
+ }
81
+ return { created, modified };
82
+ }
83
+ /**
84
+ * Reads an existing .env output without following symlinks or aliases.
85
+ *
86
+ * A symlink at the output path is rejected outright (the target could be
87
+ * the key file, and the contents must never be read through an alias). An
88
+ * existing entry must be a regular file at the lstat boundary: directories,
89
+ * FIFOs, devices, and sockets are refused BEFORE any open, so a FIFO can
90
+ * never block the open or a later read. The file is opened with `O_NOFOLLOW`
91
+ * (where supported) plus `O_NONBLOCK` (where supported), the descriptor is
92
+ * fstat-verified as a regular file BEFORE a single byte is read — this
93
+ * covers a non-regular replacement (for example a FIFO planted between the
94
+ * lstat check and the open) and prevents a FIFO open/read from blocking —
95
+ * and the descriptor inode is compared against every reserved path; any
96
+ * match is refused before reading. A missing file is `created` with empty
97
+ * content. Throws on unsafe entries; the caller maps throws to
98
+ * `output_write_failed`.
99
+ */
100
+ function readExistingEnvFile(outputPath, reservedPaths) {
101
+ try {
102
+ const lst = lstatSync(outputPath);
103
+ if (lst.isSymbolicLink()) {
104
+ throw new Error(`refusing to follow a symlink at the output path ${outputPath}; remove the symlink and retry`);
105
+ }
106
+ if (!lst.isFile()) {
107
+ // Directories, FIFOs, devices, and sockets are never opened or read:
108
+ // a FIFO open without O_NONBLOCK would block indefinitely.
109
+ throw new Error(`the output path ${outputPath} is not a regular file; remove it and retry`);
110
+ }
111
+ }
112
+ catch (error) {
113
+ if (isNodeError(error) && error.code === "ENOENT") {
114
+ return { existing: "", created: true, mode: 0 };
115
+ }
116
+ throw error;
117
+ }
118
+ let fd;
119
+ try {
120
+ fd = openSync(outputPath, constants.O_RDONLY | noFollowFlag() | nonBlockFlag());
121
+ }
122
+ catch (error) {
123
+ if (isNodeError(error) && (error.code === "ELOOP" || error.code === "EMLINK")) {
124
+ // The entry became a symlink between the lstat check and the open
125
+ // (or the platform lacks O_NOFOLLOW): refuse rather than follow.
126
+ throw new Error(`refusing to follow a symlink at the output path ${outputPath}; remove the symlink and retry`);
127
+ }
128
+ throw error;
129
+ }
130
+ try {
131
+ const stat = fstatSync(fd);
132
+ if (!stat.isFile()) {
133
+ // A non-regular entry replaced the output between the lstat check and
134
+ // the open (e.g. a FIFO planted mid-run): refuse without reading it
135
+ // (O_NONBLOCK made the open return instead of blocking on a FIFO).
136
+ throw new Error(`the output path ${outputPath} is not a regular file; remove it and retry`);
137
+ }
138
+ const alias = reservedPaths.find((path) => sameInodeAsPath(stat, path));
139
+ if (alias !== undefined) {
140
+ throw new Error(`the output path ${outputPath} aliases the reserved file ${alias}; refusing to read or write through it`);
141
+ }
142
+ // The owner bits come from the SAME secured descriptor read, so the
143
+ // mode-repair decision below can never race a check-then-use sequence.
144
+ return { existing: readFileSync(fd, "utf8"), created: false, mode: Number(stat.mode) & 0o777 };
145
+ }
146
+ finally {
147
+ try {
148
+ closeSync(fd);
149
+ }
150
+ catch {
151
+ // The read error is the one to report.
152
+ }
153
+ }
154
+ }
155
+ /** True when the given stat names the same device/inode as the path. */
156
+ function sameInodeAsPath(stat, path) {
157
+ try {
158
+ const other = statSync(path);
159
+ return stat.dev === other.dev && stat.ino === other.ino && other.ino !== 0;
160
+ }
161
+ catch {
162
+ return false;
163
+ }
164
+ }
165
+ /**
166
+ * Writes content to the output path atomically via a unique private temp.
167
+ *
168
+ * The temp file (PID + UUID sibling name) is created with exclusive
169
+ * no-follow flags and mode 0600, owner-only mode is applied and verified
170
+ * THROUGH the still-open descriptor (`fchmod` + `fstat`) before the content
171
+ * is written and fsynced, the path is re-verified against the created
172
+ * inode, and the file is renamed over the output. No pathname `chmod` is
173
+ * ever performed. Rename replaces the directory entry rather than
174
+ * following a symlink/hardlink planted after validation. Cleanup removes
175
+ * only the temp inode this invocation created. Throws on failure. `fs` is
176
+ * injectable so tests can prove the descriptor-mode behavior.
177
+ *
178
+ * Exported for the injected descriptor-mode regression; the flow calls it
179
+ * through `writeSetupEnvFile` with the default filesystem.
180
+ */
181
+ export function atomicWritePrivateFile(outputPath, content, fs = defaultSetupStateWriteFs) {
182
+ const tempPath = join(dirname(outputPath), `${ENV_TEMP_PREFIX}${process.pid}-${randomUUID()}.tmp`);
183
+ let fd;
184
+ let tempDev = 0;
185
+ let tempIno = 0;
186
+ try {
187
+ fd = fs.openSync(tempPath, exclusivePrivateOpenFlags(), ENV_FILE_MODE);
188
+ }
189
+ catch (error) {
190
+ if (isNodeError(error) && error.code === "EEXIST") {
191
+ // A pre-existing entry at the unique temp path is never followed,
192
+ // truncated, or unlinked.
193
+ throw new Error(`a conflicting entry appeared at the env temp path ${tempPath}; nothing was written`);
194
+ }
195
+ throw error;
196
+ }
197
+ try {
198
+ const stat = fs.fstatSync(fd);
199
+ tempDev = stat.dev;
200
+ tempIno = stat.ino;
201
+ // Owner-only mode is applied and verified on the STILL-OPEN descriptor
202
+ // before any content is written, so the final mode never depends on the
203
+ // process umask and a pathname chmod race is impossible.
204
+ fs.fchmodSync(fd, ENV_FILE_MODE);
205
+ const secured = fs.fstatSync(fd);
206
+ if ((Number(secured.mode) & 0o777) !== ENV_FILE_MODE) {
207
+ throw new Error(`could not verify owner-only permissions on the env temp file ${tempPath}`);
208
+ }
209
+ writeAllSync(fd, content, fs.writeSync);
210
+ fs.fsyncSync(fd);
211
+ }
212
+ catch (error) {
213
+ try {
214
+ fs.closeSync(fd);
215
+ }
216
+ catch {
217
+ // The write error is the one to report.
218
+ }
219
+ removeOwnedTempFile(tempPath, tempDev, tempIno, fs);
220
+ throw error;
221
+ }
222
+ try {
223
+ fs.closeSync(fd);
224
+ }
225
+ catch (error) {
226
+ // A close failure after a successful write/fsync still leaves the temp
227
+ // inode this invocation created: remove it (identity-checked) and
228
+ // rethrow. The rename must never run after a failed close, and the
229
+ // descriptor is never closed twice.
230
+ removeOwnedTempFile(tempPath, tempDev, tempIno, fs);
231
+ throw error;
232
+ }
233
+ // Re-verify the temp path still names the inode we created before the
234
+ // rename; a swapped alias must never be renamed onto the output.
235
+ if (!pathNamesInode(tempPath, tempDev, tempIno, fs)) {
236
+ removeOwnedTempFile(tempPath, tempDev, tempIno, fs);
237
+ throw new Error(`the env temp path ${tempPath} changed while writing; nothing was written`);
238
+ }
239
+ try {
240
+ // The mode was already applied and verified on the descriptor above;
241
+ // NO pathname chmod is performed here.
242
+ fs.renameSync(tempPath, outputPath);
243
+ }
244
+ catch (error) {
245
+ removeOwnedTempFile(tempPath, tempDev, tempIno, fs);
246
+ throw error;
247
+ }
248
+ // The rename is durable only after the containing directory is fsynced
249
+ // (same durability step as the checkpoint save): without it, a power loss
250
+ // right after the rename could leave the old entry (or nothing) at the
251
+ // output path. A failure here reports the write as failed WITHOUT
252
+ // rolling back the rename — the destination is already in place.
253
+ fsyncParentDirectory(dirname(outputPath), fs);
254
+ }
255
+ /** `O_NOFOLLOW` where the platform defines it, else 0. */
256
+ export function noFollowFlag() {
257
+ return constants.O_NOFOLLOW ?? 0;
258
+ }
259
+ /** `O_NONBLOCK` where the platform defines it, else 0. */
260
+ export function nonBlockFlag() {
261
+ return constants.O_NONBLOCK ?? 0;
262
+ }
263
+ /** Exclusive no-follow create flags for a private temp file. */
264
+ function exclusivePrivateOpenFlags() {
265
+ return constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | noFollowFlag();
266
+ }
267
+ /** True when the path currently names exactly the given device/inode. */
268
+ function pathNamesInode(path, dev, ino, fs) {
269
+ try {
270
+ const stat = fs.lstatSync(path);
271
+ return stat.dev === dev && stat.ino === ino;
272
+ }
273
+ catch {
274
+ return false;
275
+ }
276
+ }
277
+ /**
278
+ * Removes the temp file only when it is still the inode this invocation
279
+ * created; a replaced or foreign entry is never touched.
280
+ */
281
+ function removeOwnedTempFile(tempPath, dev, ino, fs) {
282
+ try {
283
+ if (pathNamesInode(tempPath, dev, ino, fs)) {
284
+ fs.unlinkSync(tempPath);
285
+ }
286
+ }
287
+ catch {
288
+ // Missing or already replaced: nothing of ours to clean up.
289
+ }
290
+ }
291
+ /**
292
+ * Re-runs the reserved-path collision check and fails closed when aliases
293
+ * changed after the initial preflight.
294
+ *
295
+ * The reserved paths (key, output, checkpoint, temp, lock) are re-resolved
296
+ * immediately before every checkpoint write and the .env write: a symlink
297
+ * or hardlink planted after the preflight must never redirect a write to a
298
+ * reserved file. Returns an error result on collision, `null` when safe.
299
+ */
300
+ export function revalidateSetupPaths(options) {
301
+ const collision = findSetupPathCollision({
302
+ keyPath: options.keyPath,
303
+ outputPath: options.outputPath,
304
+ statePath: options.statePath,
305
+ });
306
+ if (collision.status === "collision") {
307
+ return errorResult(SETUP_ERROR_CODES.INVALID_ARGS, `${collision.message}; the reserved setup paths changed during the run, so nothing was written`);
308
+ }
309
+ return null;
310
+ }
311
+ function isNodeError(error) {
312
+ return error instanceof Error && "code" in error;
313
+ }
314
+ //# sourceMappingURL=envFileWriter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envFileWriter.js","sourceRoot":"","sources":["../../src/cli/envFileWriter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Service-account key material handling for `hikoutei setup`.
3
+ *
4
+ * The secure key-file reading and validation logic of the setup flow,
5
+ * extracted verbatim: parse/validate raw key JSON, securely read an existing
6
+ * key file through ONE descriptor boundary (no-follow, non-block,
7
+ * lstat/fstat inode binding, owner-only mode 0600 applied and verified
8
+ * through the still-open descriptor before any read, sanitized path-only
9
+ * failures), and promote either the non-secret metadata or the validated
10
+ * in-memory credential. The private key exists only in process memory for
11
+ * the run and never appears in results, messages, the checkpoint, or the
12
+ * `.env` file.
13
+ */
14
+ import type { KeyMetadataResult, SecureKeyReadResult } from "./checkpoint.js";
15
+ import { type Stats } from "node:fs";
16
+ /**
17
+ * Parses and validates raw service-account key JSON (no filesystem access).
18
+ *
19
+ * Shared by the secure descriptor read and by tests. The key material is
20
+ * never returned; only the non-secret metadata is promoted. The validated
21
+ * in-memory key material is available to the credential reader
22
+ * (`readServiceAccountKeyCredentialSecurely`) for the SA verify phase.
23
+ */
24
+ export declare function parseServiceAccountKeyJson(raw: string, sourceLabel: string): KeyMetadataResult;
25
+ /**
26
+ * Filesystem operations the secure key read uses; injectable for tests.
27
+ */
28
+ export interface KeyFileFs {
29
+ lstatSync(path: string): Stats;
30
+ openSync(path: string, flags: number): number;
31
+ fchmodSync(fd: number, mode: number): void;
32
+ fstatSync(fd: number): Stats;
33
+ readFileSync(fd: number, encoding: "utf8"): string;
34
+ closeSync(fd: number): void;
35
+ }
36
+ /**
37
+ * Securely reads an existing service-account key file through ONE
38
+ * descriptor boundary and enforces owner-only mode 0600.
39
+ *
40
+ * A missing file is `absent`. An existing entry must be a regular file (a
41
+ * symlink is refused outright; directories, FIFOs, and sockets are refused
42
+ * too). The file is opened with `O_NOFOLLOW` (where the platform defines it
43
+ * — a symlink swapped in between the type check and the open fails with
44
+ * ELOOP/EMLINK and is refused) plus `O_NONBLOCK` (where the platform
45
+ * defines it — a FIFO swapped in between the type check and the open
46
+ * returns from the open instead of blocking), the descriptor is
47
+ * fstat-verified as a regular file with the SAME device/inode the lstat
48
+ * observed BEFORE any fchmod (a directory, FIFO, socket, device, or
49
+ * replaced-file entry is refused with zero chmod and zero read calls), and
50
+ * only then is mode 0600 applied THROUGH the open descriptor
51
+ * with `fchmod` and the resulting mode verified on the same descriptor
52
+ * before a single byte is read — never an existsSync/readFileSync
53
+ * check-then-use sequence, so an alias planted mid-read cannot receive the
54
+ * chmod or be read through. Only after the descriptor is secured is the
55
+ * content parsed and validated (JSON shape, RSA private key, project,
56
+ * client email, and non-secret key id). Any inspect/open/type/chmod/read/
57
+ * parse failure fails closed with a stable path-only message that never
58
+ * contains raw error text or key material, and the descriptor is closed
59
+ * exactly once (a close failure never overrides the verdict). Supported
60
+ * platforms (macOS/Linux) can
61
+ * enforce owner-only modes; Windows automatic setup is refused before this
62
+ * code runs.
63
+ */
64
+ export declare function readServiceAccountKeySecurely(keyPath: string, fs?: KeyFileFs): SecureKeyReadResult;
65
+ /**
66
+ * Result of the secure, descriptor-based key credential read.
67
+ *
68
+ * Same descriptor boundary as `readServiceAccountKeySecurely`, but the
69
+ * validated `private_key` is promoted into process memory for the SA
70
+ * access verify phase: the verifier is given the credentials in memory and
71
+ * NEVER reopens the key pathname, so a mid-run replacement cannot redirect
72
+ * it. The key material exists only in memory for the run; it is never
73
+ * returned by the setup result, never written to the checkpoint or `.env`,
74
+ * and never included in any error message.
75
+ */
76
+ export type SecureKeyCredentialReadResult = {
77
+ readonly status: "absent";
78
+ } | {
79
+ readonly status: "ok";
80
+ readonly credentials: {
81
+ readonly projectId: string;
82
+ readonly clientEmail: string;
83
+ readonly privateKey: string;
84
+ };
85
+ } | {
86
+ readonly status: "invalid";
87
+ readonly message: string;
88
+ };
89
+ /**
90
+ * Securely reads an existing service-account key file and promotes the
91
+ * validated credential into process memory.
92
+ *
93
+ * Identical descriptor security to `readServiceAccountKeySecurely`
94
+ * (regular file, no-follow, non-block, lstat/fstat inode binding, owner-only
95
+ * 0600 through the descriptor before any read, sanitized failures), but
96
+ * the returned credentials carry the validated `privateKey` so the SA
97
+ * access verifier can authenticate from memory. The private key is only in
98
+ * process memory for the run and never appears in results, messages, the
99
+ * checkpoint, or the `.env` file.
100
+ */
101
+ export declare function readServiceAccountKeyCredentialSecurely(keyPath: string, fs?: KeyFileFs): SecureKeyCredentialReadResult;
102
+ //# sourceMappingURL=keyMaterial.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyMaterial.d.ts","sourceRoot":"","sources":["../../src/cli/keyMaterial.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAI9E,OAAO,EAQL,KAAK,KAAK,EACX,MAAM,SAAS,CAAC;AAEjB;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,CAO9F;AAmED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9C,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7B,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IACnD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,EACf,EAAE,GAAE,SAA4B,GAC/B,mBAAmB,CAOrB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,6BAA6B,GACrC;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,GAC7B;IACA,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;KAC7B,CAAC;CACH,GACC;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,wBAAgB,uCAAuC,CACrD,OAAO,EAAE,MAAM,EACf,EAAE,GAAE,SAA4B,GAC/B,6BAA6B,CAO/B"}