dxcomplete 0.2.1 → 0.2.2
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/.env.example +0 -7
- package/README.md +17 -45
- package/dist/cli.js +0 -22
- package/dist/validate.js +10 -26
- package/docs/model.md +3 -3
- package/docs/taxonomy.md +1 -1
- package/package.json +23 -23
- package/templates/process/README.md +1 -1
- package/dist/http/service.d.ts +0 -7
- package/dist/http/service.js +0 -725
- package/dist/mcp/docs.d.ts +0 -114
- package/dist/mcp/docs.js +0 -626
- package/dist/mcp/server.d.ts +0 -20
- package/dist/mcp/server.js +0 -3059
- package/dist/runtime/auth.d.ts +0 -162
- package/dist/runtime/auth.js +0 -394
- package/dist/runtime/check.d.ts +0 -7
- package/dist/runtime/check.js +0 -16
- package/dist/runtime/config.d.ts +0 -17
- package/dist/runtime/config.js +0 -93
- package/dist/runtime/mongo.d.ts +0 -9
- package/dist/runtime/mongo.js +0 -56
- package/dist/runtime/records.d.ts +0 -427
- package/dist/runtime/records.js +0 -2092
- package/scripts/check-env-surface.mjs +0 -136
- package/scripts/check-public-copy.mjs +0 -263
- package/scripts/check-service-boundary.mjs +0 -63
- package/scripts/runtime-work-order.mjs +0 -506
- package/scripts/smoke-mcp-http.mjs +0 -4026
- package/src/cli.ts +0 -268
- package/src/http/server.ts +0 -314
- package/src/http/service.ts +0 -934
- package/src/init.ts +0 -262
- package/src/install-manifest.ts +0 -144
- package/src/mcp/docs.ts +0 -777
- package/src/mcp/server.ts +0 -4580
- package/src/package-root.ts +0 -31
- package/src/runtime/actor.ts +0 -61
- package/src/runtime/auth.ts +0 -673
- package/src/runtime/check.ts +0 -18
- package/src/runtime/config.ts +0 -128
- package/src/runtime/mongo.ts +0 -89
- package/src/runtime/records.ts +0 -3205
- package/src/runtime/workspace.ts +0 -155
- package/src/upgrade.ts +0 -356
- package/src/validate.ts +0 -141
- package/src/version.ts +0 -16
package/src/cli.ts
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { initProject } from "./init.js";
|
|
3
|
-
import { checkRuntime } from "./runtime/check.js";
|
|
4
|
-
import { upgradeProject } from "./upgrade.js";
|
|
5
|
-
import { validateScaffold } from "./validate.js";
|
|
6
|
-
import { DXCOMPLETE_PACKAGE_VERSION } from "./version.js";
|
|
7
|
-
|
|
8
|
-
type ParsedArgs = {
|
|
9
|
-
command?: string;
|
|
10
|
-
targetDir: string;
|
|
11
|
-
envFile?: string;
|
|
12
|
-
force: boolean;
|
|
13
|
-
dryRun: boolean;
|
|
14
|
-
apply: boolean;
|
|
15
|
-
includeGithubWorkflow: boolean;
|
|
16
|
-
packageLayout: boolean;
|
|
17
|
-
help: boolean;
|
|
18
|
-
version: boolean;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
main(process.argv.slice(2)).catch((error: unknown) => {
|
|
22
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
-
console.error(`dxcomplete: ${message}`);
|
|
24
|
-
process.exitCode = 1;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
async function main(argv: string[]): Promise<void> {
|
|
28
|
-
const args = parseArgs(argv);
|
|
29
|
-
|
|
30
|
-
if (args.version) {
|
|
31
|
-
console.log(DXCOMPLETE_PACKAGE_VERSION);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (args.help || !args.command) {
|
|
36
|
-
printHelp();
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (args.command === "init") {
|
|
41
|
-
const result = await initProject({
|
|
42
|
-
targetDir: args.targetDir,
|
|
43
|
-
force: args.force,
|
|
44
|
-
dryRun: args.dryRun,
|
|
45
|
-
includeGithubWorkflow: args.includeGithubWorkflow
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (args.dryRun) {
|
|
49
|
-
console.log(`Would initialize DX Complete scaffold in ${result.targetDir}`);
|
|
50
|
-
printList("Would write", result.planned);
|
|
51
|
-
printList("Would skip existing", result.skipped);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
console.log(`Initialized DX Complete scaffold in ${result.targetDir}`);
|
|
56
|
-
printList("Written", result.written);
|
|
57
|
-
printList("Skipped existing", result.skipped);
|
|
58
|
-
console.log("Review dxcomplete/docs/open-questions.md before treating the draft model as policy.");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (args.command === "upgrade") {
|
|
63
|
-
const result = await upgradeProject({
|
|
64
|
-
targetDir: args.targetDir,
|
|
65
|
-
apply: args.apply,
|
|
66
|
-
force: args.force
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
if (args.apply) {
|
|
70
|
-
console.log(
|
|
71
|
-
result.manualReview.length > 0
|
|
72
|
-
? `DX Complete upgrade requires manual review in ${result.targetDir}`
|
|
73
|
-
: `Upgraded DX Complete scaffold in ${result.targetDir}`
|
|
74
|
-
);
|
|
75
|
-
printList("Written", result.written);
|
|
76
|
-
printList("Already current", result.unchanged);
|
|
77
|
-
printList("Manual review required", result.manualReview);
|
|
78
|
-
printList("User-owned scaffold drift", result.userOwnedDrift);
|
|
79
|
-
if (result.manualReview.length > 0) {
|
|
80
|
-
process.exitCode = 1;
|
|
81
|
-
}
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
console.log(`Would upgrade DX Complete scaffold in ${result.targetDir}`);
|
|
86
|
-
console.log(
|
|
87
|
-
`Package version ${result.packageVersion}; workspace compatibility ${result.workspaceCompatibility}.`
|
|
88
|
-
);
|
|
89
|
-
printList("Would write", result.planned);
|
|
90
|
-
printList("Already current", result.unchanged);
|
|
91
|
-
printList("Manual review required", result.manualReview);
|
|
92
|
-
printList("User-owned scaffold drift", result.userOwnedDrift);
|
|
93
|
-
console.log("Run dxcomplete upgrade --apply to write compatibility-critical scaffold updates.");
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (args.command === "validate") {
|
|
98
|
-
const result = await validateScaffold({
|
|
99
|
-
targetDir: args.targetDir,
|
|
100
|
-
packageLayout: args.packageLayout
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
if (result.ok) {
|
|
104
|
-
console.log(`DX Complete scaffold shape is valid in ${result.targetDir}`);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
console.error(`DX Complete scaffold is missing ${result.missing.length} required file(s):`);
|
|
109
|
-
for (const file of result.missing) {
|
|
110
|
-
console.error(`- ${file}`);
|
|
111
|
-
}
|
|
112
|
-
process.exitCode = 1;
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (args.command === "check-runtime") {
|
|
117
|
-
const result = await checkRuntime({ envFile: args.envFile });
|
|
118
|
-
console.log(JSON.stringify(result, null, 2));
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
throw new Error(`Unknown command "${args.command}".`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function parseArgs(argv: string[]): ParsedArgs {
|
|
126
|
-
const parsed: ParsedArgs = {
|
|
127
|
-
targetDir: process.cwd(),
|
|
128
|
-
force: false,
|
|
129
|
-
dryRun: false,
|
|
130
|
-
apply: false,
|
|
131
|
-
includeGithubWorkflow: true,
|
|
132
|
-
packageLayout: false,
|
|
133
|
-
help: false,
|
|
134
|
-
version: false
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const positionals: string[] = [];
|
|
138
|
-
|
|
139
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
140
|
-
const arg = argv[index];
|
|
141
|
-
|
|
142
|
-
if (arg === "--help" || arg === "-h") {
|
|
143
|
-
parsed.help = true;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (arg === "--apply") {
|
|
148
|
-
parsed.apply = true;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (arg === "--version" || arg === "-v") {
|
|
153
|
-
parsed.version = true;
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (arg === "--target") {
|
|
158
|
-
const value = argv[index + 1];
|
|
159
|
-
if (!value) {
|
|
160
|
-
throw new Error("--target requires a directory.");
|
|
161
|
-
}
|
|
162
|
-
parsed.targetDir = value;
|
|
163
|
-
index += 1;
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (arg.startsWith("--target=")) {
|
|
168
|
-
parsed.targetDir = arg.slice("--target=".length);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (arg === "--env") {
|
|
173
|
-
const value = argv[index + 1];
|
|
174
|
-
if (!value) {
|
|
175
|
-
throw new Error("--env requires a file path.");
|
|
176
|
-
}
|
|
177
|
-
parsed.envFile = value;
|
|
178
|
-
index += 1;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (arg.startsWith("--env=")) {
|
|
183
|
-
parsed.envFile = arg.slice("--env=".length);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (arg === "--force") {
|
|
188
|
-
parsed.force = true;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (arg === "--dry-run") {
|
|
193
|
-
parsed.dryRun = true;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (arg === "--no-github-workflow") {
|
|
198
|
-
parsed.includeGithubWorkflow = false;
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (arg === "--package-layout") {
|
|
203
|
-
parsed.packageLayout = true;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (arg.startsWith("-")) {
|
|
208
|
-
throw new Error(`Unknown option "${arg}".`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
positionals.push(arg);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
parsed.command = positionals[0];
|
|
215
|
-
|
|
216
|
-
if (positionals.length > 1) {
|
|
217
|
-
throw new Error(`Unexpected argument "${positionals[1]}".`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (parsed.apply && parsed.command !== "upgrade") {
|
|
221
|
-
throw new Error("--apply is only supported by upgrade.");
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (parsed.command === "upgrade" && parsed.apply && parsed.dryRun) {
|
|
225
|
-
throw new Error("upgrade cannot combine --apply and --dry-run.");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return parsed;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function printHelp(): void {
|
|
232
|
-
console.log(`dxcomplete
|
|
233
|
-
|
|
234
|
-
Usage:
|
|
235
|
-
dxcomplete init [--target <dir>] [--force] [--dry-run] [--no-github-workflow]
|
|
236
|
-
dxcomplete upgrade [--target <dir>] [--apply] [--force]
|
|
237
|
-
dxcomplete validate [--target <dir>]
|
|
238
|
-
dxcomplete check-runtime [--env <file>]
|
|
239
|
-
|
|
240
|
-
Commands:
|
|
241
|
-
init Install the editable draft scaffold into a project.
|
|
242
|
-
upgrade Preview or apply compatibility-critical scaffold updates.
|
|
243
|
-
validate Validate the expected scaffold file shape.
|
|
244
|
-
check-runtime Verify MongoDB connectivity and required collections.
|
|
245
|
-
|
|
246
|
-
Options:
|
|
247
|
-
--target <dir> Target project directory. Defaults to the current directory.
|
|
248
|
-
--env <file> Runtime env file. Defaults to .env.local when present.
|
|
249
|
-
--apply Write upgrade changes. Upgrade previews by default.
|
|
250
|
-
--force Overwrite existing scaffold files.
|
|
251
|
-
--dry-run Show what would be written without changing files.
|
|
252
|
-
--no-github-workflow Skip installing .github/workflows/dxcomplete.yml.
|
|
253
|
-
--package-layout Validate this package repository layout instead of an installed scaffold.
|
|
254
|
-
--help Show help.
|
|
255
|
-
--version Show version.
|
|
256
|
-
`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function printList(label: string, values: string[]): void {
|
|
260
|
-
if (values.length === 0) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
console.log(`${label}:`);
|
|
265
|
-
for (const value of values) {
|
|
266
|
-
console.log(`- ${value}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
package/src/http/server.ts
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import { loadWorkspaceConfig, parseWorkspaceConfig, type WorkspaceConfig } from "../runtime/workspace.js";
|
|
3
|
-
|
|
4
|
-
const MCP_PATH = "/api/mcp";
|
|
5
|
-
const GOOGLE_CALLBACK_PATH = "/api/auth/callback/google";
|
|
6
|
-
const MCP_SCOPE = "mcp:tools";
|
|
7
|
-
|
|
8
|
-
type WorkspaceServiceConfig = {
|
|
9
|
-
serviceUrl: string;
|
|
10
|
-
serviceClientId: string;
|
|
11
|
-
serviceClientSecret: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
let workspaceConfigPromise: Promise<WorkspaceConfig> | undefined;
|
|
15
|
-
|
|
16
|
-
export function configureDxcompleteWorkspace(config: unknown): void {
|
|
17
|
-
workspaceConfigPromise = Promise.resolve(parseWorkspaceConfig(config, "DX Complete workspace config"));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function closeDxcompleteHttpRuntime(): Promise<void> {
|
|
21
|
-
workspaceConfigPromise = undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export default async function handleDxcompleteHttpRequest(
|
|
25
|
-
req: IncomingMessage & { body?: unknown; query?: Record<string, unknown> },
|
|
26
|
-
res: ServerResponse
|
|
27
|
-
): Promise<void> {
|
|
28
|
-
try {
|
|
29
|
-
setCorsHeaders(res);
|
|
30
|
-
|
|
31
|
-
if (req.method === "OPTIONS") {
|
|
32
|
-
res.writeHead(204).end();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const baseUrl = getBaseUrl(req);
|
|
37
|
-
const requestUrl = new URL(req.url ?? "/", baseUrl);
|
|
38
|
-
const path = normalizePath(requestUrl.pathname);
|
|
39
|
-
|
|
40
|
-
if (isProtectedResourceMetadataPath(path)) {
|
|
41
|
-
writeJson(res, 200, protectedResourceMetadata(baseUrl));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (isAuthorizationServerMetadataPath(path)) {
|
|
46
|
-
writeJson(res, 200, authorizationServerMetadata(baseUrl));
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (path === MCP_PATH && !readBearerToken(req)) {
|
|
51
|
-
await drainRequestBody(req);
|
|
52
|
-
writeOAuthChallenge(res, baseUrl);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const servicePath = servicePathForPublicPath(path);
|
|
57
|
-
if (!servicePath) {
|
|
58
|
-
writeJson(res, 404, { error: "not_found" });
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const workspaceConfig = await getWorkspaceConfig();
|
|
63
|
-
const serviceConfig = getWorkspaceServiceConfig();
|
|
64
|
-
await proxyToCentralService(req, res, {
|
|
65
|
-
serviceConfig,
|
|
66
|
-
workspaceConfig,
|
|
67
|
-
baseUrl,
|
|
68
|
-
servicePath,
|
|
69
|
-
search: requestUrl.search
|
|
70
|
-
});
|
|
71
|
-
} catch (error) {
|
|
72
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
-
if (!res.headersSent) {
|
|
74
|
-
writeJson(res, 500, { error: "server_error", error_description: message });
|
|
75
|
-
} else {
|
|
76
|
-
res.end();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function proxyToCentralService(
|
|
82
|
-
req: IncomingMessage,
|
|
83
|
-
res: ServerResponse,
|
|
84
|
-
input: {
|
|
85
|
-
serviceConfig: WorkspaceServiceConfig;
|
|
86
|
-
workspaceConfig: WorkspaceConfig;
|
|
87
|
-
baseUrl: string;
|
|
88
|
-
servicePath: string;
|
|
89
|
-
search: string;
|
|
90
|
-
}
|
|
91
|
-
): Promise<void> {
|
|
92
|
-
const targetUrl = new URL(input.servicePath + input.search, input.serviceConfig.serviceUrl);
|
|
93
|
-
const body = req.method === "GET" || req.method === "HEAD" ? undefined : await readRequestBody(req);
|
|
94
|
-
const response = await fetch(targetUrl, {
|
|
95
|
-
method: req.method,
|
|
96
|
-
headers: serviceHeaders(req.headers, input),
|
|
97
|
-
body,
|
|
98
|
-
redirect: "manual"
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const responseBody = Buffer.from(await response.arrayBuffer());
|
|
102
|
-
const headers: Record<string, string> = {};
|
|
103
|
-
response.headers.forEach((value, key) => {
|
|
104
|
-
if (!shouldForwardResponseHeader(key)) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
headers[key] = value;
|
|
108
|
-
});
|
|
109
|
-
headers["content-length"] = String(responseBody.byteLength);
|
|
110
|
-
|
|
111
|
-
res.writeHead(response.status, headers);
|
|
112
|
-
res.end(responseBody);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function serviceHeaders(
|
|
116
|
-
headers: IncomingHttpHeaders,
|
|
117
|
-
input: {
|
|
118
|
-
serviceConfig: WorkspaceServiceConfig;
|
|
119
|
-
workspaceConfig: WorkspaceConfig;
|
|
120
|
-
baseUrl: string;
|
|
121
|
-
}
|
|
122
|
-
): Headers {
|
|
123
|
-
const forwarded = new Headers();
|
|
124
|
-
|
|
125
|
-
for (const key of [
|
|
126
|
-
"accept",
|
|
127
|
-
"authorization",
|
|
128
|
-
"content-type",
|
|
129
|
-
"mcp-protocol-version",
|
|
130
|
-
"mcp-session-id",
|
|
131
|
-
"last-event-id",
|
|
132
|
-
"user-agent"
|
|
133
|
-
]) {
|
|
134
|
-
const value = firstHeader(headers[key]);
|
|
135
|
-
if (value) {
|
|
136
|
-
forwarded.set(key, value);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
forwarded.set("x-dxc-service-client-id", input.serviceConfig.serviceClientId);
|
|
141
|
-
forwarded.set("x-dxc-service-client-secret", input.serviceConfig.serviceClientSecret);
|
|
142
|
-
forwarded.set("x-dxc-workspace-id", input.workspaceConfig.workspaceId);
|
|
143
|
-
forwarded.set("x-dxc-workspace-name", input.workspaceConfig.name);
|
|
144
|
-
forwarded.set("x-dxc-forwarded-base-url", input.baseUrl);
|
|
145
|
-
|
|
146
|
-
return forwarded;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function shouldForwardResponseHeader(key: string): boolean {
|
|
150
|
-
const lowerKey = key.toLowerCase();
|
|
151
|
-
return !["connection", "content-encoding", "content-length", "keep-alive", "transfer-encoding"].includes(lowerKey);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function servicePathForPublicPath(pathname: string): string | undefined {
|
|
155
|
-
switch (pathname) {
|
|
156
|
-
case MCP_PATH:
|
|
157
|
-
return "/api/dxcomplete/service/mcp";
|
|
158
|
-
case "/api/dxcomplete/auth/register":
|
|
159
|
-
return "/api/dxcomplete/service/auth/register";
|
|
160
|
-
case "/api/dxcomplete/auth/authorize":
|
|
161
|
-
return "/api/dxcomplete/service/auth/authorize";
|
|
162
|
-
case GOOGLE_CALLBACK_PATH:
|
|
163
|
-
return "/api/dxcomplete/service/auth/google/callback";
|
|
164
|
-
case "/api/dxcomplete/auth/token":
|
|
165
|
-
return "/api/dxcomplete/service/auth/token";
|
|
166
|
-
default:
|
|
167
|
-
return undefined;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function getWorkspaceServiceConfig(env: NodeJS.ProcessEnv = process.env): WorkspaceServiceConfig {
|
|
172
|
-
return {
|
|
173
|
-
serviceUrl: readRequiredEnv(env, "DXC_SERVICE_URL"),
|
|
174
|
-
serviceClientId: readRequiredEnv(env, "DXC_SERVICE_CLIENT_ID"),
|
|
175
|
-
serviceClientSecret: readRequiredEnv(env, "DXC_SERVICE_CLIENT_SECRET")
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function getWorkspaceConfig(): Promise<WorkspaceConfig> {
|
|
180
|
-
workspaceConfigPromise ??= loadWorkspaceConfig();
|
|
181
|
-
return workspaceConfigPromise;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function protectedResourceMetadata(baseUrl: string): Record<string, unknown> {
|
|
185
|
-
return {
|
|
186
|
-
resource: mcpResourceUrl(baseUrl),
|
|
187
|
-
authorization_servers: [baseUrl],
|
|
188
|
-
scopes_supported: [MCP_SCOPE],
|
|
189
|
-
resource_name: "DX Complete"
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function authorizationServerMetadata(baseUrl: string): Record<string, unknown> {
|
|
194
|
-
return {
|
|
195
|
-
issuer: baseUrl,
|
|
196
|
-
authorization_endpoint: `${baseUrl}/api/dxcomplete/auth/authorize`,
|
|
197
|
-
token_endpoint: `${baseUrl}/api/dxcomplete/auth/token`,
|
|
198
|
-
registration_endpoint: `${baseUrl}/api/dxcomplete/auth/register`,
|
|
199
|
-
response_types_supported: ["code"],
|
|
200
|
-
code_challenge_methods_supported: ["S256"],
|
|
201
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
202
|
-
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
203
|
-
scopes_supported: [MCP_SCOPE]
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function writeOAuthChallenge(res: ServerResponse, baseUrl: string): void {
|
|
208
|
-
res.setHeader(
|
|
209
|
-
"www-authenticate",
|
|
210
|
-
`Bearer resource_metadata="${protectedResourceMetadataUrl(baseUrl)}", scope="${MCP_SCOPE}"`
|
|
211
|
-
);
|
|
212
|
-
writeJson(res, 401, { error: "unauthorized" });
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function setCorsHeaders(res: ServerResponse): void {
|
|
216
|
-
res.setHeader("access-control-allow-origin", "*");
|
|
217
|
-
res.setHeader(
|
|
218
|
-
"access-control-allow-headers",
|
|
219
|
-
"authorization,content-type,mcp-protocol-version,mcp-session-id,last-event-id"
|
|
220
|
-
);
|
|
221
|
-
res.setHeader("access-control-allow-methods", "GET,POST,DELETE,OPTIONS");
|
|
222
|
-
res.setHeader("access-control-expose-headers", "mcp-session-id,www-authenticate");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function writeJson(res: ServerResponse, status: number, value: unknown): void {
|
|
226
|
-
const body = JSON.stringify(value);
|
|
227
|
-
if (!res.headersSent) {
|
|
228
|
-
res.writeHead(status, {
|
|
229
|
-
"content-type": "application/json",
|
|
230
|
-
"content-length": String(Buffer.byteLength(body))
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
res.end(body);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function getBaseUrl(req: IncomingMessage): string {
|
|
237
|
-
const forwardedProto = firstHeader(req.headers["x-forwarded-proto"]);
|
|
238
|
-
const forwardedHost = firstHeader(req.headers["x-forwarded-host"]);
|
|
239
|
-
const host = forwardedHost || firstHeader(req.headers.host);
|
|
240
|
-
const protocol = forwardedProto || "http";
|
|
241
|
-
|
|
242
|
-
if (!host) {
|
|
243
|
-
throw new Error("Host header is required.");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return `${protocol}://${host}`;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function firstHeader(value: string | string[] | undefined): string | undefined {
|
|
250
|
-
return Array.isArray(value) ? value[0] : value;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function normalizePath(pathname: string): string {
|
|
254
|
-
if (pathname === "/api/mcp" || pathname === "/api/dxcomplete" || pathname === "/api/dxcomplete/mcp") {
|
|
255
|
-
return MCP_PATH;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return pathname.endsWith("/") && pathname.length > 1 ? pathname.slice(0, -1) : pathname;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function isProtectedResourceMetadataPath(pathname: string): boolean {
|
|
262
|
-
return (
|
|
263
|
-
pathname === protectedResourceMetadataPath() ||
|
|
264
|
-
pathname === "/.well-known/oauth-protected-resource/api/dxcomplete/mcp" ||
|
|
265
|
-
pathname === `/api/dxcomplete${protectedResourceMetadataPath()}`
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function isAuthorizationServerMetadataPath(pathname: string): boolean {
|
|
270
|
-
return (
|
|
271
|
-
pathname === "/.well-known/oauth-authorization-server" ||
|
|
272
|
-
pathname === "/api/dxcomplete/.well-known/oauth-authorization-server"
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function protectedResourceMetadataPath(): string {
|
|
277
|
-
return `/.well-known/oauth-protected-resource${MCP_PATH}`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function protectedResourceMetadataUrl(baseUrl: string): string {
|
|
281
|
-
return `${baseUrl}${protectedResourceMetadataPath()}`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function mcpResourceUrl(baseUrl: string): string {
|
|
285
|
-
return `${baseUrl}${MCP_PATH}`;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async function readRequestBody(req: IncomingMessage): Promise<Buffer> {
|
|
289
|
-
const chunks: Buffer[] = [];
|
|
290
|
-
for await (const chunk of req) {
|
|
291
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
292
|
-
}
|
|
293
|
-
return Buffer.concat(chunks);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function drainRequestBody(req: IncomingMessage): Promise<void> {
|
|
297
|
-
for await (const _ of req) {
|
|
298
|
-
// Drain request body so clients can reuse the connection.
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function readBearerToken(req: IncomingMessage): string | undefined {
|
|
303
|
-
const header = firstHeader(req.headers.authorization);
|
|
304
|
-
const match = header?.match(/^Bearer\s+(.+)$/i);
|
|
305
|
-
return match?.[1];
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function readRequiredEnv(env: NodeJS.ProcessEnv, key: string): string {
|
|
309
|
-
const value = env[key]?.trim();
|
|
310
|
-
if (!value) {
|
|
311
|
-
throw new Error(`${key} is required for the workspace MCP proxy.`);
|
|
312
|
-
}
|
|
313
|
-
return value;
|
|
314
|
-
}
|