@enterprisestandard/react 0.0.3-beta.3 → 0.0.5

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/index.js CHANGED
@@ -1,9 +1,287 @@
1
1
  // src/iam.ts
2
2
  async function iam(config) {
3
+ return {};
4
+ }
5
+
6
+ // src/oidc-schema.ts
7
+ function oidcCallbackSchema(vendor) {
3
8
  return {
4
- url: config.url,
5
- userEndpoint: config.userEndpoint,
6
- groupEndpoint: config.groupEndpoint
9
+ "~standard": {
10
+ version: 1,
11
+ vendor,
12
+ validate: (value) => {
13
+ if (typeof value !== "object" || value === null) {
14
+ return {
15
+ issues: [
16
+ {
17
+ message: "Expected an object"
18
+ }
19
+ ]
20
+ };
21
+ }
22
+ const params = value;
23
+ const issues = [];
24
+ const result = {};
25
+ if ("code" in params) {
26
+ if (typeof params.code === "string") {
27
+ result.code = params.code;
28
+ } else {
29
+ issues.push({
30
+ message: "code must be a string",
31
+ path: ["code"]
32
+ });
33
+ }
34
+ } else if (!("error" in params)) {
35
+ issues.push({
36
+ message: "code is required",
37
+ path: ["code"]
38
+ });
39
+ }
40
+ if ("state" in params) {
41
+ if (typeof params.state === "string" || params.state === undefined) {
42
+ result.state = params.state;
43
+ } else {
44
+ issues.push({
45
+ message: "state must be a string",
46
+ path: ["state"]
47
+ });
48
+ }
49
+ }
50
+ if ("session_state" in params) {
51
+ if (typeof params.session_state === "string" || params.session_state === undefined) {
52
+ result.session_state = params.session_state;
53
+ } else {
54
+ issues.push({
55
+ message: "session_state must be a string",
56
+ path: ["session_state"]
57
+ });
58
+ }
59
+ }
60
+ if ("error" in params) {
61
+ if (typeof params.error === "string") {
62
+ result.error = params.error;
63
+ } else {
64
+ issues.push({
65
+ message: "error must be a string",
66
+ path: ["error"]
67
+ });
68
+ }
69
+ if ("error_description" in params) {
70
+ if (typeof params.error_description === "string" || params.error_description === undefined) {
71
+ result.error_description = params.error_description;
72
+ } else {
73
+ issues.push({
74
+ message: "error_description must be a string",
75
+ path: ["error_description"]
76
+ });
77
+ }
78
+ }
79
+ if ("error_uri" in params) {
80
+ if (typeof params.error_uri === "string" || params.error_uri === undefined) {
81
+ result.error_uri = params.error_uri;
82
+ } else {
83
+ issues.push({
84
+ message: "error_uri must be a string",
85
+ path: ["error_uri"]
86
+ });
87
+ }
88
+ }
89
+ }
90
+ if ("iss" in params) {
91
+ if (typeof params.iss === "string" || params.iss === undefined) {
92
+ result.iss = params.iss;
93
+ } else {
94
+ issues.push({
95
+ message: "iss must be a string",
96
+ path: ["iss"]
97
+ });
98
+ }
99
+ }
100
+ if (issues.length > 0) {
101
+ return { issues };
102
+ }
103
+ return { value: result };
104
+ }
105
+ }
106
+ };
107
+ }
108
+ function tokenResponseSchema(vendor) {
109
+ return {
110
+ "~standard": {
111
+ version: 1,
112
+ vendor,
113
+ validate: (value) => {
114
+ if (typeof value !== "object" || value === null) {
115
+ return {
116
+ issues: [
117
+ {
118
+ message: "Expected an object"
119
+ }
120
+ ]
121
+ };
122
+ }
123
+ const response = value;
124
+ const issues = [];
125
+ const result = {};
126
+ if ("access_token" in response) {
127
+ if (typeof response.access_token === "string") {
128
+ result.access_token = response.access_token;
129
+ } else {
130
+ issues.push({
131
+ message: "access_token must be a string",
132
+ path: ["access_token"]
133
+ });
134
+ }
135
+ } else {
136
+ issues.push({
137
+ message: "access_token is required",
138
+ path: ["access_token"]
139
+ });
140
+ }
141
+ if ("id_token" in response) {
142
+ if (typeof response.id_token === "string") {
143
+ result.id_token = response.id_token;
144
+ } else {
145
+ issues.push({
146
+ message: "id_token must be a string",
147
+ path: ["id_token"]
148
+ });
149
+ }
150
+ } else {
151
+ issues.push({
152
+ message: "id_token is required",
153
+ path: ["id_token"]
154
+ });
155
+ }
156
+ if ("token_type" in response) {
157
+ if (typeof response.token_type === "string") {
158
+ result.token_type = response.token_type;
159
+ } else {
160
+ issues.push({
161
+ message: "token_type must be a string",
162
+ path: ["token_type"]
163
+ });
164
+ }
165
+ } else {
166
+ issues.push({
167
+ message: "token_type is required",
168
+ path: ["token_type"]
169
+ });
170
+ }
171
+ if ("refresh_token" in response) {
172
+ if (typeof response.refresh_token === "string" || response.refresh_token === undefined) {
173
+ result.refresh_token = response.refresh_token;
174
+ } else {
175
+ issues.push({
176
+ message: "refresh_token must be a string",
177
+ path: ["refresh_token"]
178
+ });
179
+ }
180
+ }
181
+ if ("scope" in response) {
182
+ if (typeof response.scope === "string" || response.scope === undefined) {
183
+ result.scope = response.scope;
184
+ } else {
185
+ issues.push({
186
+ message: "scope must be a string",
187
+ path: ["scope"]
188
+ });
189
+ }
190
+ }
191
+ if ("session_state" in response) {
192
+ if (typeof response.session_state === "string" || response.session_state === undefined) {
193
+ result.session_state = response.session_state;
194
+ } else {
195
+ issues.push({
196
+ message: "session_state must be a string",
197
+ path: ["session_state"]
198
+ });
199
+ }
200
+ }
201
+ if ("expires" in response) {
202
+ if (typeof response.expires === "string" || response.expires === undefined) {
203
+ result.expires = response.expires;
204
+ } else {
205
+ issues.push({
206
+ message: "expires must be a string",
207
+ path: ["expires"]
208
+ });
209
+ }
210
+ }
211
+ if ("expires_in" in response) {
212
+ if (typeof response.expires_in === "number" || response.expires_in === undefined) {
213
+ result.expires_in = response.expires_in;
214
+ } else {
215
+ issues.push({
216
+ message: "expires_in must be a number",
217
+ path: ["expires_in"]
218
+ });
219
+ }
220
+ }
221
+ if ("refresh_expires_in" in response) {
222
+ if (typeof response.refresh_expires_in === "number" || response.refresh_expires_in === undefined) {
223
+ result.refresh_expires_in = response.refresh_expires_in;
224
+ } else {
225
+ issues.push({
226
+ message: "refresh_expires_in must be a number",
227
+ path: ["refresh_expires_in"]
228
+ });
229
+ }
230
+ }
231
+ if (issues.length > 0) {
232
+ return { issues };
233
+ }
234
+ return { value: result };
235
+ }
236
+ }
237
+ };
238
+ }
239
+ function idTokenClaimsSchema(vendor) {
240
+ return {
241
+ "~standard": {
242
+ version: 1,
243
+ vendor,
244
+ validate: (value) => {
245
+ if (typeof value !== "object" || value === null) {
246
+ return {
247
+ issues: [
248
+ {
249
+ message: "Expected an object"
250
+ }
251
+ ]
252
+ };
253
+ }
254
+ const claims = value;
255
+ const issues = [];
256
+ const result = { ...claims };
257
+ const stringFields = ["iss", "aud", "sub", "sid", "name", "email", "preferred_username", "picture"];
258
+ for (const field of stringFields) {
259
+ if (field in claims && claims[field] !== undefined) {
260
+ if (typeof claims[field] !== "string") {
261
+ issues.push({
262
+ message: `${field} must be a string`,
263
+ path: [field]
264
+ });
265
+ }
266
+ }
267
+ }
268
+ const numberFields = ["exp", "iat"];
269
+ for (const field of numberFields) {
270
+ if (field in claims && claims[field] !== undefined) {
271
+ if (typeof claims[field] !== "number") {
272
+ issues.push({
273
+ message: `${field} must be a number`,
274
+ path: [field]
275
+ });
276
+ }
277
+ }
278
+ }
279
+ if (issues.length > 0) {
280
+ return { issues };
281
+ }
282
+ return { value: result };
283
+ }
284
+ }
7
285
  };
8
286
  }
