@cortexkit/opencode-antigravity-auth 3.0.3 → 3.0.5
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/README.md +7 -7
- package/dist/cli.js +1722 -484
- package/dist/cli.js.map +4 -4
- package/dist/cli.meta.json +440 -120
- package/dist/index.js +2157 -803
- package/dist/index.js.map +4 -4
- package/dist/index.meta.json +476 -141
- package/dist/server.js +2248 -841
- package/dist/server.js.map +4 -4
- package/dist/server.meta.json +1 -1
- package/dist/src/common-auth-embedded/claustrum/enrollment.d.ts +3 -2
- package/dist/src/common-auth-embedded/claustrum/enrollment.js +84 -42
- package/dist/src/common-auth-embedded/claustrum/roster.js +2 -2
- package/dist/src/common-auth-embedded/fs/atomic-write.d.ts +5 -0
- package/dist/src/common-auth-embedded/fs/atomic-write.js +21 -0
- package/dist/src/common-auth-embedded/fs/index.d.ts +2 -0
- package/dist/src/common-auth-embedded/fs/index.js +1 -0
- package/dist/src/common-auth-embedded/fs/private-files.d.ts +66 -0
- package/dist/src/common-auth-embedded/fs/private-files.js +302 -0
- package/dist/src/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
- package/dist/src/common-auth-embedded/fs/refresh-file-lock.js +254 -37
- package/dist/src/common-auth-embedded/fs/with-lock.d.ts +3 -0
- package/dist/src/common-auth-embedded/fs/with-lock.js +4 -0
- package/dist/src/common-auth-embedded/logger/engine.d.ts +6 -8
- package/dist/src/common-auth-embedded/logger/engine.js +126 -11
- package/dist/src/common-auth-embedded/pi-slot/slot.d.ts +2 -0
- package/dist/src/common-auth-embedded/pi-slot/slot.js +34 -6
- package/dist/src/common-auth-embedded/rpc/port-file.js +30 -8
- package/dist/src/common-auth-embedded/rpc/rpc-client.js +15 -1
- package/dist/src/common-auth-embedded/rpc/rpc-server.js +8 -3
- package/dist/src/common-auth-embedded/source-output.json +92 -76
- package/dist/src/common-auth-embedded/store/mutate.js +2 -2
- package/dist/src/common-auth-embedded/store/pool.d.ts +1 -0
- package/dist/src/common-auth-embedded/store/pool.js +3 -0
- package/dist/src/common-auth-embedded/store/refresh-lock.d.ts +4 -0
- package/dist/src/common-auth-embedded/store/refresh-lock.js +5 -0
- package/dist/src/ga/rpc/protocol.d.ts +2 -3
- package/dist/src/ga/rpc/protocol.d.ts.map +1 -1
- package/dist/src/ga/rpc/protocol.js +60 -0
- package/dist/src/ga/rpc/protocol.js.map +1 -1
- package/dist/src/ga/server/index.d.ts +3 -2
- package/dist/src/ga/server/index.d.ts.map +1 -1
- package/dist/src/ga/server/index.js +23 -7
- package/dist/src/ga/server/index.js.map +1 -1
- package/dist/src/plugin/request.d.ts.map +1 -1
- package/dist/src/plugin/request.js +8 -14
- package/dist/src/plugin/request.js.map +1 -1
- package/dist/src/plugin/vault-custody.d.ts +6 -0
- package/dist/src/plugin/vault-custody.d.ts.map +1 -1
- package/dist/src/plugin/vault-custody.js +81 -2
- package/dist/src/plugin/vault-custody.js.map +1 -1
- package/dist/tui-build-map.json +29 -13
- package/dist/tui-ga-build-map.json +1 -1
- package/dist/types/src/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
- package/dist/types/src/common-auth-embedded/logger/engine.d.ts +6 -8
- package/dist/types/src/common-auth-embedded/store/pool.d.ts +1 -0
- package/dist/types/src/common-auth-embedded/store/refresh-lock.d.ts +4 -0
- package/dist/types/src/ga/rpc/protocol.d.ts +2 -3
- package/dist/types/src/ga/rpc/protocol.d.ts.map +1 -1
- package/dist/types/src/ga/server/index.d.ts +3 -2
- package/dist/types/src/ga/server/index.d.ts.map +1 -1
- package/dist/types/src/plugin/request.d.ts.map +1 -1
- package/dist/types/src/plugin/vault-custody.d.ts +6 -0
- package/dist/types/src/plugin/vault-custody.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/common-auth-embedded/claustrum/enrollment.d.ts +3 -2
- package/src/common-auth-embedded/claustrum/enrollment.js +84 -42
- package/src/common-auth-embedded/claustrum/roster.js +2 -2
- package/src/common-auth-embedded/fs/atomic-write.d.ts +5 -0
- package/src/common-auth-embedded/fs/atomic-write.js +21 -0
- package/src/common-auth-embedded/fs/index.d.ts +2 -0
- package/src/common-auth-embedded/fs/index.js +1 -0
- package/src/common-auth-embedded/fs/private-files.d.ts +66 -0
- package/src/common-auth-embedded/fs/private-files.js +302 -0
- package/src/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
- package/src/common-auth-embedded/fs/refresh-file-lock.js +254 -37
- package/src/common-auth-embedded/fs/with-lock.d.ts +3 -0
- package/src/common-auth-embedded/fs/with-lock.js +4 -0
- package/src/common-auth-embedded/logger/engine.d.ts +6 -8
- package/src/common-auth-embedded/logger/engine.js +126 -11
- package/src/common-auth-embedded/pi-slot/slot.d.ts +2 -0
- package/src/common-auth-embedded/pi-slot/slot.js +34 -6
- package/src/common-auth-embedded/rpc/port-file.js +30 -8
- package/src/common-auth-embedded/rpc/rpc-client.js +15 -1
- package/src/common-auth-embedded/rpc/rpc-server.js +8 -3
- package/src/common-auth-embedded/source-output.json +92 -76
- package/src/common-auth-embedded/store/mutate.js +2 -2
- package/src/common-auth-embedded/store/pool.d.ts +1 -0
- package/src/common-auth-embedded/store/pool.js +3 -0
- package/src/common-auth-embedded/store/refresh-lock.d.ts +4 -0
- package/src/common-auth-embedded/store/refresh-lock.js +5 -0
- package/src/tui-compiled/common-auth-embedded/fs/private-files.js +302 -0
- package/src/tui-compiled/common-auth-embedded/logger/engine.js +126 -11
- package/src/tui-compiled/common-auth-embedded/rpc/port-file.js +30 -8
- package/src/tui-compiled/common-auth-embedded/rpc/rpc-client.js +15 -1
- package/src/tui-raw/common-auth-embedded/fs/private-files.js +302 -0
- package/src/tui-raw/common-auth-embedded/logger/engine.js +126 -11
- package/src/tui-raw/common-auth-embedded/rpc/port-file.js +30 -8
- package/src/tui-raw/common-auth-embedded/rpc/rpc-client.js +15 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
export const windowsPrivateFiles = process.platform === 'win32';
|
|
5
|
+
const migrationNotices = new Set();
|
|
6
|
+
function ioCode(detail) {
|
|
7
|
+
// Native errors retain their Win32 code. Only demonstrated absence/exclusive
|
|
8
|
+
// collisions are normalized; access/sharing failures never become lost races.
|
|
9
|
+
if (detail?.reason === 'io-error') {
|
|
10
|
+
if (detail.win32Code === 2 || detail.win32Code === 3)
|
|
11
|
+
return 'ENOENT';
|
|
12
|
+
if (detail.win32Code === 80 || detail.win32Code === 183)
|
|
13
|
+
return 'EEXIST';
|
|
14
|
+
}
|
|
15
|
+
return detail?.code ?? 'ERR_PRIVATE_FILE';
|
|
16
|
+
}
|
|
17
|
+
// Production loads this package only on Windows; POSIX keeps its own directory policy.
|
|
18
|
+
let backend;
|
|
19
|
+
/** Tests inject a backend here; production always imports the private-files package. */
|
|
20
|
+
export function setPrivateFilesBackendForTest(api) {
|
|
21
|
+
backend = api ? Promise.resolve(api) : undefined;
|
|
22
|
+
}
|
|
23
|
+
function loadBackend() {
|
|
24
|
+
backend ??= import("@cortexkit/private-files");
|
|
25
|
+
return backend;
|
|
26
|
+
}
|
|
27
|
+
export class PrivateFileError extends Error {
|
|
28
|
+
path;
|
|
29
|
+
stage;
|
|
30
|
+
code;
|
|
31
|
+
win32Code;
|
|
32
|
+
reason;
|
|
33
|
+
published = false;
|
|
34
|
+
durabilityConfirmed = false;
|
|
35
|
+
fileDurable = false;
|
|
36
|
+
attempts = 0;
|
|
37
|
+
elapsedMs = 0;
|
|
38
|
+
orphanPath;
|
|
39
|
+
constructor(path, stage, cause) {
|
|
40
|
+
const detail = cause;
|
|
41
|
+
super(`Private file refused at ${stage}: ${path} (${detail?.reason ?? detail?.code ?? 'io-error'}). Use a safe parent directory. Repair only a named secret file in safe ancestry whose owner the published policy trusts, without changing its owner or any existing directory. Previously exposed credentials may need rotation. Windows requires the private-files native prebuild and an ACL-persisting local filesystem such as NTFS.`, { cause });
|
|
42
|
+
this.path = path;
|
|
43
|
+
this.stage = stage;
|
|
44
|
+
this.name = 'PrivateFileError';
|
|
45
|
+
this.code = ioCode(detail);
|
|
46
|
+
this.win32Code = detail?.win32Code;
|
|
47
|
+
this.reason = detail?.reason;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function privateOperation(path, stage, operation) {
|
|
51
|
+
try {
|
|
52
|
+
return await operation();
|
|
53
|
+
}
|
|
54
|
+
catch (cause) {
|
|
55
|
+
if (cause instanceof PrivateFileError)
|
|
56
|
+
throw cause;
|
|
57
|
+
throw new PrivateFileError(path, stage, cause);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Tests inject metadata into safe-parent checks, including checks deep inside
|
|
61
|
+
// callers that take no policy options. Production never sets this.
|
|
62
|
+
let safeParentOptionsForTest;
|
|
63
|
+
/** Tests only: policy options used by safe-parent checks that pass none. */
|
|
64
|
+
export function setSafeParentOptionsForTest(options) {
|
|
65
|
+
safeParentOptionsForTest = options;
|
|
66
|
+
}
|
|
67
|
+
/** Existing containing directories use the shared entry-integrity policy, not secret-file permissions. */
|
|
68
|
+
export async function verifySafeDirectory(path, options) {
|
|
69
|
+
path = resolve(path);
|
|
70
|
+
options ??= safeParentOptionsForTest;
|
|
71
|
+
await privateOperation(path, 'safe-parent', async () => {
|
|
72
|
+
const result = await (await loadBackend()).verifySafeParent(path, options);
|
|
73
|
+
if (result.ok === false) {
|
|
74
|
+
// Preserve an absent parent's ENOENT so callers can create it privately.
|
|
75
|
+
if (result.reason === 'io-error' || result.reason === 'wrong-kind')
|
|
76
|
+
await lstat(path);
|
|
77
|
+
throw new PrivateFileError(result.level ?? result.path, 'safe-parent', {
|
|
78
|
+
...result,
|
|
79
|
+
code: 'ERR_PRIVATE_FILES_SECURITY',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** Repair only the specific owned secret file supplied by a caller, never an existing directory tree. */
|
|
85
|
+
export async function verifyPrivate(path, kind, migrate = false) {
|
|
86
|
+
path = resolve(path);
|
|
87
|
+
if (kind === 'directory')
|
|
88
|
+
return verifySafeDirectory(path);
|
|
89
|
+
await privateOperation(path, 'verify', async () => {
|
|
90
|
+
const api = await loadBackend();
|
|
91
|
+
const result = await api.verifyOwnerOnly(path);
|
|
92
|
+
let repaired = false;
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
const { reason } = result.refusal;
|
|
95
|
+
// The result API intentionally omits native IO codes. Use the throwing
|
|
96
|
+
// API to preserve absence/collision details, never infer them from text.
|
|
97
|
+
if (reason === 'io-error')
|
|
98
|
+
await api.verifyPrivatePath(path, kind);
|
|
99
|
+
// DACL replacement repairs permissions only, never ownership or unsafe ancestry.
|
|
100
|
+
if (migrate &&
|
|
101
|
+
[
|
|
102
|
+
api.EXTRA_ACE,
|
|
103
|
+
api.NOT_PROTECTED,
|
|
104
|
+
api.NULL_DACL,
|
|
105
|
+
api.EMPTY_DACL,
|
|
106
|
+
].includes(reason)) {
|
|
107
|
+
await verifySafeDirectory(dirname(path));
|
|
108
|
+
await api.securePrivatePath(path, kind);
|
|
109
|
+
repaired = true;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw Object.assign(new Error('Owner-only verification refused'), {
|
|
113
|
+
reason,
|
|
114
|
+
code: 'ERR_PRIVATE_FILES_SECURITY',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Verify the expected object kind and its permissions by handle after repair.
|
|
119
|
+
await api.verifyPrivatePath(path, kind);
|
|
120
|
+
if (repaired && !migrationNotices.has(path)) {
|
|
121
|
+
migrationNotices.add(path);
|
|
122
|
+
process.emitWarning(`Private permissions tightened: ${path}. This object may have been readable before; rotate credentials if needed.`, 'PrivateFileMigration');
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** Create missing components privately; inspect existing ancestors without modifying them. */
|
|
127
|
+
export async function ensurePrivateDirectory(path, _migrate = false, options) {
|
|
128
|
+
path = resolve(path);
|
|
129
|
+
await privateOperation(path, 'directory', async () => {
|
|
130
|
+
const api = await loadBackend();
|
|
131
|
+
try {
|
|
132
|
+
await lstat(path);
|
|
133
|
+
return await verifySafeDirectory(path, options);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (ioCode(error) !== 'ENOENT')
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
const parent = dirname(path);
|
|
140
|
+
if (parent === path)
|
|
141
|
+
throw Object.assign(new Error('Missing root'), { code: 'ENOENT' });
|
|
142
|
+
await ensurePrivateDirectory(parent, false, options);
|
|
143
|
+
try {
|
|
144
|
+
await api.createOwnerOnly(path, { kind: 'dir' });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (ioCode(error) !== 'EEXIST')
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
await verifySafeDirectory(path, options);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export async function readPrivateText(path, migrate = false) {
|
|
154
|
+
if (!windowsPrivateFiles)
|
|
155
|
+
return readFile(path, 'utf8');
|
|
156
|
+
return (await readPrivateBuffer(path, migrate)).toString('utf8');
|
|
157
|
+
}
|
|
158
|
+
export async function readPrivateBuffer(path, migrate = false) {
|
|
159
|
+
if (!windowsPrivateFiles)
|
|
160
|
+
return readFile(path);
|
|
161
|
+
path = resolve(path);
|
|
162
|
+
if (migrate) {
|
|
163
|
+
await verifySafeDirectory(dirname(path));
|
|
164
|
+
await verifyPrivate(path, 'file', true);
|
|
165
|
+
}
|
|
166
|
+
return privateOperation(path, 'read', async () => (await loadBackend()).readOwnerOnly(path));
|
|
167
|
+
}
|
|
168
|
+
export async function createPrivateBytes(path, bytes) {
|
|
169
|
+
await verifySafeDirectory(dirname(resolve(path)));
|
|
170
|
+
await privateOperation(path, 'create', async () => {
|
|
171
|
+
// The backend verifies the creation handle while empty, before consuming bytes.
|
|
172
|
+
await (await loadBackend()).createPrivateFile(resolve(path), bytes);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export async function createPrivateDirectoryExclusive(path) {
|
|
176
|
+
await verifySafeDirectory(dirname(resolve(path)));
|
|
177
|
+
await privateOperation(path, 'directory-create', async () => {
|
|
178
|
+
await (await loadBackend()).createOwnerOnly(resolve(path), { kind: 'dir' });
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Retry Windows rename sharing/access errors within a budget; they never prove lease contention. */
|
|
182
|
+
export async function replaceWindowsFile(from, to, options = {}) {
|
|
183
|
+
from = resolve(from);
|
|
184
|
+
to = resolve(to);
|
|
185
|
+
const now = options.now ?? (() => performance.now());
|
|
186
|
+
const start = now();
|
|
187
|
+
const deadline = Math.min(start + 500, options.deadline ?? Infinity);
|
|
188
|
+
const delays = [0, 10, 20, 40, 80, 100];
|
|
189
|
+
let original;
|
|
190
|
+
let attempts = 0;
|
|
191
|
+
for (const delay of delays) {
|
|
192
|
+
if (now() >= deadline)
|
|
193
|
+
break;
|
|
194
|
+
if (delay)
|
|
195
|
+
await (options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(Math.min(delay, Math.max(0, deadline - now())));
|
|
196
|
+
if (now() >= deadline)
|
|
197
|
+
break;
|
|
198
|
+
await options.beforeRename?.();
|
|
199
|
+
if (now() >= deadline)
|
|
200
|
+
break;
|
|
201
|
+
attempts++;
|
|
202
|
+
try {
|
|
203
|
+
await (options.rename ?? rename)(from, to);
|
|
204
|
+
return { attempts, elapsedMs: now() - start };
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
original ??= error;
|
|
208
|
+
const detail = error;
|
|
209
|
+
const candidate = detail.win32Code !== undefined
|
|
210
|
+
? detail.win32Code === 32 || detail.win32Code === 33
|
|
211
|
+
: ['EBUSY', 'EPERM', 'EACCES'].includes(detail.code ?? '') &&
|
|
212
|
+
!detail.reason;
|
|
213
|
+
if (!candidate)
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const error = new PrivateFileError(to, 'rename', original ??
|
|
218
|
+
Object.assign(new Error('Publication deadline expired'), {
|
|
219
|
+
code: 'ETIMEDOUT',
|
|
220
|
+
}));
|
|
221
|
+
error.attempts = attempts;
|
|
222
|
+
error.elapsedMs = now() - start;
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
export async function writePrivateAtomic(path, bytes, options = {}) {
|
|
226
|
+
path = resolve(path);
|
|
227
|
+
await ensurePrivateDirectory(dirname(path));
|
|
228
|
+
try {
|
|
229
|
+
await verifyPrivate(path, 'file', true);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code !== 'ENOENT')
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
const stage = resolve(options.stagePath ?? `${path}.${randomUUID()}.tmp`);
|
|
236
|
+
if (dirname(stage) !== dirname(path))
|
|
237
|
+
throw new PrivateFileError(path, 'stage-directory', { code: 'EXDEV' });
|
|
238
|
+
let created = false;
|
|
239
|
+
let published = false;
|
|
240
|
+
let outcome = { attempts: 0, elapsedMs: 0 };
|
|
241
|
+
let stageName = 'stage-create';
|
|
242
|
+
const reportOutcome = (error) => {
|
|
243
|
+
try {
|
|
244
|
+
options.onOutcome?.({
|
|
245
|
+
path,
|
|
246
|
+
stage: error?.stage ?? 'published',
|
|
247
|
+
published,
|
|
248
|
+
fileDurable: published,
|
|
249
|
+
durabilityConfirmed: false,
|
|
250
|
+
attempts: error?.attempts ?? outcome.attempts,
|
|
251
|
+
elapsedMs: error?.elapsedMs ?? outcome.elapsedMs,
|
|
252
|
+
...(error && { code: error.code, win32Code: error.win32Code }),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Outcome observers cannot change whether publication succeeded.
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
await createPrivateBytes(stage, bytes);
|
|
261
|
+
created = true;
|
|
262
|
+
stageName = 'before-rename';
|
|
263
|
+
outcome = await replaceWindowsFile(stage, path, options);
|
|
264
|
+
created = false;
|
|
265
|
+
published = true;
|
|
266
|
+
stageName = 'published-callback';
|
|
267
|
+
options.onRenamed?.();
|
|
268
|
+
stageName = 'published-verify';
|
|
269
|
+
await verifyPrivate(path, 'file');
|
|
270
|
+
// createPrivateFile flushes the file. Directory metadata durability has not
|
|
271
|
+
// been qualified on Windows, so never promise the POSIX directory fsync.
|
|
272
|
+
if (options.durable && options.durability !== 'file-only-on-windows')
|
|
273
|
+
throw new PrivateFileError(path, 'directory-sync-unsupported', {
|
|
274
|
+
code: 'ENOTSUP',
|
|
275
|
+
});
|
|
276
|
+
reportOutcome();
|
|
277
|
+
}
|
|
278
|
+
catch (cause) {
|
|
279
|
+
const error = cause instanceof PrivateFileError
|
|
280
|
+
? cause
|
|
281
|
+
: new PrivateFileError(path, stageName, cause);
|
|
282
|
+
error.published = published;
|
|
283
|
+
error.fileDurable = published;
|
|
284
|
+
if (published)
|
|
285
|
+
Object.assign(error, outcome);
|
|
286
|
+
if (created) {
|
|
287
|
+
try {
|
|
288
|
+
await unlink(stage);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
error.orphanPath = stage;
|
|
292
|
+
}
|
|
293
|
+
created = false;
|
|
294
|
+
}
|
|
295
|
+
reportOutcome(error);
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
if (created)
|
|
300
|
+
await unlink(stage).catch(() => { });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { appendFileSync, chmodSync, existsSync, renameSync, statSync, } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { createPrivateBytes, ensurePrivateDirectory, privateOperation, replaceWindowsFile, verifyPrivate, windowsPrivateFiles, } from "../fs/private-files.js";
|
|
2
5
|
import { createRedactor } from "./redact.js";
|
|
3
6
|
const ORDER = {
|
|
4
7
|
error: 0,
|
|
@@ -36,6 +39,75 @@ function rotateIfNeeded(f) {
|
|
|
36
39
|
/* never throw */
|
|
37
40
|
}
|
|
38
41
|
}
|
|
42
|
+
async function closeWindowsFile(sink) {
|
|
43
|
+
const { file, handle } = sink;
|
|
44
|
+
sink.file = undefined;
|
|
45
|
+
sink.handle = undefined;
|
|
46
|
+
if (file && handle)
|
|
47
|
+
await privateOperation(file, 'log-close', () => handle.close());
|
|
48
|
+
}
|
|
49
|
+
async function openWindowsFile(sink, file) {
|
|
50
|
+
await ensurePrivateDirectory(dirname(file));
|
|
51
|
+
try {
|
|
52
|
+
await verifyPrivate(file, 'file', true);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code !== 'ENOENT')
|
|
56
|
+
throw error;
|
|
57
|
+
try {
|
|
58
|
+
await createPrivateBytes(file, '');
|
|
59
|
+
}
|
|
60
|
+
catch (creationError) {
|
|
61
|
+
if (creationError.code !== 'EEXIST')
|
|
62
|
+
throw creationError;
|
|
63
|
+
await verifyPrivate(file, 'file', true);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// The safe-parent policy denies other ordinary users the ability to create, replace or link entries between verification and fs.open.
|
|
67
|
+
// This protects against other ordinary users, not the owner or administrators, whose ability to change that namespace is outside our threat model.
|
|
68
|
+
sink.handle = await privateOperation(file, 'log-open', () => open(file, 'a'));
|
|
69
|
+
sink.file = file;
|
|
70
|
+
return sink.handle;
|
|
71
|
+
}
|
|
72
|
+
async function flushWindowsFile(sink, file, text) {
|
|
73
|
+
file = resolve(file);
|
|
74
|
+
try {
|
|
75
|
+
if (sink.file !== file)
|
|
76
|
+
await closeWindowsFile(sink);
|
|
77
|
+
let handle = sink.handle ?? (await openWindowsFile(sink, file));
|
|
78
|
+
const info = await privateOperation(file, 'log-stat', () => handle.stat());
|
|
79
|
+
if (info.size >= MAX_BYTES) {
|
|
80
|
+
await closeWindowsFile(sink);
|
|
81
|
+
for (let i = ROTATE_KEEP - 1; i >= 0; i--) {
|
|
82
|
+
const source = i === 0 ? file : `${file}.${i}`;
|
|
83
|
+
const target = `${file}.${i + 1}`;
|
|
84
|
+
try {
|
|
85
|
+
await verifyPrivate(source, 'file', true);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code === 'ENOENT')
|
|
89
|
+
continue;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await verifyPrivate(target, 'file', true);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error.code !== 'ENOENT')
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
await replaceWindowsFile(source, target);
|
|
100
|
+
await verifyPrivate(target, 'file');
|
|
101
|
+
}
|
|
102
|
+
handle = await openWindowsFile(sink, file);
|
|
103
|
+
}
|
|
104
|
+
await privateOperation(file, 'log-append', () => handle.appendFile(text, 'utf8'));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
await closeWindowsFile(sink).catch(() => { });
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
39
111
|
function safeSerialize(data) {
|
|
40
112
|
try {
|
|
41
113
|
return ` ${JSON.stringify(data)}`;
|
|
@@ -63,7 +135,19 @@ function createEngine(options) {
|
|
|
63
135
|
let captureSink;
|
|
64
136
|
let buffer = [];
|
|
65
137
|
let timer;
|
|
138
|
+
let orderedSink = Promise.resolve();
|
|
139
|
+
let sinkError;
|
|
140
|
+
const windowsFile = {};
|
|
141
|
+
function queueWindows(operation) {
|
|
142
|
+
const next = orderedSink.then(operation);
|
|
143
|
+
orderedSink = next.catch((error) => {
|
|
144
|
+
sinkError = error;
|
|
145
|
+
});
|
|
146
|
+
return next;
|
|
147
|
+
}
|
|
66
148
|
function configure(next) {
|
|
149
|
+
if (windowsPrivateFiles && next.file !== logFileSource)
|
|
150
|
+
void queueWindows(() => closeWindowsFile(windowsFile)).catch(() => { });
|
|
67
151
|
logFileSource = next.file;
|
|
68
152
|
initLevelSource = next.level;
|
|
69
153
|
redactor = createRedactor(next);
|
|
@@ -89,25 +173,49 @@ function createEngine(options) {
|
|
|
89
173
|
return floor;
|
|
90
174
|
return 'info';
|
|
91
175
|
}
|
|
92
|
-
function flushLogs() {
|
|
176
|
+
function flushLogs(close = true) {
|
|
93
177
|
if (timer) {
|
|
94
178
|
clearTimeout(timer);
|
|
95
179
|
timer = undefined;
|
|
96
180
|
}
|
|
97
|
-
if (!buffer.length)
|
|
181
|
+
if (!buffer.length) {
|
|
182
|
+
if (windowsPrivateFiles)
|
|
183
|
+
return queueWindows(async () => {
|
|
184
|
+
if (close)
|
|
185
|
+
await closeWindowsFile(windowsFile);
|
|
186
|
+
if (sinkError)
|
|
187
|
+
throw sinkError;
|
|
188
|
+
});
|
|
98
189
|
return;
|
|
190
|
+
}
|
|
99
191
|
let file;
|
|
100
192
|
try {
|
|
101
193
|
file = logFilePath();
|
|
102
194
|
}
|
|
103
195
|
catch {
|
|
104
196
|
buffer = [];
|
|
197
|
+
if (windowsPrivateFiles && close)
|
|
198
|
+
return queueWindows(() => closeWindowsFile(windowsFile));
|
|
105
199
|
return;
|
|
106
200
|
}
|
|
107
201
|
const text = buffer.join('');
|
|
108
202
|
buffer = [];
|
|
109
|
-
if (!file)
|
|
203
|
+
if (!file) {
|
|
204
|
+
if (windowsPrivateFiles && close)
|
|
205
|
+
return queueWindows(() => closeWindowsFile(windowsFile));
|
|
110
206
|
return;
|
|
207
|
+
}
|
|
208
|
+
if (windowsPrivateFiles) {
|
|
209
|
+
return queueWindows(async () => {
|
|
210
|
+
try {
|
|
211
|
+
await flushWindowsFile(windowsFile, file, text);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
if (close)
|
|
215
|
+
await closeWindowsFile(windowsFile);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
111
219
|
try {
|
|
112
220
|
rotateIfNeeded(file);
|
|
113
221
|
if (existsSync(file))
|
|
@@ -122,7 +230,9 @@ function createEngine(options) {
|
|
|
122
230
|
if (!timer)
|
|
123
231
|
timer = setTimeout(() => {
|
|
124
232
|
timer = undefined;
|
|
125
|
-
flushLogs();
|
|
233
|
+
const flushed = flushLogs(false);
|
|
234
|
+
if (flushed instanceof Promise)
|
|
235
|
+
void flushed.catch(() => { });
|
|
126
236
|
}, 500);
|
|
127
237
|
}
|
|
128
238
|
function emit(channel, level, message, data) {
|
|
@@ -157,8 +267,11 @@ function createEngine(options) {
|
|
|
157
267
|
if (logFileSource === undefined)
|
|
158
268
|
return;
|
|
159
269
|
buffer.push(line);
|
|
160
|
-
if (buffer.length >= 50)
|
|
161
|
-
flushLogs();
|
|
270
|
+
if (buffer.length >= 50) {
|
|
271
|
+
const flushed = flushLogs(false);
|
|
272
|
+
if (flushed instanceof Promise)
|
|
273
|
+
void flushed.catch(() => { });
|
|
274
|
+
}
|
|
162
275
|
else
|
|
163
276
|
schedule();
|
|
164
277
|
}
|
|
@@ -176,6 +289,9 @@ function createEngine(options) {
|
|
|
176
289
|
};
|
|
177
290
|
}
|
|
178
291
|
function reset() {
|
|
292
|
+
if (windowsPrivateFiles)
|
|
293
|
+
void queueWindows(() => closeWindowsFile(windowsFile)).catch(() => { });
|
|
294
|
+
sinkError = undefined;
|
|
179
295
|
buffer = [];
|
|
180
296
|
if (timer) {
|
|
181
297
|
clearTimeout(timer);
|
|
@@ -222,18 +338,17 @@ export function setLogLevel(l) {
|
|
|
222
338
|
defaultEngine.setLogLevel(l);
|
|
223
339
|
}
|
|
224
340
|
/**
|
|
225
|
-
* Write whatever the default logger has buffered.
|
|
226
|
-
*
|
|
227
|
-
* shutdown.
|
|
341
|
+
* Write whatever the default logger has buffered. On Windows await the
|
|
342
|
+
* promise before shutdown; POSIX keeps synchronous process-exit flushing.
|
|
228
343
|
*/
|
|
229
344
|
export function flushLogs() {
|
|
230
|
-
defaultEngine.flushLogs();
|
|
345
|
+
return defaultEngine.flushLogs();
|
|
231
346
|
}
|
|
232
347
|
export function createLogger(channel) {
|
|
233
348
|
return defaultEngine.createLogger(channel);
|
|
234
349
|
}
|
|
235
350
|
export async function flushForTest() {
|
|
236
|
-
defaultEngine.flushLogs();
|
|
351
|
+
return defaultEngine.flushLogs();
|
|
237
352
|
}
|
|
238
353
|
/**
|
|
239
354
|
* Return the default logger to its uninitialised state. Only a test needs
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmod, mkdir, open, readdir,
|
|
2
|
+
import { chmod, mkdir, open, readdir, rename, rmdir, stat, unlink, } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { ensurePrivateDirectory, PrivateFileError, readPrivateText, verifyPrivate, windowsPrivateFiles, writePrivateAtomic, } from "../fs/private-files.js";
|
|
4
5
|
/**
|
|
5
6
|
* A PID a liveness probe may address. `process.kill(0 | negative, 0)` signals
|
|
6
7
|
* a process group rather than a process, and an unsafe integer cannot name a
|
|
@@ -68,6 +69,15 @@ export async function writePortFile(dir, entry, options = {}) {
|
|
|
68
69
|
// persistent ENOENT (e.g. permission, read-only parent) will surface on
|
|
69
70
|
// the second attempt — failing fast beats an unbounded loop.
|
|
70
71
|
const writeOnce = async () => {
|
|
72
|
+
const target = join(dir, `port-${entry.pid}.json`);
|
|
73
|
+
if (windowsPrivateFiles) {
|
|
74
|
+
await ensurePrivateDirectory(dir, options.secureDir ?? false);
|
|
75
|
+
await options.beforeWrite?.();
|
|
76
|
+
await writePrivateAtomic(target, JSON.stringify({ ...entry, startedAt: Date.now() }), {
|
|
77
|
+
stagePath: `${target}.${(options.stageName ?? randomUUID)()}.tmp`,
|
|
78
|
+
});
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
71
81
|
await mkdir(dir, {
|
|
72
82
|
recursive: true,
|
|
73
83
|
mode: options.secureDir ? 0o700 : undefined,
|
|
@@ -76,7 +86,6 @@ export async function writePortFile(dir, entry, options = {}) {
|
|
|
76
86
|
await chmod(dir, 0o700);
|
|
77
87
|
await options.beforeWrite?.();
|
|
78
88
|
const full = { ...entry, startedAt: Date.now() };
|
|
79
|
-
const target = join(dir, `port-${entry.pid}.json`);
|
|
80
89
|
const tmp = `${target}.${(options.stageName ?? randomUUID)()}.tmp`;
|
|
81
90
|
let handle;
|
|
82
91
|
let created = false;
|
|
@@ -139,9 +148,11 @@ export async function sweepRpcState(root, activeDir, isManagedDir, log) {
|
|
|
139
148
|
const portFile = join(dir, name);
|
|
140
149
|
let raw;
|
|
141
150
|
try {
|
|
142
|
-
raw = await
|
|
151
|
+
raw = await readPrivateText(portFile);
|
|
143
152
|
}
|
|
144
|
-
catch {
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error instanceof PrivateFileError && error.code !== 'ENOENT')
|
|
155
|
+
throw error;
|
|
145
156
|
continue;
|
|
146
157
|
}
|
|
147
158
|
if (raw === undefined)
|
|
@@ -172,19 +183,27 @@ export async function portFileIdentity(dir, entry) {
|
|
|
172
183
|
return null;
|
|
173
184
|
const path = join(resolve(dir), `port-${entry.pid}.json`);
|
|
174
185
|
try {
|
|
186
|
+
if (windowsPrivateFiles)
|
|
187
|
+
await verifyPrivate(path, 'file');
|
|
175
188
|
const info = await stat(path);
|
|
176
189
|
return `${path}:${info.dev}:${info.ino}:${info.mtimeMs}:${info.size}`;
|
|
177
190
|
}
|
|
178
|
-
catch {
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error instanceof PrivateFileError && error.code !== 'ENOENT')
|
|
193
|
+
throw error;
|
|
179
194
|
return null;
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
197
|
export async function discoverPortFile(dir, expectedPid, options = {}) {
|
|
183
198
|
let names;
|
|
184
199
|
try {
|
|
200
|
+
if (windowsPrivateFiles)
|
|
201
|
+
await verifyPrivate(dir, 'directory');
|
|
185
202
|
names = await readdir(dir);
|
|
186
203
|
}
|
|
187
|
-
catch {
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof PrivateFileError && error.code !== 'ENOENT')
|
|
206
|
+
throw error;
|
|
188
207
|
return null;
|
|
189
208
|
}
|
|
190
209
|
const live = [];
|
|
@@ -192,7 +211,7 @@ export async function discoverPortFile(dir, expectedPid, options = {}) {
|
|
|
192
211
|
if (!name.startsWith('port-') || !name.endsWith('.json'))
|
|
193
212
|
continue;
|
|
194
213
|
try {
|
|
195
|
-
const parsed = JSON.parse(await
|
|
214
|
+
const parsed = JSON.parse(await readPrivateText(join(dir, name)));
|
|
196
215
|
if (isUsablePortFileEntry(parsed, name)) {
|
|
197
216
|
if (pidAlive(parsed.pid))
|
|
198
217
|
live.push(parsed);
|
|
@@ -200,7 +219,10 @@ export async function discoverPortFile(dir, expectedPid, options = {}) {
|
|
|
200
219
|
await unlink(join(dir, name)).catch(() => { });
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
|
-
catch {
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof PrivateFileError && error.code !== 'ENOENT')
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
204
226
|
}
|
|
205
227
|
if (live.length === 0)
|
|
206
228
|
return null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { open } from "node:fs/promises";
|
|
2
2
|
import { connect } from "node:net";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { readPrivateText, windowsPrivateFiles } from "../fs/private-files.js";
|
|
4
5
|
import { discoverPortFile, portFileIdentity, } from "./port-file.js";
|
|
5
6
|
export const DEFAULT_RPC_TIMEOUT_MS = 2_000;
|
|
6
7
|
const selections = new Map();
|
|
@@ -12,6 +13,17 @@ const discoveries = new Map();
|
|
|
12
13
|
// those bytes still name the discovered entry.
|
|
13
14
|
async function boundIdentity(dir, entry) {
|
|
14
15
|
const path = join(resolve(dir), `port-${entry.pid}.json`);
|
|
16
|
+
if (windowsPrivateFiles) {
|
|
17
|
+
const current = JSON.parse(await readPrivateText(path));
|
|
18
|
+
if (current.pid !== entry.pid ||
|
|
19
|
+
current.port !== entry.port ||
|
|
20
|
+
current.token !== entry.token)
|
|
21
|
+
return null;
|
|
22
|
+
// private-files does not expose the read handle's identity, so a separate
|
|
23
|
+
// stat cannot bind an RPC-server cache entry to the file that supplied its
|
|
24
|
+
// token. On Windows, rediscover the port file on every call.
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
15
27
|
let handle;
|
|
16
28
|
try {
|
|
17
29
|
handle = await open(path, 'r');
|
|
@@ -32,7 +44,9 @@ async function boundIdentity(dir, entry) {
|
|
|
32
44
|
}
|
|
33
45
|
async function select(key, dir, expectedPid, options) {
|
|
34
46
|
const cached = selections.get(key);
|
|
35
|
-
if (
|
|
47
|
+
if (!windowsPrivateFiles &&
|
|
48
|
+
cached &&
|
|
49
|
+
(await portFileIdentity(dir, cached.entry)) === cached.identity)
|
|
36
50
|
return cached;
|
|
37
51
|
selections.delete(key);
|
|
38
52
|
const pending = discoveries.get(key);
|