@kya-os/create-mcpi-app 1.7.36 → 1.7.38-canary.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/ARCHITECTURE_ANALYSIS.md +392 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/helpers/config-builder.d.ts +1 -1
- package/dist/helpers/config-builder.d.ts.map +1 -1
- package/dist/helpers/config-builder.js +1 -1
- package/dist/helpers/config-builder.js.map +1 -1
- package/dist/helpers/create.d.ts +1 -1
- package/dist/helpers/create.d.ts.map +1 -1
- package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts.map +1 -1
- package/dist/helpers/fetch-cloudflare-mcpi-template.js +2 -1
- package/dist/helpers/fetch-cloudflare-mcpi-template.js.map +1 -1
- package/dist/helpers/generate-config.d.ts.map +1 -1
- package/dist/helpers/generate-config.js.map +1 -1
- package/dist/index.js +18 -12
- package/dist/index.js.map +1 -1
- package/dist/utils/fetch-remote-config.d.ts +74 -0
- package/dist/utils/fetch-remote-config.d.ts.map +1 -0
- package/dist/utils/fetch-remote-config.js +109 -0
- package/dist/utils/fetch-remote-config.js.map +1 -0
- package/package.json +7 -12
- package/src/helpers/config-builder.ts +1 -1
- package/src/helpers/create.ts +1 -1
- package/src/helpers/fetch-cloudflare-mcpi-template.ts +2 -1
- package/src/helpers/generate-config.ts +0 -1
- package/src/index.ts +66 -26
- package/src/utils/__tests__/fetch-remote-config.test.ts +271 -0
- package/src/utils/fetch-remote-config.ts +179 -0
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-test$colon$coverage.log +0 -305
- package/.turbo/turbo-test.log +0 -483
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -268
- package/coverage/config-builder.ts.html +0 -580
- package/coverage/coverage-final.json +0 -7
- package/coverage/favicon.png +0 -0
- package/coverage/fetch-cloudflare-mcpi-template.ts.html +0 -7345
- package/coverage/generate-config.ts.html +0 -442
- package/coverage/generate-identity.ts.html +0 -574
- package/coverage/index.html +0 -191
- package/coverage/install.ts.html +0 -322
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/validate-project-structure.ts.html +0 -466
- package/dist/helpers/__tests__/config-builder.spec.d.ts +0 -8
- package/dist/helpers/__tests__/config-builder.spec.d.ts.map +0 -1
- package/dist/helpers/__tests__/config-builder.spec.js +0 -182
- package/dist/helpers/__tests__/config-builder.spec.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -15,8 +15,10 @@ import {
|
|
|
15
15
|
validateProjectName,
|
|
16
16
|
validateDirectoryAvailability,
|
|
17
17
|
} from "./utils/validate-project-name.js";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
resetIdentity,
|
|
20
|
+
regenerateIdentity,
|
|
21
|
+
} from "./helpers/identity-manager.js";
|
|
20
22
|
|
|
21
23
|
checkNodeVersion();
|
|
22
24
|
|
|
@@ -44,10 +46,7 @@ const program = new Command()
|
|
|
44
46
|
.option("--use-pnpm", "Use pnpm as package manager")
|
|
45
47
|
.option("--skip-install", "Skip installing dependencies", false)
|
|
46
48
|
.option("--skip-identity", "Skip identity generation", false)
|
|
47
|
-
.option(
|
|
48
|
-
"--no-identity",
|
|
49
|
-
"Create plain MCP project without identity features"
|
|
50
|
-
)
|
|
49
|
+
.option("--no-identity", "Create plain MCP project without identity features")
|
|
51
50
|
.option("--vercel", "Add Vercel support for deployment", false)
|
|
52
51
|
.option("--http", "Enable HTTP transport", false)
|
|
53
52
|
.option("--stdio", "Enable STDIO transport", false)
|
|
@@ -55,11 +54,18 @@ const program = new Command()
|
|
|
55
54
|
.option("--mcpi-version <version>", "Specify MCP-I version (e.g., ^1.2.7)")
|
|
56
55
|
.option("--no-animation", "Skip the black hole animation", false)
|
|
57
56
|
.option("--fast", "Use shorter animation duration", false)
|
|
58
|
-
.option(
|
|
57
|
+
.option(
|
|
58
|
+
"--platform <name>",
|
|
59
|
+
"Deployment platform: node (default), cloudflare",
|
|
60
|
+
"node"
|
|
61
|
+
)
|
|
59
62
|
.option("--mode <name>", "Server mode: mcp-i (default), verifier", "mcp-i")
|
|
60
63
|
.option("--template <name>", "[DEPRECATED] Use --platform and --mode instead")
|
|
61
64
|
.option("--apikey <key>", "AgentShield API key for Cloudflare deployment")
|
|
62
|
-
.option(
|
|
65
|
+
.option(
|
|
66
|
+
"--project <id>",
|
|
67
|
+
"AgentShield Project ID for Cloudflare deployment (enables project-scoped tool protection)"
|
|
68
|
+
)
|
|
63
69
|
.action(async (projectDir, options) => {
|
|
64
70
|
console.log(chalk.bold(`\ncreate-mcpi-app@${packageJson.version}`));
|
|
65
71
|
|
|
@@ -122,7 +128,11 @@ const program = new Command()
|
|
|
122
128
|
|
|
123
129
|
// Backward compatibility: map --template to --platform/--mode
|
|
124
130
|
if (options.template) {
|
|
125
|
-
console.log(
|
|
131
|
+
console.log(
|
|
132
|
+
chalk.yellow(
|
|
133
|
+
"⚠️ --template is deprecated. Use --platform and --mode instead."
|
|
134
|
+
)
|
|
135
|
+
);
|
|
126
136
|
if (options.template === "cloudflare") {
|
|
127
137
|
platform = "cloudflare";
|
|
128
138
|
mode = "verifier";
|
|
@@ -144,7 +154,9 @@ const program = new Command()
|
|
|
144
154
|
);
|
|
145
155
|
|
|
146
156
|
if (!isValid) {
|
|
147
|
-
console.error(
|
|
157
|
+
console.error(
|
|
158
|
+
chalk.red(`\n❌ Invalid platform/mode combination: ${platform}/${mode}`)
|
|
159
|
+
);
|
|
148
160
|
console.log(chalk.yellow("\nSupported combinations:"));
|
|
149
161
|
console.log(chalk.cyan(" - node + mcp-i (default)"));
|
|
150
162
|
console.log(chalk.cyan(" - cloudflare + mcp-i"));
|
|
@@ -212,7 +224,11 @@ const program = new Command()
|
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
// Vercel deployment option (only for Node.js platform)
|
|
215
|
-
if (
|
|
227
|
+
if (
|
|
228
|
+
!options.vercel &&
|
|
229
|
+
transports.includes("http") &&
|
|
230
|
+
platform === "node"
|
|
231
|
+
) {
|
|
216
232
|
const vercelAnswers = await inquirer.prompt([
|
|
217
233
|
{
|
|
218
234
|
type: "confirm",
|
|
@@ -349,15 +365,27 @@ const program = new Command()
|
|
|
349
365
|
}
|
|
350
366
|
console.log();
|
|
351
367
|
console.log("2. Create KV namespaces (creates both NONCE and PROOF):");
|
|
352
|
-
console.log(
|
|
353
|
-
|
|
368
|
+
console.log(
|
|
369
|
+
` ${chalk.cyan(`${packageManager === "npm" ? "npm run" : packageManager} kv:create`)}`
|
|
370
|
+
);
|
|
371
|
+
console.log(
|
|
372
|
+
chalk.gray(
|
|
373
|
+
" Copy both namespace IDs from output and update wrangler.toml"
|
|
374
|
+
)
|
|
375
|
+
);
|
|
354
376
|
console.log();
|
|
355
377
|
console.log("3. Test locally:");
|
|
356
|
-
console.log(
|
|
378
|
+
console.log(
|
|
379
|
+
` ${chalk.cyan(`${packageManager === "npm" ? "npm run" : packageManager} dev`)}`
|
|
380
|
+
);
|
|
357
381
|
console.log();
|
|
358
382
|
console.log("4. Deploy to Cloudflare:");
|
|
359
|
-
console.log(
|
|
360
|
-
|
|
383
|
+
console.log(
|
|
384
|
+
` ${chalk.cyan("npx wrangler login")} ${chalk.gray("(first time only)")}`
|
|
385
|
+
);
|
|
386
|
+
console.log(
|
|
387
|
+
` ${chalk.cyan(`${packageManager === "npm" ? "npm run" : packageManager} deploy`)}`
|
|
388
|
+
);
|
|
361
389
|
} else {
|
|
362
390
|
// Default MCP-I server instructions
|
|
363
391
|
// Show agent management with claim URL first
|
|
@@ -449,30 +477,42 @@ const program = new Command()
|
|
|
449
477
|
|
|
450
478
|
// Add subcommand for resetting identity
|
|
451
479
|
program
|
|
452
|
-
.command(
|
|
453
|
-
.description(
|
|
454
|
-
.option(
|
|
480
|
+
.command("reset-identity")
|
|
481
|
+
.description("Reset the current project's identity (removes identity files)")
|
|
482
|
+
.option(
|
|
483
|
+
"-d, --dir <directory>",
|
|
484
|
+
"Project directory (defaults to current directory)"
|
|
485
|
+
)
|
|
455
486
|
.action(async (options) => {
|
|
456
487
|
try {
|
|
457
|
-
const projectPath = options.dir
|
|
488
|
+
const projectPath = options.dir
|
|
489
|
+
? path.resolve(process.cwd(), options.dir)
|
|
490
|
+
: process.cwd();
|
|
458
491
|
await resetIdentity(projectPath);
|
|
459
492
|
} catch (error) {
|
|
460
|
-
console.error(chalk.red(
|
|
493
|
+
console.error(chalk.red("Failed to reset identity:"), error);
|
|
461
494
|
process.exit(1);
|
|
462
495
|
}
|
|
463
496
|
});
|
|
464
497
|
|
|
465
498
|
// Add subcommand for regenerating identity
|
|
466
499
|
program
|
|
467
|
-
.command(
|
|
468
|
-
.description(
|
|
469
|
-
|
|
500
|
+
.command("regenerate-identity")
|
|
501
|
+
.description(
|
|
502
|
+
"Regenerate the current project's identity (creates new DID and keys)"
|
|
503
|
+
)
|
|
504
|
+
.option(
|
|
505
|
+
"-d, --dir <directory>",
|
|
506
|
+
"Project directory (defaults to current directory)"
|
|
507
|
+
)
|
|
470
508
|
.action(async (options) => {
|
|
471
509
|
try {
|
|
472
|
-
const projectPath = options.dir
|
|
510
|
+
const projectPath = options.dir
|
|
511
|
+
? path.resolve(process.cwd(), options.dir)
|
|
512
|
+
: process.cwd();
|
|
473
513
|
await regenerateIdentity(projectPath);
|
|
474
514
|
} catch (error) {
|
|
475
|
-
console.error(chalk.red(
|
|
515
|
+
console.error(chalk.red("Failed to regenerate identity:"), error);
|
|
476
516
|
process.exit(1);
|
|
477
517
|
}
|
|
478
518
|
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Remote Configuration Fetching
|
|
3
|
+
*
|
|
4
|
+
* Validates that remote config fetching works correctly with caching
|
|
5
|
+
* and fallback behavior.
|
|
6
|
+
*
|
|
7
|
+
* These tests mirror the tests in @kya-os/mcp-i-core to ensure
|
|
8
|
+
* behavior matches the original implementation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
fetchRemoteConfig,
|
|
14
|
+
type RemoteConfigOptions,
|
|
15
|
+
type RemoteConfigCache
|
|
16
|
+
} from '../fetch-remote-config.js';
|
|
17
|
+
import type { MCPIConfig } from '@kya-os/contracts/config';
|
|
18
|
+
|
|
19
|
+
describe('fetchRemoteConfig', () => {
|
|
20
|
+
let mockFetch: ReturnType<typeof vi.fn>;
|
|
21
|
+
let mockCache: RemoteConfigCache;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
mockFetch = vi.fn();
|
|
25
|
+
mockCache = {
|
|
26
|
+
get: vi.fn(),
|
|
27
|
+
set: vi.fn()
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Cache hit scenario', () => {
|
|
32
|
+
it('should return cached config if available and not expired', async () => {
|
|
33
|
+
const cachedConfig: MCPIConfig = {
|
|
34
|
+
environment: 'production',
|
|
35
|
+
identity: { enabled: true, environment: 'production' }
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
vi.mocked(mockCache.get).mockResolvedValue(
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
config: cachedConfig,
|
|
41
|
+
expiresAt: Date.now() + 60000 // 1 minute in future
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const options: RemoteConfigOptions = {
|
|
46
|
+
apiUrl: 'https://kya.vouched.id',
|
|
47
|
+
apiKey: 'test-key',
|
|
48
|
+
projectId: 'test-project',
|
|
49
|
+
fetchProvider: mockFetch
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = await fetchRemoteConfig(options, mockCache);
|
|
53
|
+
|
|
54
|
+
expect(result).toEqual(cachedConfig);
|
|
55
|
+
expect(mockCache.get).toHaveBeenCalledWith('config:project:test-project');
|
|
56
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should fetch fresh config if cache expired', async () => {
|
|
60
|
+
const cachedConfig: MCPIConfig = {
|
|
61
|
+
environment: 'production'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
vi.mocked(mockCache.get).mockResolvedValue(
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
config: cachedConfig,
|
|
67
|
+
expiresAt: Date.now() - 1000 // Expired
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const freshConfig: MCPIConfig = {
|
|
72
|
+
environment: 'production',
|
|
73
|
+
identity: { enabled: true, environment: 'production' }
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
mockFetch.mockResolvedValue({
|
|
77
|
+
ok: true,
|
|
78
|
+
json: async () => ({ success: true, data: freshConfig })
|
|
79
|
+
} as Response);
|
|
80
|
+
|
|
81
|
+
const options: RemoteConfigOptions = {
|
|
82
|
+
apiUrl: 'https://kya.vouched.id',
|
|
83
|
+
apiKey: 'test-key',
|
|
84
|
+
projectId: 'test-project',
|
|
85
|
+
fetchProvider: mockFetch
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = await fetchRemoteConfig(options, mockCache);
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual(freshConfig);
|
|
91
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('Cache miss scenario', () => {
|
|
96
|
+
it('should fetch from API when cache is empty', async () => {
|
|
97
|
+
vi.mocked(mockCache.get).mockResolvedValue(null);
|
|
98
|
+
|
|
99
|
+
const config: MCPIConfig = {
|
|
100
|
+
environment: 'production',
|
|
101
|
+
identity: { enabled: true, environment: 'production' }
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
mockFetch.mockResolvedValue({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: async () => ({ success: true, data: config })
|
|
107
|
+
} as Response);
|
|
108
|
+
|
|
109
|
+
const options: RemoteConfigOptions = {
|
|
110
|
+
apiUrl: 'https://kya.vouched.id',
|
|
111
|
+
apiKey: 'test-key',
|
|
112
|
+
projectId: 'test-project',
|
|
113
|
+
fetchProvider: mockFetch
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = await fetchRemoteConfig(options, mockCache);
|
|
117
|
+
|
|
118
|
+
expect(result).toEqual(config);
|
|
119
|
+
expect(mockCache.set).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should use agentDid when projectId not available', async () => {
|
|
123
|
+
vi.mocked(mockCache.get).mockResolvedValue(null);
|
|
124
|
+
|
|
125
|
+
const config: MCPIConfig = {
|
|
126
|
+
environment: 'production'
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
mockFetch.mockResolvedValue({
|
|
130
|
+
ok: true,
|
|
131
|
+
json: async () => ({ success: true, data: config })
|
|
132
|
+
} as Response);
|
|
133
|
+
|
|
134
|
+
const options: RemoteConfigOptions = {
|
|
135
|
+
apiUrl: 'https://kya.vouched.id',
|
|
136
|
+
apiKey: 'test-key',
|
|
137
|
+
agentDid: 'did:key:test',
|
|
138
|
+
fetchProvider: mockFetch
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const result = await fetchRemoteConfig(options, mockCache);
|
|
142
|
+
|
|
143
|
+
expect(result).toEqual(config);
|
|
144
|
+
expect(mockCache.get).toHaveBeenCalledWith('config:agent:did:key:test');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('Error handling', () => {
|
|
149
|
+
it('should return null if API request fails', async () => {
|
|
150
|
+
vi.mocked(mockCache.get).mockResolvedValue(null);
|
|
151
|
+
|
|
152
|
+
mockFetch.mockResolvedValue({
|
|
153
|
+
ok: false,
|
|
154
|
+
status: 404,
|
|
155
|
+
statusText: 'Not Found'
|
|
156
|
+
} as Response);
|
|
157
|
+
|
|
158
|
+
const options: RemoteConfigOptions = {
|
|
159
|
+
apiUrl: 'https://kya.vouched.id',
|
|
160
|
+
apiKey: 'test-key',
|
|
161
|
+
projectId: 'test-project',
|
|
162
|
+
fetchProvider: mockFetch
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = await fetchRemoteConfig(options, mockCache);
|
|
166
|
+
|
|
167
|
+
expect(result).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should return null if API throws error', async () => {
|
|
171
|
+
vi.mocked(mockCache.get).mockResolvedValue(null);
|
|
172
|
+
|
|
173
|
+
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
174
|
+
|
|
175
|
+
const options: RemoteConfigOptions = {
|
|
176
|
+
apiUrl: 'https://kya.vouched.id',
|
|
177
|
+
apiKey: 'test-key',
|
|
178
|
+
projectId: 'test-project',
|
|
179
|
+
fetchProvider: mockFetch
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const result = await fetchRemoteConfig(options, mockCache);
|
|
183
|
+
|
|
184
|
+
expect(result).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should return null if neither projectId nor agentDid provided', async () => {
|
|
188
|
+
const options: RemoteConfigOptions = {
|
|
189
|
+
apiUrl: 'https://kya.vouched.id',
|
|
190
|
+
apiKey: 'test-key',
|
|
191
|
+
fetchProvider: mockFetch
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const result = await fetchRemoteConfig(options, mockCache);
|
|
195
|
+
|
|
196
|
+
expect(result).toBeNull();
|
|
197
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should handle cache read errors gracefully', async () => {
|
|
201
|
+
vi.mocked(mockCache.get).mockRejectedValue(new Error('Cache error'));
|
|
202
|
+
|
|
203
|
+
const config: MCPIConfig = {
|
|
204
|
+
environment: 'production'
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
mockFetch.mockResolvedValue({
|
|
208
|
+
ok: true,
|
|
209
|
+
json: async () => ({ success: true, data: config })
|
|
210
|
+
} as Response);
|
|
211
|
+
|
|
212
|
+
const options: RemoteConfigOptions = {
|
|
213
|
+
apiUrl: 'https://kya.vouched.id',
|
|
214
|
+
apiKey: 'test-key',
|
|
215
|
+
projectId: 'test-project',
|
|
216
|
+
fetchProvider: mockFetch
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const result = await fetchRemoteConfig(options, mockCache);
|
|
220
|
+
|
|
221
|
+
expect(result).toEqual(config);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('Response format handling', () => {
|
|
226
|
+
it('should handle different API response formats', async () => {
|
|
227
|
+
vi.mocked(mockCache.get).mockResolvedValue(null);
|
|
228
|
+
|
|
229
|
+
const config: MCPIConfig = {
|
|
230
|
+
environment: 'production'
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Format 1: { config: {...} }
|
|
234
|
+
mockFetch.mockResolvedValueOnce({
|
|
235
|
+
ok: true,
|
|
236
|
+
json: async () => ({ config })
|
|
237
|
+
} as Response);
|
|
238
|
+
|
|
239
|
+
const options: RemoteConfigOptions = {
|
|
240
|
+
apiUrl: 'https://kya.vouched.id',
|
|
241
|
+
apiKey: 'test-key',
|
|
242
|
+
projectId: 'test-project',
|
|
243
|
+
fetchProvider: mockFetch
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const result1 = await fetchRemoteConfig(options, mockCache);
|
|
247
|
+
expect(result1).toEqual(config);
|
|
248
|
+
|
|
249
|
+
// Format 2: { data: { config: {...} } }
|
|
250
|
+
vi.mocked(mockCache.get).mockResolvedValue(null);
|
|
251
|
+
mockFetch.mockResolvedValueOnce({
|
|
252
|
+
ok: true,
|
|
253
|
+
json: async () => ({ data: { config } })
|
|
254
|
+
} as Response);
|
|
255
|
+
|
|
256
|
+
const result2 = await fetchRemoteConfig(options, mockCache);
|
|
257
|
+
expect(result2).toEqual(config);
|
|
258
|
+
|
|
259
|
+
// Format 3: { success: true, data: {...} }
|
|
260
|
+
vi.mocked(mockCache.get).mockResolvedValue(null);
|
|
261
|
+
mockFetch.mockResolvedValueOnce({
|
|
262
|
+
ok: true,
|
|
263
|
+
json: async () => ({ success: true, data: config })
|
|
264
|
+
} as Response);
|
|
265
|
+
|
|
266
|
+
const result3 = await fetchRemoteConfig(options, mockCache);
|
|
267
|
+
expect(result3).toEqual(config);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Configuration Fetching
|
|
3
|
+
*
|
|
4
|
+
* Service for fetching configuration from remote APIs (AgentShield dashboard)
|
|
5
|
+
* with caching support for performance optimization.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: This implementation is extracted from @kya-os/mcp-i-core/src/config/remote-config.ts
|
|
8
|
+
* to avoid runtime dependency on the entire mcp-i-core package.
|
|
9
|
+
*
|
|
10
|
+
* Source: packages/mcp-i-core/src/config/remote-config.ts
|
|
11
|
+
*
|
|
12
|
+
* @module @kya-os/create-mcpi-app/utils/fetch-remote-config
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { MCPIConfig } from '@kya-os/contracts/config';
|
|
16
|
+
import { AGENTSHIELD_ENDPOINTS } from '@kya-os/contracts/agentshield-api';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options for fetching remote configuration
|
|
20
|
+
*/
|
|
21
|
+
export interface RemoteConfigOptions {
|
|
22
|
+
/**
|
|
23
|
+
* API base URL
|
|
24
|
+
* @example 'https://kya.vouched.id'
|
|
25
|
+
*/
|
|
26
|
+
apiUrl: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* API key for authentication
|
|
30
|
+
*/
|
|
31
|
+
apiKey: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Project ID (optional, preferred over agentDid)
|
|
35
|
+
* Used for project-scoped configuration
|
|
36
|
+
*/
|
|
37
|
+
projectId?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Agent DID (optional, used when projectId not available)
|
|
41
|
+
* Used for agent-scoped configuration
|
|
42
|
+
*/
|
|
43
|
+
agentDid?: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Cache TTL in milliseconds
|
|
47
|
+
* @default 300000 (5 minutes)
|
|
48
|
+
*/
|
|
49
|
+
cacheTtl?: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fetch provider function
|
|
53
|
+
* Platform-agnostic fetch implementation
|
|
54
|
+
*/
|
|
55
|
+
fetchProvider: (url: string, options: RequestInit) => Promise<Response>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Cache interface for remote configuration
|
|
60
|
+
* Abstracts platform-specific caching (KV, Redis, Memory, etc.)
|
|
61
|
+
*/
|
|
62
|
+
export interface RemoteConfigCache {
|
|
63
|
+
/**
|
|
64
|
+
* Get a cached value
|
|
65
|
+
*/
|
|
66
|
+
get(key: string): Promise<string | null>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set a cached value with TTL
|
|
70
|
+
*/
|
|
71
|
+
set(key: string, value: string, ttl: number): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch configuration from remote API (AgentShield dashboard)
|
|
76
|
+
*
|
|
77
|
+
* Attempts to fetch configuration from the AgentShield API with caching support.
|
|
78
|
+
* Falls back gracefully if remote fetch fails.
|
|
79
|
+
*
|
|
80
|
+
* @param options - Remote config options
|
|
81
|
+
* @param cache - Optional cache implementation
|
|
82
|
+
* @returns Configuration object or null if fetch fails
|
|
83
|
+
*/
|
|
84
|
+
export async function fetchRemoteConfig(
|
|
85
|
+
options: RemoteConfigOptions,
|
|
86
|
+
cache?: RemoteConfigCache
|
|
87
|
+
): Promise<MCPIConfig | null> {
|
|
88
|
+
const { apiUrl, apiKey, projectId, agentDid, cacheTtl = 300000, fetchProvider } = options;
|
|
89
|
+
|
|
90
|
+
// Generate cache key
|
|
91
|
+
const cacheKey = projectId
|
|
92
|
+
? `config:project:${projectId}`
|
|
93
|
+
: agentDid
|
|
94
|
+
? `config:agent:${agentDid}`
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
// Try cache first
|
|
98
|
+
if (cache && cacheKey) {
|
|
99
|
+
try {
|
|
100
|
+
const cached = await cache.get(cacheKey);
|
|
101
|
+
if (cached) {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(cached) as { config: MCPIConfig; expiresAt: number };
|
|
104
|
+
if (parsed.expiresAt > Date.now()) {
|
|
105
|
+
return parsed.config;
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Invalid cache entry, continue to fetch
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// Cache read failed, continue to fetch
|
|
113
|
+
console.warn('[RemoteConfig] Cache read failed:', error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fetch from API
|
|
118
|
+
try {
|
|
119
|
+
// Build API URL
|
|
120
|
+
let url: string;
|
|
121
|
+
if (projectId) {
|
|
122
|
+
// Use project-scoped endpoint (preferred)
|
|
123
|
+
url = `${apiUrl}${AGENTSHIELD_ENDPOINTS.CONFIG(projectId)}`;
|
|
124
|
+
} else if (agentDid) {
|
|
125
|
+
// Use agent-scoped endpoint
|
|
126
|
+
url = `${apiUrl}/api/v1/bouncer/config?agent_did=${encodeURIComponent(agentDid)}`;
|
|
127
|
+
} else {
|
|
128
|
+
console.warn('[RemoteConfig] Neither projectId nor agentDid provided');
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const response = await fetchProvider(url, {
|
|
133
|
+
headers: {
|
|
134
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
135
|
+
'Content-Type': 'application/json'
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
console.warn(`[RemoteConfig] API returned ${response.status}: ${response.statusText}`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const data = await response.json();
|
|
145
|
+
|
|
146
|
+
// Extract config from API response
|
|
147
|
+
// API response format: { success: boolean, data: { config: MCPIConfig } }
|
|
148
|
+
const responseData = data as { config?: MCPIConfig; data?: { config?: MCPIConfig }; success?: boolean };
|
|
149
|
+
const config = responseData.config || responseData.data?.config || (responseData.success ? responseData.data as MCPIConfig | null : null) as MCPIConfig | null;
|
|
150
|
+
|
|
151
|
+
if (!config) {
|
|
152
|
+
console.warn('[RemoteConfig] No config found in API response');
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Cache the result
|
|
157
|
+
if (cache && cacheKey) {
|
|
158
|
+
try {
|
|
159
|
+
await cache.set(
|
|
160
|
+
cacheKey,
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
config,
|
|
163
|
+
expiresAt: Date.now() + cacheTtl
|
|
164
|
+
}),
|
|
165
|
+
cacheTtl
|
|
166
|
+
);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// Cache write failed, but we got the config so continue
|
|
169
|
+
console.warn('[RemoteConfig] Cache write failed:', error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return config as MCPIConfig;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.warn('[RemoteConfig] Failed to fetch config:', error);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
package/.turbo/turbo-build.log
DELETED