@mhingston5/conduit 1.1.2 → 1.1.3
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 +27 -3
- package/dist/assets/deno-shim.ts +93 -0
- package/dist/assets/python-shim.py +21 -0
- package/dist/executors/pyodide.worker.d.ts +2 -0
- package/dist/executors/pyodide.worker.js +163 -0
- package/dist/executors/pyodide.worker.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3250 -0
- package/dist/index.js.map +1 -0
- package/docs/ARCHITECTURE.md +1 -1
- package/package.json +6 -1
- package/src/auth.cmd.ts +95 -0
- package/src/core/config.service.ts +5 -2
- package/src/core/request.controller.ts +4 -0
- package/src/core/security.service.ts +3 -3
- package/src/gateway/auth.service.ts +19 -18
- package/src/index.ts +52 -8
- package/src/transport/socket.transport.ts +8 -3
- package/tests/auth.service.test.ts +8 -13
- package/tests/config.service.test.ts +31 -0
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mhingston5/conduit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A secure Code Mode execution substrate for MCP agents",
|
|
6
6
|
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"conduit": "./dist/index.js"
|
|
9
|
+
},
|
|
7
10
|
"publishConfig": {
|
|
8
11
|
"access": "public"
|
|
9
12
|
},
|
|
@@ -47,11 +50,13 @@
|
|
|
47
50
|
"ajv": "^8.17.1",
|
|
48
51
|
"ajv-formats": "^3.0.1",
|
|
49
52
|
"axios": "^1.13.2",
|
|
53
|
+
"commander": "^14.0.2",
|
|
50
54
|
"dotenv": "^17.2.3",
|
|
51
55
|
"fastify": "^5.6.2",
|
|
52
56
|
"isolated-vm": "^6.0.2",
|
|
53
57
|
"js-yaml": "^4.1.1",
|
|
54
58
|
"lru-cache": "^11.2.4",
|
|
59
|
+
"open": "^11.0.0",
|
|
55
60
|
"p-limit": "^7.2.0",
|
|
56
61
|
"pino": "^10.1.0",
|
|
57
62
|
"pyodide": "^0.29.0",
|
package/src/auth.cmd.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
|
|
6
|
+
export interface AuthOptions {
|
|
7
|
+
authUrl: string;
|
|
8
|
+
tokenUrl: string;
|
|
9
|
+
clientId: string;
|
|
10
|
+
clientSecret: string;
|
|
11
|
+
scopes?: string;
|
|
12
|
+
port?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function handleAuth(options: AuthOptions) {
|
|
16
|
+
const port = options.port || 3333;
|
|
17
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
18
|
+
const state = uuidv4();
|
|
19
|
+
|
|
20
|
+
const fastify = Fastify();
|
|
21
|
+
|
|
22
|
+
return new Promise<void>((resolve, reject) => {
|
|
23
|
+
fastify.get('/callback', async (request, reply) => {
|
|
24
|
+
const { code, state: returnedState, error, error_description } = request.query as any;
|
|
25
|
+
|
|
26
|
+
if (error) {
|
|
27
|
+
reply.send(`Authentication failed: ${error} - ${error_description}`);
|
|
28
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (returnedState !== state) {
|
|
33
|
+
reply.send('Invalid state parameter');
|
|
34
|
+
reject(new Error('State mismatch'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response = await axios.post(options.tokenUrl, {
|
|
40
|
+
grant_type: 'authorization_code',
|
|
41
|
+
code,
|
|
42
|
+
redirect_uri: redirectUri,
|
|
43
|
+
client_id: options.clientId,
|
|
44
|
+
client_secret: options.clientSecret,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { refresh_token, access_token } = response.data;
|
|
48
|
+
|
|
49
|
+
console.log('\n--- Authentication Successful ---\n');
|
|
50
|
+
console.log('Use these values in your conduit.yaml:\n');
|
|
51
|
+
console.log('credentials:');
|
|
52
|
+
console.log(' type: oauth2');
|
|
53
|
+
console.log(` clientId: ${options.clientId}`);
|
|
54
|
+
console.log(` clientSecret: ${options.clientSecret}`);
|
|
55
|
+
console.log(` tokenUrl: "${options.tokenUrl}"`);
|
|
56
|
+
console.log(` refreshToken: "${refresh_token || 'N/A (No refresh token returned)'}"`);
|
|
57
|
+
|
|
58
|
+
if (!refresh_token) {
|
|
59
|
+
console.log('\nWarning: No refresh token was returned. Ensure your app has "offline_access" scope or similar.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('\nRaw response data:', JSON.stringify(response.data, null, 2));
|
|
63
|
+
|
|
64
|
+
reply.send('Authentication successful! You can close this window and return to the terminal.');
|
|
65
|
+
resolve();
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
const msg = err.response?.data?.error_description || err.response?.data?.error || err.message;
|
|
68
|
+
reply.send(`Failed to exchange code for token: ${msg}`);
|
|
69
|
+
reject(new Error(`Token exchange failed: ${msg}`));
|
|
70
|
+
} finally {
|
|
71
|
+
setTimeout(() => fastify.close(), 1000);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
fastify.listen({ port: port, host: '127.0.0.1' }, async (err) => {
|
|
76
|
+
if (err) {
|
|
77
|
+
reject(err);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const authUrl = new URL(options.authUrl);
|
|
82
|
+
authUrl.searchParams.append('client_id', options.clientId);
|
|
83
|
+
authUrl.searchParams.append('redirect_uri', redirectUri);
|
|
84
|
+
authUrl.searchParams.append('response_type', 'code');
|
|
85
|
+
authUrl.searchParams.append('state', state);
|
|
86
|
+
if (options.scopes) {
|
|
87
|
+
authUrl.searchParams.append('scope', options.scopes);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(`Opening browser to: ${authUrl.toString()}`);
|
|
91
|
+
console.log('Waiting for callback...');
|
|
92
|
+
await open(authUrl.toString());
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import dotenv from 'dotenv';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
5
6
|
import yaml from 'js-yaml';
|
|
6
7
|
|
|
7
8
|
// Silence dotenv logging
|
|
@@ -22,12 +23,14 @@ export const ResourceLimitsSchema = z.object({
|
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
export const UpstreamCredentialsSchema = z.object({
|
|
25
|
-
type: z.enum(['oauth2', '
|
|
26
|
+
type: z.enum(['oauth2', 'apiKey', 'bearer']), // Align with AuthType
|
|
26
27
|
clientId: z.string().optional(),
|
|
27
28
|
clientSecret: z.string().optional(),
|
|
28
29
|
tokenUrl: z.string().optional(),
|
|
30
|
+
refreshToken: z.string().optional(),
|
|
29
31
|
scopes: z.array(z.string()).optional(),
|
|
30
32
|
apiKey: z.string().optional(),
|
|
33
|
+
bearerToken: z.string().optional(),
|
|
31
34
|
headerName: z.string().optional(),
|
|
32
35
|
});
|
|
33
36
|
|
|
@@ -63,7 +66,7 @@ export const ConfigSchema = z.object({
|
|
|
63
66
|
secretRedactionPatterns: z.array(z.string()).default([
|
|
64
67
|
'[A-Za-z0-9-_]{20,}', // Default pattern from spec
|
|
65
68
|
]),
|
|
66
|
-
ipcBearerToken: z.string().optional().default(() =>
|
|
69
|
+
ipcBearerToken: z.string().optional().default(() => crypto.randomUUID()),
|
|
67
70
|
maxConcurrent: z.number().default(10),
|
|
68
71
|
denoMaxPoolSize: z.number().default(10),
|
|
69
72
|
pyodideMaxPoolSize: z.number().default(3),
|
|
@@ -209,6 +209,7 @@ export class RequestController {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
private async handleCallTool(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
|
|
212
|
+
if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
|
|
212
213
|
const { name, arguments: toolArgs } = params;
|
|
213
214
|
|
|
214
215
|
// Route built-in tools to their specific handlers
|
|
@@ -226,6 +227,7 @@ export class RequestController {
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
private async handleExecuteTypeScript(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
|
|
230
|
+
if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
|
|
229
231
|
const { code, limits, allowedTools } = params;
|
|
230
232
|
|
|
231
233
|
if (Array.isArray(allowedTools)) {
|
|
@@ -250,6 +252,7 @@ export class RequestController {
|
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
private async handleExecutePython(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
|
|
255
|
+
if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
|
|
253
256
|
const { code, limits, allowedTools } = params;
|
|
254
257
|
|
|
255
258
|
if (Array.isArray(allowedTools)) {
|
|
@@ -299,6 +302,7 @@ export class RequestController {
|
|
|
299
302
|
}
|
|
300
303
|
|
|
301
304
|
private async handleExecuteIsolate(params: any, context: ExecutionContext, id: string | number): Promise<JSONRPCResponse> {
|
|
305
|
+
if (!params) return this.errorResponse(id, -32602, 'Missing parameters');
|
|
302
306
|
const { code, limits, allowedTools } = params;
|
|
303
307
|
|
|
304
308
|
if (Array.isArray(allowedTools)) {
|
|
@@ -9,11 +9,11 @@ export type { Session };
|
|
|
9
9
|
|
|
10
10
|
export class SecurityService implements IUrlValidator {
|
|
11
11
|
private logger: Logger;
|
|
12
|
-
private ipcToken: string;
|
|
12
|
+
private ipcToken: string | undefined;
|
|
13
13
|
private networkPolicy: NetworkPolicyService;
|
|
14
14
|
private sessionManager: SessionManager;
|
|
15
15
|
|
|
16
|
-
constructor(logger: Logger, ipcToken: string) {
|
|
16
|
+
constructor(logger: Logger, ipcToken: string | undefined) {
|
|
17
17
|
this.logger = logger;
|
|
18
18
|
this.ipcToken = ipcToken;
|
|
19
19
|
this.networkPolicy = new NetworkPolicyService(logger);
|
|
@@ -67,7 +67,7 @@ export class SecurityService implements IUrlValidator {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
getIpcToken(): string {
|
|
70
|
+
getIpcToken(): string | undefined {
|
|
71
71
|
return this.ipcToken;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -7,12 +7,10 @@ export interface UpstreamCredentials {
|
|
|
7
7
|
type: AuthType;
|
|
8
8
|
apiKey?: string;
|
|
9
9
|
bearerToken?: string;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
refreshToken: string;
|
|
15
|
-
};
|
|
10
|
+
clientId?: string;
|
|
11
|
+
clientSecret?: string;
|
|
12
|
+
tokenUrl?: string;
|
|
13
|
+
refreshToken?: string;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
interface CachedToken {
|
|
@@ -45,10 +43,11 @@ export class AuthService {
|
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
private async getOAuth2Token(creds: UpstreamCredentials): Promise<string> {
|
|
48
|
-
if (!creds.
|
|
46
|
+
if (!creds.tokenUrl || !creds.clientId) {
|
|
47
|
+
throw new Error('OAuth2 credentials missing required fields (tokenUrl, clientId)');
|
|
48
|
+
}
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
const cacheKey = `${oauth2.clientId}:${oauth2.tokenUrl}`;
|
|
50
|
+
const cacheKey = `${creds.clientId}:${creds.tokenUrl}`;
|
|
52
51
|
|
|
53
52
|
// Check cache first (with 30s buffer)
|
|
54
53
|
const cached = this.tokenCache.get(cacheKey);
|
|
@@ -74,17 +73,18 @@ export class AuthService {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
private async doRefresh(creds: UpstreamCredentials, cacheKey: string): Promise<string> {
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId || !creds.clientSecret) {
|
|
77
|
+
throw new Error('OAuth2 credentials missing required fields for refresh');
|
|
78
|
+
}
|
|
79
79
|
|
|
80
|
-
this.logger.info('Refreshing OAuth2 token');
|
|
80
|
+
this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, 'Refreshing OAuth2 token');
|
|
81
81
|
|
|
82
82
|
try {
|
|
83
|
-
const response = await axios.post(
|
|
83
|
+
const response = await axios.post(creds.tokenUrl, {
|
|
84
84
|
grant_type: 'refresh_token',
|
|
85
|
-
refresh_token:
|
|
86
|
-
client_id:
|
|
87
|
-
client_secret:
|
|
85
|
+
refresh_token: creds.refreshToken,
|
|
86
|
+
client_id: creds.clientId,
|
|
87
|
+
client_secret: creds.clientSecret,
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
const { access_token, expires_in } = response.data;
|
|
@@ -97,8 +97,9 @@ export class AuthService {
|
|
|
97
97
|
|
|
98
98
|
return `Bearer ${access_token}`;
|
|
99
99
|
} catch (err: any) {
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const errorMsg = err.response?.data?.error_description || err.response?.data?.error || err.message;
|
|
101
|
+
this.logger.error({ err: errorMsg }, 'Failed to refresh OAuth2 token');
|
|
102
|
+
throw new Error(`OAuth2 refresh failed: ${errorMsg}`);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
1
2
|
import { ConfigService } from './core/config.service.js';
|
|
2
3
|
import { createLogger, loggerStorage } from './core/logger.js';
|
|
3
4
|
import { SocketTransport } from './transport/socket.transport.js';
|
|
@@ -14,10 +15,56 @@ import { IsolateExecutor } from './executors/isolate.executor.js';
|
|
|
14
15
|
import { ExecutorRegistry } from './core/registries/executor.registry.js';
|
|
15
16
|
import { ExecutionService } from './core/execution.service.js';
|
|
16
17
|
import { buildDefaultMiddleware } from './core/middleware/middleware.builder.js';
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
import { handleAuth } from './auth.cmd.js';
|
|
19
|
+
|
|
20
|
+
const program = new Command();
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name('conduit')
|
|
24
|
+
.description('A secure Code Mode execution substrate for MCP agents')
|
|
25
|
+
.version('1.0.0');
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('serve', { isDefault: true })
|
|
29
|
+
.description('Start the Conduit server')
|
|
30
|
+
.option('--stdio', 'Use stdio transport')
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
try {
|
|
33
|
+
await startServer();
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('Failed to start Conduit:', err);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('auth')
|
|
42
|
+
.description('Help set up OAuth for an upstream MCP server')
|
|
43
|
+
.requiredOption('--client-id <id>', 'OAuth Client ID')
|
|
44
|
+
.requiredOption('--client-secret <secret>', 'OAuth Client Secret')
|
|
45
|
+
.requiredOption('--auth-url <url>', 'OAuth Authorization URL')
|
|
46
|
+
.requiredOption('--token-url <url>', 'OAuth Token URL')
|
|
47
|
+
.option('--scopes <scopes>', 'OAuth Scopes (comma separated)')
|
|
48
|
+
.option('--port <port>', 'Port for the local callback server', '3333')
|
|
49
|
+
.action(async (options) => {
|
|
50
|
+
try {
|
|
51
|
+
await handleAuth({
|
|
52
|
+
clientId: options.clientId,
|
|
53
|
+
clientSecret: options.clientSecret,
|
|
54
|
+
authUrl: options.authUrl,
|
|
55
|
+
tokenUrl: options.tokenUrl,
|
|
56
|
+
scopes: options.scopes,
|
|
57
|
+
port: parseInt(options.port, 10),
|
|
58
|
+
});
|
|
59
|
+
console.log('\nSuccess! Configuration generated.');
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
console.error('Authentication helper failed:', err.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
async function startServer() {
|
|
19
67
|
const configService = new ConfigService();
|
|
20
|
-
// console.error('DEBUG: Config loaded');
|
|
21
68
|
const logger = createLogger(configService);
|
|
22
69
|
|
|
23
70
|
const otelService = new OtelService(logger);
|
|
@@ -78,7 +125,7 @@ async function main() {
|
|
|
78
125
|
const port = configService.get('port');
|
|
79
126
|
address = await transport.listen({ port });
|
|
80
127
|
}
|
|
81
|
-
executionService.ipcAddress = address;
|
|
128
|
+
executionService.ipcAddress = address;
|
|
82
129
|
|
|
83
130
|
// Pre-warm workers
|
|
84
131
|
await requestController.warmup();
|
|
@@ -102,7 +149,4 @@ async function main() {
|
|
|
102
149
|
});
|
|
103
150
|
}
|
|
104
151
|
|
|
105
|
-
|
|
106
|
-
console.error('Failed to start Conduit:', err);
|
|
107
|
-
process.exit(1);
|
|
108
|
-
});
|
|
152
|
+
program.parse(process.argv);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { Logger } from 'pino';
|
|
@@ -46,9 +47,13 @@ export class SocketTransport {
|
|
|
46
47
|
|
|
47
48
|
// Cleanup existing socket if needed (unlikely on Windows, but good for Unix)
|
|
48
49
|
if (os.platform() !== 'win32' && path.isAbsolute(socketPath)) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
try {
|
|
51
|
+
fs.unlinkSync(socketPath);
|
|
52
|
+
} catch (error: any) {
|
|
53
|
+
if (error.code !== 'ENOENT') {
|
|
54
|
+
this.logger.warn({ err: error, socketPath }, 'Failed to unlink socket before binding');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
this.server.listen(socketPath, () => {
|
|
@@ -25,13 +25,10 @@ describe('AuthService', () => {
|
|
|
25
25
|
it('should refresh OAuth2 token when expired', async () => {
|
|
26
26
|
const creds: any = {
|
|
27
27
|
type: 'oauth2',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
refreshToken: 'refresh',
|
|
33
|
-
expiresAt: Date.now() - 1000, // Expired
|
|
34
|
-
},
|
|
28
|
+
clientId: 'id',
|
|
29
|
+
clientSecret: 'secret',
|
|
30
|
+
tokenUrl: 'http://token',
|
|
31
|
+
refreshToken: 'refresh',
|
|
35
32
|
};
|
|
36
33
|
|
|
37
34
|
(axios.post as any).mockResolvedValue({
|
|
@@ -50,12 +47,10 @@ describe('AuthService', () => {
|
|
|
50
47
|
// First call - will trigger refresh
|
|
51
48
|
const creds: any = {
|
|
52
49
|
type: 'oauth2',
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
refreshToken: 'refresh',
|
|
58
|
-
},
|
|
50
|
+
clientId: 'id',
|
|
51
|
+
clientSecret: 'secret',
|
|
52
|
+
tokenUrl: 'http://token',
|
|
53
|
+
refreshToken: 'refresh',
|
|
59
54
|
};
|
|
60
55
|
|
|
61
56
|
(axios.post as any).mockResolvedValue({
|
|
@@ -67,4 +67,35 @@ describe('ConfigService', () => {
|
|
|
67
67
|
existsSpy.mockRestore();
|
|
68
68
|
readSpy.mockRestore();
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
it('should parse OAuth2 credentials correctly', () => {
|
|
72
|
+
const existsSpy = vi.spyOn(fs, 'existsSync').mockImplementation((p: any) => p.endsWith('conduit.test.yaml'));
|
|
73
|
+
const readSpy = vi.spyOn(fs, 'readFileSync').mockReturnValue(`
|
|
74
|
+
upstreams:
|
|
75
|
+
- id: test-oauth
|
|
76
|
+
type: http
|
|
77
|
+
url: http://upstream
|
|
78
|
+
credentials:
|
|
79
|
+
type: oauth2
|
|
80
|
+
clientId: my-id
|
|
81
|
+
clientSecret: my-secret
|
|
82
|
+
tokenUrl: http://token
|
|
83
|
+
refreshToken: my-refresh
|
|
84
|
+
`);
|
|
85
|
+
|
|
86
|
+
vi.stubEnv('CONFIG_FILE', 'conduit.test.yaml');
|
|
87
|
+
const configService = new ConfigService();
|
|
88
|
+
const upstreams = configService.get('upstreams');
|
|
89
|
+
expect(upstreams).toHaveLength(1);
|
|
90
|
+
expect(upstreams![0].credentials).toEqual({
|
|
91
|
+
type: 'oauth2',
|
|
92
|
+
clientId: 'my-id',
|
|
93
|
+
clientSecret: 'my-secret',
|
|
94
|
+
tokenUrl: 'http://token',
|
|
95
|
+
refreshToken: 'my-refresh'
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
existsSpy.mockRestore();
|
|
99
|
+
readSpy.mockRestore();
|
|
100
|
+
});
|
|
70
101
|
});
|