@enterprisestandard/esv 0.0.5-beta.20260115.1 → 0.0.5-beta.20260115.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/dist/iam/index.js +664 -5500
- package/dist/iam/index.js.map +1 -23
- package/dist/index.js +156 -6554
- package/dist/index.js.map +1 -27
- package/dist/runner.js +280 -10928
- package/dist/runner.js.map +1 -33
- package/dist/server/crypto.js +134 -0
- package/dist/server/crypto.js.map +1 -0
- package/dist/server/iam.js +402 -0
- package/dist/server/iam.js.map +1 -0
- package/dist/server/index.js +34 -1380
- package/dist/server/index.js.map +1 -16
- package/dist/server/server.js +223 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/sso.js +428 -0
- package/dist/server/sso.js.map +1 -0
- package/dist/server/state.js +152 -0
- package/dist/server/state.js.map +1 -0
- package/dist/server/vault.js +92 -0
- package/dist/server/vault.js.map +1 -0
- package/dist/server/workload.js +226 -0
- package/dist/server/workload.js.map +1 -0
- package/dist/sso/index.js +355 -428
- package/dist/sso/index.js.map +1 -11
- package/dist/tenant/index.js +300 -0
- package/dist/tenant/index.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.js +139 -0
- package/dist/utils.js.map +1 -0
- package/dist/workload/index.js +404 -474
- package/dist/workload/index.js.map +1 -11
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1,1380 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for (var name in all)
|
|
36
|
-
__defProp(target, name, {
|
|
37
|
-
get: all[name],
|
|
38
|
-
enumerable: true,
|
|
39
|
-
configurable: true,
|
|
40
|
-
set: (newValue) => all[name] = () => newValue
|
|
41
|
-
});
|
|
42
|
-
};
|
|
43
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
44
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
45
|
-
|
|
46
|
-
// packages/esv/src/server/crypto.ts
|
|
47
|
-
import * as crypto from "node:crypto";
|
|
48
|
-
var privateKey;
|
|
49
|
-
var publicKey;
|
|
50
|
-
var keyId;
|
|
51
|
-
function initializeKeys() {
|
|
52
|
-
const { publicKey: pubKey, privateKey: privKey } = crypto.generateKeyPairSync("rsa", {
|
|
53
|
-
modulusLength: 2048
|
|
54
|
-
});
|
|
55
|
-
privateKey = privKey;
|
|
56
|
-
publicKey = pubKey;
|
|
57
|
-
keyId = crypto.randomBytes(8).toString("hex");
|
|
58
|
-
}
|
|
59
|
-
function getKeyId() {
|
|
60
|
-
return keyId;
|
|
61
|
-
}
|
|
62
|
-
function base64UrlEncode(data) {
|
|
63
|
-
const buffer = typeof data === "string" ? Buffer.from(data) : data;
|
|
64
|
-
return buffer.toString("base64url");
|
|
65
|
-
}
|
|
66
|
-
function base64UrlDecode(data) {
|
|
67
|
-
return Buffer.from(data, "base64url");
|
|
68
|
-
}
|
|
69
|
-
function signJwt(payload, expiresInSeconds = 3600) {
|
|
70
|
-
const now = Math.floor(Date.now() / 1000);
|
|
71
|
-
const header = {
|
|
72
|
-
alg: "RS256",
|
|
73
|
-
typ: "JWT",
|
|
74
|
-
kid: keyId
|
|
75
|
-
};
|
|
76
|
-
const claims = {
|
|
77
|
-
...payload,
|
|
78
|
-
iat: now,
|
|
79
|
-
exp: now + expiresInSeconds
|
|
80
|
-
};
|
|
81
|
-
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
82
|
-
const encodedPayload = base64UrlEncode(JSON.stringify(claims));
|
|
83
|
-
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
84
|
-
const sign = crypto.createSign("RSA-SHA256");
|
|
85
|
-
sign.update(signatureInput);
|
|
86
|
-
const signature = sign.sign(privateKey);
|
|
87
|
-
return `${signatureInput}.${base64UrlEncode(signature)}`;
|
|
88
|
-
}
|
|
89
|
-
function verifyJwt(token) {
|
|
90
|
-
try {
|
|
91
|
-
const parts = token.split(".");
|
|
92
|
-
if (parts.length !== 3) {
|
|
93
|
-
return { valid: false, error: "Invalid JWT format" };
|
|
94
|
-
}
|
|
95
|
-
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
96
|
-
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
97
|
-
const signature = base64UrlDecode(encodedSignature);
|
|
98
|
-
const verify = crypto.createVerify("RSA-SHA256");
|
|
99
|
-
verify.update(signatureInput);
|
|
100
|
-
const isValid = verify.verify(publicKey, signature);
|
|
101
|
-
if (!isValid) {
|
|
102
|
-
return { valid: false, error: "Invalid signature" };
|
|
103
|
-
}
|
|
104
|
-
const payload = JSON.parse(base64UrlDecode(encodedPayload).toString("utf-8"));
|
|
105
|
-
const now = Math.floor(Date.now() / 1000);
|
|
106
|
-
if (payload.exp && payload.exp < now) {
|
|
107
|
-
return { valid: false, error: "Token expired" };
|
|
108
|
-
}
|
|
109
|
-
return { valid: true, payload };
|
|
110
|
-
} catch (error) {
|
|
111
|
-
return { valid: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function getJwks() {
|
|
115
|
-
const jwk = publicKey.export({ format: "jwk" });
|
|
116
|
-
return {
|
|
117
|
-
keys: [
|
|
118
|
-
{
|
|
119
|
-
kty: jwk.kty,
|
|
120
|
-
n: jwk.n,
|
|
121
|
-
e: jwk.e,
|
|
122
|
-
kid: keyId,
|
|
123
|
-
use: "sig",
|
|
124
|
-
alg: "RS256"
|
|
125
|
-
}
|
|
126
|
-
]
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function generateRandomString(length = 32) {
|
|
130
|
-
return crypto.randomBytes(length).toString("hex").substring(0, length);
|
|
131
|
-
}
|
|
132
|
-
function generateUUID() {
|
|
133
|
-
return crypto.randomUUID();
|
|
134
|
-
}
|
|
135
|
-
function verifyCodeChallenge(codeVerifier, codeChallenge) {
|
|
136
|
-
const hash = crypto.createHash("sha256").update(codeVerifier).digest();
|
|
137
|
-
const computed = base64UrlEncode(hash);
|
|
138
|
-
return computed === codeChallenge;
|
|
139
|
-
}
|
|
140
|
-
// packages/esv/src/server/server.ts
|
|
141
|
-
import { createServer } from "node:http";
|
|
142
|
-
|
|
143
|
-
// packages/esv/src/server/state.ts
|
|
144
|
-
var users = new Map;
|
|
145
|
-
var groups = new Map;
|
|
146
|
-
var authCodes = new Map;
|
|
147
|
-
var refreshTokens = new Map;
|
|
148
|
-
var sessions = new Map;
|
|
149
|
-
var defaultTestUser = {
|
|
150
|
-
id: "test-user-001",
|
|
151
|
-
userName: "testuser@example.com",
|
|
152
|
-
email: "testuser@example.com",
|
|
153
|
-
name: "Test User",
|
|
154
|
-
givenName: "Test",
|
|
155
|
-
familyName: "User"
|
|
156
|
-
};
|
|
157
|
-
function getUsers() {
|
|
158
|
-
return Array.from(users.values());
|
|
159
|
-
}
|
|
160
|
-
function getUser(id) {
|
|
161
|
-
return users.get(id);
|
|
162
|
-
}
|
|
163
|
-
function createUser(user) {
|
|
164
|
-
const now = new Date().toISOString();
|
|
165
|
-
user.meta = {
|
|
166
|
-
resourceType: "User",
|
|
167
|
-
created: now,
|
|
168
|
-
lastModified: now,
|
|
169
|
-
location: `/Users/${user.id}`
|
|
170
|
-
};
|
|
171
|
-
users.set(user.id, user);
|
|
172
|
-
return user;
|
|
173
|
-
}
|
|
174
|
-
function updateUser(id, updates) {
|
|
175
|
-
const user = users.get(id);
|
|
176
|
-
if (!user)
|
|
177
|
-
return;
|
|
178
|
-
const updated = { ...user, ...updates };
|
|
179
|
-
if (updated.meta) {
|
|
180
|
-
updated.meta.lastModified = new Date().toISOString();
|
|
181
|
-
}
|
|
182
|
-
users.set(id, updated);
|
|
183
|
-
return updated;
|
|
184
|
-
}
|
|
185
|
-
function deleteUser(id) {
|
|
186
|
-
return users.delete(id);
|
|
187
|
-
}
|
|
188
|
-
function getGroups() {
|
|
189
|
-
return Array.from(groups.values());
|
|
190
|
-
}
|
|
191
|
-
function getGroup(id) {
|
|
192
|
-
return groups.get(id);
|
|
193
|
-
}
|
|
194
|
-
function createGroup(group) {
|
|
195
|
-
const now = new Date().toISOString();
|
|
196
|
-
group.meta = {
|
|
197
|
-
resourceType: "Group",
|
|
198
|
-
created: now,
|
|
199
|
-
lastModified: now,
|
|
200
|
-
location: `/Groups/${group.id}`
|
|
201
|
-
};
|
|
202
|
-
groups.set(group.id, group);
|
|
203
|
-
return group;
|
|
204
|
-
}
|
|
205
|
-
function updateGroup(id, updates) {
|
|
206
|
-
const group = groups.get(id);
|
|
207
|
-
if (!group)
|
|
208
|
-
return;
|
|
209
|
-
const updated = { ...group, ...updates };
|
|
210
|
-
if (updated.meta) {
|
|
211
|
-
updated.meta.lastModified = new Date().toISOString();
|
|
212
|
-
}
|
|
213
|
-
groups.set(id, updated);
|
|
214
|
-
return updated;
|
|
215
|
-
}
|
|
216
|
-
function deleteGroup(id) {
|
|
217
|
-
return groups.delete(id);
|
|
218
|
-
}
|
|
219
|
-
function storeAuthCode(authCode) {
|
|
220
|
-
authCodes.set(authCode.code, authCode);
|
|
221
|
-
}
|
|
222
|
-
function getAuthCode(code) {
|
|
223
|
-
return authCodes.get(code);
|
|
224
|
-
}
|
|
225
|
-
function deleteAuthCode(code) {
|
|
226
|
-
return authCodes.delete(code);
|
|
227
|
-
}
|
|
228
|
-
function storeRefreshToken(refreshToken) {
|
|
229
|
-
refreshTokens.set(refreshToken.token, refreshToken);
|
|
230
|
-
}
|
|
231
|
-
function getRefreshToken(token) {
|
|
232
|
-
return refreshTokens.get(token);
|
|
233
|
-
}
|
|
234
|
-
function deleteRefreshToken(token) {
|
|
235
|
-
return refreshTokens.delete(token);
|
|
236
|
-
}
|
|
237
|
-
function deleteRefreshTokensBySession(sessionId) {
|
|
238
|
-
for (const [token, data] of refreshTokens.entries()) {
|
|
239
|
-
if (data.sessionId === sessionId) {
|
|
240
|
-
refreshTokens.delete(token);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
function createSession(session) {
|
|
245
|
-
sessions.set(session.id, session);
|
|
246
|
-
}
|
|
247
|
-
function deleteSession(id) {
|
|
248
|
-
deleteRefreshTokensBySession(id);
|
|
249
|
-
return sessions.delete(id);
|
|
250
|
-
}
|
|
251
|
-
function getTestUser() {
|
|
252
|
-
return defaultTestUser;
|
|
253
|
-
}
|
|
254
|
-
function setTestUser(user) {
|
|
255
|
-
defaultTestUser = user;
|
|
256
|
-
}
|
|
257
|
-
function resetState() {
|
|
258
|
-
users.clear();
|
|
259
|
-
groups.clear();
|
|
260
|
-
authCodes.clear();
|
|
261
|
-
refreshTokens.clear();
|
|
262
|
-
sessions.clear();
|
|
263
|
-
defaultTestUser = {
|
|
264
|
-
id: "test-user-001",
|
|
265
|
-
userName: "testuser@example.com",
|
|
266
|
-
email: "testuser@example.com",
|
|
267
|
-
name: "Test User",
|
|
268
|
-
givenName: "Test",
|
|
269
|
-
familyName: "User"
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// packages/esv/src/server/iam.ts
|
|
274
|
-
var SCIM_CONTENT_TYPE = "application/scim+json";
|
|
275
|
-
async function parseJsonBody(req) {
|
|
276
|
-
return new Promise((resolve, reject) => {
|
|
277
|
-
let body = "";
|
|
278
|
-
req.on("data", (chunk) => {
|
|
279
|
-
body += chunk.toString();
|
|
280
|
-
});
|
|
281
|
-
req.on("end", () => {
|
|
282
|
-
try {
|
|
283
|
-
resolve(body ? JSON.parse(body) : {});
|
|
284
|
-
} catch (error) {
|
|
285
|
-
reject(error);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
req.on("error", reject);
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
function validateAuth(req, res) {
|
|
292
|
-
const auth = req.headers.authorization;
|
|
293
|
-
if (!auth || !auth.startsWith("Bearer ")) {
|
|
294
|
-
res.writeHead(401, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
295
|
-
res.end(JSON.stringify({
|
|
296
|
-
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
297
|
-
status: "401",
|
|
298
|
-
detail: "Authorization required"
|
|
299
|
-
}));
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
function sendScimError(res, status, detail, scimType) {
|
|
305
|
-
res.writeHead(status, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
306
|
-
res.end(JSON.stringify({
|
|
307
|
-
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
308
|
-
status: String(status),
|
|
309
|
-
scimType,
|
|
310
|
-
detail
|
|
311
|
-
}));
|
|
312
|
-
}
|
|
313
|
-
function sendListResponse(res, resources, totalResults) {
|
|
314
|
-
res.writeHead(200, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
315
|
-
res.end(JSON.stringify({
|
|
316
|
-
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
317
|
-
totalResults: totalResults ?? resources.length,
|
|
318
|
-
startIndex: 1,
|
|
319
|
-
itemsPerPage: resources.length,
|
|
320
|
-
Resources: resources
|
|
321
|
-
}));
|
|
322
|
-
}
|
|
323
|
-
async function handleIamRequest(req, res, pathname) {
|
|
324
|
-
const iamPath = pathname.replace(/^\/iam/, "");
|
|
325
|
-
if (!validateAuth(req, res)) {
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const usersMatch = iamPath.match(/^\/Users(?:\/([^/]+))?$/);
|
|
329
|
-
const groupsMatch = iamPath.match(/^\/Groups(?:\/([^/]+))?$/);
|
|
330
|
-
if (usersMatch) {
|
|
331
|
-
const userId = usersMatch[1];
|
|
332
|
-
await handleUsersRequest(req, res, userId);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
if (groupsMatch) {
|
|
336
|
-
const groupId = groupsMatch[1];
|
|
337
|
-
await handleGroupsRequest(req, res, groupId);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
sendScimError(res, 404, "Resource not found");
|
|
341
|
-
}
|
|
342
|
-
async function handleUsersRequest(req, res, userId) {
|
|
343
|
-
if (userId) {
|
|
344
|
-
switch (req.method) {
|
|
345
|
-
case "GET":
|
|
346
|
-
handleGetUser(res, userId);
|
|
347
|
-
break;
|
|
348
|
-
case "PUT":
|
|
349
|
-
await handleReplaceUser(req, res, userId);
|
|
350
|
-
break;
|
|
351
|
-
case "PATCH":
|
|
352
|
-
await handlePatchUser(req, res, userId);
|
|
353
|
-
break;
|
|
354
|
-
case "DELETE":
|
|
355
|
-
handleDeleteUser(res, userId);
|
|
356
|
-
break;
|
|
357
|
-
default:
|
|
358
|
-
sendScimError(res, 405, "Method not allowed");
|
|
359
|
-
}
|
|
360
|
-
} else {
|
|
361
|
-
switch (req.method) {
|
|
362
|
-
case "GET":
|
|
363
|
-
handleListUsers(res);
|
|
364
|
-
break;
|
|
365
|
-
case "POST":
|
|
366
|
-
await handleCreateUser(req, res);
|
|
367
|
-
break;
|
|
368
|
-
default:
|
|
369
|
-
sendScimError(res, 405, "Method not allowed");
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
async function handleGroupsRequest(req, res, groupId) {
|
|
374
|
-
if (groupId) {
|
|
375
|
-
switch (req.method) {
|
|
376
|
-
case "GET":
|
|
377
|
-
handleGetGroup(res, groupId);
|
|
378
|
-
break;
|
|
379
|
-
case "PUT":
|
|
380
|
-
await handleReplaceGroup(req, res, groupId);
|
|
381
|
-
break;
|
|
382
|
-
case "PATCH":
|
|
383
|
-
await handlePatchGroup(req, res, groupId);
|
|
384
|
-
break;
|
|
385
|
-
case "DELETE":
|
|
386
|
-
handleDeleteGroup(res, groupId);
|
|
387
|
-
break;
|
|
388
|
-
default:
|
|
389
|
-
sendScimError(res, 405, "Method not allowed");
|
|
390
|
-
}
|
|
391
|
-
} else {
|
|
392
|
-
switch (req.method) {
|
|
393
|
-
case "GET":
|
|
394
|
-
handleListGroups(res);
|
|
395
|
-
break;
|
|
396
|
-
case "POST":
|
|
397
|
-
await handleCreateGroup(req, res);
|
|
398
|
-
break;
|
|
399
|
-
default:
|
|
400
|
-
sendScimError(res, 405, "Method not allowed");
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
function handleListUsers(res) {
|
|
405
|
-
const users2 = getUsers();
|
|
406
|
-
sendListResponse(res, users2);
|
|
407
|
-
}
|
|
408
|
-
function handleGetUser(res, id) {
|
|
409
|
-
const user = getUser(id);
|
|
410
|
-
if (!user) {
|
|
411
|
-
sendScimError(res, 404, `User ${id} not found`, "invalidValue");
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
res.writeHead(200, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
415
|
-
res.end(JSON.stringify(user));
|
|
416
|
-
}
|
|
417
|
-
async function handleCreateUser(req, res) {
|
|
418
|
-
try {
|
|
419
|
-
const body = await parseJsonBody(req);
|
|
420
|
-
if (!body.userName) {
|
|
421
|
-
sendScimError(res, 400, "userName is required", "invalidValue");
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
const user = {
|
|
425
|
-
id: generateUUID(),
|
|
426
|
-
schemas: body.schemas || [
|
|
427
|
-
"urn:ietf:params:scim:schemas:core:2.0:User",
|
|
428
|
-
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
|
|
429
|
-
],
|
|
430
|
-
userName: body.userName,
|
|
431
|
-
displayName: body.displayName,
|
|
432
|
-
name: body.name,
|
|
433
|
-
emails: body.emails,
|
|
434
|
-
active: body.active ?? true,
|
|
435
|
-
externalId: body.externalId,
|
|
436
|
-
groups: [],
|
|
437
|
-
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": body["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"]
|
|
438
|
-
};
|
|
439
|
-
const created = createUser(user);
|
|
440
|
-
res.writeHead(201, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
441
|
-
res.end(JSON.stringify(created));
|
|
442
|
-
} catch (_error) {
|
|
443
|
-
sendScimError(res, 400, "Invalid request body");
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
async function handleReplaceUser(req, res, id) {
|
|
447
|
-
const existing = getUser(id);
|
|
448
|
-
if (!existing) {
|
|
449
|
-
sendScimError(res, 404, `User ${id} not found`, "invalidValue");
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
try {
|
|
453
|
-
const body = await parseJsonBody(req);
|
|
454
|
-
const updated = updateUser(id, { ...body, id });
|
|
455
|
-
if (updated) {
|
|
456
|
-
res.writeHead(200, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
457
|
-
res.end(JSON.stringify(updated));
|
|
458
|
-
} else {
|
|
459
|
-
sendScimError(res, 404, `User ${id} not found`);
|
|
460
|
-
}
|
|
461
|
-
} catch (_error) {
|
|
462
|
-
sendScimError(res, 400, "Invalid request body");
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
async function handlePatchUser(req, res, id) {
|
|
466
|
-
const existing = getUser(id);
|
|
467
|
-
if (!existing) {
|
|
468
|
-
sendScimError(res, 404, `User ${id} not found`, "invalidValue");
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
try {
|
|
472
|
-
const body = await parseJsonBody(req);
|
|
473
|
-
const operations = body.Operations || [];
|
|
474
|
-
const updated = { ...existing };
|
|
475
|
-
for (const op of operations) {
|
|
476
|
-
if (op.op === "replace" && op.path && op.value !== undefined) {
|
|
477
|
-
if (op.path === "displayName") {
|
|
478
|
-
updated.displayName = op.value;
|
|
479
|
-
} else if (op.path === "active") {
|
|
480
|
-
updated.active = op.value;
|
|
481
|
-
} else if (op.path.startsWith("name.")) {
|
|
482
|
-
const namePart = op.path.split(".")[1];
|
|
483
|
-
updated.name = { ...updated.name, [namePart]: op.value };
|
|
484
|
-
}
|
|
485
|
-
} else if (op.op === "add" && op.path && op.value !== undefined) {
|
|
486
|
-
if (op.path === "emails") {
|
|
487
|
-
updated.emails = [...updated.emails || [], ...op.value || []];
|
|
488
|
-
}
|
|
489
|
-
} else if (op.op === "remove" && op.path) {
|
|
490
|
-
if (op.path === "displayName") {
|
|
491
|
-
updated.displayName = undefined;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
const result = updateUser(id, updated);
|
|
496
|
-
if (result) {
|
|
497
|
-
res.writeHead(200, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
498
|
-
res.end(JSON.stringify(result));
|
|
499
|
-
} else {
|
|
500
|
-
sendScimError(res, 404, `User ${id} not found`);
|
|
501
|
-
}
|
|
502
|
-
} catch (_error) {
|
|
503
|
-
sendScimError(res, 400, "Invalid request body");
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function handleDeleteUser(res, id) {
|
|
507
|
-
const deleted = deleteUser(id);
|
|
508
|
-
if (!deleted) {
|
|
509
|
-
sendScimError(res, 404, `User ${id} not found`, "invalidValue");
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
res.writeHead(204);
|
|
513
|
-
res.end();
|
|
514
|
-
}
|
|
515
|
-
function handleListGroups(res) {
|
|
516
|
-
const groups2 = getGroups();
|
|
517
|
-
sendListResponse(res, groups2);
|
|
518
|
-
}
|
|
519
|
-
function handleGetGroup(res, id) {
|
|
520
|
-
const group = getGroup(id);
|
|
521
|
-
if (!group) {
|
|
522
|
-
sendScimError(res, 404, `Group ${id} not found`, "invalidValue");
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
res.writeHead(200, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
526
|
-
res.end(JSON.stringify(group));
|
|
527
|
-
}
|
|
528
|
-
async function handleCreateGroup(req, res) {
|
|
529
|
-
try {
|
|
530
|
-
const body = await parseJsonBody(req);
|
|
531
|
-
if (!body.displayName) {
|
|
532
|
-
sendScimError(res, 400, "displayName is required", "invalidValue");
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
const group = {
|
|
536
|
-
id: generateUUID(),
|
|
537
|
-
schemas: body.schemas || ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
|
538
|
-
displayName: body.displayName,
|
|
539
|
-
externalId: body.externalId,
|
|
540
|
-
members: body.members || []
|
|
541
|
-
};
|
|
542
|
-
const created = createGroup(group);
|
|
543
|
-
res.writeHead(201, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
544
|
-
res.end(JSON.stringify(created));
|
|
545
|
-
} catch (_error) {
|
|
546
|
-
sendScimError(res, 400, "Invalid request body");
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
async function handleReplaceGroup(req, res, id) {
|
|
550
|
-
const existing = getGroup(id);
|
|
551
|
-
if (!existing) {
|
|
552
|
-
sendScimError(res, 404, `Group ${id} not found`, "invalidValue");
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
try {
|
|
556
|
-
const body = await parseJsonBody(req);
|
|
557
|
-
const updated = updateGroup(id, { ...body, id });
|
|
558
|
-
if (updated) {
|
|
559
|
-
res.writeHead(200, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
560
|
-
res.end(JSON.stringify(updated));
|
|
561
|
-
} else {
|
|
562
|
-
sendScimError(res, 404, `Group ${id} not found`);
|
|
563
|
-
}
|
|
564
|
-
} catch (_error) {
|
|
565
|
-
sendScimError(res, 400, "Invalid request body");
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
async function handlePatchGroup(req, res, id) {
|
|
569
|
-
const existing = getGroup(id);
|
|
570
|
-
if (!existing) {
|
|
571
|
-
sendScimError(res, 404, `Group ${id} not found`, "invalidValue");
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
try {
|
|
575
|
-
const body = await parseJsonBody(req);
|
|
576
|
-
const operations = body.Operations || [];
|
|
577
|
-
const updated = { ...existing };
|
|
578
|
-
for (const op of operations) {
|
|
579
|
-
if (op.op === "replace" && op.path && op.value !== undefined) {
|
|
580
|
-
if (op.path === "displayName") {
|
|
581
|
-
updated.displayName = op.value;
|
|
582
|
-
}
|
|
583
|
-
} else if (op.op === "add" && op.path && op.value !== undefined) {
|
|
584
|
-
if (op.path === "members") {
|
|
585
|
-
updated.members = [...updated.members || [], ...op.value || []];
|
|
586
|
-
}
|
|
587
|
-
} else if (op.op === "remove" && op.path) {
|
|
588
|
-
if (op.path.startsWith("members[")) {
|
|
589
|
-
const match = op.path.match(/members\[value eq "([^"]+)"\]/);
|
|
590
|
-
if (match) {
|
|
591
|
-
updated.members = (updated.members || []).filter((m) => m.value !== match[1]);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
const result = updateGroup(id, updated);
|
|
597
|
-
if (result) {
|
|
598
|
-
res.writeHead(200, { "Content-Type": SCIM_CONTENT_TYPE });
|
|
599
|
-
res.end(JSON.stringify(result));
|
|
600
|
-
} else {
|
|
601
|
-
sendScimError(res, 404, `Group ${id} not found`);
|
|
602
|
-
}
|
|
603
|
-
} catch (_error) {
|
|
604
|
-
sendScimError(res, 400, "Invalid request body");
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
function handleDeleteGroup(res, id) {
|
|
608
|
-
const deleted = deleteGroup(id);
|
|
609
|
-
if (!deleted) {
|
|
610
|
-
sendScimError(res, 404, `Group ${id} not found`, "invalidValue");
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
res.writeHead(204);
|
|
614
|
-
res.end();
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// packages/esv/src/server/sso.ts
|
|
618
|
-
var ISSUER = "http://localhost:3555/sso";
|
|
619
|
-
var TOKEN_EXPIRY = 3600;
|
|
620
|
-
var REFRESH_EXPIRY = 86400;
|
|
621
|
-
var CODE_EXPIRY = 300;
|
|
622
|
-
async function parseFormBody(req) {
|
|
623
|
-
return new Promise((resolve, reject) => {
|
|
624
|
-
let body = "";
|
|
625
|
-
req.on("data", (chunk) => {
|
|
626
|
-
body += chunk.toString();
|
|
627
|
-
});
|
|
628
|
-
req.on("end", () => {
|
|
629
|
-
resolve(new URLSearchParams(body));
|
|
630
|
-
});
|
|
631
|
-
req.on("error", reject);
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
async function handleSsoRequest(req, res, pathname) {
|
|
635
|
-
const ssoPath = pathname.replace(/^\/sso/, "");
|
|
636
|
-
if (req.method === "GET" && ssoPath === "/authorize") {
|
|
637
|
-
await handleAuthorize(req, res);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
if (req.method === "POST" && ssoPath === "/token") {
|
|
641
|
-
await handleToken(req, res);
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
if (req.method === "GET" && ssoPath === "/certs") {
|
|
645
|
-
handleCerts(res);
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
if (req.method === "POST" && ssoPath === "/revoke") {
|
|
649
|
-
await handleRevoke(req, res);
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
if (req.method === "GET" && ssoPath === "/userinfo") {
|
|
653
|
-
handleUserInfo(req, res);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
if (req.method === "GET" && ssoPath === "/logout") {
|
|
657
|
-
handleLogout(req, res);
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
if (req.method === "POST" && ssoPath === "/logout/backchannel") {
|
|
661
|
-
await handleBackChannelLogout(req, res);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
if (req.method === "GET" && ssoPath === "/.well-known/openid-configuration") {
|
|
665
|
-
handleOpenIDConfig(res);
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
669
|
-
res.end(JSON.stringify({ error: "not_found" }));
|
|
670
|
-
}
|
|
671
|
-
async function handleAuthorize(req, res) {
|
|
672
|
-
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
673
|
-
const params = url.searchParams;
|
|
674
|
-
const clientId = params.get("client_id");
|
|
675
|
-
const redirectUri = params.get("redirect_uri");
|
|
676
|
-
const responseType = params.get("response_type");
|
|
677
|
-
const scope = params.get("scope") || "openid";
|
|
678
|
-
const state = params.get("state");
|
|
679
|
-
const codeChallenge = params.get("code_challenge");
|
|
680
|
-
const codeChallengeMethod = params.get("code_challenge_method");
|
|
681
|
-
if (!clientId || !redirectUri || !responseType) {
|
|
682
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
683
|
-
res.end(JSON.stringify({
|
|
684
|
-
error: "invalid_request",
|
|
685
|
-
error_description: "Missing required parameters"
|
|
686
|
-
}));
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
if (responseType !== "code") {
|
|
690
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
691
|
-
res.end(JSON.stringify({
|
|
692
|
-
error: "unsupported_response_type",
|
|
693
|
-
error_description: "Only code response type is supported"
|
|
694
|
-
}));
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
const testUser = getTestUser();
|
|
698
|
-
const code = generateRandomString(32);
|
|
699
|
-
const authCode = {
|
|
700
|
-
code,
|
|
701
|
-
userId: testUser.id,
|
|
702
|
-
clientId,
|
|
703
|
-
redirectUri,
|
|
704
|
-
scope,
|
|
705
|
-
codeChallenge: codeChallenge || undefined,
|
|
706
|
-
codeChallengeMethod: codeChallengeMethod || undefined,
|
|
707
|
-
state: state || undefined,
|
|
708
|
-
createdAt: new Date,
|
|
709
|
-
expiresAt: new Date(Date.now() + CODE_EXPIRY * 1000)
|
|
710
|
-
};
|
|
711
|
-
storeAuthCode(authCode);
|
|
712
|
-
const redirectUrl = new URL(redirectUri);
|
|
713
|
-
redirectUrl.searchParams.set("code", code);
|
|
714
|
-
if (state) {
|
|
715
|
-
redirectUrl.searchParams.set("state", state);
|
|
716
|
-
}
|
|
717
|
-
res.writeHead(302, { Location: redirectUrl.toString() });
|
|
718
|
-
res.end();
|
|
719
|
-
}
|
|
720
|
-
async function handleToken(req, res) {
|
|
721
|
-
const body = await parseFormBody(req);
|
|
722
|
-
const grantType = body.get("grant_type");
|
|
723
|
-
if (grantType === "authorization_code") {
|
|
724
|
-
await handleAuthorizationCodeGrant(body, res);
|
|
725
|
-
} else if (grantType === "refresh_token") {
|
|
726
|
-
await handleRefreshTokenGrant(body, res);
|
|
727
|
-
} else {
|
|
728
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
729
|
-
res.end(JSON.stringify({
|
|
730
|
-
error: "unsupported_grant_type",
|
|
731
|
-
error_description: "Only authorization_code and refresh_token are supported"
|
|
732
|
-
}));
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
async function handleAuthorizationCodeGrant(body, res) {
|
|
736
|
-
const code = body.get("code");
|
|
737
|
-
const redirectUri = body.get("redirect_uri");
|
|
738
|
-
const codeVerifier = body.get("code_verifier");
|
|
739
|
-
if (!code || !redirectUri) {
|
|
740
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
741
|
-
res.end(JSON.stringify({
|
|
742
|
-
error: "invalid_request",
|
|
743
|
-
error_description: "Missing code or redirect_uri"
|
|
744
|
-
}));
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
const authCode = getAuthCode(code);
|
|
748
|
-
if (!authCode) {
|
|
749
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
750
|
-
res.end(JSON.stringify({
|
|
751
|
-
error: "invalid_grant",
|
|
752
|
-
error_description: "Invalid or expired authorization code"
|
|
753
|
-
}));
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
if (authCode.redirectUri !== redirectUri) {
|
|
757
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
758
|
-
res.end(JSON.stringify({
|
|
759
|
-
error: "invalid_grant",
|
|
760
|
-
error_description: "Redirect URI mismatch"
|
|
761
|
-
}));
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
if (authCode.codeChallenge && authCode.codeChallengeMethod === "S256") {
|
|
765
|
-
if (!codeVerifier || !verifyCodeChallenge(codeVerifier, authCode.codeChallenge)) {
|
|
766
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
767
|
-
res.end(JSON.stringify({
|
|
768
|
-
error: "invalid_grant",
|
|
769
|
-
error_description: "Invalid code verifier"
|
|
770
|
-
}));
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
deleteAuthCode(code);
|
|
775
|
-
const sessionId = generateUUID();
|
|
776
|
-
createSession({
|
|
777
|
-
id: sessionId,
|
|
778
|
-
userId: authCode.userId,
|
|
779
|
-
createdAt: new Date,
|
|
780
|
-
lastActivityAt: new Date
|
|
781
|
-
});
|
|
782
|
-
const testUser = getTestUser();
|
|
783
|
-
const tokens = generateTokens(testUser, authCode.clientId, authCode.scope, sessionId);
|
|
784
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
785
|
-
res.end(JSON.stringify(tokens));
|
|
786
|
-
}
|
|
787
|
-
async function handleRefreshTokenGrant(body, res) {
|
|
788
|
-
const refreshToken = body.get("refresh_token");
|
|
789
|
-
if (!refreshToken) {
|
|
790
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
791
|
-
res.end(JSON.stringify({
|
|
792
|
-
error: "invalid_request",
|
|
793
|
-
error_description: "Missing refresh_token"
|
|
794
|
-
}));
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
const tokenData = getRefreshToken(refreshToken);
|
|
798
|
-
if (!tokenData || tokenData.expiresAt < new Date) {
|
|
799
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
800
|
-
res.end(JSON.stringify({
|
|
801
|
-
error: "invalid_grant",
|
|
802
|
-
error_description: "Invalid or expired refresh token"
|
|
803
|
-
}));
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
deleteRefreshToken(refreshToken);
|
|
807
|
-
const testUser = getTestUser();
|
|
808
|
-
const tokens = generateTokens(testUser, tokenData.clientId, tokenData.scope, tokenData.sessionId);
|
|
809
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
810
|
-
res.end(JSON.stringify(tokens));
|
|
811
|
-
}
|
|
812
|
-
function generateTokens(user, clientId, scope, sessionId) {
|
|
813
|
-
const now = Math.floor(Date.now() / 1000);
|
|
814
|
-
const idTokenClaims = {
|
|
815
|
-
iss: ISSUER,
|
|
816
|
-
sub: user.id,
|
|
817
|
-
aud: clientId,
|
|
818
|
-
nonce: generateRandomString(16),
|
|
819
|
-
sid: sessionId,
|
|
820
|
-
auth_time: now,
|
|
821
|
-
email: user.email,
|
|
822
|
-
email_verified: true,
|
|
823
|
-
name: user.name,
|
|
824
|
-
given_name: user.givenName,
|
|
825
|
-
family_name: user.familyName,
|
|
826
|
-
preferred_username: user.userName,
|
|
827
|
-
picture: user.picture
|
|
828
|
-
};
|
|
829
|
-
const accessTokenClaims = {
|
|
830
|
-
iss: ISSUER,
|
|
831
|
-
sub: user.id,
|
|
832
|
-
aud: clientId,
|
|
833
|
-
scope,
|
|
834
|
-
sid: sessionId
|
|
835
|
-
};
|
|
836
|
-
const idToken = signJwt(idTokenClaims, TOKEN_EXPIRY);
|
|
837
|
-
const accessToken = signJwt(accessTokenClaims, TOKEN_EXPIRY);
|
|
838
|
-
const refreshTokenValue = generateRandomString(64);
|
|
839
|
-
const refreshToken = {
|
|
840
|
-
token: refreshTokenValue,
|
|
841
|
-
userId: user.id,
|
|
842
|
-
clientId,
|
|
843
|
-
scope,
|
|
844
|
-
sessionId,
|
|
845
|
-
createdAt: new Date,
|
|
846
|
-
expiresAt: new Date(Date.now() + REFRESH_EXPIRY * 1000)
|
|
847
|
-
};
|
|
848
|
-
storeRefreshToken(refreshToken);
|
|
849
|
-
return {
|
|
850
|
-
access_token: accessToken,
|
|
851
|
-
token_type: "Bearer",
|
|
852
|
-
expires_in: TOKEN_EXPIRY,
|
|
853
|
-
refresh_token: refreshTokenValue,
|
|
854
|
-
refresh_expires_in: REFRESH_EXPIRY,
|
|
855
|
-
id_token: idToken,
|
|
856
|
-
scope,
|
|
857
|
-
session_state: sessionId
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
function handleCerts(res) {
|
|
861
|
-
const jwks = getJwks();
|
|
862
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
863
|
-
res.end(JSON.stringify(jwks));
|
|
864
|
-
}
|
|
865
|
-
async function handleRevoke(req, res) {
|
|
866
|
-
const body = await parseFormBody(req);
|
|
867
|
-
const token = body.get("token");
|
|
868
|
-
const tokenTypeHint = body.get("token_type_hint");
|
|
869
|
-
if (token) {
|
|
870
|
-
if (tokenTypeHint === "refresh_token" || !tokenTypeHint) {
|
|
871
|
-
deleteRefreshToken(token);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
875
|
-
res.end(JSON.stringify({ success: true }));
|
|
876
|
-
}
|
|
877
|
-
function handleUserInfo(req, res) {
|
|
878
|
-
const authHeader = req.headers.authorization;
|
|
879
|
-
if (!authHeader?.startsWith("Bearer ")) {
|
|
880
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
881
|
-
res.end(JSON.stringify({ error: "invalid_token" }));
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
const user = getTestUser();
|
|
885
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
886
|
-
res.end(JSON.stringify({
|
|
887
|
-
sub: user.id,
|
|
888
|
-
email: user.email,
|
|
889
|
-
email_verified: true,
|
|
890
|
-
name: user.name,
|
|
891
|
-
given_name: user.givenName,
|
|
892
|
-
family_name: user.familyName,
|
|
893
|
-
preferred_username: user.userName,
|
|
894
|
-
picture: user.picture
|
|
895
|
-
}));
|
|
896
|
-
}
|
|
897
|
-
function handleLogout(req, res) {
|
|
898
|
-
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
899
|
-
const postLogoutRedirectUri = url.searchParams.get("post_logout_redirect_uri");
|
|
900
|
-
if (postLogoutRedirectUri) {
|
|
901
|
-
res.writeHead(302, { Location: postLogoutRedirectUri });
|
|
902
|
-
res.end();
|
|
903
|
-
} else {
|
|
904
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
905
|
-
res.end("<html><body><h1>Logged out</h1></body></html>");
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
async function handleBackChannelLogout(req, res) {
|
|
909
|
-
const body = await parseFormBody(req);
|
|
910
|
-
const logoutToken = body.get("logout_token");
|
|
911
|
-
if (!logoutToken) {
|
|
912
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
913
|
-
res.end(JSON.stringify({ error: "invalid_request", error_description: "Missing logout_token" }));
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
try {
|
|
917
|
-
const parts = logoutToken.split(".");
|
|
918
|
-
if (parts.length >= 2) {
|
|
919
|
-
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
920
|
-
if (payload.sid) {
|
|
921
|
-
deleteSession(payload.sid);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
} catch {}
|
|
925
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
926
|
-
res.end(JSON.stringify({ success: true }));
|
|
927
|
-
}
|
|
928
|
-
function handleOpenIDConfig(res) {
|
|
929
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
930
|
-
res.end(JSON.stringify({
|
|
931
|
-
issuer: ISSUER,
|
|
932
|
-
authorization_endpoint: `${ISSUER}/authorize`,
|
|
933
|
-
token_endpoint: `${ISSUER}/token`,
|
|
934
|
-
userinfo_endpoint: `${ISSUER}/userinfo`,
|
|
935
|
-
jwks_uri: `${ISSUER}/certs`,
|
|
936
|
-
end_session_endpoint: `${ISSUER}/logout`,
|
|
937
|
-
revocation_endpoint: `${ISSUER}/revoke`,
|
|
938
|
-
response_types_supported: ["code"],
|
|
939
|
-
subject_types_supported: ["public"],
|
|
940
|
-
id_token_signing_alg_values_supported: ["RS256"],
|
|
941
|
-
scopes_supported: ["openid", "profile", "email"],
|
|
942
|
-
token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
|
|
943
|
-
claims_supported: [
|
|
944
|
-
"sub",
|
|
945
|
-
"iss",
|
|
946
|
-
"aud",
|
|
947
|
-
"exp",
|
|
948
|
-
"iat",
|
|
949
|
-
"nonce",
|
|
950
|
-
"email",
|
|
951
|
-
"email_verified",
|
|
952
|
-
"name",
|
|
953
|
-
"given_name",
|
|
954
|
-
"family_name",
|
|
955
|
-
"preferred_username",
|
|
956
|
-
"picture",
|
|
957
|
-
"sid"
|
|
958
|
-
],
|
|
959
|
-
code_challenge_methods_supported: ["S256"],
|
|
960
|
-
backchannel_logout_supported: true,
|
|
961
|
-
backchannel_logout_session_supported: true
|
|
962
|
-
}));
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// packages/esv/src/server/vault.ts
|
|
966
|
-
function getEsvConfig(baseUrl = "http://localhost:3555") {
|
|
967
|
-
return {
|
|
968
|
-
sso: {
|
|
969
|
-
authority: `${baseUrl}/sso`,
|
|
970
|
-
token_url: `${baseUrl}/sso/token`,
|
|
971
|
-
authorization_url: `${baseUrl}/sso/authorize`,
|
|
972
|
-
jwks_uri: `${baseUrl}/sso/certs`,
|
|
973
|
-
client_id: "local-test-client",
|
|
974
|
-
client_secret: "local-test-secret",
|
|
975
|
-
redirect_uri: "http://localhost:3000/api/auth/callback",
|
|
976
|
-
scope: "openid profile email",
|
|
977
|
-
revocation_endpoint: `${baseUrl}/sso/revoke`,
|
|
978
|
-
end_session_endpoint: `${baseUrl}/sso/logout`
|
|
979
|
-
},
|
|
980
|
-
iam: {
|
|
981
|
-
url: `${baseUrl}/iam`
|
|
982
|
-
},
|
|
983
|
-
workload: {
|
|
984
|
-
token_url: `${baseUrl}/workload/token`,
|
|
985
|
-
jwks_uri: `${baseUrl}/workload/certs`,
|
|
986
|
-
client_id: "local-workload-client",
|
|
987
|
-
client_secret: "local-workload-secret",
|
|
988
|
-
issuer: `${baseUrl}/workload`,
|
|
989
|
-
audience: `${baseUrl}/workload`
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
function handleVaultRequest(req, res, pathname) {
|
|
994
|
-
const vaultPath = pathname.replace(/^\/vault/, "");
|
|
995
|
-
if (req.method === "GET" && vaultPath === "/v1/secret/data/esv/config") {
|
|
996
|
-
const token = req.headers["x-vault-token"];
|
|
997
|
-
if (token !== "local-esv-token") {
|
|
998
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
999
|
-
res.end(JSON.stringify({ errors: ["permission denied"] }));
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
const config = getEsvConfig();
|
|
1003
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1004
|
-
res.end(JSON.stringify({
|
|
1005
|
-
request_id: "local-esv-request",
|
|
1006
|
-
lease_id: "",
|
|
1007
|
-
renewable: false,
|
|
1008
|
-
lease_duration: 0,
|
|
1009
|
-
data: {
|
|
1010
|
-
data: config,
|
|
1011
|
-
metadata: {
|
|
1012
|
-
created_time: new Date().toISOString(),
|
|
1013
|
-
deletion_time: "",
|
|
1014
|
-
destroyed: false,
|
|
1015
|
-
version: 1
|
|
1016
|
-
}
|
|
1017
|
-
},
|
|
1018
|
-
wrap_info: null,
|
|
1019
|
-
warnings: null,
|
|
1020
|
-
auth: null
|
|
1021
|
-
}));
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
if (req.method === "GET" && vaultPath.startsWith("/v1/secret/data/")) {
|
|
1025
|
-
const token = req.headers["x-vault-token"];
|
|
1026
|
-
if (token !== "local-esv-token") {
|
|
1027
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1028
|
-
res.end(JSON.stringify({ errors: ["permission denied"] }));
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1032
|
-
res.end(JSON.stringify({ errors: ["secret not found"] }));
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1036
|
-
res.end(JSON.stringify({ errors: ["path not found"] }));
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
// packages/esv/src/server/workload.ts
|
|
1040
|
-
var ISSUER2 = "http://localhost:3555/workload";
|
|
1041
|
-
var TOKEN_EXPIRY2 = 3600;
|
|
1042
|
-
var VALID_CLIENTS = {
|
|
1043
|
-
"local-workload-client": "local-workload-secret"
|
|
1044
|
-
};
|
|
1045
|
-
async function parseFormBody2(req) {
|
|
1046
|
-
return new Promise((resolve, reject) => {
|
|
1047
|
-
let body = "";
|
|
1048
|
-
req.on("data", (chunk) => {
|
|
1049
|
-
body += chunk.toString();
|
|
1050
|
-
});
|
|
1051
|
-
req.on("end", () => {
|
|
1052
|
-
resolve(new URLSearchParams(body));
|
|
1053
|
-
});
|
|
1054
|
-
req.on("error", reject);
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
async function handleWorkloadRequest(req, res, pathname) {
|
|
1058
|
-
const workloadPath = pathname.replace(/^\/workload/, "");
|
|
1059
|
-
if (req.method === "POST" && workloadPath === "/token") {
|
|
1060
|
-
await handleToken2(req, res);
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
if (req.method === "GET" && workloadPath === "/certs") {
|
|
1064
|
-
handleCerts2(res);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
if (req.method === "POST" && workloadPath === "/validate") {
|
|
1068
|
-
handleValidate(req, res);
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1072
|
-
res.end(JSON.stringify({ error: "not_found" }));
|
|
1073
|
-
}
|
|
1074
|
-
async function handleToken2(req, res) {
|
|
1075
|
-
const body = await parseFormBody2(req);
|
|
1076
|
-
const grantType = body.get("grant_type");
|
|
1077
|
-
if (grantType === "client_credentials") {
|
|
1078
|
-
handleClientCredentials(body, res);
|
|
1079
|
-
} else if (grantType === "urn:ietf:params:oauth:grant-type:jwt-bearer") {
|
|
1080
|
-
handleJwtBearer(body, res);
|
|
1081
|
-
} else {
|
|
1082
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1083
|
-
res.end(JSON.stringify({
|
|
1084
|
-
error: "unsupported_grant_type",
|
|
1085
|
-
error_description: "Only client_credentials and jwt-bearer are supported"
|
|
1086
|
-
}));
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
function handleClientCredentials(body, res) {
|
|
1090
|
-
const clientId = body.get("client_id");
|
|
1091
|
-
const clientSecret = body.get("client_secret");
|
|
1092
|
-
const scope = body.get("scope") || "";
|
|
1093
|
-
if (!clientId || !clientSecret) {
|
|
1094
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1095
|
-
res.end(JSON.stringify({
|
|
1096
|
-
error: "invalid_request",
|
|
1097
|
-
error_description: "Missing client_id or client_secret"
|
|
1098
|
-
}));
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
const expectedSecret = VALID_CLIENTS[clientId];
|
|
1102
|
-
if (!expectedSecret || expectedSecret !== clientSecret) {
|
|
1103
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1104
|
-
res.end(JSON.stringify({
|
|
1105
|
-
error: "invalid_client",
|
|
1106
|
-
error_description: "Invalid client credentials"
|
|
1107
|
-
}));
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
const accessTokenClaims = {
|
|
1111
|
-
iss: ISSUER2,
|
|
1112
|
-
sub: clientId,
|
|
1113
|
-
aud: ISSUER2,
|
|
1114
|
-
client_id: clientId,
|
|
1115
|
-
scope
|
|
1116
|
-
};
|
|
1117
|
-
const accessToken = signJwt(accessTokenClaims, TOKEN_EXPIRY2);
|
|
1118
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1119
|
-
res.end(JSON.stringify({
|
|
1120
|
-
access_token: accessToken,
|
|
1121
|
-
token_type: "Bearer",
|
|
1122
|
-
expires_in: TOKEN_EXPIRY2,
|
|
1123
|
-
scope
|
|
1124
|
-
}));
|
|
1125
|
-
}
|
|
1126
|
-
function handleJwtBearer(body, res) {
|
|
1127
|
-
const assertion = body.get("assertion");
|
|
1128
|
-
const scope = body.get("scope") || "";
|
|
1129
|
-
if (!assertion) {
|
|
1130
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1131
|
-
res.end(JSON.stringify({
|
|
1132
|
-
error: "invalid_request",
|
|
1133
|
-
error_description: "Missing assertion"
|
|
1134
|
-
}));
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
const result = verifyJwt(assertion);
|
|
1138
|
-
if (!result.valid) {
|
|
1139
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1140
|
-
res.end(JSON.stringify({
|
|
1141
|
-
error: "invalid_grant",
|
|
1142
|
-
error_description: result.error || "Invalid JWT assertion"
|
|
1143
|
-
}));
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
const workloadId = result.payload?.sub;
|
|
1147
|
-
const accessTokenClaims = {
|
|
1148
|
-
iss: ISSUER2,
|
|
1149
|
-
sub: workloadId,
|
|
1150
|
-
aud: ISSUER2,
|
|
1151
|
-
workload_id: workloadId,
|
|
1152
|
-
scope: scope || result.payload?.scope || ""
|
|
1153
|
-
};
|
|
1154
|
-
const accessToken = signJwt(accessTokenClaims, TOKEN_EXPIRY2);
|
|
1155
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1156
|
-
res.end(JSON.stringify({
|
|
1157
|
-
access_token: accessToken,
|
|
1158
|
-
token_type: "Bearer",
|
|
1159
|
-
expires_in: TOKEN_EXPIRY2,
|
|
1160
|
-
scope: accessTokenClaims.scope
|
|
1161
|
-
}));
|
|
1162
|
-
}
|
|
1163
|
-
function handleCerts2(res) {
|
|
1164
|
-
const jwks = getJwks();
|
|
1165
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1166
|
-
res.end(JSON.stringify(jwks));
|
|
1167
|
-
}
|
|
1168
|
-
function handleValidate(req, res) {
|
|
1169
|
-
const authHeader = req.headers.authorization;
|
|
1170
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
1171
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1172
|
-
res.end(JSON.stringify({
|
|
1173
|
-
valid: false,
|
|
1174
|
-
error: "Missing Authorization header"
|
|
1175
|
-
}));
|
|
1176
|
-
return;
|
|
1177
|
-
}
|
|
1178
|
-
const token = authHeader.substring(7);
|
|
1179
|
-
const result = verifyJwt(token);
|
|
1180
|
-
if (!result.valid) {
|
|
1181
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1182
|
-
res.end(JSON.stringify({
|
|
1183
|
-
valid: false,
|
|
1184
|
-
error: result.error
|
|
1185
|
-
}));
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1189
|
-
res.end(JSON.stringify({
|
|
1190
|
-
valid: true,
|
|
1191
|
-
claims: result.payload,
|
|
1192
|
-
expiresAt: result.payload?.exp ? new Date(result.payload.exp * 1000).toISOString() : undefined
|
|
1193
|
-
}));
|
|
1194
|
-
}
|
|
1195
|
-
function handleWhoamiRequest(req, res) {
|
|
1196
|
-
const authHeader = req.headers.authorization;
|
|
1197
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
1198
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1199
|
-
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
const token = authHeader.substring(7);
|
|
1203
|
-
const result = verifyJwt(token);
|
|
1204
|
-
if (!result.valid) {
|
|
1205
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1206
|
-
res.end(JSON.stringify({ error: "Invalid token", details: result.error }));
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1210
|
-
res.end(JSON.stringify({
|
|
1211
|
-
user: null,
|
|
1212
|
-
workload: {
|
|
1213
|
-
workload_id: result.payload?.sub,
|
|
1214
|
-
client_id: result.payload?.client_id,
|
|
1215
|
-
scope: result.payload?.scope
|
|
1216
|
-
}
|
|
1217
|
-
}));
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// packages/esv/src/server/server.ts
|
|
1221
|
-
async function handleWebhook(req, res) {
|
|
1222
|
-
if (req.method !== "POST") {
|
|
1223
|
-
res.writeHead(405, { "Content-Type": "application/json" });
|
|
1224
|
-
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
1225
|
-
return;
|
|
1226
|
-
}
|
|
1227
|
-
let body = "";
|
|
1228
|
-
req.on("data", (chunk) => {
|
|
1229
|
-
body += chunk.toString();
|
|
1230
|
-
});
|
|
1231
|
-
req.on("end", () => {
|
|
1232
|
-
try {
|
|
1233
|
-
const payload = JSON.parse(body);
|
|
1234
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1235
|
-
res.end(JSON.stringify({ received: true, payload }));
|
|
1236
|
-
} catch (_error) {
|
|
1237
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1238
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
1239
|
-
}
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
var DEFAULT_PORT = 3555;
|
|
1243
|
-
var DEFAULT_HOST = "localhost";
|
|
1244
|
-
var server = null;
|
|
1245
|
-
var isStarted = false;
|
|
1246
|
-
async function handleRequest(req, res, verbose) {
|
|
1247
|
-
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
1248
|
-
const pathname = url.pathname;
|
|
1249
|
-
if (verbose) {
|
|
1250
|
-
console.log(`[ESV Server] ${req.method} ${pathname}`);
|
|
1251
|
-
}
|
|
1252
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1253
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
|
|
1254
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Vault-Token");
|
|
1255
|
-
if (req.method === "OPTIONS") {
|
|
1256
|
-
res.writeHead(204);
|
|
1257
|
-
res.end();
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
try {
|
|
1261
|
-
if (pathname.startsWith("/vault")) {
|
|
1262
|
-
handleVaultRequest(req, res, pathname);
|
|
1263
|
-
} else if (pathname.startsWith("/sso")) {
|
|
1264
|
-
await handleSsoRequest(req, res, pathname);
|
|
1265
|
-
} else if (pathname.startsWith("/iam")) {
|
|
1266
|
-
await handleIamRequest(req, res, pathname);
|
|
1267
|
-
} else if (pathname.startsWith("/workload")) {
|
|
1268
|
-
await handleWorkloadRequest(req, res, pathname);
|
|
1269
|
-
} else if (pathname === "/api/whoami") {
|
|
1270
|
-
handleWhoamiRequest(req, res);
|
|
1271
|
-
} else if (pathname === "/webhook") {
|
|
1272
|
-
handleWebhook(req, res);
|
|
1273
|
-
} else if (pathname === "/health" || pathname === "/") {
|
|
1274
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1275
|
-
res.end(JSON.stringify({ status: "ok", service: "esv-mock-server" }));
|
|
1276
|
-
} else {
|
|
1277
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1278
|
-
res.end(JSON.stringify({ error: "Not found" }));
|
|
1279
|
-
}
|
|
1280
|
-
} catch (error) {
|
|
1281
|
-
console.error("[ESV Server] Error handling request:", error);
|
|
1282
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1283
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
function startServer(options = {}) {
|
|
1287
|
-
const { port = DEFAULT_PORT, host = DEFAULT_HOST, verbose = false } = options;
|
|
1288
|
-
if (isStarted && server) {
|
|
1289
|
-
if (verbose) {
|
|
1290
|
-
console.log("[ESV Server] Server already running");
|
|
1291
|
-
}
|
|
1292
|
-
return Promise.resolve();
|
|
1293
|
-
}
|
|
1294
|
-
return new Promise((resolve, reject) => {
|
|
1295
|
-
try {
|
|
1296
|
-
initializeKeys();
|
|
1297
|
-
resetState();
|
|
1298
|
-
server = createServer((req, res) => {
|
|
1299
|
-
handleRequest(req, res, verbose).catch((error) => {
|
|
1300
|
-
console.error("[ESV Server] Unhandled error:", error);
|
|
1301
|
-
if (!res.headersSent) {
|
|
1302
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1303
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
1304
|
-
}
|
|
1305
|
-
});
|
|
1306
|
-
});
|
|
1307
|
-
server.on("error", (error) => {
|
|
1308
|
-
if (error.code === "EADDRINUSE") {
|
|
1309
|
-
if (verbose) {
|
|
1310
|
-
console.log(`[ESV Server] Port ${port} already in use, assuming server is running`);
|
|
1311
|
-
}
|
|
1312
|
-
isStarted = true;
|
|
1313
|
-
resolve();
|
|
1314
|
-
} else {
|
|
1315
|
-
reject(error);
|
|
1316
|
-
}
|
|
1317
|
-
});
|
|
1318
|
-
server.listen(port, host, () => {
|
|
1319
|
-
isStarted = true;
|
|
1320
|
-
console.log(`[ESV Server] Running at http://${host}:${port}/`);
|
|
1321
|
-
console.log("[ESV Server] Endpoints:");
|
|
1322
|
-
console.log(` - Vault: http://${host}:${port}/vault/v1/secret/data/esv/config`);
|
|
1323
|
-
console.log(` - SSO: http://${host}:${port}/sso/*`);
|
|
1324
|
-
console.log(` - IAM: http://${host}:${port}/iam/*`);
|
|
1325
|
-
console.log(` - Workload: http://${host}:${port}/workload/*`);
|
|
1326
|
-
console.log(` - Whoami: http://${host}:${port}/api/whoami`);
|
|
1327
|
-
console.log(` - Webhook: http://${host}:${port}/webhook`);
|
|
1328
|
-
resolve();
|
|
1329
|
-
});
|
|
1330
|
-
} catch (error) {
|
|
1331
|
-
reject(error);
|
|
1332
|
-
}
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
function stopServer() {
|
|
1336
|
-
return new Promise((resolve, reject) => {
|
|
1337
|
-
if (!server) {
|
|
1338
|
-
isStarted = false;
|
|
1339
|
-
resolve();
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
server.close((error) => {
|
|
1343
|
-
if (error) {
|
|
1344
|
-
if (error.code === "ERR_SERVER_NOT_RUNNING") {
|
|
1345
|
-
server = null;
|
|
1346
|
-
isStarted = false;
|
|
1347
|
-
resolve();
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
reject(error);
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
server = null;
|
|
1354
|
-
isStarted = false;
|
|
1355
|
-
console.log("[ESV Server] Stopped");
|
|
1356
|
-
resolve();
|
|
1357
|
-
});
|
|
1358
|
-
});
|
|
1359
|
-
}
|
|
1360
|
-
function isServerRunning() {
|
|
1361
|
-
return isStarted;
|
|
1362
|
-
}
|
|
1363
|
-
function getServerUrl(options = {}) {
|
|
1364
|
-
const { port = DEFAULT_PORT, host = DEFAULT_HOST } = options;
|
|
1365
|
-
return `http://${host}:${port}`;
|
|
1366
|
-
}
|
|
1367
|
-
export {
|
|
1368
|
-
verifyJwt,
|
|
1369
|
-
stopServer,
|
|
1370
|
-
startServer,
|
|
1371
|
-
signJwt,
|
|
1372
|
-
setTestUser,
|
|
1373
|
-
resetState,
|
|
1374
|
-
isServerRunning,
|
|
1375
|
-
getServerUrl,
|
|
1376
|
-
getKeyId,
|
|
1377
|
-
getJwks
|
|
1378
|
-
};
|
|
1379
|
-
|
|
1380
|
-
//# debugId=26AB5076DF5FC52A64756E2164756E21
|
|
1
|
+
/**
|
|
2
|
+
* ESV Mock Server
|
|
3
|
+
*
|
|
4
|
+
* A zero-dependency mock server for testing Enterprise Standard integrations locally.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { startServer, stopServer } from '@enterprisestandard/esv/server';
|
|
9
|
+
* import { enterpriseStandard } from '@enterprisestandard/react';
|
|
10
|
+
*
|
|
11
|
+
* // In your test setup
|
|
12
|
+
* beforeAll(async () => {
|
|
13
|
+
* await startServer();
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* afterAll(async () => {
|
|
17
|
+
* await stopServer();
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Initialize with ES_VAULT_URL, ES_VAULT_TOKEN, and ES_VAULT_PATH set
|
|
21
|
+
* const es = await enterpriseStandard(undefined, {
|
|
22
|
+
* vaultUrl: process.env.ES_VAULT_URL,
|
|
23
|
+
* vaultToken: process.env.ES_VAULT_TOKEN,
|
|
24
|
+
* vaultPath: process.env.ES_VAULT_PATH,
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
// Crypto utilities (for advanced testing scenarios)
|
|
29
|
+
export { getJwks, getKeyId, signJwt, verifyJwt } from './crypto.js';
|
|
30
|
+
// Main server functions
|
|
31
|
+
export { getServerUrl, isServerRunning, startServer, stopServer } from './server.js';
|
|
32
|
+
// State management (useful for test setup)
|
|
33
|
+
export { resetState, setTestUser } from './state.js';
|
|
34
|
+
//# sourceMappingURL=index.js.map
|