9
287
 
@@ -34,10 +312,17 @@ var jwksCache = new Map;
34
312
  function sso(config) {
35
313
  const configWithDefaults = {
36
314
  ...config,
37
- secure: config.secure !== undefined ? config.secure : false,
38
- sameSite: config.sameSite !== undefined ? config.sameSite : "Lax",
39
- cookiePrefix: config.cookiePrefix ?? `es.sso.${config.client_id}`,
40
- cookiePath: config.cookiePath ?? "/"
315
+ authority: must(config.authority, "Missing 'authority' from SSO Config"),
316
+ token_url: must(config.token_url, "Missing 'token_url' from SSO Config"),
317
+ authorization_url: must(config.authorization_url, "Missing 'authorization_url' from SSO Config"),
318
+ client_id: must(config.client_id, "Missing 'client_id' from SSO Config"),
319
+ redirect_uri: must(config.redirect_uri, "Missing 'redirect_uri' from SSO Config"),
320
+ scope: must(config.scope, "Missing 'scope' from SSO Config"),
321
+ response_type: config.response_type ?? "code",
322
+ cookies_secure: config.cookies_secure !== undefined ? config.cookies_secure : true,
323
+ cookies_same_site: config.cookies_same_site !== undefined ? config.cookies_same_site : "Strict",
324
+ cookies_prefix: config.cookies_prefix ?? `es.sso.${config.client_id}`,
325
+ cookies_path: config.cookies_path ?? "/"
41
326
  };
42
327
  async function getUser(request) {
43
328
  if (!configWithDefaults) {
@@ -93,16 +378,123 @@ function sso(config) {
93
378
  }
94
379
  });
95
380
  }
