dxcomplete 0.2.1 → 0.3.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.
Files changed (89) hide show
  1. package/.env.example +0 -7
  2. package/README.md +68 -103
  3. package/dist/cli.js +2 -24
  4. package/dist/validate.js +10 -26
  5. package/docs/cost-model.md +2 -2
  6. package/docs/decision-basis.md +5 -11
  7. package/docs/diagrams.md +3 -3
  8. package/docs/index.md +25 -39
  9. package/docs/model.md +15 -23
  10. package/docs/open-questions.md +1 -1
  11. package/docs/taxonomy.md +7 -8
  12. package/docs/workflows.md +3 -3
  13. package/package.json +24 -24
  14. package/templates/process/README.md +11 -11
  15. package/templates/process/controls.yml +19 -19
  16. package/templates/process/cost-model.yml +3 -3
  17. package/templates/process/decision-basis.yml +4 -4
  18. package/templates/process/diagrams/00-decision-basis.mmd +1 -1
  19. package/templates/process/diagrams/00-overview.mmd +1 -1
  20. package/templates/process/diagrams/01-intake-triage.mmd +4 -4
  21. package/templates/process/diagrams/02-product-definition.mmd +3 -3
  22. package/templates/process/diagrams/03-engineering-execution.mmd +1 -1
  23. package/templates/process/diagrams/04-qa-verification.mmd +1 -1
  24. package/templates/process/diagrams/05-product-validation.mmd +1 -1
  25. package/templates/process/diagrams/06-change-release-control.mmd +1 -1
  26. package/templates/process/diagrams/07-deployment-operations.mmd +1 -1
  27. package/templates/process/diagrams/08-support-incident-management.mmd +1 -1
  28. package/templates/process/diagrams/09-problem-improvement.mmd +1 -1
  29. package/templates/process/diagrams/10-risk-control-management.mmd +1 -1
  30. package/templates/process/diagrams/11-audit-evidence-capture.mmd +1 -1
  31. package/templates/process/roles.yml +6 -6
  32. package/templates/process/taxonomy.yml +46 -46
  33. package/templates/process/workflows.yml +29 -29
  34. package/website/account.html +57 -0
  35. package/website/app.js +177 -0
  36. package/website/flow.html +4 -0
  37. package/website/glossary.html +4 -0
  38. package/website/index.html +4 -0
  39. package/website/objects.html +4 -0
  40. package/website/operating-guide.html +4 -0
  41. package/website/outcomes.html +4 -0
  42. package/website/phase-build.html +4 -0
  43. package/website/phase-elicit.html +4 -0
  44. package/website/phase-go-live.html +4 -0
  45. package/website/phase-measure.html +4 -0
  46. package/website/phase-operate.html +4 -0
  47. package/website/phase-orient.html +4 -0
  48. package/website/phase-weigh.html +4 -0
  49. package/website/roles.html +4 -0
  50. package/website/styles.css +217 -1
  51. package/dist/http/service.d.ts +0 -7
  52. package/dist/http/service.js +0 -725
  53. package/dist/mcp/docs.d.ts +0 -114
  54. package/dist/mcp/docs.js +0 -626
  55. package/dist/mcp/server.d.ts +0 -20
  56. package/dist/mcp/server.js +0 -3059
  57. package/dist/runtime/auth.d.ts +0 -162
  58. package/dist/runtime/auth.js +0 -394
  59. package/dist/runtime/check.d.ts +0 -7
  60. package/dist/runtime/check.js +0 -16
  61. package/dist/runtime/config.d.ts +0 -17
  62. package/dist/runtime/config.js +0 -93
  63. package/dist/runtime/mongo.d.ts +0 -9
  64. package/dist/runtime/mongo.js +0 -56
  65. package/dist/runtime/records.d.ts +0 -427
  66. package/dist/runtime/records.js +0 -2092
  67. package/scripts/check-env-surface.mjs +0 -136
  68. package/scripts/check-public-copy.mjs +0 -263
  69. package/scripts/check-service-boundary.mjs +0 -63
  70. package/scripts/runtime-work-order.mjs +0 -506
  71. package/scripts/smoke-mcp-http.mjs +0 -4026
  72. package/src/cli.ts +0 -268
  73. package/src/http/server.ts +0 -314
  74. package/src/http/service.ts +0 -934
  75. package/src/init.ts +0 -262
  76. package/src/install-manifest.ts +0 -144
  77. package/src/mcp/docs.ts +0 -777
  78. package/src/mcp/server.ts +0 -4580
  79. package/src/package-root.ts +0 -31
  80. package/src/runtime/actor.ts +0 -61
  81. package/src/runtime/auth.ts +0 -673
  82. package/src/runtime/check.ts +0 -18
  83. package/src/runtime/config.ts +0 -128
  84. package/src/runtime/mongo.ts +0 -89
  85. package/src/runtime/records.ts +0 -3205
  86. package/src/runtime/workspace.ts +0 -155
  87. package/src/upgrade.ts +0 -356
  88. package/src/validate.ts +0 -141
  89. package/src/version.ts +0 -16
