@getmarrow/install 0.1.35 → 0.1.37

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,242 @@
1
+ const crypto = require('node:crypto');
2
+ const fs = require('node:fs');
3
+ const http = require('node:http');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const MAX_BODY_BYTES = 64 * 1024;
8
+
9
+ function sidecarStateDir() {
10
+ return process.env.MARROW_SIDECAR_STATE_DIR || path.join(os.homedir(), '.marrow', 'sidecar');
11
+ }
12
+
13
+ function currentUid() {
14
+ return typeof process.getuid === 'function' ? process.getuid() : null;
15
+ }
16
+
17
+ function createPrivateDirectoryWithoutSymlinks(directory) {
18
+ const resolved = path.resolve(directory);
19
+ const parsed = path.parse(resolved);
20
+ let current = parsed.root;
21
+ for (const segment of resolved.slice(parsed.root.length).split(path.sep).filter(Boolean)) {
22
+ current = path.join(current, segment);
23
+ try {
24
+ fs.mkdirSync(current, { mode: 0o700 });
25
+ } catch (error) {
26
+ if (error?.code !== 'EEXIST') throw error;
27
+ }
28
+ const stat = fs.lstatSync(current);
29
+ if (stat.isSymbolicLink() || !stat.isDirectory() || fs.realpathSync(current) !== current) {
30
+ throw new Error('Sidecar state directory cannot contain symlinked path components.');
31
+ }
32
+ if ((stat.mode & 0o022) !== 0 && (stat.mode & 0o1000) === 0) {
33
+ throw new Error('Sidecar state directory cannot be nested under a non-sticky writable ancestor.');
34
+ }
35
+ }
36
+ return resolved;
37
+ }
38
+
39
+ function assertPrivateStateDirectory(directory) {
40
+ const resolved = createPrivateDirectoryWithoutSymlinks(directory);
41
+ const stat = fs.lstatSync(resolved);
42
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
43
+ throw new Error('Sidecar state directory must be a private real directory.');
44
+ }
45
+ if (fs.realpathSync(resolved) !== resolved) {
46
+ throw new Error('Sidecar state directory cannot contain symlinked path components.');
47
+ }
48
+ const uid = currentUid();
49
+ if (uid !== null && stat.uid !== uid) {
50
+ throw new Error('Sidecar state directory must be owned by the current user.');
51
+ }
52
+ if ((stat.mode & 0o077) !== 0) {
53
+ throw new Error('Sidecar state directory permissions must be 0700 or stricter.');
54
+ }
55
+ return resolved;
56
+ }
57
+
58
+ function assertSafeStateFile(filePath) {
59
+ if (!fs.existsSync(filePath)) return;
60
+ const stat = fs.lstatSync(filePath);
61
+ const uid = currentUid();
62
+ if (stat.isSymbolicLink() || !stat.isFile()) {
63
+ throw new Error('Sidecar state file must be a private regular file.');
64
+ }
65
+ if (uid !== null && stat.uid !== uid) {
66
+ throw new Error('Sidecar state file must be owned by the current user.');
67
+ }
68
+ if ((stat.mode & 0o077) !== 0) {
69
+ throw new Error('Sidecar state file permissions must be 0600 or stricter.');
70
+ }
71
+ }
72
+
73
+ function writePrivateJsonAtomic(filePath, value) {
74
+ const directory = assertPrivateStateDirectory(path.dirname(filePath));
75
+ const target = path.join(directory, path.basename(filePath));
76
+ assertSafeStateFile(target);
77
+ const temporary = path.join(directory, '.active-' + process.pid + '-' + crypto.randomBytes(8).toString('hex') + '.tmp');
78
+ let descriptor;
79
+ try {
80
+ descriptor = fs.openSync(temporary, 'wx', 0o600);
81
+ fs.writeFileSync(descriptor, JSON.stringify(value, null, 2) + '\n', 'utf8');
82
+ fs.fsyncSync(descriptor);
83
+ fs.closeSync(descriptor);
84
+ descriptor = undefined;
85
+ assertSafeStateFile(target);
86
+ fs.renameSync(temporary, target);
87
+ fs.chmodSync(target, 0o600);
88
+ } finally {
89
+ if (descriptor !== undefined) {
90
+ try { fs.closeSync(descriptor); } catch {}
91
+ }
92
+ try { fs.unlinkSync(temporary); } catch {}
93
+ }
94
+ }
95
+
96
+ function unlinkPrivateStateFile(filePath, expectedInstanceId) {
97
+ try {
98
+ const stat = fs.lstatSync(filePath);
99
+ const uid = currentUid();
100
+ if (!stat.isSymbolicLink() && stat.isFile() && (uid === null || stat.uid === uid)) {
101
+ const raw = stat.size <= 8 * 1024 ? fs.readFileSync(filePath, 'utf8') : '';
102
+ const current = raw ? JSON.parse(raw) : null;
103
+ if (current?.instance_id === expectedInstanceId) fs.unlinkSync(filePath);
104
+ }
105
+ } catch {}
106
+ }
107
+
108
+ async function readJson(req) {
109
+ const chunks = [];
110
+ let bytes = 0;
111
+ for await (const chunk of req) {
112
+ bytes += chunk.length;
113
+ if (bytes > MAX_BODY_BYTES) throw new Error('request_too_large');
114
+ chunks.push(chunk);
115
+ }
116
+ if (chunks.length === 0) return {};
117
+ const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
118
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid_json');
119
+ return value;
120
+ }
121
+
122
+ function json(res, status, value) {
123
+ res.writeHead(status, {
124
+ 'Content-Type': 'application/json; charset=utf-8',
125
+ 'Cache-Control': 'no-store',
126
+ 'X-Content-Type-Options': 'nosniff',
127
+ });
128
+ res.end(JSON.stringify(value));
129
+ }
130
+
131
+ async function startGovernanceSidecar(options, handlers) {
132
+ if (!options.apiKey) throw new Error('MARROW_API_KEY is required to start the governance sidecar.');
133
+ const port = Number(options.sidecarPort || process.env.MARROW_SIDECAR_PORT || 0);
134
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Invalid sidecar port.');
135
+ const authToken = crypto.randomBytes(32).toString('hex');
136
+ const instanceId = `sidecar-${crypto.randomUUID()}`;
137
+ const startedAt = new Date().toISOString();
138
+ let latestCoverage = null;
139
+ let latestMaintenance = {
140
+ state: handlers.maintain ? 'pending' : 'unavailable',
141
+ checked_at: null,
142
+ repaired: [],
143
+ exact_fix: handlers.maintain ? null : 'Run npx @getmarrow/install --repair in the managed project.',
144
+ };
145
+
146
+ const server = http.createServer(async (req, res) => {
147
+ try {
148
+ if (req.socket.remoteAddress !== '127.0.0.1' && req.socket.remoteAddress !== '::1') {
149
+ return json(res, 403, { ok: false, error: 'loopback_only' });
150
+ }
151
+ if (req.headers.authorization !== `Bearer ${authToken}`) {
152
+ return json(res, 401, { ok: false, error: 'invalid_sidecar_token' });
153
+ }
154
+ const url = new URL(req.url || '/', 'http://127.0.0.1');
155
+ if (req.method === 'GET' && url.pathname === '/health') {
156
+ return json(res, 200, {
157
+ ok: true,
158
+ instance_id: instanceId,
159
+ pid: process.pid,
160
+ started_at: startedAt,
161
+ maintenance: latestMaintenance,
162
+ });
163
+ }
164
+ if (req.method === 'GET' && url.pathname === '/coverage') {
165
+ latestCoverage = await handlers.coverage();
166
+ return json(res, 200, latestCoverage);
167
+ }
168
+ if (req.method === 'POST' && ['/permit', '/verify', '/close'].includes(url.pathname)) {
169
+ const body = await readJson(req);
170
+ const operation = url.pathname.slice(1);
171
+ return json(res, 200, await handlers[operation](body));
172
+ }
173
+ return json(res, 404, { ok: false, error: 'not_found' });
174
+ } catch (error) {
175
+ return json(res, error?.message === 'request_too_large' ? 413 : 400, {
176
+ ok: false,
177
+ error: error instanceof Error ? error.message : 'sidecar_request_failed',
178
+ });
179
+ }
180
+ });
181
+
182
+ await new Promise((resolve, reject) => {
183
+ server.once('error', reject);
184
+ server.listen(port, '127.0.0.1', resolve);
185
+ });
186
+ const address = server.address();
187
+ const boundPort = typeof address === 'object' && address ? address.port : port;
188
+ const stateFile = path.join(sidecarStateDir(), 'active.json');
189
+ try {
190
+ writePrivateJsonAtomic(stateFile, {
191
+ instance_id: instanceId,
192
+ pid: process.pid,
193
+ host: '127.0.0.1',
194
+ port: boundPort,
195
+ token: authToken,
196
+ started_at: startedAt,
197
+ });
198
+ } catch (error) {
199
+ await new Promise((resolve) => server.close(resolve));
200
+ throw error;
201
+ }
202
+
203
+ const heartbeat = async () => {
204
+ if (handlers.maintain) {
205
+ try {
206
+ latestMaintenance = await handlers.maintain();
207
+ } catch {
208
+ latestMaintenance = {
209
+ state: 'attention_required',
210
+ checked_at: new Date().toISOString(),
211
+ repaired: [],
212
+ exact_fix: 'Run npx @getmarrow/install --repair in the managed project.',
213
+ };
214
+ }
215
+ }
216
+ try {
217
+ latestCoverage = await handlers.heartbeat({ sidecarInstanceId: instanceId });
218
+ } catch {
219
+ // Coverage will mark stale heartbeat; never weaken execution policy here.
220
+ }
221
+ };
222
+ await heartbeat();
223
+ const timer = setInterval(heartbeat, 30_000);
224
+ timer.unref();
225
+
226
+ let closed = false;
227
+ const close = () => {
228
+ if (closed) return;
229
+ closed = true;
230
+ clearInterval(timer);
231
+ unlinkPrivateStateFile(stateFile, instanceId);
232
+ process.off('SIGINT', close);
233
+ process.off('SIGTERM', close);
234
+ server.close();
235
+ };
236
+ process.once('SIGINT', close);
237
+ process.once('SIGTERM', close);
238
+
239
+ return { server, instanceId, port: boundPort, stateFile, close };
240
+ }
241
+
242
+ module.exports = { startGovernanceSidecar, sidecarStateDir };