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.
- package/.env.example +0 -7
- package/README.md +68 -103
- package/dist/cli.js +2 -24
- package/dist/validate.js +10 -26
- package/docs/cost-model.md +2 -2
- package/docs/decision-basis.md +5 -11
- package/docs/diagrams.md +3 -3
- package/docs/index.md +25 -39
- package/docs/model.md +15 -23
- package/docs/open-questions.md +1 -1
- package/docs/taxonomy.md +7 -8
- package/docs/workflows.md +3 -3
- package/package.json +24 -24
- package/templates/process/README.md +11 -11
- package/templates/process/controls.yml +19 -19
- package/templates/process/cost-model.yml +3 -3
- package/templates/process/decision-basis.yml +4 -4
- package/templates/process/diagrams/00-decision-basis.mmd +1 -1
- package/templates/process/diagrams/00-overview.mmd +1 -1
- package/templates/process/diagrams/01-intake-triage.mmd +4 -4
- package/templates/process/diagrams/02-product-definition.mmd +3 -3
- package/templates/process/diagrams/03-engineering-execution.mmd +1 -1
- package/templates/process/diagrams/04-qa-verification.mmd +1 -1
- package/templates/process/diagrams/05-product-validation.mmd +1 -1
- package/templates/process/diagrams/06-change-release-control.mmd +1 -1
- package/templates/process/diagrams/07-deployment-operations.mmd +1 -1
- package/templates/process/diagrams/08-support-incident-management.mmd +1 -1
- package/templates/process/diagrams/09-problem-improvement.mmd +1 -1
- package/templates/process/diagrams/10-risk-control-management.mmd +1 -1
- package/templates/process/diagrams/11-audit-evidence-capture.mmd +1 -1
- package/templates/process/roles.yml +6 -6
- package/templates/process/taxonomy.yml +46 -46
- package/templates/process/workflows.yml +29 -29
- package/website/account.html +57 -0
- package/website/app.js +177 -0
- package/website/flow.html +4 -0
- package/website/glossary.html +4 -0
- package/website/index.html +4 -0
- package/website/objects.html +4 -0
- package/website/operating-guide.html +4 -0
- package/website/outcomes.html +4 -0
- package/website/phase-build.html +4 -0
- package/website/phase-elicit.html +4 -0
- package/website/phase-go-live.html +4 -0
- package/website/phase-measure.html +4 -0
- package/website/phase-operate.html +4 -0
- package/website/phase-orient.html +4 -0
- package/website/phase-weigh.html +4 -0
- package/website/roles.html +4 -0
- package/website/styles.css +217 -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/runtime/auth.ts
DELETED
|
@@ -1,673 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import type { Db } from "mongodb";
|
|
3
|
-
import { createGoogleActorContext, normalizeEmail, type ActorContext } from "./actor.js";
|
|
4
|
-
import type { RuntimeConfig } from "./config.js";
|
|
5
|
-
import type { DxcRecord } from "./records.js";
|
|
6
|
-
import type { WorkspaceConfig } from "./workspace.js";
|
|
7
|
-
|
|
8
|
-
export const WORKSPACE_MEMBERSHIPS_COLLECTION = "workspace_memberships";
|
|
9
|
-
export const OAUTH_CLIENTS_COLLECTION = "oauth_clients";
|
|
10
|
-
export const OAUTH_AUTH_REQUESTS_COLLECTION = "oauth_authorization_requests";
|
|
11
|
-
export const OAUTH_CODES_COLLECTION = "oauth_authorization_codes";
|
|
12
|
-
export const OAUTH_TOKENS_COLLECTION = "oauth_tokens";
|
|
13
|
-
export const WORKSPACE_SERVICE_CLIENTS_COLLECTION = "workspace_service_clients";
|
|
14
|
-
|
|
15
|
-
export type WorkspaceRole = "owner" | "engineer" | "tester" | "operator" | "support_agent" | "end_user";
|
|
16
|
-
|
|
17
|
-
export type WorkspaceMembership = {
|
|
18
|
-
_id: string;
|
|
19
|
-
workspaceId: string;
|
|
20
|
-
email: string;
|
|
21
|
-
roles: WorkspaceRole[];
|
|
22
|
-
role?: "owner" | "member";
|
|
23
|
-
provider?: "google";
|
|
24
|
-
providerSubject?: string;
|
|
25
|
-
createdAt: string;
|
|
26
|
-
updatedAt: string;
|
|
27
|
-
updatedBy: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type WorkspaceServiceClientRecord = {
|
|
31
|
-
_id: string;
|
|
32
|
-
workspaceId: string;
|
|
33
|
-
clientId: string;
|
|
34
|
-
secretHash: string;
|
|
35
|
-
name: string;
|
|
36
|
-
createdAt: string;
|
|
37
|
-
createdBy: string;
|
|
38
|
-
updatedAt: string;
|
|
39
|
-
updatedBy: string;
|
|
40
|
-
revokedAt?: string;
|
|
41
|
-
revokedBy?: string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export type OAuthClientRecord = {
|
|
45
|
-
_id: string;
|
|
46
|
-
clientId: string;
|
|
47
|
-
clientName?: string;
|
|
48
|
-
redirectUris: string[];
|
|
49
|
-
grantTypes: string[];
|
|
50
|
-
responseTypes: string[];
|
|
51
|
-
createdAt: string;
|
|
52
|
-
updatedAt: string;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export type OAuthAuthorizationRequest = {
|
|
56
|
-
_id: string;
|
|
57
|
-
clientId: string;
|
|
58
|
-
redirectUri: string;
|
|
59
|
-
codeChallenge: string;
|
|
60
|
-
codeChallengeMethod: "S256";
|
|
61
|
-
state?: string;
|
|
62
|
-
scope: string;
|
|
63
|
-
resource: string;
|
|
64
|
-
workspaceId: string;
|
|
65
|
-
createdAt: string;
|
|
66
|
-
expiresAt: Date;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export type OAuthAuthorizationCode = OAuthAuthorizationRequest & {
|
|
70
|
-
actor: ActorContext;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export type OAuthTokenRecord = {
|
|
74
|
-
_id: string;
|
|
75
|
-
kind: "access" | "refresh";
|
|
76
|
-
clientId: string;
|
|
77
|
-
workspaceId: string;
|
|
78
|
-
actor: ActorContext;
|
|
79
|
-
scope: string;
|
|
80
|
-
resource: string;
|
|
81
|
-
createdAt: string;
|
|
82
|
-
expiresAt: Date;
|
|
83
|
-
revokedAt?: string;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export type GoogleProfile = {
|
|
87
|
-
email: string;
|
|
88
|
-
subject: string;
|
|
89
|
-
displayName?: string;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const MCP_SCOPE = "mcp:tools";
|
|
93
|
-
const ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
94
|
-
const REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
95
|
-
const AUTH_REQUEST_TTL_SECONDS = 10 * 60;
|
|
96
|
-
const SERVICE_CLIENT_SECRET_PREFIX = "dxc_service_secret_";
|
|
97
|
-
const SERVICE_CLIENT_HASH_PREFIX = "sha256:";
|
|
98
|
-
|
|
99
|
-
export async function ensureWorkspaceBootstrap(db: Db, config: WorkspaceConfig, actorId: string): Promise<void> {
|
|
100
|
-
const now = new Date().toISOString();
|
|
101
|
-
|
|
102
|
-
await db.collection<DxcRecord>("workspaces").updateOne(
|
|
103
|
-
{ _id: config.workspaceId },
|
|
104
|
-
{
|
|
105
|
-
$set: {
|
|
106
|
-
recordType: "workspaces",
|
|
107
|
-
title: config.name,
|
|
108
|
-
fields: {
|
|
109
|
-
name: config.name,
|
|
110
|
-
...(config.mode ? { mode: config.mode } : {})
|
|
111
|
-
},
|
|
112
|
-
updatedAt: now,
|
|
113
|
-
updatedBy: actorId
|
|
114
|
-
},
|
|
115
|
-
$setOnInsert: {
|
|
116
|
-
_id: config.workspaceId,
|
|
117
|
-
links: [],
|
|
118
|
-
createdAt: now,
|
|
119
|
-
createdBy: actorId
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
{ upsert: true }
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
await Promise.all(
|
|
126
|
-
config.bootstrapMembers.map((member) =>
|
|
127
|
-
upsertWorkspaceMembership(db, {
|
|
128
|
-
workspaceId: config.workspaceId,
|
|
129
|
-
email: member.email,
|
|
130
|
-
roles: member.roles,
|
|
131
|
-
actorId
|
|
132
|
-
})
|
|
133
|
-
)
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export async function upsertWorkspaceMembership(
|
|
138
|
-
db: Db,
|
|
139
|
-
input: {
|
|
140
|
-
workspaceId: string;
|
|
141
|
-
email: string;
|
|
142
|
-
role?: WorkspaceRole | "member";
|
|
143
|
-
roles?: WorkspaceRole[];
|
|
144
|
-
actorId: string;
|
|
145
|
-
provider?: "google";
|
|
146
|
-
providerSubject?: string;
|
|
147
|
-
}
|
|
148
|
-
): Promise<WorkspaceMembership> {
|
|
149
|
-
const email = normalizeEmail(input.email);
|
|
150
|
-
const now = new Date().toISOString();
|
|
151
|
-
const id = membershipId(input.workspaceId, email);
|
|
152
|
-
const roles = normalizeWorkspaceRoles(input.roles ?? (input.role ? [input.role] : []));
|
|
153
|
-
|
|
154
|
-
await db.collection<WorkspaceMembership>(WORKSPACE_MEMBERSHIPS_COLLECTION).updateOne(
|
|
155
|
-
{ _id: id },
|
|
156
|
-
{
|
|
157
|
-
$set: {
|
|
158
|
-
workspaceId: input.workspaceId,
|
|
159
|
-
email,
|
|
160
|
-
roles,
|
|
161
|
-
...(input.provider ? { provider: input.provider } : {}),
|
|
162
|
-
...(input.providerSubject ? { providerSubject: input.providerSubject } : {}),
|
|
163
|
-
updatedAt: now,
|
|
164
|
-
updatedBy: input.actorId
|
|
165
|
-
},
|
|
166
|
-
$unset: {
|
|
167
|
-
role: ""
|
|
168
|
-
},
|
|
169
|
-
$setOnInsert: {
|
|
170
|
-
_id: id,
|
|
171
|
-
createdAt: now
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
{ upsert: true }
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const record = await db.collection<WorkspaceMembership>(WORKSPACE_MEMBERSHIPS_COLLECTION).findOne({ _id: id });
|
|
178
|
-
if (!record) {
|
|
179
|
-
throw new Error("Workspace membership was not written.");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return record;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export async function assertWorkspaceMembership(
|
|
186
|
-
db: Db,
|
|
187
|
-
workspaceId: string,
|
|
188
|
-
actor: ActorContext
|
|
189
|
-
): Promise<WorkspaceMembership> {
|
|
190
|
-
if (!actor.email) {
|
|
191
|
-
throw new Error("Authenticated actor email is required for workspace authorization.");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const email = normalizeEmail(actor.email);
|
|
195
|
-
const membership = await db
|
|
196
|
-
.collection<WorkspaceMembership>(WORKSPACE_MEMBERSHIPS_COLLECTION)
|
|
197
|
-
.findOne({ _id: membershipId(workspaceId, email) });
|
|
198
|
-
|
|
199
|
-
if (!membership) {
|
|
200
|
-
throw new Error(`Workspace access denied for ${email}.`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (membership.providerSubject && actor.providerSubject && membership.providerSubject !== actor.providerSubject) {
|
|
204
|
-
throw new Error(`Workspace access denied for ${email}.`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (actor.provider === "google" && actor.providerSubject && !membership.providerSubject) {
|
|
208
|
-
await upsertWorkspaceMembership(db, {
|
|
209
|
-
workspaceId,
|
|
210
|
-
email,
|
|
211
|
-
roles: membershipRoles(membership),
|
|
212
|
-
actorId: actor.actorId,
|
|
213
|
-
provider: "google",
|
|
214
|
-
providerSubject: actor.providerSubject
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!Array.isArray(membership.roles) || membership.roles.length === 0 || membership.role) {
|
|
219
|
-
return upsertWorkspaceMembership(db, {
|
|
220
|
-
workspaceId,
|
|
221
|
-
email,
|
|
222
|
-
roles: membershipRoles(membership),
|
|
223
|
-
actorId: actor.actorId,
|
|
224
|
-
...(membership.provider ? { provider: membership.provider } : {}),
|
|
225
|
-
...(membership.providerSubject ? { providerSubject: membership.providerSubject } : {})
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return membership;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export async function createWorkspaceServiceClient(
|
|
233
|
-
db: Db,
|
|
234
|
-
input: {
|
|
235
|
-
workspaceId: string;
|
|
236
|
-
name: string;
|
|
237
|
-
actorId: string;
|
|
238
|
-
}
|
|
239
|
-
): Promise<{ record: WorkspaceServiceClientRecord; secret: string }> {
|
|
240
|
-
const now = new Date().toISOString();
|
|
241
|
-
const clientId = `dxc_service_client_${randomUUID()}`;
|
|
242
|
-
const secret = `${SERVICE_CLIENT_SECRET_PREFIX}${randomBytes(32).toString("base64url")}`;
|
|
243
|
-
const record: WorkspaceServiceClientRecord = {
|
|
244
|
-
_id: clientId,
|
|
245
|
-
workspaceId: input.workspaceId,
|
|
246
|
-
clientId,
|
|
247
|
-
secretHash: hashServiceClientSecret(secret),
|
|
248
|
-
name: input.name,
|
|
249
|
-
createdAt: now,
|
|
250
|
-
createdBy: input.actorId,
|
|
251
|
-
updatedAt: now,
|
|
252
|
-
updatedBy: input.actorId
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
await db.collection<WorkspaceServiceClientRecord>(WORKSPACE_SERVICE_CLIENTS_COLLECTION).insertOne(record);
|
|
256
|
-
return { record, secret };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export async function verifyWorkspaceServiceClient(
|
|
260
|
-
db: Db,
|
|
261
|
-
input: {
|
|
262
|
-
clientId?: string;
|
|
263
|
-
secret?: string;
|
|
264
|
-
workspaceId?: string;
|
|
265
|
-
}
|
|
266
|
-
): Promise<WorkspaceServiceClientRecord> {
|
|
267
|
-
if (!input.clientId || !input.secret) {
|
|
268
|
-
throw new Error("DX Complete workspace service credentials are required.");
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const record = await db
|
|
272
|
-
.collection<WorkspaceServiceClientRecord>(WORKSPACE_SERVICE_CLIENTS_COLLECTION)
|
|
273
|
-
.findOne({ _id: input.clientId });
|
|
274
|
-
|
|
275
|
-
if (!record || record.revokedAt) {
|
|
276
|
-
throw new Error("DX Complete workspace service client was not found or is revoked.");
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (input.workspaceId && record.workspaceId !== input.workspaceId) {
|
|
280
|
-
throw new Error("DX Complete workspace service client is not bound to this workspace.");
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (record.secretHash !== hashServiceClientSecret(input.secret)) {
|
|
284
|
-
throw new Error("DX Complete workspace service client secret is invalid.");
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return record;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export async function registerOAuthClient(
|
|
291
|
-
db: Db,
|
|
292
|
-
input: {
|
|
293
|
-
clientName?: string;
|
|
294
|
-
redirectUris: string[];
|
|
295
|
-
grantTypes?: string[];
|
|
296
|
-
responseTypes?: string[];
|
|
297
|
-
}
|
|
298
|
-
): Promise<OAuthClientRecord> {
|
|
299
|
-
if (input.redirectUris.length === 0) {
|
|
300
|
-
throw new Error("redirect_uris must contain at least one URI.");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const now = new Date().toISOString();
|
|
304
|
-
const clientId = `dxc_client_${randomUUID()}`;
|
|
305
|
-
const record: OAuthClientRecord = {
|
|
306
|
-
_id: clientId,
|
|
307
|
-
clientId,
|
|
308
|
-
clientName: input.clientName,
|
|
309
|
-
redirectUris: input.redirectUris,
|
|
310
|
-
grantTypes: input.grantTypes?.length ? input.grantTypes : ["authorization_code", "refresh_token"],
|
|
311
|
-
responseTypes: input.responseTypes?.length ? input.responseTypes : ["code"],
|
|
312
|
-
createdAt: now,
|
|
313
|
-
updatedAt: now
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
await db.collection<OAuthClientRecord>(OAUTH_CLIENTS_COLLECTION).insertOne(record);
|
|
317
|
-
return record;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export async function getOAuthClient(db: Db, clientId: string): Promise<OAuthClientRecord | null> {
|
|
321
|
-
return db.collection<OAuthClientRecord>(OAUTH_CLIENTS_COLLECTION).findOne({ _id: clientId });
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export async function createOAuthAuthorizationRequest(
|
|
325
|
-
db: Db,
|
|
326
|
-
input: Omit<OAuthAuthorizationRequest, "_id" | "createdAt" | "expiresAt">
|
|
327
|
-
): Promise<OAuthAuthorizationRequest> {
|
|
328
|
-
const now = new Date();
|
|
329
|
-
const record: OAuthAuthorizationRequest = {
|
|
330
|
-
_id: `dxc_state_${randomBytes(24).toString("base64url")}`,
|
|
331
|
-
...input,
|
|
332
|
-
createdAt: now.toISOString(),
|
|
333
|
-
expiresAt: new Date(now.getTime() + AUTH_REQUEST_TTL_SECONDS * 1000)
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
await db.collection<OAuthAuthorizationRequest>(OAUTH_AUTH_REQUESTS_COLLECTION).insertOne(record);
|
|
337
|
-
return record;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export async function consumeOAuthAuthorizationRequest(
|
|
341
|
-
db: Db,
|
|
342
|
-
state: string
|
|
343
|
-
): Promise<OAuthAuthorizationRequest> {
|
|
344
|
-
const collection = db.collection<OAuthAuthorizationRequest>(OAUTH_AUTH_REQUESTS_COLLECTION);
|
|
345
|
-
const record = await collection.findOne({ _id: state });
|
|
346
|
-
|
|
347
|
-
if (!record || isExpired(record.expiresAt)) {
|
|
348
|
-
throw new Error("OAuth authorization request expired or was not found.");
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
await collection.deleteOne({ _id: state });
|
|
352
|
-
return record;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export async function createOAuthAuthorizationCode(
|
|
356
|
-
db: Db,
|
|
357
|
-
request: OAuthAuthorizationRequest,
|
|
358
|
-
actor: ActorContext
|
|
359
|
-
): Promise<string> {
|
|
360
|
-
const code = `dxc_code_${randomBytes(32).toString("base64url")}`;
|
|
361
|
-
const record: OAuthAuthorizationCode = {
|
|
362
|
-
...request,
|
|
363
|
-
_id: code,
|
|
364
|
-
actor
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
await db.collection<OAuthAuthorizationCode>(OAUTH_CODES_COLLECTION).insertOne(record);
|
|
368
|
-
return code;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export async function exchangeOAuthAuthorizationCode(
|
|
372
|
-
db: Db,
|
|
373
|
-
input: {
|
|
374
|
-
code: string;
|
|
375
|
-
clientId: string;
|
|
376
|
-
redirectUri: string;
|
|
377
|
-
codeVerifier: string;
|
|
378
|
-
resource: string;
|
|
379
|
-
}
|
|
380
|
-
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number; scope: string }> {
|
|
381
|
-
const collection = db.collection<OAuthAuthorizationCode>(OAUTH_CODES_COLLECTION);
|
|
382
|
-
const record = await collection.findOne({ _id: input.code });
|
|
383
|
-
|
|
384
|
-
if (!record || isExpired(record.expiresAt)) {
|
|
385
|
-
throw new Error("Authorization code expired or was not found.");
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (
|
|
389
|
-
record.clientId !== input.clientId ||
|
|
390
|
-
record.redirectUri !== input.redirectUri ||
|
|
391
|
-
record.resource !== input.resource
|
|
392
|
-
) {
|
|
393
|
-
throw new Error("Authorization code request does not match the original authorization.");
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (!verifyPkce(input.codeVerifier, record.codeChallenge)) {
|
|
397
|
-
throw new Error("Invalid PKCE verifier.");
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
await collection.deleteOne({ _id: input.code });
|
|
401
|
-
return issueMcpTokenPair(db, record);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
export async function exchangeOAuthRefreshToken(
|
|
405
|
-
db: Db,
|
|
406
|
-
input: {
|
|
407
|
-
refreshToken: string;
|
|
408
|
-
clientId: string;
|
|
409
|
-
resource: string;
|
|
410
|
-
}
|
|
411
|
-
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number; scope: string }> {
|
|
412
|
-
const existing = await findUsableToken(db, input.refreshToken, "refresh");
|
|
413
|
-
if (existing.clientId !== input.clientId || existing.resource !== input.resource) {
|
|
414
|
-
throw new Error("Refresh token does not match this client or resource.");
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await db.collection<OAuthTokenRecord>(OAUTH_TOKENS_COLLECTION).updateOne(
|
|
418
|
-
{ _id: existing._id },
|
|
419
|
-
{
|
|
420
|
-
$set: {
|
|
421
|
-
revokedAt: new Date().toISOString()
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
);
|
|
425
|
-
|
|
426
|
-
return issueMcpTokenPair(db, {
|
|
427
|
-
clientId: existing.clientId,
|
|
428
|
-
scope: existing.scope,
|
|
429
|
-
resource: existing.resource,
|
|
430
|
-
workspaceId: existing.workspaceId,
|
|
431
|
-
actor: existing.actor
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
export async function issueMcpTokenPair(
|
|
436
|
-
db: Db,
|
|
437
|
-
input: {
|
|
438
|
-
clientId: string;
|
|
439
|
-
scope: string;
|
|
440
|
-
resource: string;
|
|
441
|
-
workspaceId: string;
|
|
442
|
-
actor: ActorContext;
|
|
443
|
-
}
|
|
444
|
-
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number; scope: string }> {
|
|
445
|
-
const accessToken = `dxc_access_${randomBytes(32).toString("base64url")}`;
|
|
446
|
-
const refreshToken = `dxc_refresh_${randomBytes(32).toString("base64url")}`;
|
|
447
|
-
const now = new Date();
|
|
448
|
-
|
|
449
|
-
await db.collection<OAuthTokenRecord>(OAUTH_TOKENS_COLLECTION).insertMany([
|
|
450
|
-
{
|
|
451
|
-
_id: hashToken(accessToken),
|
|
452
|
-
kind: "access",
|
|
453
|
-
clientId: input.clientId,
|
|
454
|
-
workspaceId: input.workspaceId,
|
|
455
|
-
actor: input.actor,
|
|
456
|
-
scope: input.scope || MCP_SCOPE,
|
|
457
|
-
resource: input.resource,
|
|
458
|
-
createdAt: now.toISOString(),
|
|
459
|
-
expiresAt: new Date(now.getTime() + ACCESS_TOKEN_TTL_SECONDS * 1000)
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
_id: hashToken(refreshToken),
|
|
463
|
-
kind: "refresh",
|
|
464
|
-
clientId: input.clientId,
|
|
465
|
-
workspaceId: input.workspaceId,
|
|
466
|
-
actor: input.actor,
|
|
467
|
-
scope: input.scope || MCP_SCOPE,
|
|
468
|
-
resource: input.resource,
|
|
469
|
-
createdAt: now.toISOString(),
|
|
470
|
-
expiresAt: new Date(now.getTime() + REFRESH_TOKEN_TTL_SECONDS * 1000)
|
|
471
|
-
}
|
|
472
|
-
]);
|
|
473
|
-
|
|
474
|
-
return {
|
|
475
|
-
accessToken,
|
|
476
|
-
refreshToken,
|
|
477
|
-
expiresIn: ACCESS_TOKEN_TTL_SECONDS,
|
|
478
|
-
scope: input.scope || MCP_SCOPE
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export async function verifyMcpAccessToken(
|
|
483
|
-
db: Db,
|
|
484
|
-
input: {
|
|
485
|
-
token: string;
|
|
486
|
-
workspaceId: string;
|
|
487
|
-
resource: string;
|
|
488
|
-
}
|
|
489
|
-
): Promise<OAuthTokenRecord> {
|
|
490
|
-
const record = await findUsableToken(db, input.token, "access");
|
|
491
|
-
|
|
492
|
-
if (record.workspaceId !== input.workspaceId || record.resource !== input.resource) {
|
|
493
|
-
throw new Error("Access token is not valid for this workspace resource.");
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
await assertWorkspaceMembership(db, input.workspaceId, record.actor);
|
|
497
|
-
return record;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
export async function exchangeGoogleCodeForProfile(
|
|
501
|
-
config: RuntimeConfig,
|
|
502
|
-
input: {
|
|
503
|
-
code: string;
|
|
504
|
-
redirectUri: string;
|
|
505
|
-
}
|
|
506
|
-
): Promise<GoogleProfile> {
|
|
507
|
-
if (!config.googleClientId || !config.googleClientSecret) {
|
|
508
|
-
throw new Error("Google OAuth is not configured.");
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const response = await fetch("https://oauth2.googleapis.com/token", {
|
|
512
|
-
method: "POST",
|
|
513
|
-
headers: {
|
|
514
|
-
"content-type": "application/x-www-form-urlencoded"
|
|
515
|
-
},
|
|
516
|
-
body: new URLSearchParams({
|
|
517
|
-
code: input.code,
|
|
518
|
-
client_id: config.googleClientId,
|
|
519
|
-
client_secret: config.googleClientSecret,
|
|
520
|
-
redirect_uri: input.redirectUri,
|
|
521
|
-
grant_type: "authorization_code"
|
|
522
|
-
})
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
if (!response.ok) {
|
|
526
|
-
throw new Error(`Google token exchange failed: ${await response.text()}`);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const tokens = (await response.json()) as { id_token?: string };
|
|
530
|
-
if (!tokens.id_token) {
|
|
531
|
-
throw new Error("Google token exchange did not return an id_token.");
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const profileResponse = await fetch(
|
|
535
|
-
`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(tokens.id_token)}`
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
if (!profileResponse.ok) {
|
|
539
|
-
throw new Error(`Google token verification failed: ${await profileResponse.text()}`);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const profile = (await profileResponse.json()) as {
|
|
543
|
-
aud?: string;
|
|
544
|
-
sub?: string;
|
|
545
|
-
email?: string;
|
|
546
|
-
email_verified?: string | boolean;
|
|
547
|
-
name?: string;
|
|
548
|
-
};
|
|
549
|
-
|
|
550
|
-
if (profile.aud !== config.googleClientId) {
|
|
551
|
-
throw new Error("Google token audience did not match this DX Complete deployment.");
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (profile.email_verified !== true && profile.email_verified !== "true") {
|
|
555
|
-
throw new Error("Google email is not verified.");
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (!profile.email || !profile.sub) {
|
|
559
|
-
throw new Error("Google profile did not include email and subject.");
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return {
|
|
563
|
-
email: normalizeEmail(profile.email),
|
|
564
|
-
subject: profile.sub,
|
|
565
|
-
displayName: profile.name
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
export function googleAuthorizationUrl(
|
|
570
|
-
config: RuntimeConfig,
|
|
571
|
-
input: {
|
|
572
|
-
redirectUri: string;
|
|
573
|
-
state: string;
|
|
574
|
-
}
|
|
575
|
-
): string {
|
|
576
|
-
if (!config.googleClientId) {
|
|
577
|
-
throw new Error("Google OAuth is not configured.");
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
|
581
|
-
url.searchParams.set("client_id", config.googleClientId);
|
|
582
|
-
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
583
|
-
url.searchParams.set("response_type", "code");
|
|
584
|
-
url.searchParams.set("scope", "openid email profile");
|
|
585
|
-
url.searchParams.set("state", input.state);
|
|
586
|
-
url.searchParams.set("prompt", "select_account");
|
|
587
|
-
return url.href;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
export function createGoogleActor(profile: GoogleProfile): ActorContext {
|
|
591
|
-
return createGoogleActorContext({
|
|
592
|
-
email: profile.email,
|
|
593
|
-
subject: profile.subject,
|
|
594
|
-
displayName: profile.displayName
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
export function membershipRoles(membership: WorkspaceMembership): WorkspaceRole[] {
|
|
599
|
-
return normalizeWorkspaceRoles(
|
|
600
|
-
Array.isArray(membership.roles) && membership.roles.length > 0
|
|
601
|
-
? membership.roles
|
|
602
|
-
: membership.role
|
|
603
|
-
? [membership.role]
|
|
604
|
-
: []
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async function findUsableToken(db: Db, token: string, kind: "access" | "refresh"): Promise<OAuthTokenRecord> {
|
|
609
|
-
const record = await db.collection<OAuthTokenRecord>(OAUTH_TOKENS_COLLECTION).findOne({
|
|
610
|
-
_id: hashToken(token),
|
|
611
|
-
kind,
|
|
612
|
-
revokedAt: { $exists: false }
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
if (!record || isExpired(record.expiresAt)) {
|
|
616
|
-
throw new Error("Token expired or was not found.");
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
return record;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function verifyPkce(verifier: string, challenge: string): boolean {
|
|
623
|
-
const actual = createHash("sha256").update(verifier).digest("base64url");
|
|
624
|
-
return actual === challenge;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function hashToken(token: string): string {
|
|
628
|
-
return createHash("sha256").update(token).digest("hex");
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function hashServiceClientSecret(secret: string): string {
|
|
632
|
-
return `${SERVICE_CLIENT_HASH_PREFIX}${createHash("sha256").update(secret).digest("hex")}`;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function isExpired(value: string | Date): boolean {
|
|
636
|
-
return new Date(value).getTime() <= Date.now();
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function membershipId(workspaceId: string, email: string): string {
|
|
640
|
-
return `${workspaceId}:${normalizeEmail(email)}`;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function normalizeWorkspaceRoles(values: Array<WorkspaceRole | "member">): WorkspaceRole[] {
|
|
644
|
-
const roles = new Set<WorkspaceRole>();
|
|
645
|
-
|
|
646
|
-
for (const value of values) {
|
|
647
|
-
if (value === "member") {
|
|
648
|
-
roles.add("end_user");
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (isWorkspaceRole(value)) {
|
|
653
|
-
roles.add(value);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (roles.size === 0) {
|
|
658
|
-
roles.add("end_user");
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
return [...roles].sort();
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function isWorkspaceRole(value: string): value is WorkspaceRole {
|
|
665
|
-
return (
|
|
666
|
-
value === "owner" ||
|
|
667
|
-
value === "engineer" ||
|
|
668
|
-
value === "tester" ||
|
|
669
|
-
value === "operator" ||
|
|
670
|
-
value === "support_agent" ||
|
|
671
|
-
value === "end_user"
|
|
672
|
-
);
|
|
673
|
-
}
|
package/src/runtime/check.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { RuntimeOptions } from "./config.js";
|
|
2
|
-
import { connectRuntime } from "./mongo.js";
|
|
3
|
-
import { COLLECTION_NAMES } from "./records.js";
|
|
4
|
-
|
|
5
|
-
export async function checkRuntime(options: RuntimeOptions = {}) {
|
|
6
|
-
const runtime = await connectRuntime(options);
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
return {
|
|
10
|
-
ok: true,
|
|
11
|
-
databaseName: runtime.config.databaseName,
|
|
12
|
-
envFilePath: runtime.config.envFilePath,
|
|
13
|
-
collections: COLLECTION_NAMES
|
|
14
|
-
};
|
|
15
|
-
} finally {
|
|
16
|
-
await runtime.close();
|
|
17
|
-
}
|
|
18
|
-
}
|