@@ -1,725 +0,0 @@
1
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
- import { assertWorkspaceMembership, consumeOAuthAuthorizationRequest, createGoogleActor, createOAuthAuthorizationCode, createOAuthAuthorizationRequest, createWorkspaceServiceClient, ensureWorkspaceBootstrap, exchangeGoogleCodeForProfile, exchangeOAuthAuthorizationCode, exchangeOAuthRefreshToken, getOAuthClient, googleAuthorizationUrl, membershipRoles, registerOAuthClient, upsertWorkspaceMembership, verifyWorkspaceServiceClient, verifyMcpAccessToken } from "../runtime/auth.js";
3
- import { createMcpServer } from "../mcp/server.js";
4
- import { connectRuntime } from "../runtime/mongo.js";
5
- import { RUNTIME_ACTOR_ID } from "../runtime/records.js";
6
- const MCP_PATH = "/api/mcp";
7
- const GOOGLE_CALLBACK_PATH = "/api/auth/callback/google";
8
- const MCP_SCOPE = "mcp:tools";
9
- const AUTH_DIAGNOSTICS_COLLECTION = "auth_diagnostics";
10
- let runtimePromise;
11
- export async function closeDxcompleteServiceRuntime() {
12
- if (runtimePromise) {
13
- const runtime = await runtimePromise;
14
- await runtime.close();
15
- }
16
- runtimePromise = undefined;
17
- }
18
- export async function closeDxcompleteHttpRuntime() {
19
- await closeDxcompleteServiceRuntime();
20
- }
21
- export default async function handleDxcompleteServiceRequest(req, res) {
22
- try {
23
- setCorsHeaders(res);
24
- if (req.method === "OPTIONS") {
25
- res.writeHead(204).end();
26
- return;
27
- }
28
- const runtime = await getRuntime();
29
- const ownBaseUrl = getBaseUrl(req);
30
- const requestUrl = new URL(req.url ?? "/", ownBaseUrl);
31
- const path = normalizeServicePath(requestUrl.pathname);
32
- if (path === "/api/dxcomplete/service/provision") {
33
- await handleWorkspaceProvisioning(req, res, runtime);
34
- return;
35
- }
36
- const workspaceConfig = await authenticateWorkspaceService(runtime, req);
37
- const baseUrl = forwardedWorkspaceBaseUrl(req) ?? ownBaseUrl;
38
- if (isProtectedResourceMetadataPath(path)) {
39
- writeJson(res, 200, protectedResourceMetadata(baseUrl));
40
- return;
41
- }
42
- if (isAuthorizationServerMetadataPath(path)) {
43
- writeJson(res, 200, authorizationServerMetadata(baseUrl));
44
- return;
45
- }
46
- if (path === "/api/dxcomplete/auth/register") {
47
- await handleClientRegistration(req, res, runtime);
48
- return;
49
- }
50
- if (path === "/api/dxcomplete/auth/authorize") {
51
- await handleAuthorize(req, res, runtime, workspaceConfig, baseUrl, requestUrl);
52
- return;
53
- }
54
- if (path === GOOGLE_CALLBACK_PATH) {
55
- await handleGoogleCallback(res, runtime, workspaceConfig, baseUrl, requestUrl);
56
- return;
57
- }
58
- if (path === "/api/dxcomplete/auth/token") {
59
- await handleToken(req, res, runtime, baseUrl);
60
- return;
61
- }
62
- if (path === MCP_PATH) {
63
- await handleMcp(req, res, baseUrl, workspaceConfig);
64
- return;
65
- }
66
- writeJson(res, 404, { error: "not_found" });
67
- }
68
- catch (error) {
69
- const message = error instanceof Error ? error.message : String(error);
70
- const responseError = responseErrorForMessage(message);
71
- if (!res.headersSent) {
72
- writeJson(res, responseError.status, {
73
- error: responseError.error,
74
- error_description: message
75
- });
76
- }
77
- else {
78
- res.end();
79
- }
80
- }
81
- }
82
- function responseErrorForMessage(message) {
83
- if (message.includes("service client") || message.includes("service credentials") || message.includes("Provisioning")) {
84
- return { status: 401, error: "unauthorized" };
85
- }
86
- if (message.includes("Workspace access denied")) {
87
- return { status: 403, error: "access_denied" };
88
- }
89
- if (message.includes("OAuth client was not found")) {
90
- return { status: 400, error: "invalid_client" };
91
- }
92
- if (message.includes("authorization request expired") ||
93
- message.includes("Authorization code expired") ||
94
- message.includes("Invalid PKCE verifier") ||
95
- message.includes("Refresh token") ||
96
- message.includes("Token expired")) {
97
- return { status: 400, error: "invalid_grant" };
98
- }
99
- if (message.includes("redirect_uri") ||
100
- message.includes("response_type") ||
101
- message.includes("code_challenge") ||
102
- message.includes("grant_type") ||
103
- message.includes("OAuth resource")) {
104
- return { status: 400, error: "invalid_request" };
105
- }
106
- return { status: 500, error: "server_error" };
107
- }
108
- async function authenticateWorkspaceService(runtime, req) {
109
- const serviceClient = await verifyWorkspaceServiceClient(runtime.db, {
110
- clientId: firstHeader(req.headers["x-dxc-service-client-id"]),
111
- secret: firstHeader(req.headers["x-dxc-service-client-secret"]),
112
- workspaceId: firstHeader(req.headers["x-dxc-workspace-id"])
113
- });
114
- const workspace = await runtime.db.collection("workspaces").findOne({ _id: serviceClient.workspaceId });
115
- return {
116
- workspaceId: serviceClient.workspaceId,
117
- name: readRecordName(workspace) ??
118
- firstHeader(req.headers["x-dxc-workspace-name"]) ??
119
- serviceClient.workspaceId,
120
- bootstrapMembers: []
121
- };
122
- }
123
- async function handleWorkspaceProvisioning(req, res, runtime) {
124
- if (req.method !== "POST") {
125
- writeJson(res, 405, { error: "method_not_allowed" });
126
- return;
127
- }
128
- const expectedSecret = runtime.config.serviceProvisioningSecret;
129
- const suppliedSecret = readProvisioningSecret(req);
130
- if (!expectedSecret || !suppliedSecret || suppliedSecret !== expectedSecret) {
131
- writeJson(res, 401, { error: "unauthorized", error_description: "Provisioning authorization failed." });
132
- return;
133
- }
134
- const body = await readJsonBody(req);
135
- const workspaceId = readRequiredBodyString(body, "workspaceId");
136
- const name = readRequiredBodyString(body, "name");
137
- const ownerEmail = readRequiredBodyString(body, "ownerEmail");
138
- const serviceClientName = readRequiredBodyString(body, "serviceClientName");
139
- const mode = readOptionalWorkspaceMode(body.mode);
140
- await ensureWorkspaceBootstrap(runtime.db, {
141
- workspaceId,
142
- name,
143
- ...(mode ? { mode } : {}),
144
- bootstrapMembers: [{ email: ownerEmail, roles: ["owner"] }]
145
- }, RUNTIME_ACTOR_ID);
146
- const membership = await upsertWorkspaceMembership(runtime.db, {
147
- workspaceId,
148
- email: ownerEmail,
149
- roles: ["owner"],
150
- actorId: RUNTIME_ACTOR_ID
151
- });
152
- const { record: serviceClient, secret } = await createWorkspaceServiceClient(runtime.db, {
153
- workspaceId,
154
- name: serviceClientName,
155
- actorId: RUNTIME_ACTOR_ID
156
- });
157
- const workspace = await runtime.db.collection("workspaces").findOne({ _id: workspaceId });
158
- writeJson(res, 201, {
159
- workspace,
160
- ownerMembership: {
161
- _id: membership._id,
162
- workspaceId: membership.workspaceId,
163
- email: membership.email,
164
- roles: membershipRoles(membership)
165
- },
166
- serviceClient: {
167
- clientId: serviceClient.clientId,
168
- name: serviceClient.name,
169
- workspaceId: serviceClient.workspaceId,
170
- secret
171
- }
172
- });
173
- }
174
- function readRecordName(workspace) {
175
- if (!workspace) {
176
- return undefined;
177
- }
178
- if (typeof workspace.fields.name === "string" && workspace.fields.name.trim()) {
179
- return workspace.fields.name.trim();
180
- }
181
- return workspace.title;
182
- }
183
- function readProvisioningSecret(req) {
184
- const authorization = firstHeader(req.headers.authorization);
185
- const bearerMatch = authorization?.match(/^Bearer\s+(.+)$/i);
186
- return bearerMatch?.[1]?.trim() || firstHeader(req.headers["x-dxc-provisioning-secret"])?.trim();
187
- }
188
- function readRequiredBodyString(body, key) {
189
- const value = body[key];
190
- if (typeof value !== "string" || !value.trim()) {
191
- throw new Error(`${key} is required.`);
192
- }
193
- return value.trim();
194
- }
195
- function readOptionalWorkspaceMode(value) {
196
- if (value === undefined) {
197
- return undefined;
198
- }
199
- if (value === "transformation" || value === "greenfield" || value === "limited-disclosure") {
200
- return value;
201
- }
202
- throw new Error("mode must be transformation, greenfield, or limited-disclosure when provided.");
203
- }
204
- function forwardedWorkspaceBaseUrl(req) {
205
- const value = firstHeader(req.headers["x-dxc-forwarded-base-url"]);
206
- if (!value) {
207
- return undefined;
208
- }
209
- return new URL(value).origin;
210
- }
211
- async function handleMcp(req, res, baseUrl, workspaceConfig) {
212
- const resource = mcpResourceUrl(baseUrl);
213
- const token = readBearerToken(req);
214
- if (!token) {
215
- await drainRequestBody(req);
216
- writeOAuthChallenge(res, baseUrl);
217
- return;
218
- }
219
- const runtime = await getRuntime();
220
- let tokenRecord;
221
- let membership;
222
- try {
223
- tokenRecord = await verifyMcpAccessToken(runtime.db, {
224
- token,
225
- workspaceId: workspaceConfig.workspaceId,
226
- resource
227
- });
228
- membership = await assertWorkspaceMembership(runtime.db, workspaceConfig.workspaceId, tokenRecord.actor);
229
- }
230
- catch (error) {
231
- const message = error instanceof Error ? error.message : String(error);
232
- await recordAuthDiagnostic(runtime.db, "mcp.token_rejected", req, {
233
- workspaceId: workspaceConfig.workspaceId,
234
- resource,
235
- error: message,
236
- status: message.includes("Workspace access denied") ? 403 : 401
237
- });
238
- if (message.includes("Workspace access denied")) {
239
- await drainRequestBody(req);
240
- writeJson(res, 403, { error: "access_denied", error_description: "Workspace access denied." });
241
- return;
242
- }
243
- await drainRequestBody(req);
244
- writeOAuthChallenge(res, baseUrl);
245
- return;
246
- }
247
- const server = createMcpServer(runtime, {
248
- actor: tokenRecord.actor,
249
- recordActorId: tokenRecord.actor.actorId,
250
- hostedWorkspace: {
251
- workspaceId: workspaceConfig.workspaceId,
252
- name: workspaceConfig.name
253
- },
254
- workspaceRoles: membershipRoles(membership),
255
- hostedHttp: {
256
- canonicalMcpPath: MCP_PATH,
257
- canonicalMcpUrl: resource,
258
- protectedResourceMetadataUrl: protectedResourceMetadataUrl(baseUrl),
259
- googleCallbackUrl: googleCallbackUrl(baseUrl)
260
- }
261
- });
262
- const transport = new StreamableHTTPServerTransport({
263
- sessionIdGenerator: undefined,
264
- enableJsonResponse: true
265
- });
266
- req.auth = {
267
- token,
268
- clientId: tokenRecord.clientId,
269
- scopes: tokenRecord.scope.split(/\s+/).filter(Boolean),
270
- expiresAt: Math.floor(new Date(tokenRecord.expiresAt).getTime() / 1000),
271
- resource: new URL(resource),
272
- extra: {
273
- actor: tokenRecord.actor,
274
- workspaceId: tokenRecord.workspaceId
275
- }
276
- };
277
- await recordAuthDiagnostic(runtime.db, "mcp.token_accepted", req, {
278
- clientId: tokenRecord.clientId,
279
- workspaceId: tokenRecord.workspaceId,
280
- resource: tokenRecord.resource,
281
- actorEmail: tokenRecord.actor.email,
282
- status: 200
283
- });
284
- ensureStreamableHttpAcceptHeader(req);
285
- await server.connect(transport);
286
- const parsedBody = req.method === "POST" ? await readJsonBody(req) : undefined;
287
- try {
288
- await transport.handleRequest(req, res, parsedBody);
289
- await recordAuthDiagnostic(runtime.db, "mcp.transport_completed", req, {
290
- clientId: tokenRecord.clientId,
291
- workspaceId: tokenRecord.workspaceId,
292
- resource: tokenRecord.resource,
293
- actorEmail: tokenRecord.actor.email,
294
- status: res.statusCode,
295
- responseContentType: firstHeader(res.getHeader("content-type"))
296
- });
297
- }
298
- catch (error) {
299
- await recordAuthDiagnostic(runtime.db, "mcp.transport_failed", req, {
300
- clientId: tokenRecord.clientId,
301
- workspaceId: tokenRecord.workspaceId,
302
- resource: tokenRecord.resource,
303
- actorEmail: tokenRecord.actor.email,
304
- error: error instanceof Error ? error.message : String(error),
305
- status: res.statusCode
306
- });
307
- throw error;
308
- }
309
- res.on("close", () => {
310
- void transport.close().catch(() => undefined);
311
- void server.close().catch(() => undefined);
312
- });
313
- }
314
- async function handleClientRegistration(req, res, runtime) {
315
- if (req.method !== "POST") {
316
- writeJson(res, 405, { error: "method_not_allowed" });
317
- return;
318
- }
319
- const body = await readJsonBody(req);
320
- const redirectUris = readStringArray(body.redirect_uris);
321
- const client = await registerOAuthClient(runtime.db, {
322
- clientName: typeof body.client_name === "string" ? body.client_name : undefined,
323
- redirectUris,
324
- grantTypes: readStringArray(body.grant_types, ["authorization_code", "refresh_token"]),
325
- responseTypes: readStringArray(body.response_types, ["code"])
326
- });
327
- await recordAuthDiagnostic(runtime.db, "oauth.client_registered", req, {
328
- clientId: client.clientId,
329
- redirectUris: client.redirectUris,
330
- status: 201
331
- });
332
- writeJson(res, 201, {
333
- client_id: client.clientId,
334
- client_id_issued_at: Math.floor(new Date(client.createdAt).getTime() / 1000),
335
- redirect_uris: client.redirectUris,
336
- grant_types: client.grantTypes,
337
- response_types: client.responseTypes,
338
- token_endpoint_auth_method: "none"
339
- });
340
- }
341
- async function handleAuthorize(req, res, runtime, workspaceConfig, baseUrl, requestUrl) {
342
- if (req.method !== "GET") {
343
- writeJson(res, 405, { error: "method_not_allowed" });
344
- return;
345
- }
346
- const clientId = requiredParam(requestUrl, "client_id");
347
- const redirectUri = requiredParam(requestUrl, "redirect_uri");
348
- const responseType = requiredParam(requestUrl, "response_type");
349
- const codeChallenge = requiredParam(requestUrl, "code_challenge");
350
- const codeChallengeMethod = requiredParam(requestUrl, "code_challenge_method");
351
- const resource = requestUrl.searchParams.get("resource") || mcpResourceUrl(baseUrl);
352
- if (responseType !== "code") {
353
- throw new Error("Only authorization code response_type is supported.");
354
- }
355
- if (codeChallengeMethod !== "S256") {
356
- throw new Error("Only S256 PKCE is supported.");
357
- }
358
- const client = await getOAuthClient(runtime.db, clientId);
359
- if (!client) {
360
- throw new Error("OAuth client was not found.");
361
- }
362
- if (!client.redirectUris.includes(redirectUri)) {
363
- throw new Error("redirect_uri was not registered for this OAuth client.");
364
- }
365
- if (resource !== mcpResourceUrl(baseUrl)) {
366
- throw new Error("OAuth resource must match this MCP endpoint.");
367
- }
368
- const authRequest = await createOAuthAuthorizationRequest(runtime.db, {
369
- clientId,
370
- redirectUri,
371
- codeChallenge,
372
- codeChallengeMethod: "S256",
373
- state: requestUrl.searchParams.get("state") ?? undefined,
374
- scope: requestUrl.searchParams.get("scope") || MCP_SCOPE,
375
- resource,
376
- workspaceId: workspaceConfig.workspaceId
377
- });
378
- const googleRedirectUri = googleCallbackUrl(baseUrl);
379
- await recordAuthDiagnostic(runtime.db, "oauth.authorize_redirect", req, {
380
- clientId,
381
- workspaceId: workspaceConfig.workspaceId,
382
- resource,
383
- redirectUri,
384
- googleRedirectUri,
385
- status: 302
386
- });
387
- redirect(res, googleAuthorizationUrl(runtime.config, {
388
- redirectUri: googleRedirectUri,
389
- state: authRequest._id
390
- }));
391
- }
392
- async function handleGoogleCallback(res, runtime, workspaceConfig, baseUrl, requestUrl) {
393
- const state = requiredParam(requestUrl, "state");
394
- const googleCode = requiredParam(requestUrl, "code");
395
- const authRequest = await consumeOAuthAuthorizationRequest(runtime.db, state);
396
- const profile = await exchangeGoogleCodeForProfile(runtime.config, {
397
- code: googleCode,
398
- redirectUri: googleCallbackUrl(baseUrl)
399
- });
400
- const actor = createGoogleActor(profile);
401
- await assertWorkspaceMembership(runtime.db, workspaceConfig.workspaceId, actor);
402
- const code = await createOAuthAuthorizationCode(runtime.db, authRequest, actor);
403
- await recordAuthDiagnostic(runtime.db, "oauth.callback_authorized", undefined, {
404
- clientId: authRequest.clientId,
405
- workspaceId: workspaceConfig.workspaceId,
406
- resource: authRequest.resource,
407
- redirectUri: authRequest.redirectUri,
408
- actorEmail: actor.email,
409
- status: 302
410
- });
411
- const callback = new URL(authRequest.redirectUri);
412
- callback.searchParams.set("code", code);
413
- if (authRequest.state) {
414
- callback.searchParams.set("state", authRequest.state);
415
- }
416
- redirect(res, callback.href);
417
- }
418
- async function handleToken(req, res, runtime, baseUrl) {
419
- if (req.method !== "POST") {
420
- writeJson(res, 405, { error: "method_not_allowed" });
421
- return;
422
- }
423
- const body = await readFormBody(req);
424
- const grantType = requiredFormValue(body, "grant_type");
425
- const clientId = readClientId(req, body);
426
- const client = await getOAuthClient(runtime.db, clientId);
427
- const resource = body.get("resource") || mcpResourceUrl(baseUrl);
428
- if (!client) {
429
- throw new Error("OAuth client was not found.");
430
- }
431
- if (resource !== mcpResourceUrl(baseUrl)) {
432
- throw new Error("OAuth resource must match this MCP endpoint.");
433
- }
434
- if (grantType === "authorization_code") {
435
- const redirectUri = requiredFormValue(body, "redirect_uri");
436
- const tokenPair = await exchangeOAuthAuthorizationCode(runtime.db, {
437
- code: requiredFormValue(body, "code"),
438
- clientId,
439
- redirectUri,
440
- codeVerifier: requiredFormValue(body, "code_verifier"),
441
- resource
442
- });
443
- await recordAuthDiagnostic(runtime.db, "oauth.token_issued", req, {
444
- clientId,
445
- resource,
446
- redirectUri,
447
- grantType,
448
- status: 200
449
- });
450
- writeTokenResponse(res, tokenPair);
451
- return;
452
- }
453
- if (grantType === "refresh_token") {
454
- const tokenPair = await exchangeOAuthRefreshToken(runtime.db, {
455
- refreshToken: requiredFormValue(body, "refresh_token"),
456
- clientId,
457
- resource
458
- });
459
- await recordAuthDiagnostic(runtime.db, "oauth.token_refreshed", req, {
460
- clientId,
461
- resource,
462
- grantType,
463
- status: 200
464
- });
465
- writeTokenResponse(res, tokenPair);
466
- return;
467
- }
468
- writeJson(res, 400, { error: "unsupported_grant_type" });
469
- }
470
- function protectedResourceMetadata(baseUrl) {
471
- return {
472
- resource: mcpResourceUrl(baseUrl),
473
- authorization_servers: [baseUrl],
474
- scopes_supported: [MCP_SCOPE],
475
- resource_name: "DX Complete"
476
- };
477
- }
478
- function authorizationServerMetadata(baseUrl) {
479
- return {
480
- issuer: baseUrl,
481
- authorization_endpoint: `${baseUrl}/api/dxcomplete/auth/authorize`,
482
- token_endpoint: `${baseUrl}/api/dxcomplete/auth/token`,
483
- registration_endpoint: `${baseUrl}/api/dxcomplete/auth/register`,
484
- response_types_supported: ["code"],
485
- code_challenge_methods_supported: ["S256"],
486
- token_endpoint_auth_methods_supported: ["none"],
487
- grant_types_supported: ["authorization_code", "refresh_token"],
488
- scopes_supported: [MCP_SCOPE]
489
- };
490
- }
491
- function writeTokenResponse(res, tokenPair) {
492
- res.setHeader("cache-control", "no-store");
493
- res.setHeader("pragma", "no-cache");
494
- writeJson(res, 200, {
495
- access_token: tokenPair.accessToken,
496
- refresh_token: tokenPair.refreshToken,
497
- token_type: "Bearer",
498
- expires_in: tokenPair.expiresIn,
499
- scope: tokenPair.scope
500
- });
501
- }
502
- function writeOAuthChallenge(res, baseUrl) {
503
- res.setHeader("www-authenticate", `Bearer resource_metadata="${protectedResourceMetadataUrl(baseUrl)}", scope="${MCP_SCOPE}"`);
504
- writeJson(res, 401, { error: "unauthorized" });
505
- }
506
- function ensureStreamableHttpAcceptHeader(req) {
507
- const accept = firstHeader(req.headers.accept)?.toLowerCase() ?? "";
508
- if (req.method === "POST" && (!accept.includes("application/json") || !accept.includes("text/event-stream"))) {
509
- setIncomingHeader(req, "accept", "application/json, text/event-stream");
510
- }
511
- if (req.method === "GET" && !accept.includes("text/event-stream")) {
512
- setIncomingHeader(req, "accept", "text/event-stream");
513
- }
514
- }
515
- function setIncomingHeader(req, key, value) {
516
- const lowerKey = key.toLowerCase();
517
- req.headers[lowerKey] = value;
518
- const rawIndex = req.rawHeaders.findIndex((entry) => entry.toLowerCase() === lowerKey);
519
- if (rawIndex >= 0) {
520
- req.rawHeaders[rawIndex + 1] = value;
521
- return;
522
- }
523
- req.rawHeaders.push(key, value);
524
- }
525
- async function recordAuthDiagnostic(db, event, req, fields) {
526
- try {
527
- await db.collection(AUTH_DIAGNOSTICS_COLLECTION).insertOne({
528
- event,
529
- createdAt: new Date().toISOString(),
530
- method: req?.method,
531
- path: req ? normalizePath(new URL(req.url ?? "/", "http://localhost").pathname) : undefined,
532
- headers: req ? diagnosticHeaders(req) : undefined,
533
- ...sanitizeDiagnosticFields(fields)
534
- });
535
- }
536
- catch {
537
- // Diagnostics must never affect the OAuth or MCP response path.
538
- }
539
- }
540
- function diagnosticHeaders(req) {
541
- return {
542
- accept: firstHeader(req.headers.accept),
543
- contentType: firstHeader(req.headers["content-type"]),
544
- mcpProtocolVersion: firstHeader(req.headers["mcp-protocol-version"]),
545
- origin: firstHeader(req.headers.origin),
546
- userAgent: firstHeader(req.headers["user-agent"])
547
- };
548
- }
549
- function sanitizeDiagnosticFields(fields) {
550
- const sanitized = {};
551
- for (const [key, value] of Object.entries(fields)) {
552
- if (value === undefined) {
553
- continue;
554
- }
555
- if (key.toLowerCase().includes("token") || key.toLowerCase().includes("code")) {
556
- continue;
557
- }
558
- sanitized[key] = typeof value === "string" ? truncateDiagnosticValue(value) : value;
559
- }
560
- return sanitized;
561
- }
562
- function truncateDiagnosticValue(value) {
563
- return value.length > 500 ? `${value.slice(0, 500)}...` : value;
564
- }
565
- function setCorsHeaders(res) {
566
- res.setHeader("access-control-allow-origin", "*");
567
- res.setHeader("access-control-allow-headers", "authorization,content-type,mcp-protocol-version,mcp-session-id,last-event-id,x-dxc-service-client-id,x-dxc-service-client-secret,x-dxc-workspace-id,x-dxc-workspace-name,x-dxc-forwarded-base-url,x-dxc-provisioning-secret");
568
- res.setHeader("access-control-allow-methods", "GET,POST,DELETE,OPTIONS");
569
- res.setHeader("access-control-expose-headers", "mcp-session-id,www-authenticate");
570
- }
571
- function writeJson(res, status, value) {
572
- const body = JSON.stringify(value);
573
- if (!res.headersSent) {
574
- res.writeHead(status, {
575
- "content-type": "application/json",
576
- "content-length": String(Buffer.byteLength(body))
577
- });
578
- }
579
- res.end(body);
580
- }
581
- function redirect(res, location) {
582
- res.writeHead(302, { location });
583
- res.end();
584
- }
585
- async function getRuntime() {
586
- runtimePromise ??= connectRuntime();
587
- return runtimePromise;
588
- }
589
- function getBaseUrl(req) {
590
- const forwardedProto = firstHeader(req.headers["x-forwarded-proto"]);
591
- const forwardedHost = firstHeader(req.headers["x-forwarded-host"]);
592
- const host = forwardedHost || firstHeader(req.headers.host);
593
- const protocol = forwardedProto || "http";
594
- if (!host) {
595
- throw new Error("Host header is required.");
596
- }
597
- return `${protocol}://${host}`;
598
- }
599
- function firstHeader(value) {
600
- return Array.isArray(value) ? value[0] : value;
601
- }
602
- function normalizePath(pathname) {
603
- if (pathname === "/api/mcp" || pathname === "/api/dxcomplete" || pathname === "/api/dxcomplete/mcp") {
604
- return MCP_PATH;
605
- }
606
- return pathname.endsWith("/") && pathname.length > 1 ? pathname.slice(0, -1) : pathname;
607
- }
608
- function normalizeServicePath(pathname) {
609
- const path = pathname.endsWith("/") && pathname.length > 1 ? pathname.slice(0, -1) : pathname;
610
- switch (path) {
611
- case "/api/dxcomplete/service/mcp":
612
- return MCP_PATH;
613
- case "/api/dxcomplete/service/auth/register":
614
- return "/api/dxcomplete/auth/register";
615
- case "/api/dxcomplete/service/auth/authorize":
616
- return "/api/dxcomplete/auth/authorize";
617
- case "/api/dxcomplete/service/auth/google/callback":
618
- return GOOGLE_CALLBACK_PATH;
619
- case "/api/dxcomplete/service/auth/token":
620
- return "/api/dxcomplete/auth/token";
621
- default:
622
- return path;
623
- }
624
- }
625
- function isProtectedResourceMetadataPath(pathname) {
626
- return (pathname === protectedResourceMetadataPath() ||
627
- pathname === "/.well-known/oauth-protected-resource/api/dxcomplete/mcp" ||
628
- pathname === `/api/dxcomplete${protectedResourceMetadataPath()}`);
629
- }
630
- function isAuthorizationServerMetadataPath(pathname) {
631
- return pathname === "/.well-known/oauth-authorization-server" || pathname === "/api/dxcomplete/.well-known/oauth-authorization-server";
632
- }
633
- function protectedResourceMetadataPath() {
634
- return `/.well-known/oauth-protected-resource${MCP_PATH}`;
635
- }
636
- function protectedResourceMetadataUrl(baseUrl) {
637
- return `${baseUrl}${protectedResourceMetadataPath()}`;
638
- }
639
- function mcpResourceUrl(baseUrl) {
640
- return `${baseUrl}${MCP_PATH}`;
641
- }
642
- function googleCallbackUrl(baseUrl) {
643
- return `${baseUrl}${GOOGLE_CALLBACK_PATH}`;
644
- }
645
- function readBearerToken(req) {
646
- const authorization = firstHeader(req.headers.authorization);
647
- const match = authorization?.match(/^Bearer\s+(.+)$/i);
648
- return match?.[1]?.trim();
649
- }
650
- async function readJsonBody(req) {
651
- if (req.body && typeof req.body === "object" && !Buffer.isBuffer(req.body)) {
652
- return req.body;
653
- }
654
- const text = typeof req.body === "string" ? req.body : await readRawBody(req);
655
- return text.trim() ? JSON.parse(text) : {};
656
- }
657
- async function readFormBody(req) {
658
- if (req.body && typeof req.body === "object" && !Buffer.isBuffer(req.body)) {
659
- const params = new URLSearchParams();
660
- for (const [key, value] of Object.entries(req.body)) {
661
- if (typeof value === "string") {
662
- params.set(key, value);
663
- }
664
- }
665
- return params;
666
- }
667
- const text = typeof req.body === "string" ? req.body : await readRawBody(req);
668
- return new URLSearchParams(text);
669
- }
670
- function readRawBody(req) {
671
- return new Promise((resolve, reject) => {
672
- let body = "";
673
- req.setEncoding("utf8");
674
- req.on("data", (chunk) => {
675
- body += chunk;
676
- });
677
- req.on("end", () => resolve(body));
678
- req.on("error", reject);
679
- });
680
- }
681
- async function drainRequestBody(req) {
682
- if (req.method !== "POST" || req.complete || req.destroyed) {
683
- return;
684
- }
685
- await readRawBody(req).catch(() => undefined);
686
- }
687
- function readStringArray(value, fallback) {
688
- if (value === undefined && fallback) {
689
- return fallback;
690
- }
691
- if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string" && entry.trim())) {
692
- throw new Error("Expected a non-empty string array.");
693
- }
694
- return value.map((entry) => entry.trim());
695
- }
696
- function requiredParam(url, key) {
697
- const value = url.searchParams.get(key)?.trim();
698
- if (!value) {
699
- throw new Error(`${key} is required.`);
700
- }
701
- return value;
702
- }
703
- function requiredFormValue(body, key) {
704
- const value = body.get(key)?.trim();
705
- if (!value) {
706
- throw new Error(`${key} is required.`);
707
- }
708
- return value;
709
- }
710
- function readClientId(req, body) {
711
- const bodyClientId = body.get("client_id")?.trim();
712
- if (bodyClientId) {
713
- return bodyClientId;
714
- }
715
- const authorization = firstHeader(req.headers.authorization);
716
- const match = authorization?.match(/^Basic\s+(.+)$/i);
717
- if (match?.[1]) {
718
- const decoded = Buffer.from(match[1], "base64").toString("utf8");
719
- const [clientId] = decoded.split(":");
720
- if (clientId) {
721
- return clientId;
722
- }
723
- }
724
- throw new Error("client_id is required.");
725
- }