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.
- package/dist/cli/checkpoint.d.ts +1 -87
- package/dist/cli/checkpoint.d.ts.map +1 -1
- package/dist/cli/checkpoint.js +6 -290
- package/dist/cli/checkpoint.js.map +1 -1
- package/dist/cli/envFileWriter.d.ts +85 -0
- package/dist/cli/envFileWriter.d.ts.map +1 -0
- package/dist/cli/envFileWriter.js +314 -0
- package/dist/cli/envFileWriter.js.map +1 -0
- package/dist/cli/keyMaterial.d.ts +102 -0
- package/dist/cli/keyMaterial.d.ts.map +1 -0
- package/dist/cli/keyMaterial.js +305 -0
- package/dist/cli/keyMaterial.js.map +1 -0
- package/dist/cli/setupFlow.d.ts +3 -91
- package/dist/cli/setupFlow.d.ts.map +1 -1
- package/dist/cli/setupFlow.js +8 -423
- package/dist/cli/setupFlow.js.map +1 -1
- package/dist/cli/setupPathCollision.d.ts +45 -0
- package/dist/cli/setupPathCollision.d.ts.map +1 -0
- package/dist/cli/setupPathCollision.js +143 -0
- package/dist/cli/setupPathCollision.js.map +1 -0
- package/package.json +1 -1
|
@@ -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"}
|