vault-go 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ export interface UpdateState {
2
+ enabled: boolean;
3
+ current: string;
4
+ latest?: string;
5
+ updateAvailable: boolean;
6
+ status: 'idle' | 'disabled' | 'current' | 'available' | 'ready' | 'failed';
7
+ lastCheckedAt?: string;
8
+ nextCheckAt?: string;
9
+ lastError?: 'check_failed' | 'invalid_metadata' | 'install_failed' | 'update_busy';
10
+ packageRoot?: string;
11
+ restartRequired: boolean;
12
+ }
13
+ export interface UpdateProcessOptions {
14
+ cwd: string;
15
+ env: NodeJS.ProcessEnv;
16
+ timeout: number;
17
+ maxBuffer: number;
18
+ shell: false;
19
+ }
20
+ export interface UpdateOptions {
21
+ force?: boolean;
22
+ now?: () => number;
23
+ fetch?: (url: string, init: RequestInit) => Promise<Response>;
24
+ execute?: (file: string, args: readonly string[], options: UpdateProcessOptions) => Promise<{
25
+ stdout: string;
26
+ }>;
27
+ timeoutMs?: number;
28
+ lockTimeoutMs?: number;
29
+ }
30
+ /** Only allowlisted state is exposed; credentials and raw process errors never enter this file. */
31
+ export declare function updateStatus(home: string): UpdateState;
32
+ export declare function setAutoUpdate(home: string, enabled: boolean): UpdateState;
33
+ export declare function checkForUpdate(home: string, options?: UpdateOptions): Promise<UpdateState>;
34
+ export declare function installUpdate(home: string, options?: UpdateOptions): Promise<UpdateState>;
@@ -0,0 +1,404 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { delimiter, dirname, join, resolve, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { promisify } from 'node:util';
7
+ const REGISTRY = 'https://registry.npmjs.org';
8
+ const METADATA_URL = `${REGISTRY}/vault-go/latest`;
9
+ const DAY = 86_400_000;
10
+ const RETRY = 900_000;
11
+ const MAX_METADATA = 1_048_576;
12
+ const executeFile = promisify(execFile);
13
+ const STATUS_VALUES = new Set(['idle', 'disabled', 'current', 'available', 'ready', 'failed']);
14
+ const ERROR_VALUES = new Set(['check_failed', 'invalid_metadata', 'install_failed', 'update_busy']);
15
+ function stableVersion(value) {
16
+ if (typeof value !== 'string' || value.length > 48 || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value))
17
+ return false;
18
+ return value.split('.').every(part => Number.isSafeInteger(Number(part)));
19
+ }
20
+ function newer(candidate, current) {
21
+ const next = candidate.split('.').map(Number);
22
+ const previous = current.split('.').map(Number);
23
+ for (let i = 0; i < 3; i++) {
24
+ if (next[i] !== previous[i])
25
+ return next[i] > previous[i];
26
+ }
27
+ return false;
28
+ }
29
+ function readObject(path, maxBytes = 65_536) {
30
+ try {
31
+ if (statSync(path).size > maxBytes)
32
+ return {};
33
+ const value = JSON.parse(readFileSync(path, 'utf8'));
34
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ }
40
+ function currentVersion(home) {
41
+ const active = readObject(join(home, 'local-runtime', 'package.json'));
42
+ if ((active.name === 'vault-go-local-runtime' || active.name === 'vault-go') && stableVersion(active.version))
43
+ return active.version;
44
+ const packaged = readObject(fileURLToPath(new URL('../package.json', import.meta.url)));
45
+ if (packaged.name !== 'vault-go' || !stableVersion(packaged.version))
46
+ throw Error('Invalid installed Vault Go version');
47
+ return packaged.version;
48
+ }
49
+ function timestamp(value) {
50
+ return typeof value === 'string' && value.length <= 30 && Number.isFinite(Date.parse(value));
51
+ }
52
+ function releaseRoot(home, version) {
53
+ return join(resolve(home), 'releases', version, 'node_modules', 'vault-go');
54
+ }
55
+ /** Only allowlisted state is exposed; credentials and raw process errors never enter this file. */
56
+ export function updateStatus(home) {
57
+ const saved = readObject(join(home, 'update-state.json'));
58
+ const current = currentVersion(home);
59
+ const enabled = typeof saved.enabled === 'boolean' ? saved.enabled : true;
60
+ const latest = stableVersion(saved.latest) ? saved.latest : undefined;
61
+ const updateAvailable = latest !== undefined && newer(latest, current);
62
+ const state = {
63
+ enabled, current, updateAvailable, restartRequired: false,
64
+ status: typeof saved.status === 'string' && STATUS_VALUES.has(saved.status) ? saved.status : 'idle',
65
+ };
66
+ if (latest)
67
+ state.latest = latest;
68
+ if (timestamp(saved.lastCheckedAt))
69
+ state.lastCheckedAt = saved.lastCheckedAt;
70
+ if (timestamp(saved.nextCheckAt))
71
+ state.nextCheckAt = saved.nextCheckAt;
72
+ if (typeof saved.lastError === 'string' && ERROR_VALUES.has(saved.lastError))
73
+ state.lastError = saved.lastError;
74
+ if (latest && updateAvailable && state.status === 'ready' && saved.packageRoot === releaseRoot(home, latest)) {
75
+ try {
76
+ if (lstatSync(join(home, 'releases')).isSymbolicLink() || lstatSync(join(home, 'releases', latest)).isSymbolicLink())
77
+ throw new UpdateFailure('install_failed');
78
+ validatePackage(releaseRoot(home, latest), latest, join(resolve(home), 'releases', latest));
79
+ state.packageRoot = releaseRoot(home, latest);
80
+ state.restartRequired = true;
81
+ }
82
+ catch {
83
+ state.status = 'available';
84
+ }
85
+ }
86
+ else if (state.status === 'ready')
87
+ state.status = updateAvailable ? 'available' : 'current';
88
+ return state;
89
+ }
90
+ function ensureHome(home) {
91
+ mkdirSync(home, { recursive: true, mode: 0o700 });
92
+ chmodSync(home, 0o700);
93
+ }
94
+ function saveState(home, state) {
95
+ ensureHome(home);
96
+ const temporary = join(home, `.update-state-${randomUUID()}.tmp`);
97
+ try {
98
+ writeFileSync(temporary, JSON.stringify(state, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
99
+ renameSync(temporary, join(home, 'update-state.json'));
100
+ }
101
+ finally {
102
+ rmSync(temporary, { force: true });
103
+ }
104
+ return updateStatus(home);
105
+ }
106
+ export function setAutoUpdate(home, enabled) {
107
+ const state = updateStatus(home);
108
+ state.enabled = enabled;
109
+ if (!enabled)
110
+ state.status = 'disabled';
111
+ else if (state.status === 'disabled')
112
+ state.status = state.updateAvailable ? 'available' : 'idle';
113
+ return saveState(home, state);
114
+ }
115
+ class UpdateFailure extends Error {
116
+ code;
117
+ constructor(code) {
118
+ super(code);
119
+ this.code = code;
120
+ }
121
+ }
122
+ function alive(pid) {
123
+ try {
124
+ process.kill(pid, 0);
125
+ return true;
126
+ }
127
+ catch (error) {
128
+ return error.code !== 'ESRCH';
129
+ }
130
+ }
131
+ async function withLock(home, options, action) {
132
+ ensureHome(home);
133
+ const path = join(home, 'update.lock');
134
+ const token = randomUUID();
135
+ const started = Date.now();
136
+ const waitLimit = Math.max(1, Math.min(options.lockTimeoutMs ?? 360_000, 360_000));
137
+ for (;;) {
138
+ let descriptor;
139
+ try {
140
+ descriptor = openSync(path, 'wx', 0o600);
141
+ writeFileSync(descriptor, JSON.stringify({ pid: process.pid, token, createdAt: Date.now() }));
142
+ closeSync(descriptor);
143
+ break;
144
+ }
145
+ catch (error) {
146
+ if (descriptor !== undefined)
147
+ closeSync(descriptor);
148
+ if (error.code !== 'EEXIST')
149
+ throw error;
150
+ try {
151
+ const before = lstatSync(path);
152
+ const existing = readObject(path);
153
+ const knownPid = typeof existing.pid === 'number' && Number.isInteger(existing.pid) && existing.pid > 0;
154
+ const stale = knownPid ? !alive(existing.pid) : Date.now() - before.mtimeMs > 600_000;
155
+ // Compare the file identity before removing a dead owner's lock.
156
+ if (stale && lstatSync(path).ino === before.ino) {
157
+ unlinkSync(path);
158
+ continue;
159
+ }
160
+ }
161
+ catch (error) {
162
+ if (error.code === 'ENOENT')
163
+ continue;
164
+ }
165
+ if (Date.now() - started >= waitLimit)
166
+ throw new UpdateFailure('update_busy');
167
+ await new Promise(resolveWait => setTimeout(resolveWait, Math.min(25, waitLimit)));
168
+ }
169
+ }
170
+ try {
171
+ return await action();
172
+ }
173
+ finally {
174
+ if (readObject(path).token === token)
175
+ unlinkSync(path);
176
+ }
177
+ }
178
+ function failureState(home, code, persist = true, now = Date.now()) {
179
+ const state = updateStatus(home);
180
+ state.status = 'failed';
181
+ state.lastError = code;
182
+ state.restartRequired = false;
183
+ state.nextCheckAt = new Date(now + RETRY).toISOString();
184
+ delete state.packageRoot;
185
+ return persist ? saveState(home, state) : state;
186
+ }
187
+ async function registryVersion(options) {
188
+ const controller = new AbortController();
189
+ const timeout = Math.max(1, Math.min(options.timeoutMs ?? 10_000, 30_000));
190
+ let timer;
191
+ const deadline = new Promise((_resolve, reject) => {
192
+ timer = setTimeout(() => { controller.abort(); reject(new UpdateFailure('check_failed')); }, timeout);
193
+ });
194
+ const request = async () => {
195
+ const response = await (options.fetch ?? fetch)(METADATA_URL, { signal: controller.signal, redirect: 'error', headers: { Accept: 'application/json' } });
196
+ if (!response.ok || !response.body)
197
+ throw new UpdateFailure('check_failed');
198
+ const advertisedSize = Number(response.headers.get('content-length'));
199
+ if (advertisedSize > MAX_METADATA) {
200
+ await response.body.cancel();
201
+ throw new UpdateFailure('invalid_metadata');
202
+ }
203
+ const reader = response.body.getReader();
204
+ const chunks = [];
205
+ let size = 0;
206
+ try {
207
+ for (;;) {
208
+ const part = await reader.read();
209
+ if (part.done)
210
+ break;
211
+ size += part.value.byteLength;
212
+ if (size > MAX_METADATA)
213
+ throw new UpdateFailure('invalid_metadata');
214
+ chunks.push(part.value);
215
+ }
216
+ }
217
+ finally {
218
+ await reader.cancel().catch(() => { });
219
+ }
220
+ let value;
221
+ try {
222
+ value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
223
+ }
224
+ catch {
225
+ throw new UpdateFailure('invalid_metadata');
226
+ }
227
+ if (!value || typeof value !== 'object' || Array.isArray(value))
228
+ throw new UpdateFailure('invalid_metadata');
229
+ const metadata = value;
230
+ if (metadata.name !== 'vault-go' || !stableVersion(metadata.version))
231
+ throw new UpdateFailure('invalid_metadata');
232
+ return metadata.version;
233
+ };
234
+ try {
235
+ return await Promise.race([request(), deadline]);
236
+ }
237
+ finally {
238
+ if (timer)
239
+ clearTimeout(timer);
240
+ }
241
+ }
242
+ async function checkUnlocked(home, options) {
243
+ const previous = updateStatus(home);
244
+ if (!previous.enabled && !options.force)
245
+ return { ...previous, status: 'disabled' };
246
+ const now = options.now?.() ?? Date.now();
247
+ if (!options.force && previous.nextCheckAt && Date.parse(previous.nextCheckAt) > now)
248
+ return previous;
249
+ let latest;
250
+ try {
251
+ latest = await registryVersion(options);
252
+ }
253
+ catch (error) {
254
+ const state = updateStatus(home);
255
+ state.lastCheckedAt = new Date(now).toISOString();
256
+ state.nextCheckAt = new Date(now + RETRY).toISOString();
257
+ state.status = 'failed';
258
+ state.lastError = error instanceof UpdateFailure ? error.code : 'check_failed';
259
+ state.restartRequired = false;
260
+ delete state.packageRoot;
261
+ return saveState(home, state);
262
+ }
263
+ const state = updateStatus(home); // Preserve a preference changed while awaiting the network.
264
+ state.latest = latest;
265
+ state.updateAvailable = newer(latest, state.current);
266
+ state.lastCheckedAt = new Date(now).toISOString();
267
+ state.nextCheckAt = new Date(now + DAY).toISOString();
268
+ state.status = state.updateAvailable ? 'available' : 'current';
269
+ state.restartRequired = false;
270
+ delete state.packageRoot;
271
+ delete state.lastError;
272
+ return saveState(home, state);
273
+ }
274
+ export async function checkForUpdate(home, options = {}) {
275
+ try {
276
+ return await withLock(home, options, () => checkUnlocked(home, options));
277
+ }
278
+ catch (error) {
279
+ const code = error instanceof UpdateFailure ? error.code : 'check_failed';
280
+ return failureState(home, code, code !== 'update_busy');
281
+ }
282
+ }
283
+ function contained(path, root) {
284
+ const realRoot = realpathSync(root);
285
+ const realPath = realpathSync(path);
286
+ return realPath === realRoot || realPath.startsWith(realRoot + sep);
287
+ }
288
+ function validatePackage(root, version, container) {
289
+ if (lstatSync(root).isSymbolicLink() || !contained(root, container))
290
+ throw new UpdateFailure('install_failed');
291
+ const manifestPath = join(root, 'package.json');
292
+ if (!contained(manifestPath, root))
293
+ throw new UpdateFailure('install_failed');
294
+ const manifest = readObject(manifestPath);
295
+ if (manifest.name !== 'vault-go' || manifest.version !== version)
296
+ throw new UpdateFailure('install_failed');
297
+ for (const file of ['index.js', 'local-worker.js', 'hook-runner.js']) {
298
+ const path = join(root, 'dist', file);
299
+ if (!contained(path, root) || !statSync(path).isFile())
300
+ throw new UpdateFailure('install_failed');
301
+ }
302
+ }
303
+ function processOptions(directory) {
304
+ // Allowlist instead of inheriting login credentials, NODE_OPTIONS or npm hooks/config.
305
+ const env = {
306
+ PATH: [dirname(process.execPath), '/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin'].join(delimiter),
307
+ NPM_CONFIG_USERCONFIG: join(directory, '.npmrc'),
308
+ NPM_CONFIG_GLOBALCONFIG: join(directory, '.npmrc-global'),
309
+ NPM_CONFIG_CACHE: join(directory, '.npm-cache'),
310
+ NPM_CONFIG_REGISTRY: REGISTRY,
311
+ NPM_CONFIG_IGNORE_SCRIPTS: 'true',
312
+ NPM_CONFIG_UPDATE_NOTIFIER: 'false',
313
+ VAULT_GO_HOME: join(directory, '.smoke-home'),
314
+ };
315
+ if (process.platform === 'win32') {
316
+ for (const name of ['SystemRoot', 'WINDIR', 'TEMP', 'TMP'])
317
+ if (process.env[name])
318
+ env[name] = process.env[name];
319
+ }
320
+ return { cwd: directory, env, timeout: 300_000, maxBuffer: 65_536, shell: false };
321
+ }
322
+ function npmProcess(args) {
323
+ if (process.platform !== 'win32')
324
+ return { file: 'npm', args };
325
+ // .cmd launchers require a shell on Windows; invoke npm's JS entry with the runtime instead.
326
+ const candidates = [join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'), join(dirname(process.execPath), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js')];
327
+ const entry = candidates.find(candidate => existsSync(candidate));
328
+ if (!entry)
329
+ throw new UpdateFailure('install_failed');
330
+ return { file: process.execPath, args: [entry, ...args] };
331
+ }
332
+ async function runProcess(file, args, options) {
333
+ const result = await executeFile(file, [...args], { ...options, encoding: 'utf8' });
334
+ return { stdout: result.stdout };
335
+ }
336
+ async function smoke(root, version, directory, options) {
337
+ const result = await (options.execute ?? runProcess)(process.execPath, [join(root, 'dist', 'index.js'), '--version'], { ...processOptions(directory), timeout: 15_000 });
338
+ if (result.stdout.trim() !== version)
339
+ throw new UpdateFailure('install_failed');
340
+ }
341
+ export async function installUpdate(home, options = {}) {
342
+ try {
343
+ return await withLock(home, options, async () => {
344
+ const checked = await checkUnlocked(home, options);
345
+ if (checked.status === 'failed' || (!checked.enabled && !options.force) || !checked.updateAvailable || !checked.latest)
346
+ return checked;
347
+ const version = checked.latest;
348
+ const releases = join(resolve(home), 'releases');
349
+ mkdirSync(releases, { recursive: true, mode: 0o700 });
350
+ if (lstatSync(releases).isSymbolicLink())
351
+ throw new UpdateFailure('install_failed');
352
+ chmodSync(releases, 0o700);
353
+ const destination = join(releases, version);
354
+ const packageRoot = releaseRoot(home, version);
355
+ let staging;
356
+ try {
357
+ if (existsSync(destination)) {
358
+ if (lstatSync(destination).isSymbolicLink())
359
+ throw new UpdateFailure('install_failed');
360
+ validatePackage(packageRoot, version, destination);
361
+ await smoke(packageRoot, version, destination, options);
362
+ }
363
+ else {
364
+ staging = mkdtempSync(join(releases, `.staging-${version}-`));
365
+ chmodSync(staging, 0o700);
366
+ writeFileSync(join(staging, 'package.json'), JSON.stringify({ name: 'vault-go-update-stage', private: true }), { mode: 0o600 });
367
+ writeFileSync(join(staging, '.npmrc'), '', { mode: 0o600 });
368
+ writeFileSync(join(staging, '.npmrc-global'), '', { mode: 0o600 });
369
+ const command = npmProcess(['install', '--ignore-scripts', '--no-audit', '--no-fund', '--prefix', staging, `vault-go@${version}`, `--registry=${REGISTRY}`]);
370
+ await (options.execute ?? runProcess)(command.file, command.args, processOptions(staging));
371
+ const root = join(staging, 'node_modules', 'vault-go');
372
+ validatePackage(root, version, staging);
373
+ await smoke(root, version, staging, options);
374
+ // The lock serializes cooperating updaters. Never replace an already present release.
375
+ if (existsSync(destination))
376
+ throw new UpdateFailure('install_failed');
377
+ renameSync(staging, destination);
378
+ staging = undefined;
379
+ }
380
+ const state = updateStatus(home);
381
+ if (!newer(version, state.current))
382
+ return saveState(home, { ...state, updateAvailable: false, restartRequired: false, status: 'current' });
383
+ state.latest = version;
384
+ state.updateAvailable = true;
385
+ state.packageRoot = packageRoot;
386
+ state.restartRequired = true;
387
+ state.status = 'ready';
388
+ delete state.lastError;
389
+ return saveState(home, state);
390
+ }
391
+ catch {
392
+ return failureState(home, 'install_failed', true, options.now?.() ?? Date.now());
393
+ }
394
+ finally {
395
+ if (staging)
396
+ rmSync(staging, { recursive: true, force: true });
397
+ }
398
+ });
399
+ }
400
+ catch (error) {
401
+ const code = error instanceof UpdateFailure ? error.code : 'install_failed';
402
+ return failureState(home, code, code !== 'update_busy', options.now?.() ?? Date.now());
403
+ }
404
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vault-go",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Servidor MCP universal com autenticação e instalação multi-cliente para a plataforma Vault.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -66,7 +66,23 @@
66
66
  "dist/server.d.ts",
67
67
  "assets/",
68
68
  "README.md",
69
- "LICENSE"
69
+ "LICENSE",
70
+ "dist/mcp-result.js",
71
+ "dist/mcp-result.d.ts",
72
+ "dist/parity-tools.js",
73
+ "dist/parity-tools.d.ts",
74
+ "dist/capabilities.js",
75
+ "dist/capabilities.d.ts",
76
+ "dist/smart-code.js",
77
+ "dist/smart-code.d.ts",
78
+ "dist/updater.js",
79
+ "dist/updater.d.ts",
80
+ "dist/update-control.js",
81
+ "dist/update-control.d.ts",
82
+ "dist/update-runner.js",
83
+ "dist/update-runner.d.ts",
84
+ "dist/modes.js",
85
+ "dist/modes.d.ts"
70
86
  ],
71
87
  "scripts": {
72
88
  "clean": "bun -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
@@ -111,6 +127,7 @@
111
127
  "author": "Gutierrez Henrique",
112
128
  "license": "MIT",
113
129
  "dependencies": {
130
+ "@ast-grep/napi": "0.45.3",
114
131
  "@modelcontextprotocol/sdk": "1.29.0",
115
132
  "zod": "3.25.76"
116
133
  },