toolfunnel 0.7.0 → 0.8.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.
- package/README.md +238 -235
- package/SECURITY.md +84 -84
- package/bin/toolfunnel.js +530 -451
- package/docs/KNOWN_BUGS.md +100 -80
- package/docs/MANUAL.pdf +0 -0
- package/docs/design.md +84 -84
- package/docs/packaging.md +116 -115
- package/hooks/hooks.manifest.json +27 -27
- package/hooks/scripts/example-audit.js +21 -21
- package/hooks/scripts/example-deny-dangerous.js +39 -39
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -147
- package/mcp/servers/mock-upstream/server.js +379 -380
- package/package.json +67 -67
- package/src/auth/config.js +172 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +358 -353
- package/src/core/atomic-write.js +97 -0
- package/src/core/child-reaper.js +61 -61
- package/src/core/config-home.js +226 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +304 -329
- package/src/core/hook-loader.js +766 -824
- package/src/core/hook-runner.js +615 -566
- package/src/core/logger.js +295 -297
- package/src/core/matcher.js +100 -100
- package/src/core/metrics.js +90 -75
- package/src/core/output-cap.js +31 -0
- package/src/core/rename-migration.js +178 -163
- package/src/core/requires.js +138 -138
- package/src/core/server-config.js +98 -98
- package/src/extend/add-hook.md +155 -152
- package/src/extend/add-mcp.md +119 -119
- package/src/extend/configure.md +88 -88
- package/src/extend/create-tool.md +168 -168
- package/src/extend/howto.js +102 -102
- package/src/extend/package.md +190 -190
- package/src/extend/wrap.md +99 -99
- package/src/mcp/aggregator.js +2248 -1674
- package/src/mcp/expose-store.js +755 -577
- package/src/mcp/gated-run.js +210 -210
- package/src/mcp/http-transport.js +1657 -1563
- package/src/mcp/mcp-client.js +1174 -1070
- package/src/mcp/modern.js +409 -406
- package/src/mcp/protocol.js +473 -468
- package/src/mcp/server.js +3317 -3112
- package/src/mcp/tool-approvals.js +396 -0
- package/src/tools/registry.js +607 -661
- package/src/tools/tool-state.js +170 -170
- package/src/ui/public/app.js +1246 -1187
- package/src/ui/public/index.html +286 -286
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1742 -1616
- 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-accept-change.js +67 -0
- 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 -120
- package/tools/scripts/tf-list.js +192 -192
- package/tools/scripts/tf-log.js +86 -86
- package/tools/scripts/tf-mcp-add.js +130 -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 +16 -5
package/package.json
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "toolfunnel",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A zero-dependency MCP gateway: host your own tools, forward and curate tools from other MCP servers, expose them leanly to cut agent token cost, and gate every call through your own policy hooks before it runs.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"mcp",
|
|
7
|
-
"model-context-protocol",
|
|
8
|
-
"mcp-gateway",
|
|
9
|
-
"mcp-proxy",
|
|
10
|
-
"mcp-server",
|
|
11
|
-
"ai",
|
|
12
|
-
"agents",
|
|
13
|
-
"llm",
|
|
14
|
-
"tools",
|
|
15
|
-
"claude",
|
|
16
|
-
"anthropic",
|
|
17
|
-
"aggregator",
|
|
18
|
-
"token-efficiency"
|
|
19
|
-
],
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"type": "commonjs",
|
|
22
|
-
"main": "src/mcp/server.js",
|
|
23
|
-
"bin": {
|
|
24
|
-
"toolfunnel": "bin/toolfunnel.js"
|
|
25
|
-
},
|
|
26
|
-
"engines": {
|
|
27
|
-
"node": ">=18"
|
|
28
|
-
},
|
|
29
|
-
"scripts": {
|
|
30
|
-
"start": "node bin/toolfunnel.js",
|
|
31
|
-
"build": "echo toolfunnel has no build step - zero-dependency, runs from source",
|
|
32
|
-
"http": "node bin/toolfunnel.js --http",
|
|
33
|
-
"install-oauth": "node bin/toolfunnel.js install-oauth",
|
|
34
|
-
"test": "node test/run-all.js",
|
|
35
|
-
"prepublishOnly": "node test/run-all.js",
|
|
36
|
-
"release": "node scripts/release.js",
|
|
37
|
-
"test:integration": "node test/integration-real-mcp.test.js && node test/integration-http-client.test.js"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {},
|
|
40
|
-
"author": "rendeverance",
|
|
41
|
-
"repository": {
|
|
42
|
-
"type": "git",
|
|
43
|
-
"url": "git+https://github.com/Rendeverance/toolfunnel.git"
|
|
44
|
-
},
|
|
45
|
-
"homepage": "https://github.com/Rendeverance/toolfunnel#readme",
|
|
46
|
-
"bugs": {
|
|
47
|
-
"url": "https://github.com/Rendeverance/toolfunnel/issues"
|
|
48
|
-
},
|
|
49
|
-
"files": [
|
|
50
|
-
"bin/",
|
|
51
|
-
"src/",
|
|
52
|
-
"tools/tools.register.json",
|
|
53
|
-
"tools/scripts/",
|
|
54
|
-
"mcp/expose.example.json",
|
|
55
|
-
"mcp/servers/",
|
|
56
|
-
"hooks/hooks.manifest.json",
|
|
57
|
-
"hooks/scripts/",
|
|
58
|
-
"docs/",
|
|
59
|
-
"README.md",
|
|
60
|
-
"SECURITY.md",
|
|
61
|
-
"LICENSE"
|
|
62
|
-
],
|
|
63
|
-
"devDependencies": {
|
|
64
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
65
|
-
"jose": "^5.10.0"
|
|
66
|
-
}
|
|
67
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "toolfunnel",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "A zero-dependency MCP gateway: host your own tools, forward and curate tools from other MCP servers, expose them leanly to cut agent token cost, and gate every call through your own policy hooks before it runs.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"mcp-gateway",
|
|
9
|
+
"mcp-proxy",
|
|
10
|
+
"mcp-server",
|
|
11
|
+
"ai",
|
|
12
|
+
"agents",
|
|
13
|
+
"llm",
|
|
14
|
+
"tools",
|
|
15
|
+
"claude",
|
|
16
|
+
"anthropic",
|
|
17
|
+
"aggregator",
|
|
18
|
+
"token-efficiency"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "commonjs",
|
|
22
|
+
"main": "src/mcp/server.js",
|
|
23
|
+
"bin": {
|
|
24
|
+
"toolfunnel": "bin/toolfunnel.js"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"start": "node bin/toolfunnel.js",
|
|
31
|
+
"build": "echo toolfunnel has no build step - zero-dependency, runs from source",
|
|
32
|
+
"http": "node bin/toolfunnel.js --http",
|
|
33
|
+
"install-oauth": "node bin/toolfunnel.js install-oauth",
|
|
34
|
+
"test": "node test/run-all.js",
|
|
35
|
+
"prepublishOnly": "node test/run-all.js",
|
|
36
|
+
"release": "node scripts/release.js",
|
|
37
|
+
"test:integration": "node test/integration-real-mcp.test.js && node test/integration-http-client.test.js"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {},
|
|
40
|
+
"author": "rendeverance",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/Rendeverance/toolfunnel.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/Rendeverance/toolfunnel#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/Rendeverance/toolfunnel/issues"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"bin/",
|
|
51
|
+
"src/",
|
|
52
|
+
"tools/tools.register.json",
|
|
53
|
+
"tools/scripts/",
|
|
54
|
+
"mcp/expose.example.json",
|
|
55
|
+
"mcp/servers/",
|
|
56
|
+
"hooks/hooks.manifest.json",
|
|
57
|
+
"hooks/scripts/",
|
|
58
|
+
"docs/",
|
|
59
|
+
"README.md",
|
|
60
|
+
"SECURITY.md",
|
|
61
|
+
"LICENSE"
|
|
62
|
+
],
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
65
|
+
"jose": "^5.10.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/auth/config.js
CHANGED
|
@@ -1,174 +1,172 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* auth/config.js - the toggle + settings for OPTIONAL OAuth 2.1 resource-server validation.
|
|
5
|
-
*
|
|
6
|
-
* DEFAULT OFF (the gateway is loopback-only and unauthenticated until you turn this on). Nothing
|
|
7
|
-
* is read or required unless auth has been explicitly enabled via setConfig(). A MISSING config
|
|
8
|
-
* file means disabled - the safe default - so a fresh checkout authenticates nothing and pulls in
|
|
9
|
-
* no dependency.
|
|
10
|
-
*
|
|
11
|
-
* Design rules (mirrors src/core/logger.js - the proven config pattern):
|
|
12
|
-
* - Zero RUNTIME dependencies. Node built-ins only (fs, path). The jose library is pulled in ONLY
|
|
13
|
-
* by the validator (src/auth/resource-server.js), ONLY when auth is enabled, and is installed
|
|
14
|
-
* on demand (CLI `install-oauth` / the UI button). This module never touches jose.
|
|
15
|
-
* - Config is read FRESH on every call (no caching), so a toggle/edit takes effect for the very
|
|
16
|
-
* next request without a reconnect or restart - exactly like the logger + tool-state overlays.
|
|
17
|
-
* - Nothing here throws. A bad/unreadable/malformed config degrades to the safe default (disabled)
|
|
18
|
-
* so a misconfiguration can never crash the transport.
|
|
19
|
-
*
|
|
20
|
-
* Config file: <root>/auth/auth.config.json (NOT created until setConfig() writes it)
|
|
21
|
-
* {
|
|
22
|
-
* "enabled": false,
|
|
23
|
-
* "issuer": "https://auth.example.com", // the OAuth 2.1 / OIDC authorization server
|
|
24
|
-
* "audience": "https://gateway.example.com", // THIS gateway's resource URI - RFC 8707 audience
|
|
25
|
-
* // binding; the confused-deputy defence
|
|
26
|
-
* "jwksUri": "https://auth.example.com/.well-known/jwks.json", // explicit JWKS; else derived
|
|
27
|
-
* // from the issuer via OIDC discovery
|
|
28
|
-
* "algorithms": ["RS256", "ES256"], // the PINNED signature-algorithm allowlist
|
|
29
|
-
* "requiredScopes": [], // optional - every listed scope must be present
|
|
30
|
-
* "clockToleranceSec": 30 // exp/nbf skew tolerance (seconds)
|
|
31
|
-
* }
|
|
32
|
-
*
|
|
33
|
-
* CommonJS only.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
const fs = require('node:fs');
|
|
37
|
-
const path = require('node:path');
|
|
38
|
-
|
|
39
|
-
/** The CONFIG HOME (TOOLFUNNEL_HOME / --config-dir; defaults to the package root - see
|
|
40
|
-
* src/core/config-home.js). Auth config is user-state, so it lives with the home. */
|
|
41
|
-
const { resolveConfigHome } = require('../core/config-home');
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* @
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (!c.
|
|
131
|
-
if (!c.
|
|
132
|
-
if (!
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* @
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
module.exports = { getConfig, setConfig, configError, CONFIG_PATH, DEFAULTS };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auth/config.js - the toggle + settings for OPTIONAL OAuth 2.1 resource-server validation.
|
|
5
|
+
*
|
|
6
|
+
* DEFAULT OFF (the gateway is loopback-only and unauthenticated until you turn this on). Nothing
|
|
7
|
+
* is read or required unless auth has been explicitly enabled via setConfig(). A MISSING config
|
|
8
|
+
* file means disabled - the safe default - so a fresh checkout authenticates nothing and pulls in
|
|
9
|
+
* no dependency.
|
|
10
|
+
*
|
|
11
|
+
* Design rules (mirrors src/core/logger.js - the proven config pattern):
|
|
12
|
+
* - Zero RUNTIME dependencies. Node built-ins only (fs, path). The jose library is pulled in ONLY
|
|
13
|
+
* by the validator (src/auth/resource-server.js), ONLY when auth is enabled, and is installed
|
|
14
|
+
* on demand (CLI `install-oauth` / the UI button). This module never touches jose.
|
|
15
|
+
* - Config is read FRESH on every call (no caching), so a toggle/edit takes effect for the very
|
|
16
|
+
* next request without a reconnect or restart - exactly like the logger + tool-state overlays.
|
|
17
|
+
* - Nothing here throws. A bad/unreadable/malformed config degrades to the safe default (disabled)
|
|
18
|
+
* so a misconfiguration can never crash the transport.
|
|
19
|
+
*
|
|
20
|
+
* Config file: <root>/auth/auth.config.json (NOT created until setConfig() writes it)
|
|
21
|
+
* {
|
|
22
|
+
* "enabled": false,
|
|
23
|
+
* "issuer": "https://auth.example.com", // the OAuth 2.1 / OIDC authorization server
|
|
24
|
+
* "audience": "https://gateway.example.com", // THIS gateway's resource URI - RFC 8707 audience
|
|
25
|
+
* // binding; the confused-deputy defence
|
|
26
|
+
* "jwksUri": "https://auth.example.com/.well-known/jwks.json", // explicit JWKS; else derived
|
|
27
|
+
* // from the issuer via OIDC discovery
|
|
28
|
+
* "algorithms": ["RS256", "ES256"], // the PINNED signature-algorithm allowlist
|
|
29
|
+
* "requiredScopes": [], // optional - every listed scope must be present
|
|
30
|
+
* "clockToleranceSec": 30 // exp/nbf skew tolerance (seconds)
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* CommonJS only.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const fs = require('node:fs');
|
|
37
|
+
const path = require('node:path');
|
|
38
|
+
|
|
39
|
+
/** The CONFIG HOME (TOOLFUNNEL_HOME / --config-dir; defaults to the package root - see
|
|
40
|
+
* src/core/config-home.js). Auth config is user-state, so it lives with the home. */
|
|
41
|
+
const { resolveConfigHome } = require('../core/config-home');
|
|
42
|
+
const { atomicWriteJson } = require('../core/atomic-write'); // THE atomic writer, shared by every store
|
|
43
|
+
const ROOT = resolveConfigHome();
|
|
44
|
+
|
|
45
|
+
/** The toggle/config file. NOT created until setConfig() writes it. */
|
|
46
|
+
const CONFIG_PATH = path.join(ROOT, 'auth', 'auth.config.json');
|
|
47
|
+
|
|
48
|
+
/** Safe defaults - used whenever the config file is absent or unreadable. */
|
|
49
|
+
const DEFAULTS = Object.freeze({
|
|
50
|
+
enabled: false,
|
|
51
|
+
issuer: '',
|
|
52
|
+
audience: '',
|
|
53
|
+
jwksUri: '',
|
|
54
|
+
algorithms: ['RS256', 'ES256'],
|
|
55
|
+
requiredScopes: [],
|
|
56
|
+
clockToleranceSec: 30,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** A string field, defaulting to '' when not a non-empty string. */
|
|
60
|
+
function str(v, dflt) {
|
|
61
|
+
return typeof v === 'string' && v.length > 0 ? v : dflt;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A string[] field - keep only non-empty strings; fall back to `dflt` when not a usable array. */
|
|
65
|
+
function strArray(v, dflt) {
|
|
66
|
+
if (!Array.isArray(v)) return dflt.slice();
|
|
67
|
+
const out = v.filter((x) => typeof x === 'string' && x.length > 0);
|
|
68
|
+
return out.length > 0 ? out : dflt.slice();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* getConfig - the resolved auth config. Reads the file fresh; a missing/unreadable/malformed file
|
|
73
|
+
* resolves to the safe defaults (disabled). NEVER throws.
|
|
74
|
+
*
|
|
75
|
+
* @returns {{ enabled:boolean, issuer:string, audience:string, jwksUri:string,
|
|
76
|
+
* algorithms:string[], requiredScopes:string[], clockToleranceSec:number }}
|
|
77
|
+
*/
|
|
78
|
+
function getConfig() {
|
|
79
|
+
try {
|
|
80
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
83
|
+
return cloneDefaults();
|
|
84
|
+
}
|
|
85
|
+
const clock = Number(parsed.clockToleranceSec);
|
|
86
|
+
return {
|
|
87
|
+
enabled: parsed.enabled === true,
|
|
88
|
+
issuer: str(parsed.issuer, DEFAULTS.issuer),
|
|
89
|
+
audience: str(parsed.audience, DEFAULTS.audience),
|
|
90
|
+
jwksUri: str(parsed.jwksUri, DEFAULTS.jwksUri),
|
|
91
|
+
algorithms: strArray(parsed.algorithms, DEFAULTS.algorithms),
|
|
92
|
+
requiredScopes: strArray(parsed.requiredScopes, DEFAULTS.requiredScopes),
|
|
93
|
+
// Clamp to a sane, non-negative tolerance; a >5min skew signals a clock problem, not validity.
|
|
94
|
+
clockToleranceSec:
|
|
95
|
+
Number.isFinite(clock) && clock >= 0 ? Math.min(Math.floor(clock), 300) : DEFAULTS.clockToleranceSec,
|
|
96
|
+
};
|
|
97
|
+
} catch (_err) {
|
|
98
|
+
// Missing file = disabled (the safe default); also covers unreadable/malformed.
|
|
99
|
+
return cloneDefaults();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A fresh, mutation-safe copy of the defaults (arrays cloned so a caller can't corrupt the frozen one). */
|
|
104
|
+
function cloneDefaults() {
|
|
105
|
+
return {
|
|
106
|
+
enabled: DEFAULTS.enabled,
|
|
107
|
+
issuer: DEFAULTS.issuer,
|
|
108
|
+
audience: DEFAULTS.audience,
|
|
109
|
+
jwksUri: DEFAULTS.jwksUri,
|
|
110
|
+
algorithms: DEFAULTS.algorithms.slice(),
|
|
111
|
+
requiredScopes: DEFAULTS.requiredScopes.slice(),
|
|
112
|
+
clockToleranceSec: DEFAULTS.clockToleranceSec,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* configError - a human-readable reason the CURRENT config could not gate requests, or null if it
|
|
118
|
+
* is coherent enough to enforce. Used by the transport + UI to fail CLOSED with a clear message
|
|
119
|
+
* rather than silently mis-validating. Only meaningful when `enabled` is true.
|
|
120
|
+
*
|
|
121
|
+
* Enforcement requires: an issuer, an audience (the RFC 8707 confused-deputy defence - a gateway
|
|
122
|
+
* that does not bind tokens to its own resource URI is unsafe), at least one pinned algorithm,
|
|
123
|
+
* and a key source (explicit jwksUri OR an issuer to derive one from via OIDC discovery).
|
|
124
|
+
*
|
|
125
|
+
* @param {object} [cfg] defaults to getConfig()
|
|
126
|
+
* @returns {string|null}
|
|
127
|
+
*/
|
|
128
|
+
function configError(cfg) {
|
|
129
|
+
const c = cfg || getConfig();
|
|
130
|
+
if (!c.enabled) return null; // disabled -> nothing to enforce, no error
|
|
131
|
+
if (!c.issuer) return 'auth enabled but "issuer" is not set';
|
|
132
|
+
if (!c.audience) return 'auth enabled but "audience" (this gateway\'s resource URI) is not set - required to bind tokens (RFC 8707)';
|
|
133
|
+
if (!Array.isArray(c.algorithms) || c.algorithms.length === 0) return 'auth enabled but no signature "algorithms" are pinned';
|
|
134
|
+
// (A key source is always available: issuer is mandatory above, and jwksUri is derived from it
|
|
135
|
+
// via OIDC discovery when not given - so no separate jwksUri-or-issuer check is needed here.)
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* setConfig - atomically merge a patch into auth/auth.config.json (temp + rename). Merges with the
|
|
141
|
+
* current resolved config so a partial patch (e.g. { enabled: true }) preserves the other fields.
|
|
142
|
+
* Creates the auth/ dir and the file if absent - this is the ONLY function that creates it.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} patch any subset of the config fields
|
|
145
|
+
* @returns {object} the merged, written config
|
|
146
|
+
*/
|
|
147
|
+
function setConfig(patch) {
|
|
148
|
+
const current = getConfig();
|
|
149
|
+
const p = patch && typeof patch === 'object' && !Array.isArray(patch) ? patch : {};
|
|
150
|
+
|
|
151
|
+
const next = {
|
|
152
|
+
enabled: typeof p.enabled === 'boolean' ? p.enabled : current.enabled,
|
|
153
|
+
issuer: typeof p.issuer === 'string' ? p.issuer : current.issuer,
|
|
154
|
+
audience: typeof p.audience === 'string' ? p.audience : current.audience,
|
|
155
|
+
jwksUri: typeof p.jwksUri === 'string' ? p.jwksUri : current.jwksUri,
|
|
156
|
+
algorithms: Array.isArray(p.algorithms) ? strArray(p.algorithms, current.algorithms) : current.algorithms,
|
|
157
|
+
requiredScopes: Array.isArray(p.requiredScopes)
|
|
158
|
+
? p.requiredScopes.filter((x) => typeof x === 'string' && x.length > 0)
|
|
159
|
+
: current.requiredScopes,
|
|
160
|
+
clockToleranceSec:
|
|
161
|
+
Number.isFinite(Number(p.clockToleranceSec)) && Number(p.clockToleranceSec) >= 0
|
|
162
|
+
? Math.min(Math.floor(Number(p.clockToleranceSec)), 300)
|
|
163
|
+
: current.clockToleranceSec,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
167
|
+
atomicWriteJson(CONFIG_PATH, next); // temp + fsync + rename, failure-path unlink, stale sweep
|
|
168
|
+
|
|
169
|
+
return next;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { getConfig, setConfig, configError, CONFIG_PATH, DEFAULTS };
|