toolfunnel 0.6.0 → 0.7.1
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 +228 -231
- package/SECURITY.md +84 -0
- package/bin/toolfunnel.js +451 -372
- package/docs/KNOWN_BUGS.md +80 -99
- package/docs/MANUAL.pdf +0 -0
- package/docs/design.md +84 -70
- package/docs/packaging.md +115 -115
- package/hooks/hooks.manifest.json +27 -27
- package/hooks/scripts/example-audit.js +21 -21
- package/hooks/scripts/example-deny-dangerous.js +39 -31
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -147
- package/mcp/servers/mock-upstream/server.js +380 -350
- package/package.json +3 -1
- package/src/auth/config.js +174 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +353 -344
- package/src/core/child-reaper.js +61 -0
- package/src/core/config-home.js +134 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +329 -275
- package/src/core/hook-loader.js +824 -791
- package/src/core/hook-runner.js +566 -346
- package/src/core/logger.js +297 -171
- package/src/core/matcher.js +100 -51
- package/src/core/metrics.js +75 -75
- package/src/core/rename-migration.js +163 -0
- package/src/core/requires.js +138 -138
- package/src/core/server-config.js +98 -98
- package/src/extend/add-hook.md +152 -152
- package/src/extend/add-mcp.md +119 -122
- package/src/extend/configure.md +88 -87
- package/src/extend/create-tool.md +168 -168
- package/src/extend/howto.js +102 -102
- package/src/extend/package.md +190 -187
- package/src/extend/wrap.md +99 -99
- package/src/mcp/aggregator.js +1719 -1246
- package/src/mcp/expose-store.js +577 -460
- package/src/mcp/gated-run.js +210 -201
- package/src/mcp/http-transport.js +1563 -1311
- package/src/mcp/mcp-client.js +1070 -907
- package/src/mcp/modern.js +406 -372
- package/src/mcp/protocol.js +468 -468
- package/src/mcp/server.js +3112 -2688
- package/src/tools/registry.js +661 -513
- package/src/tools/tool-state.js +170 -170
- package/src/ui/public/app.js +1187 -1174
- package/src/ui/public/index.html +286 -286
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1616 -1348
- package/tools/scripts/base64.js +139 -139
- package/tools/scripts/danger.js +61 -61
- package/tools/scripts/echo.js +39 -39
- package/tools/scripts/hash.js +101 -101
- package/tools/scripts/json.js +109 -109
- package/tools/scripts/text-stats.js +131 -131
- package/tools/scripts/tf-env.js +43 -43
- package/tools/scripts/tf-hook-add.js +151 -151
- package/tools/scripts/tf-hook-set.js +120 -112
- package/tools/scripts/tf-list.js +192 -192
- package/tools/scripts/tf-log.js +86 -86
- package/tools/scripts/tf-mcp-add.js +117 -117
- package/tools/scripts/tf-mcp-set.js +121 -121
- package/tools/scripts/tf-pack.js +237 -237
- package/tools/scripts/tf-tool-add.js +134 -134
- package/tools/scripts/tf-tool-set.js +142 -142
- package/tools/scripts/tf-wrap.js +103 -103
- package/tools/scripts/uuid.js +102 -102
- package/tools/tools.register.json +227 -227
- package/src/core/tool-registry.js +0 -180
- package/src/packages/packager.js +0 -715
- package/src/tools/drift.js +0 -138
package/src/auth/install.js
CHANGED
|
@@ -1,181 +1,181 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* auth/install.js - install the OPTIONAL OAuth dependency (`jose`) on demand.
|
|
5
|
-
*
|
|
6
|
-
* The whole point of ToolFunnel's zero-dependency stance is that a default install pulls NOTHING.
|
|
7
|
-
* OAuth is opt-in, so its one library is installed only when a user decides they need it - via the
|
|
8
|
-
* CLI (`toolfunnel install-oauth`) or the admin-UI button, both of which call installJose() here.
|
|
9
|
-
*
|
|
10
|
-
* It shells out to the user's own `npm` to install the PINNED jose version into the gateway
|
|
11
|
-
* package's own node_modules (resolved from this file's location, NOT process.cwd(), so it lands
|
|
12
|
-
* beside the engine wherever ToolFunnel is installed). It uses `--no-save` - the goal is simply to
|
|
13
|
-
* make `require('jose')` resolvable; there is no manifest to update, and this keeps ToolFunnel's
|
|
14
|
-
* own `dependencies: {}` untouched in every install shape.
|
|
15
|
-
*
|
|
16
|
-
* WINDOWS NOTE: do NOT spawn `npm.cmd`. Since the CVE-2024-27980 mitigation, spawning a `.cmd`
|
|
17
|
-
* without a shell throws `EINVAL`; and spawning it WITH a shell is unreliable on portable Node
|
|
18
|
-
* installs - the `npm.cmd` shim misresolves its own path under cmd.exe ("Cannot find module
|
|
19
|
-
* ...\node_modules\npm\bin\npm-cli.js"). The robust, cross-platform approach is to run npm's JS entry
|
|
20
|
-
* (`npm-cli.js`, located beside the running node binary) with `process.execPath` directly -
|
|
21
|
-
* `shell:false`, no shim, no PATH lookup, no cwd sensitivity. We fall back to a shell-spawned `npm`
|
|
22
|
-
* only if `npm-cli.js` can't be located; the spec is allowlist-validated so that fallback can't inject.
|
|
23
|
-
*
|
|
24
|
-
* NEVER throws - it resolves to a structured { ok, code, stdout, stderr, message } so the CLI/UI can
|
|
25
|
-
* report the outcome cleanly even when npm is missing or the network is down.
|
|
26
|
-
*
|
|
27
|
-
* CommonJS only; Node built-ins (child_process, path, fs) + the pin from resource-server.js.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
const path = require('node:path');
|
|
31
|
-
const fs = require('node:fs');
|
|
32
|
-
const { spawn } = require('node:child_process');
|
|
33
|
-
|
|
34
|
-
const { JOSE_PIN, isJoseInstalled, _resetJoseCache } = require('./resource-server');
|
|
35
|
-
|
|
36
|
-
/** The gateway package root: <root>/src/auth/install.js -> two dirs up. npm runs here so jose lands
|
|
37
|
-
* in the engine's own node_modules regardless of the caller's cwd. */
|
|
38
|
-
const PKG_ROOT = path.resolve(__dirname, '..', '..');
|
|
39
|
-
|
|
40
|
-
/** npm is `npm.cmd` on Windows, `npm` elsewhere. We spawn it as a child rather than require()-ing. */
|
|
41
|
-
const IS_WIN = process.platform === 'win32';
|
|
42
|
-
const NPM_BIN = IS_WIN ? 'npm.cmd' : 'npm';
|
|
43
|
-
|
|
44
|
-
/** A strict allowlist for an npm version spec. Covers what a single-package install actually uses -
|
|
45
|
-
* `name@<exact|caret|tilde|x-range|dist-tag>` and scoped names - and NOTHING a shell could act on, so
|
|
46
|
-
* the fallback shell path can never inject. Comparator/whitespace ranges (`>=1 <2`, `1 || 2`) and the
|
|
47
|
-
* `*` wildcard are deliberately OUT: `> < = | *` and spaces are shell-meaningful. This is fine - the
|
|
48
|
-
* gateway only ever installs the caret-pinned `jose@^5.10.0`. Anything outside the set is refused
|
|
49
|
-
* before npm is ever invoked. */
|
|
50
|
-
const SPEC_RE = /^[A-Za-z0-9@._/^~+-]+$/;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Locate npm's JS entry (`npm-cli.js`) so it can be run with `node` directly, bypassing the
|
|
54
|
-
* `npm.cmd`/`npm` shim. Checks the layouts beside the running node binary (Windows/portable, then
|
|
55
|
-
* POSIX), then the Windows per-user global. Returns the absolute path, or null if not found.
|
|
56
|
-
* @returns {string|null}
|
|
57
|
-
*/
|
|
58
|
-
function findNpmCli() {
|
|
59
|
-
const dir = path.dirname(process.execPath);
|
|
60
|
-
const candidates = [
|
|
61
|
-
path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js'), // Windows / portable
|
|
62
|
-
path.join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'), // POSIX (/usr/bin -> /usr/lib)
|
|
63
|
-
];
|
|
64
|
-
if (process.env.APPDATA) {
|
|
65
|
-
candidates.push(path.join(process.env.APPDATA, 'npm', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
66
|
-
}
|
|
67
|
-
for (const c of candidates) {
|
|
68
|
-
try { if (fs.existsSync(c)) return c; } catch (_e) { /* ignore */ }
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* installJose - run `npm install jose@<PIN>` in the gateway package root. Resolves a structured
|
|
75
|
-
* result; never rejects. A 5-minute hard timeout guards against a hung registry.
|
|
76
|
-
*
|
|
77
|
-
* @param {object} [opts]
|
|
78
|
-
* @param {number} [opts.timeoutMs] default 300000 (5 min).
|
|
79
|
-
* @param {string} [opts.spec] override the install spec (tests). Default `jose@<JOSE_PIN>`.
|
|
80
|
-
* @returns {Promise<{ ok:boolean, code:(number|null), stdout:string, stderr:string, message:string }>}
|
|
81
|
-
*/
|
|
82
|
-
function installJose(opts) {
|
|
83
|
-
const o = opts || {};
|
|
84
|
-
const spec = typeof o.spec === 'string' && o.spec.length > 0 ? o.spec : `jose@${JOSE_PIN}`;
|
|
85
|
-
const timeoutMs = Number.isFinite(o.timeoutMs) && o.timeoutMs > 0 ? o.timeoutMs : 300000;
|
|
86
|
-
|
|
87
|
-
if (isJoseInstalled()) {
|
|
88
|
-
return Promise.resolve({ ok: true, code: 0, stdout: '', stderr: '', message: `jose already installed (${spec})` });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Refuse a spec carrying anything outside the strict allowlist BEFORE any shell is involved.
|
|
92
|
-
if (!SPEC_RE.test(spec)) {
|
|
93
|
-
return Promise.resolve({ ok: false, code: null, stdout: '', stderr: '', message: `refusing to install: invalid package spec "${spec}"` });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return new Promise((resolve) => {
|
|
97
|
-
let child;
|
|
98
|
-
let stdout = '';
|
|
99
|
-
let stderr = '';
|
|
100
|
-
let settled = false;
|
|
101
|
-
const done = (result) => {
|
|
102
|
-
if (settled) return;
|
|
103
|
-
settled = true;
|
|
104
|
-
clearTimeout(timer);
|
|
105
|
-
// On ANY failure, attach the guaranteed-correct manual fallback. Auto-install can't be made
|
|
106
|
-
// universal - npm-cli.js sits in different places relative to `node` across install methods
|
|
107
|
-
// (Homebrew/Volta/Docker/odd nvm). But the user definitely has a working npm (they used it to
|
|
108
|
-
// install/run the gateway), so a copyable command they run in their OWN shell always works.
|
|
109
|
-
// The UI + CLI surface result.message, so a failed auto-install becomes an actionable next
|
|
110
|
-
// step rather than a dead end.
|
|
111
|
-
if (result && result.ok !== true) {
|
|
112
|
-
result.manualCommand = 'npm install jose@' + JOSE_PIN;
|
|
113
|
-
result.cwd = PKG_ROOT;
|
|
114
|
-
if (typeof result.message === 'string' && result.message.indexOf('manually') === -1) {
|
|
115
|
-
result.message += ` - or install it manually: run \`${result.manualCommand}\` in ${PKG_ROOT}`;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
resolve(result);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const timer = setTimeout(() => {
|
|
122
|
-
try { if (child) child.kill('SIGKILL'); } catch (_e) { /* ignore */ }
|
|
123
|
-
done({ ok: false, code: null, stdout, stderr, message: `npm install timed out after ${timeoutMs}ms` });
|
|
124
|
-
}, timeoutMs);
|
|
125
|
-
if (typeof timer.unref === 'function') timer.unref();
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const npmCli = findNpmCli();
|
|
129
|
-
if (npmCli) {
|
|
130
|
-
// PREFERRED: run npm's JS entry with the current node binary - no shell, no .cmd shim, no
|
|
131
|
-
// PATH/cwd sensitivity. This is what actually works on a portable/Windows Node install.
|
|
132
|
-
child = spawn(process.execPath, [npmCli, 'install', spec, '--no-save'], {
|
|
133
|
-
cwd: PKG_ROOT,
|
|
134
|
-
env: process.env,
|
|
135
|
-
windowsHide: true,
|
|
136
|
-
shell: false,
|
|
137
|
-
});
|
|
138
|
-
} else if (IS_WIN) {
|
|
139
|
-
// Fallback (npm-cli.js not found): shell out to npm.cmd. The spec is allowlist-validated
|
|
140
|
-
// above and double-quoted so cmd.exe can't act on the `^` in a caret range or inject.
|
|
141
|
-
child = spawn(`"${NPM_BIN}" install "${spec}" --no-save`, {
|
|
142
|
-
cwd: PKG_ROOT,
|
|
143
|
-
env: process.env,
|
|
144
|
-
windowsHide: true,
|
|
145
|
-
shell: true,
|
|
146
|
-
});
|
|
147
|
-
} else {
|
|
148
|
-
child = spawn(NPM_BIN, ['install', spec, '--no-save'], {
|
|
149
|
-
cwd: PKG_ROOT,
|
|
150
|
-
env: process.env,
|
|
151
|
-
windowsHide: true,
|
|
152
|
-
shell: false,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
} catch (e) {
|
|
156
|
-
return done({ ok: false, code: null, stdout, stderr, message: `failed to spawn npm: ${(e && e.message) || e}` });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (child.stdout) child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
160
|
-
if (child.stderr) child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
161
|
-
child.on('error', (e) => {
|
|
162
|
-
done({ ok: false, code: null, stdout, stderr, message: `npm not runnable: ${(e && e.message) || e}` });
|
|
163
|
-
});
|
|
164
|
-
child.on('close', (code) => {
|
|
165
|
-
// Clear the lazy cache so a freshly-installed jose is picked up WITHOUT a process restart.
|
|
166
|
-
try { _resetJoseCache(); } catch (_e) { /* ignore */ }
|
|
167
|
-
const ok = code === 0 && isJoseInstalled();
|
|
168
|
-
done({
|
|
169
|
-
ok,
|
|
170
|
-
code,
|
|
171
|
-
stdout,
|
|
172
|
-
stderr,
|
|
173
|
-
message: ok
|
|
174
|
-
? `installed ${spec}`
|
|
175
|
-
: `npm install exited ${code}${isJoseInstalled() ? '' : ' (jose still not loadable)'}`,
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
module.exports = { installJose, findNpmCli, SPEC_RE, PKG_ROOT, NPM_BIN };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auth/install.js - install the OPTIONAL OAuth dependency (`jose`) on demand.
|
|
5
|
+
*
|
|
6
|
+
* The whole point of ToolFunnel's zero-dependency stance is that a default install pulls NOTHING.
|
|
7
|
+
* OAuth is opt-in, so its one library is installed only when a user decides they need it - via the
|
|
8
|
+
* CLI (`toolfunnel install-oauth`) or the admin-UI button, both of which call installJose() here.
|
|
9
|
+
*
|
|
10
|
+
* It shells out to the user's own `npm` to install the PINNED jose version into the gateway
|
|
11
|
+
* package's own node_modules (resolved from this file's location, NOT process.cwd(), so it lands
|
|
12
|
+
* beside the engine wherever ToolFunnel is installed). It uses `--no-save` - the goal is simply to
|
|
13
|
+
* make `require('jose')` resolvable; there is no manifest to update, and this keeps ToolFunnel's
|
|
14
|
+
* own `dependencies: {}` untouched in every install shape.
|
|
15
|
+
*
|
|
16
|
+
* WINDOWS NOTE: do NOT spawn `npm.cmd`. Since the CVE-2024-27980 mitigation, spawning a `.cmd`
|
|
17
|
+
* without a shell throws `EINVAL`; and spawning it WITH a shell is unreliable on portable Node
|
|
18
|
+
* installs - the `npm.cmd` shim misresolves its own path under cmd.exe ("Cannot find module
|
|
19
|
+
* ...\node_modules\npm\bin\npm-cli.js"). The robust, cross-platform approach is to run npm's JS entry
|
|
20
|
+
* (`npm-cli.js`, located beside the running node binary) with `process.execPath` directly -
|
|
21
|
+
* `shell:false`, no shim, no PATH lookup, no cwd sensitivity. We fall back to a shell-spawned `npm`
|
|
22
|
+
* only if `npm-cli.js` can't be located; the spec is allowlist-validated so that fallback can't inject.
|
|
23
|
+
*
|
|
24
|
+
* NEVER throws - it resolves to a structured { ok, code, stdout, stderr, message } so the CLI/UI can
|
|
25
|
+
* report the outcome cleanly even when npm is missing or the network is down.
|
|
26
|
+
*
|
|
27
|
+
* CommonJS only; Node built-ins (child_process, path, fs) + the pin from resource-server.js.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const { spawn } = require('node:child_process');
|
|
33
|
+
|
|
34
|
+
const { JOSE_PIN, isJoseInstalled, _resetJoseCache } = require('./resource-server');
|
|
35
|
+
|
|
36
|
+
/** The gateway package root: <root>/src/auth/install.js -> two dirs up. npm runs here so jose lands
|
|
37
|
+
* in the engine's own node_modules regardless of the caller's cwd. */
|
|
38
|
+
const PKG_ROOT = path.resolve(__dirname, '..', '..');
|
|
39
|
+
|
|
40
|
+
/** npm is `npm.cmd` on Windows, `npm` elsewhere. We spawn it as a child rather than require()-ing. */
|
|
41
|
+
const IS_WIN = process.platform === 'win32';
|
|
42
|
+
const NPM_BIN = IS_WIN ? 'npm.cmd' : 'npm';
|
|
43
|
+
|
|
44
|
+
/** A strict allowlist for an npm version spec. Covers what a single-package install actually uses -
|
|
45
|
+
* `name@<exact|caret|tilde|x-range|dist-tag>` and scoped names - and NOTHING a shell could act on, so
|
|
46
|
+
* the fallback shell path can never inject. Comparator/whitespace ranges (`>=1 <2`, `1 || 2`) and the
|
|
47
|
+
* `*` wildcard are deliberately OUT: `> < = | *` and spaces are shell-meaningful. This is fine - the
|
|
48
|
+
* gateway only ever installs the caret-pinned `jose@^5.10.0`. Anything outside the set is refused
|
|
49
|
+
* before npm is ever invoked. */
|
|
50
|
+
const SPEC_RE = /^[A-Za-z0-9@._/^~+-]+$/;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Locate npm's JS entry (`npm-cli.js`) so it can be run with `node` directly, bypassing the
|
|
54
|
+
* `npm.cmd`/`npm` shim. Checks the layouts beside the running node binary (Windows/portable, then
|
|
55
|
+
* POSIX), then the Windows per-user global. Returns the absolute path, or null if not found.
|
|
56
|
+
* @returns {string|null}
|
|
57
|
+
*/
|
|
58
|
+
function findNpmCli() {
|
|
59
|
+
const dir = path.dirname(process.execPath);
|
|
60
|
+
const candidates = [
|
|
61
|
+
path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js'), // Windows / portable
|
|
62
|
+
path.join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'), // POSIX (/usr/bin -> /usr/lib)
|
|
63
|
+
];
|
|
64
|
+
if (process.env.APPDATA) {
|
|
65
|
+
candidates.push(path.join(process.env.APPDATA, 'npm', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
66
|
+
}
|
|
67
|
+
for (const c of candidates) {
|
|
68
|
+
try { if (fs.existsSync(c)) return c; } catch (_e) { /* ignore */ }
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* installJose - run `npm install jose@<PIN>` in the gateway package root. Resolves a structured
|
|
75
|
+
* result; never rejects. A 5-minute hard timeout guards against a hung registry.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} [opts]
|
|
78
|
+
* @param {number} [opts.timeoutMs] default 300000 (5 min).
|
|
79
|
+
* @param {string} [opts.spec] override the install spec (tests). Default `jose@<JOSE_PIN>`.
|
|
80
|
+
* @returns {Promise<{ ok:boolean, code:(number|null), stdout:string, stderr:string, message:string }>}
|
|
81
|
+
*/
|
|
82
|
+
function installJose(opts) {
|
|
83
|
+
const o = opts || {};
|
|
84
|
+
const spec = typeof o.spec === 'string' && o.spec.length > 0 ? o.spec : `jose@${JOSE_PIN}`;
|
|
85
|
+
const timeoutMs = Number.isFinite(o.timeoutMs) && o.timeoutMs > 0 ? o.timeoutMs : 300000;
|
|
86
|
+
|
|
87
|
+
if (isJoseInstalled()) {
|
|
88
|
+
return Promise.resolve({ ok: true, code: 0, stdout: '', stderr: '', message: `jose already installed (${spec})` });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Refuse a spec carrying anything outside the strict allowlist BEFORE any shell is involved.
|
|
92
|
+
if (!SPEC_RE.test(spec)) {
|
|
93
|
+
return Promise.resolve({ ok: false, code: null, stdout: '', stderr: '', message: `refusing to install: invalid package spec "${spec}"` });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
let child;
|
|
98
|
+
let stdout = '';
|
|
99
|
+
let stderr = '';
|
|
100
|
+
let settled = false;
|
|
101
|
+
const done = (result) => {
|
|
102
|
+
if (settled) return;
|
|
103
|
+
settled = true;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
// On ANY failure, attach the guaranteed-correct manual fallback. Auto-install can't be made
|
|
106
|
+
// universal - npm-cli.js sits in different places relative to `node` across install methods
|
|
107
|
+
// (Homebrew/Volta/Docker/odd nvm). But the user definitely has a working npm (they used it to
|
|
108
|
+
// install/run the gateway), so a copyable command they run in their OWN shell always works.
|
|
109
|
+
// The UI + CLI surface result.message, so a failed auto-install becomes an actionable next
|
|
110
|
+
// step rather than a dead end.
|
|
111
|
+
if (result && result.ok !== true) {
|
|
112
|
+
result.manualCommand = 'npm install jose@' + JOSE_PIN;
|
|
113
|
+
result.cwd = PKG_ROOT;
|
|
114
|
+
if (typeof result.message === 'string' && result.message.indexOf('manually') === -1) {
|
|
115
|
+
result.message += ` - or install it manually: run \`${result.manualCommand}\` in ${PKG_ROOT}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
resolve(result);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
try { if (child) child.kill('SIGKILL'); } catch (_e) { /* ignore */ }
|
|
123
|
+
done({ ok: false, code: null, stdout, stderr, message: `npm install timed out after ${timeoutMs}ms` });
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const npmCli = findNpmCli();
|
|
129
|
+
if (npmCli) {
|
|
130
|
+
// PREFERRED: run npm's JS entry with the current node binary - no shell, no .cmd shim, no
|
|
131
|
+
// PATH/cwd sensitivity. This is what actually works on a portable/Windows Node install.
|
|
132
|
+
child = spawn(process.execPath, [npmCli, 'install', spec, '--no-save'], {
|
|
133
|
+
cwd: PKG_ROOT,
|
|
134
|
+
env: process.env,
|
|
135
|
+
windowsHide: true,
|
|
136
|
+
shell: false,
|
|
137
|
+
});
|
|
138
|
+
} else if (IS_WIN) {
|
|
139
|
+
// Fallback (npm-cli.js not found): shell out to npm.cmd. The spec is allowlist-validated
|
|
140
|
+
// above and double-quoted so cmd.exe can't act on the `^` in a caret range or inject.
|
|
141
|
+
child = spawn(`"${NPM_BIN}" install "${spec}" --no-save`, {
|
|
142
|
+
cwd: PKG_ROOT,
|
|
143
|
+
env: process.env,
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
shell: true,
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
child = spawn(NPM_BIN, ['install', spec, '--no-save'], {
|
|
149
|
+
cwd: PKG_ROOT,
|
|
150
|
+
env: process.env,
|
|
151
|
+
windowsHide: true,
|
|
152
|
+
shell: false,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} catch (e) {
|
|
156
|
+
return done({ ok: false, code: null, stdout, stderr, message: `failed to spawn npm: ${(e && e.message) || e}` });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (child.stdout) child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
160
|
+
if (child.stderr) child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
161
|
+
child.on('error', (e) => {
|
|
162
|
+
done({ ok: false, code: null, stdout, stderr, message: `npm not runnable: ${(e && e.message) || e}` });
|
|
163
|
+
});
|
|
164
|
+
child.on('close', (code) => {
|
|
165
|
+
// Clear the lazy cache so a freshly-installed jose is picked up WITHOUT a process restart.
|
|
166
|
+
try { _resetJoseCache(); } catch (_e) { /* ignore */ }
|
|
167
|
+
const ok = code === 0 && isJoseInstalled();
|
|
168
|
+
done({
|
|
169
|
+
ok,
|
|
170
|
+
code,
|
|
171
|
+
stdout,
|
|
172
|
+
stderr,
|
|
173
|
+
message: ok
|
|
174
|
+
? `installed ${spec}`
|
|
175
|
+
: `npm install exited ${code}${isJoseInstalled() ? '' : ' (jose still not loadable)'}`,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { installJose, findNpmCli, SPEC_RE, PKG_ROOT, NPM_BIN };
|