96
- async function callbackHandler(request) {
381
+ async function logout(request, _config) {
382
+ try {
383
+ const refreshToken2 = getCookie("refresh", request);
384
+ if (refreshToken2) {
385
+ await revokeToken(refreshToken2);
386
+ }
387
+ } catch (error) {
388
+ console.warn("Failed to revoke token:", error);
389
+ }
390
+ if (config.session_store) {
391
+ try {
392
+ const user = await getUser(request);
393
+ if (user?.sso?.profile.sid) {
394
+ const sid = user.sso.profile.sid;
395
+ await config.session_store.delete(sid);
396
+ console.log(`Session ${sid} deleted from store`);
397
+ }
398
+ } catch (error) {
399
+ console.warn("Failed to delete session:", error);
400
+ }
401
+ }
402
+ const clearHeaders = [
403
+ ["Set-Cookie", clearCookie("access")],
404
+ ["Set-Cookie", clearCookie("id")],
405
+ ["Set-Cookie", clearCookie("refresh")],
406
+ ["Set-Cookie", clearCookie("control")],
407
+ ["Set-Cookie", clearCookie("state")]
408
+ ];
409
+ const url = new URL(request.url);
410
+ const redirectTo = url.searchParams.get("redirect");
411
+ if (redirectTo) {
412
+ return new Response("Logged out", {
413
+ status: 302,
414
+ headers: [["Location", redirectTo], ...clearHeaders]
415
+ });
416
+ }
417
+ const accept = request.headers.get("accept");
418
+ const isAjax = accept?.includes("application/json") || accept?.includes("text/javascript");
419
+ if (isAjax) {
420
+ return new Response(JSON.stringify({ success: true, message: "Logged out" }), {
421
+ status: 200,
422
+ headers: [["Content-Type", "application/json"], ...clearHeaders]
423
+ });
424
+ } else {
425
+ return new Response(`
426
+ <!DOCTYPE html><html lang="en"><body>
427
+ <h1>Logout Complete</h1>
428
+ <div style="display: none">
429
+ It is not recommended to show the default logout page. Include '?redirect=/someHomePage' or logout asynchronously.
430
+ Check the <a href="https://EnterpriseStandard.com/sso#logout">Enterprise Standard Packages</a> for more information.
431
+ </div>
432
+ </body></html>
433
+ `, {
434
+ status: 200,
435
+ headers: [["Content-Type", "text/html"], ...clearHeaders]
436
+ });
437
+ }
438
+ }
439
+ async function logoutBackChannel(request) {
440
+ if (!configWithDefaults.session_store) {
441
+ return new Response("Back-Channel Logout requires session_store configuration", {
442
+ status: 400,
443
+ statusText: "Bad Request"
444
+ });
445
+ }
446
+ try {
447
+ const contentType = request.headers.get("content-type");
448
+ if (!contentType || !contentType.includes("application/x-www-form-urlencoded")) {
449
+ return new Response("Invalid Content-Type, expected application/x-www-form-urlencoded", {
450
+ status: 400
451
+ });
452
+ }
453
+ const body = await request.text();
454
+ const params = new URLSearchParams(body);
455
+ const logoutToken = params.get("logout_token");
456
+ if (!logoutToken) {
457
+ return new Response("Missing logout_token parameter", { status: 400 });
458
+ }
459
+ const claims = await parseJwt(logoutToken);
460
+ const sid = claims.sid;
461
+ if (!sid) {
462
+ console.warn("Back-Channel Logout: logout_token missing sid claim");
463
+ return new Response("Invalid logout_token: missing sid claim", { status: 400 });
464
+ }
465
+ await configWithDefaults.session_store.delete(sid);
466
+ console.log(`Back-Channel Logout: successfully deleted session ${sid}`);
467
+ return new Response("OK", { status: 200 });
468
+ } catch (error) {
469
+ console.error("Error during back-channel logout:", error);
470
+ return new Response("Internal Server Error", { status: 500 });
471
+ }
472
+ }
473
+ async function callbackHandler(request, validation) {
97
474
  if (!configWithDefaults) {
98
475
  console.error("SSO Manager not initialized");
99
476
  return Promise.resolve(new Response("SSO Manager not initialized", { status: 503 }));
100
477
  }
101
478
  const url = new URL(request.url);
102
479
  const params = new URLSearchParams(url.search);
480
+ const callbackParamsValidator = validation?.callbackParams ?? oidcCallbackSchema("builtin");
481
+ const paramsObject = Object.fromEntries(params.entries());
482
+ const paramsResult = await callbackParamsValidator["~standard"].validate(paramsObject);
483
+ if ("issues" in paramsResult) {
484
+ return new Response(JSON.stringify({
485
+ error: "validation_failed",
486
+ message: "OIDC callback parameters validation failed",
487
+ issues: paramsResult.issues?.map((i) => ({
488
+ path: i.path?.join("."),
489
+ message: i.message
490
+ }))
491
+ }), {
492
+ status: 400,
493
+ headers: { "Content-Type": "application/json" }
494
+ });
495
+ }
496
+ const { code: codeFromUrl, state: stateFromUrl } = paramsResult.value;
103
497
  try {
104
- const codeFromUrl = must(params.get("code"), 'OIDC "code" was not passed as a search param, ensure that the SSO login completed successfully');
105
- const stateFromUrl = must(params.get("state"), 'OIDC "state" was not passed as a search param, ensure that the SSO login completed successfully');
106
498
  const cookie = getCookie("state", request, true);
107
499
  const { codeVerifier, state, landingUrl } = cookie ?? {};
108
500
  must(codeVerifier, 'OIDC "codeVerifier" was not present in cookies, ensure that the SSO login was initiated correctly');
@@ -111,8 +503,27 @@ function sso(config) {
111
503
  if (stateFromUrl !== state) {
112
504
  throw new Error('SSO State Verifier failed, the "state" request parameter does not equal the "state" in the SSO cookie');
113
505
  }
114
- const tokenResponse = await exchangeCodeForToken(codeFromUrl, codeVerifier);
115
- const user = await parseUser(tokenResponse);
506
+ const tokenResponse = await exchangeCodeForToken(codeFromUrl, codeVerifier, validation);
507
+ const user = await parseUser(tokenResponse, validation);
508
+ if (config.session_store) {
509
+ try {
510
+ const sid = user.sso.profile.sid;
511
+ const sub = user.id;
512
+ if (sid && sub) {
513
+ const session = {
514
+ sid,
515
+ sub,
516
+ createdAt: new Date,
517
+ lastActivityAt: new Date
518
+ };
519
+ await config.session_store.create(session);
520
+ } else {
521
+ console.warn("Session creation skipped: missing sid or sub in ID token claims");
522
+ }
523
+ } catch (error) {
524
+ console.warn("Failed to create session:", error);
525
+ }
526
+ }
116
527
  return new Response("Authentication successful, redirecting", {
117
528
  status: 302,
118
529
  headers: [
@@ -141,13 +552,14 @@ function sso(config) {
141
552
  });
142
553
  }
143
554
  }
144
- async function parseUser(token) {
555
+ async function parseUser(token, validation) {
145
556
  if (!configWithDefaults)
146
557
  throw new Error("SSO Manager not initialized");
147
- const idToken = await parseJwt(token.id_token);
558
+ const idToken = await parseJwt(token.id_token, validation);
148
559
  const expiresIn = Number(token.refresh_expires_in ?? token.expires_in ?? 3600);
149
560
  const expires = token.expires ? new Date(token.expires) : new Date(Date.now() + expiresIn * 1000);
150
561
  return {
562
+ id: idToken.sub,
151
563
  userName: idToken.preferred_username || "",
152
564
  name: idToken.name || "",
153
565
  email: idToken.email || "",
@@ -175,7 +587,7 @@ function sso(config) {
175
587
  }
176
588
  };
177
589
  }
178
- async function exchangeCodeForToken(code, codeVerifier) {
590
+ async function exchangeCodeForToken(code, codeVerifier, validation) {
179
591
  if (!configWithDefaults)
180
592
  throw new Error("SSO Manager not initialized");
181
593
  const tokenUrl = configWithDefaults.token_url;
@@ -199,10 +611,16 @@ function sso(config) {
199
611
  console.error("Token exchange error:", data);
200
612
  throw new Error(`Token exchange failed: ${data.error || response.statusText} - ${data.error_description || ""}`.trim());
201
613
  }
202
- return data;
614
+ const tokenResponseValidator = validation?.tokenResponse ?? tokenResponseSchema("builtin");
615
+ const tokenResult = await tokenResponseValidator["~standard"].validate(data);
616
+ if ("issues" in tokenResult) {
617
+ console.error("Token response validation failed:", tokenResult.issues);
618
+ throw new Error(`Token response validation failed: ${tokenResult.issues?.map((i) => i.message).join("; ")}`);
619
+ }
620
+ return tokenResult.value;
203
621
  } catch (error) {
204
622
  console.error("Error during token exchange:", error);
205
- throw new Error("Error during token exchange");
623
+ throw error;
206
624
  }
207
625
  }
208
626
  async function refreshToken(refreshToken2) {
@@ -230,6 +648,33 @@ function sso(config) {
230
648
  return data;
231
649
  });
232
650
  }
651
+ async function revokeToken(token) {
652
+ try {
653
+ if (!configWithDefaults)
654
+ throw new Error("SSO Manager not initialized");
655
+ if (!configWithDefaults.revocation_endpoint) {
656
+ return;
657
+ }
658
+ const body = new URLSearchParams;
659
+ body.append("token", token);
660
+ body.append("token_type_hint", "refresh_token");
661
+ body.append("client_id", configWithDefaults.client_id);
662
+ const response = await fetch(configWithDefaults.revocation_endpoint, {
663
+ method: "POST",
664
+ headers: {
665
+ "Content-Type": "application/x-www-form-urlencoded"
666
+ },
667
+ body: body.toString()
668
+ });
669
+ if (!response.ok) {
670
+ console.warn("Token revocation failed:", response.status, response.statusText);
671
+ } else {
672
+ console.log("Token revoked successfully");
673
+ }
674
+ } catch (error) {
675
+ console.warn("Error revoking token:", error);
676
+ }
677
+ }
233
678
  async function fetchJwks() {
234
679
  const url = configWithDefaults.jwks_uri || `${configWithDefaults.authority}/protocol/openid-connect/certs`;
235
680
  const cached = jwksCache.get(url);
@@ -267,7 +712,7 @@ function sso(config) {
267
712
  }
268
713
  throw lastError;
269
714
  }
270
- async function parseJwt(token) {
715
+ async function parseJwt(token, validation) {
271
716
  try {
272
717
  const parts = token.split(".");
273
718
  if (parts.length !== 3)
@@ -281,7 +726,13 @@ function sso(config) {
281
726
  const isValid = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)), data);
282
727
  if (!isValid)
283
728
  throw new Error("Invalid JWT signature");
284
- return payload;
729
+ const idTokenClaimsValidator = validation?.idTokenClaims ?? idTokenClaimsSchema("builtin");
730
+ const claimsResult = await idTokenClaimsValidator["~standard"].validate(payload);
731
+ if ("issues" in claimsResult) {
732
+ console.error("ID token claims validation failed:", claimsResult.issues);
733
+ throw new Error(`ID token claims validation failed: ${claimsResult.issues?.map((i) => i.message).join("; ")}`);
734
+ }
735
+ return claimsResult.value;
285
736
  } catch (e) {
286
737
  console.error("Error verifying JWT:", e);
287
738
  throw e;
@@ -357,7 +808,7 @@ function sso(config) {
357
808
  return tokens.access_token;
358
809
  }
359
810
  function createCookie(name, value, expires) {
360
- name = `${configWithDefaults.cookiePrefix}.${name}`;
811
+ name = `${configWithDefaults.cookies_prefix}.${name}`;
361
812
  if (typeof value !== "string") {
362
813
  value = btoa(JSON.stringify(value));
363
814
  }
@@ -372,16 +823,16 @@ function sso(config) {
372
823
  if (value.length > 4000) {
373
824
  throw new Error(`Error setting cookie: ${name}. Cookie length is: ${value.length}`);
374
825
  }
375
- return `${name}=${value}; ${exp}; Path=${configWithDefaults.cookiePath}; HttpOnly;${configWithDefaults.secure ? " Secure;" : ""} SameSite=${configWithDefaults.sameSite};`;
826
+ return `${name}=${value}; ${exp}; Path=${configWithDefaults.cookies_path}; HttpOnly;${configWithDefaults.cookies_secure ? " Secure;" : ""} SameSite=${configWithDefaults.cookies_same_site};`;
376
827
  }
377
828
  function clearCookie(name) {
378
- return `${configWithDefaults.cookiePrefix}.${name}=; Max-Age=0; Path=${configWithDefaults.cookiePath}; HttpOnly;${configWithDefaults.secure ? " Secure;" : ""} SameSite=${configWithDefaults.sameSite};`;
829
+ return `${configWithDefaults.cookies_prefix}.${name}=; Max-Age=0; Path=${configWithDefaults.cookies_path}; HttpOnly;${configWithDefaults.cookies_secure ? " Secure;" : ""} SameSite=${configWithDefaults.cookies_same_site};`;
379
830
  }
380
831
  function getCookie(name, req, parse = false) {
381
832
  const header = req.headers.get("cookie");
382
833
  if (!header)
383
834
  return null;
384
- const cookie = header.split(";").find((row) => row.trim().startsWith(`${configWithDefaults.cookiePrefix}.${name}=`));
835
+ const cookie = header.split(";").find((row) => row.trim().startsWith(`${configWithDefaults.cookies_prefix}.${name}=`));
385
836
  if (!cookie)
386
837
  return null;
387
838
  const val = cookie.split("=")[1].trim();
@@ -391,12 +842,19 @@ function sso(config) {
391
842
  return JSON.parse(str);
392
843
  }
393
844
  async function handler(request, handlerConfig) {
394
- let { loginUrl, userUrl, errorUrl, landingUrl, tokenUrl, refreshUrl } = handlerConfig ?? {};
395
- if (!loginUrl)
396
- loginUrl = "*";
845
+ const { loginUrl, userUrl, errorUrl, landingUrl, tokenUrl, refreshUrl, logoutUrl, logoutBackChannelUrl, jwksUrl, validation } = handlerConfig ?? {};
846
+ if (!loginUrl) {
847
+ console.error("loginUrl is required");
848
+ }
397
849
  const path = new URL(request.url).pathname;
398
- if (new URL(config.redirect_uri).pathname === path) {
399
- return callbackHandler(request);
850
+ if (new URL(configWithDefaults.redirect_uri).pathname === path) {
851
+ return callbackHandler(request, validation);
852
+ }
853
+ if (loginUrl === path) {
854
+ return initiateLogin({
855
+ landingUrl: landingUrl || "/",
856
+ errorUrl
857
+ });
400
858
  }
401
859
  if (userUrl === path) {
402
860
  const { tokens, refreshHeaders } = await getTokenFromCookies(request);
@@ -433,10 +891,16 @@ function sso(config) {
433
891
  headers: refreshHeaders
434
892
  });
435
893
  }
436
- if (loginUrl === "*" || loginUrl === path) {
437
- return initiateLogin({
438
- landingUrl: landingUrl || "/",
439
- errorUrl
894
+ if (logoutUrl === path) {
895
+ return logout(request, { landingUrl: landingUrl || "/" });
896
+ }
897
+ if (logoutBackChannelUrl === path) {
898
+ return logoutBackChannel(request);
899
+ }
900
+ if (jwksUrl === path) {
901
+ const jwks = await fetchJwks();
902
+ return new Response(JSON.stringify(jwks), {
903
+ headers: [["Content-Type", "application/json"]]
440
904
  });
441
905
  }
442
906
  return new Response("Not Found", { status: 404 });
@@ -446,14 +910,15 @@ function sso(config) {
446
910
  getRequiredUser,
447
911
  getJwt,
448
912
  initiateLogin,
913
+ logout,
449
914
  callbackHandler,
450
915
  handler
451
916
  };
452
917
  }
453
918
 
454
919
  // src/vault.ts
455
- function vault(url, token) {
456
- async function getFullSecret(path) {
920
+ function vault(url) {
921
+ async function getFullSecret(path, token) {
457
922
  const resp = await fetch(`${url}/${path}`, { headers: { "X-Vault-Token": token } });
458
923
  if (resp.status !== 200) {
459
924
  throw new Error(`Vault returned invalid status, ${resp.status}: '${resp.statusText}' from URL: ${url}`);
@@ -468,111 +933,8 @@ function vault(url, token) {
468
933
  return {
469
934
  url,
470
935
  getFullSecret,
471
- getSecret: async (path) => {
472
- return (await getFullSecret(path)).data;
473
- }
474
- };
475
- }
476
-
477
- // src/oidc-schema.ts
478
- function oidcCallbackSchema(vendor) {
479
- return {
480
- "~standard": {
481
- version: 1,
482
- vendor,
483
- validate: (value) => {
484
- if (typeof value !== "object" || value === null) {
485
- return {
486
- issues: [
487
- {
488
- message: "Expected an object"
489
- }
490
- ]
491
- };
492
- }
493
- const params = value;
494
- const issues = [];
495
- const result = {};
496
- if ("code" in params) {
497
- if (typeof params.code === "string") {
498
- result.code = params.code;
499
- } else {
500
- issues.push({
501
- message: "code must be a string",
502
- path: ["code"]
503
- });
504
- }
505
- } else if (!("error" in params)) {
506
- issues.push({
507
- message: "code is required",
508
- path: ["code"]
509
- });
510
- }
511
- if ("state" in params) {
512
- if (typeof params.state === "string" || params.state === undefined) {
513
- result.state = params.state;
514
- } else {
515
- issues.push({
516
- message: "state must be a string",
517
- path: ["state"]
518
- });
519
- }
520
- }
521
- if ("session_state" in params) {
522
- if (typeof params.session_state === "string" || params.session_state === undefined) {
523
- result.session_state = params.session_state;
524
- } else {
525
- issues.push({
526
- message: "session_state must be a string",
527
- path: ["session_state"]
528
- });
529
- }
530
- }
531
- if ("error" in params) {
532
- if (typeof params.error === "string") {
533
- result.error = params.error;
534
- } else {
535
- issues.push({
536
- message: "error must be a string",
537
- path: ["error"]
538
- });
539
- }
540
- if ("error_description" in params) {
541
- if (typeof params.error_description === "string" || params.error_description === undefined) {
542
- result.error_description = params.error_description;
543
- } else {
544
- issues.push({
545
- message: "error_description must be a string",
546
- path: ["error_description"]
547
- });
548
- }
549
- }
550
- if ("error_uri" in params) {
551
- if (typeof params.error_uri === "string" || params.error_uri === undefined) {
552
- result.error_uri = params.error_uri;
553
- } else {
554
- issues.push({
555
- message: "error_uri must be a string",
556
- path: ["error_uri"]
557
- });
558
- }
559
- }
560
- }
561
- if ("iss" in params) {
562
- if (typeof params.iss === "string" || params.iss === undefined) {
563
- result.iss = params.iss;
564
- } else {
565
- issues.push({
566
- message: "iss must be a string",
567
- path: ["iss"]
568
- });
569
- }
570
- }
571
- if (issues.length > 0) {
572
- return { issues };
573
- }
574
- return { value: result };
575
- }
936
+ getSecret: async (path, token) => {
937
+ return (await getFullSecret(path, token)).data;
576
938
  }
577
939
  };
578
940
  }
@@ -619,6 +981,30 @@ async function handler(request, config) {
619
981
  throw unavailable();
620
982
  return sso2.handler(request, config);
621
983
  }
984
+ // src/session-store.ts
985
+ class InMemorySessionStore {
986
+ sessions = new Map;
987
+ async create(session) {
988
+ if (this.sessions.has(session.sid)) {
989
+ throw new Error(`Session with sid ${session.sid} already exists`);
990
+ }
991
+ this.sessions.set(session.sid, session);
992
+ }
993
+ async get(sid) {
994
+ return this.sessions.get(sid) ?? null;
995
+ }
996
+ async update(sid, data) {
997
+ const session = this.sessions.get(sid);
998
+ if (!session) {
999
+ throw new Error(`Session with sid ${sid} not found`);
1000
+ }
1001
+ const updated = { ...session, ...data };
1002
+ this.sessions.set(sid, updated);
1003
+ }
1004
+ async delete(sid) {
1005
+ this.sessions.delete(sid);
1006
+ }
1007
+ }
622
1008
  // src/ui/sign-in-loading.tsx
623
1009
  import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
624
1010
  function SignInLoading({ complete = false, children }) {
@@ -774,9 +1160,14 @@ function SSOProvider({
774
1160
  }
775
1161
  }
776
1162
  };
1163
+ const handleLogout = () => {
1164
+ setUserState(null);
1165
+ };
777
1166
  window.addEventListener("storage", handleStorageChange);
1167
+ window.addEventListener("es-sso-logout", handleLogout);
778
1168
  return () => {
779
1169
  window.removeEventListener("storage", handleStorageChange);
1170
+ window.removeEventListener("es-sso-logout", handleLogout);
780
1171
  };
781
1172
  }, [disableListener, storage, actualStorageKey, isValidUser]);
782
1173
  const contextValue = {
@@ -875,34 +1266,76 @@ function useToken() {
875
1266
  refresh
876
1267
  };
877
1268
  }
1269
+ async function logout(logoutUrl) {
1270
+ try {
1271
+ const response = await fetch(logoutUrl, {
1272
+ headers: { Accept: "application/json" }
1273
+ });
1274
+ if (!response.ok) {
1275
+ return { success: false, error: `HTTP ${response.status}` };
1276
+ }
1277
+ const data = await response.json();
1278
+ if (!data.success) {
1279
+ return { success: false, error: data.message || "Logout failed" };
1280
+ }
1281
+ if (typeof window !== "undefined") {
1282
+ for (let i = localStorage.length - 1;i >= 0; i--) {
1283
+ const key = localStorage.key(i);
1284
+ if (key?.startsWith("es-sso-user")) {
1285
+ localStorage.removeItem(key);
1286
+ }
1287
+ }
1288
+ for (let i = sessionStorage.length - 1;i >= 0; i--) {
1289
+ const key = sessionStorage.key(i);
1290
+ if (key?.startsWith("es-sso-user")) {
1291
+ sessionStorage.removeItem(key);
1292
+ }
1293
+ }
1294
+ window.dispatchEvent(new CustomEvent("es-sso-logout"));
1295
+ }
1296
+ return { success: true };
1297
+ } catch (error) {
1298
+ return {
1299
+ success: false,
1300
+ error: error instanceof Error ? error.message : "Network error"
1301
+ };
1302
+ }
1303
+ }
878
1304
 
879
1305
  // src/index.ts
880
- async function enterpriseStandard(appId, appKey, initConfig) {
1306
+ async function enterpriseStandard(appKey, initConfig) {
881
1307
  let vaultUrl;
882
1308
  let vaultToken;
883
- let paths;
1309
+ let secrets;
884
1310
  const ioniteUrl = initConfig?.ioniteUrl ?? "https://ionite.com";
885
- if (appId === "IONITE_PUBLIC_DEMO") {
1311
+ if (appKey?.startsWith("IONITE_PUBLIC_DEMO_")) {
886
1312
  vaultUrl = "https://vault-ionite.ionite.dev/v1/secret/data";
887
- vaultToken = "hvs.NuiBSLuFk5Ju4JDOUwTOlSlP";
888
- paths = { sso: "ionite/IONITE_PUBLIC_DEMO" };
1313
+ const port = appKey.slice("IONITE_PUBLIC_DEMO_".length);
1314
+ secrets = {
1315
+ sso: {
1316
+ path: `public/IONITE_PUBLIC_DEMO_SSO_${port}`,
1317
+ token: "hvs.VGhD2hmXDH9PmZjTacZx0G5K"
1318
+ }
1319
+ };
889
1320
  } else if (appKey) {
890
1321
  if (!vaultUrl || !vaultToken) {
891
1322
  throw new Error("TODO something is wrong with the ionite config, handle this error");
892
1323
  }
893
- paths = {};
1324
+ secrets = {};
894
1325
  } else {
895
1326
  throw new Error("TODO tell them how to connect to ionite");
896
1327
  }
897
1328
  const defaultInstance2 = getDefaultInstance();
898
- const vaultClient = await vault(vaultUrl, vaultToken);
1329
+ const vaultClient = vault(vaultUrl);
899
1330
  const result = {
900
- appId,
901
1331
  ioniteUrl,
902
1332
  defaultInstance: initConfig?.defaultInstance || initConfig?.defaultInstance !== false && !defaultInstance2,
903
1333
  vault: vaultClient,
904
- sso: paths.sso ? sso(await vaultClient.getSecret(paths.sso)) : undefined,
905
- iam: paths.iam ? await iam(await vaultClient.getSecret(paths.iam)) : undefined
1334
+ sso: secrets.sso ? sso({
1335
+ ...await vaultClient.getSecret(secrets.sso.path, secrets.sso.token),
1336
+ ...initConfig
1337
+ }) : undefined,
1338
+ iam: secrets.iam ? await iam(await vaultClient.getSecret(secrets.iam.path, secrets.iam.token)) : undefined
906
1339
  };
907
1340
  if (result.defaultInstance) {
908
1341
  if (defaultInstance2) {
@@ -915,8 +1348,11 @@ async function enterpriseStandard(appId, appKey, initConfig) {
915
1348
  export {
916
1349
  useUser,
917
1350
  useToken,
1351
+ tokenResponseSchema,
918
1352
  oidcCallbackSchema,
1353
+ logout,
919
1354
  initiateLogin,
1355
+ idTokenClaimsSchema,
920
1356
  handler,
921
1357
  getUser,
922
1358
  getRequiredUser,
@@ -925,5 +1361,6 @@ export {
925
1361
  SignedOut,
926
1362
  SignedIn,
927
1363
  SignInLoading,
928
- SSOProvider
1364
+ SSOProvider,
1365
+ InMemorySessionStore
929
1366
  };