oidc-spa 6.13.2 → 6.14.1

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.
@@ -4,6 +4,7 @@ import {
4
4
  type User as OidcClientTsUser,
5
5
  InMemoryWebStorage
6
6
  } from "../vendor/frontend/oidc-client-ts-and-jwt-decode";
7
+ import type { OidcMetadata } from "./OidcMetadata";
7
8
  import { id, assert, is, type Equals } from "../vendor/frontend/tsafe";
8
9
  import { setTimeout, clearTimeout } from "../tools/workerTimers";
9
10
  import { Deferred } from "../tools/Deferred";
@@ -165,6 +166,19 @@ export type ParamsOfCreateOidc<
165
166
  * Use at your own risk, this is a hack.
166
167
  */
167
168
  __unsafe_useIdTokenAsAccessToken?: boolean;
169
+
170
+ /**
171
+ * This option should only be used as a last resort.
172
+ *
173
+ * If your OIDC provider is correctly configured, this should not be necessary.
174
+ *
175
+ * The metadata is normally retrieved automatically from:
176
+ * `${issuerUri}/.well-known/openid-configuration`
177
+ *
178
+ * Use this only if that endpoint is not accessible (e.g. due to missing CORS headers
179
+ * or non-standard deployments), and you cannot fix the server-side configuration.
180
+ */
181
+ __metadata?: Partial<OidcMetadata>;
168
182
  };
169
183
 
170
184
  const globalContext = {
@@ -287,7 +301,8 @@ export async function createOidc_nonMemoized<
287
301
  autoLogin = false,
288
302
  postLoginRedirectUrl: postLoginRedirectUrl_default,
289
303
  __unsafe_clientSecret,
290
- __unsafe_useIdTokenAsAccessToken = false
304
+ __unsafe_useIdTokenAsAccessToken = false,
305
+ __metadata
291
306
  } = params;
292
307
 
293
308
  const { issuerUri, clientId, scopes, configId, log } = preProcessedParams;
@@ -416,7 +431,8 @@ export async function createOidc_nonMemoized<
416
431
  }),
417
432
  stateStore: new WebStorageStateStore({ store: localStorage, prefix: STATE_STORE_KEY_PREFIX }),
418
433
  client_secret: __unsafe_clientSecret,
419
- fetch: trustedFetch
434
+ fetch: trustedFetch,
435
+ metadata: __metadata
420
436
  });
421
437
 
422
438
  const evtIsUserLoggedIn = createEvt<boolean>();
@@ -638,7 +654,8 @@ export async function createOidc_nonMemoized<
638
654
  configId,
639
655
  transformUrlBeforeRedirect_next,
640
656
  getExtraQueryParams,
641
- getExtraTokenParams
657
+ getExtraTokenParams,
658
+ autoLogin
642
659
  });
643
660
 
644
661
  assert(result_loginSilent.outcome !== "token refreshed using refresh token", "876995");
@@ -1029,7 +1046,8 @@ export async function createOidc_nonMemoized<
1029
1046
  configId,
1030
1047
  transformUrlBeforeRedirect_next,
1031
1048
  getExtraQueryParams,
1032
- getExtraTokenParams: () => extraTokenParams
1049
+ getExtraTokenParams: () => extraTokenParams,
1050
+ autoLogin
1033
1051
  });
1034
1052
 
1035
1053
  if (result_loginSilent.outcome === "failure") {
@@ -0,0 +1,85 @@
1
+ import { getUserEnvironmentInfo } from "../tools/getUserEnvironmentInfo";
2
+
3
+ const LOCAL_STORAGE_KEY = "oidc-spa_966975_diagnostic";
4
+ const appInstanceId = Math.random().toString(36).slice(2);
5
+
6
+ type LogEntry = {
7
+ appInstanceId: string;
8
+ time: number;
9
+ message: string;
10
+ };
11
+
12
+ function log(message: string) {
13
+ const logEntry: LogEntry = {
14
+ appInstanceId,
15
+ time: Date.now(),
16
+ message
17
+ };
18
+
19
+ const value = localStorage.getItem(LOCAL_STORAGE_KEY);
20
+
21
+ const value_parsed: LogEntry[] = value === null ? [] : JSON.parse(value);
22
+
23
+ if (value_parsed.length === 100) {
24
+ value_parsed.shift();
25
+ }
26
+
27
+ value_parsed.push(logEntry);
28
+
29
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(value_parsed));
30
+ }
31
+
32
+ function report() {
33
+ const logEntries: LogEntry[] = (() => {
34
+ const value = localStorage.getItem(LOCAL_STORAGE_KEY);
35
+
36
+ if (value === null) {
37
+ return [];
38
+ }
39
+
40
+ return JSON.parse(value);
41
+ })();
42
+
43
+ localStorage.removeItem(LOCAL_STORAGE_KEY);
44
+
45
+ const report = [
46
+ getUserEnvironmentInfo(),
47
+ ...logEntries.map(
48
+ ({ appInstanceId, time, message }) => `${appInstanceId} (${time}): ${message}`
49
+ ),
50
+ getOidcLocalStorageDump()
51
+ ].join("\n");
52
+
53
+ const key_report = `${LOCAL_STORAGE_KEY}_report_b64`;
54
+
55
+ localStorage.setItem(key_report, btoa(report));
56
+
57
+ console.warn(
58
+ [
59
+ "If you see this message there's been unexpected behavior in oidc-spa",
60
+ "It is a hard to reproduce case, could you please open an issue on https://github.com/keycloakify/oidc-spa",
61
+ `and include the value of \`localStorage["${key_report}"]\` in the message?`,
62
+ "Alternatively, you can send an email at joseph.garrone@protonmail.com",
63
+ "Thanks in advance for helping me figure this out"
64
+ ].join(" ")
65
+ );
66
+ }
67
+
68
+ export const debug966975 = {
69
+ log,
70
+ report
71
+ };
72
+
73
+ function getOidcLocalStorageDump(): string {
74
+ const entries: string[] = [];
75
+
76
+ for (let i = 0; i < localStorage.length; i++) {
77
+ const key = localStorage.key(i);
78
+ if (key && key.startsWith("oidc.")) {
79
+ const value = localStorage.getItem(key);
80
+ entries.push(`${key} = ${value}`);
81
+ }
82
+ }
83
+
84
+ return entries.join("\n");
85
+ }
@@ -8,6 +8,7 @@ import { assert, id } from "../vendor/frontend/tsafe";
8
8
  import type { AuthResponse } from "./AuthResponse";
9
9
  import { initialLocationHref } from "./initialLocationHref";
10
10
  import { captureFetch } from "./trustedFetch";
11
+ import { debug966975 } from "./debug966975";
11
12
 
12
13
  captureFetch();
13
14
 
@@ -15,8 +16,19 @@ const globalContext = {
15
16
  previousCall: id<{ isHandled: boolean } | undefined>(undefined)
16
17
  };
17
18
 
19
+ debug966975.log(
20
+ `=================== Evaluating the handleOidcCallback file, isInIframe: ${
21
+ window.self !== window.top ? "true" : "false"
22
+ }, location.href: ${initialLocationHref}`
23
+ );
24
+
18
25
  export function handleOidcCallback(): { isHandled: boolean } {
19
26
  if (globalContext.previousCall !== undefined) {
27
+ debug966975.log(
28
+ `handleOidcCallback() call, it has been called previously ${JSON.stringify(
29
+ globalContext.previousCall
30
+ )}`
31
+ );
20
32
  return globalContext.previousCall;
21
33
  }
22
34
 
@@ -24,16 +36,20 @@ export function handleOidcCallback(): { isHandled: boolean } {
24
36
  }
25
37
 
26
38
  function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
39
+ debug966975.log(`In handleOidcCallback_nonMemoized()`);
40
+
27
41
  const location_urlObj = new URL(initialLocationHref);
28
42
 
29
43
  const stateQueryParamValue = (() => {
30
44
  const stateQueryParamValue = location_urlObj.searchParams.get("state");
31
45
 
32
46
  if (stateQueryParamValue === null) {
47
+ debug966975.log("No state in url");
33
48
  return undefined;
34
49
  }
35
50
 
36
51
  if (!getIsStatQueryParamValue({ maybeStateQueryParamValue: stateQueryParamValue })) {
52
+ debug966975.log(`State query param value ${stateQueryParamValue} is malformed`);
37
53
  return undefined;
38
54
  }
39
55
 
@@ -42,6 +58,9 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
42
58
  location_urlObj.searchParams.get("response_type") !== null &&
43
59
  location_urlObj.searchParams.get("redirect_uri") !== null
44
60
  ) {
61
+ debug966975.log(
62
+ "NOTE: We are probably in a Keycloakify theme and oidc-spa was loaded by mistake."
63
+ );
45
64
  // NOTE: We are probably in a Keycloakify theme and oidc-spa was loaded by mistake.
46
65
  return undefined;
47
66
  }
@@ -49,9 +68,13 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
49
68
  return stateQueryParamValue;
50
69
  })();
51
70
 
71
+ debug966975.log(`state query param value ${stateQueryParamValue ?? "undefined"}`);
72
+
52
73
  if (stateQueryParamValue === undefined) {
53
74
  const backForwardTracker = readBackForwardTracker();
54
75
 
76
+ debug966975.log(`backForwardTracker: ${JSON.stringify(backForwardTracker)}`);
77
+
55
78
  if (backForwardTracker !== undefined) {
56
79
  writeBackForwardTracker({
57
80
  backForwardTracker: {
@@ -61,6 +84,8 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
61
84
  });
62
85
  }
63
86
 
87
+ debug966975.log("returning isHandled false");
88
+
64
89
  return { isHandled: false };
65
90
  }
66
91
 
@@ -73,6 +98,8 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
73
98
 
74
99
  const stateData = getStateData({ stateQueryParamValue });
75
100
 
101
+ debug966975.log(`stateData: ${JSON.stringify(stateData)}`);
102
+
76
103
  if (
77
104
  stateData === undefined ||
78
105
  (stateData.context === "redirect" && stateData.hasBeenProcessedByCallback)
@@ -96,6 +123,8 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
96
123
  }
97
124
  })();
98
125
 
126
+ debug966975.log(`historyMethod: ${historyMethod}`);
127
+
99
128
  writeBackForwardTracker({
100
129
  backForwardTracker: {
101
130
  previousHistoryMethod: historyMethod,
@@ -105,7 +134,13 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
105
134
 
106
135
  reloadOnBfCacheNavigation();
107
136
 
108
- window.history[historyMethod]();
137
+ setTimeout(() => {
138
+ debug966975.log(`(callback 0) Calling window.history.${historyMethod}()`);
139
+
140
+ window.history[historyMethod]();
141
+ }, 0);
142
+
143
+ debug966975.log(`returning isHandled: ${isHandled ? "true" : "false"}`);
109
144
 
110
145
  return { isHandled };
111
146
  }
@@ -118,9 +153,14 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
118
153
 
119
154
  assert(authResponse.state !== "", "063965");
120
155
 
156
+ debug966975.log(`authResponse: ${JSON.stringify(authResponse)}`);
157
+
121
158
  switch (stateData.context) {
122
159
  case "iframe":
123
- parent.postMessage(authResponse, location.origin);
160
+ setTimeout(() => {
161
+ debug966975.log(`(callback 0) posting message to parent`);
162
+ parent.postMessage(authResponse, location.origin);
163
+ }, 0);
124
164
  break;
125
165
  case "redirect":
126
166
  markStateDataAsProcessedByCallback({ stateQueryParamValue });
@@ -129,16 +169,24 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
129
169
  authResponses: [...readRedirectAuthResponses(), authResponse]
130
170
  });
131
171
  reloadOnBfCacheNavigation();
132
- location.href = (() => {
133
- if (stateData.action === "login" && authResponse.error === "consent_required") {
134
- return stateData.redirectUrl_consentRequiredCase;
135
- }
172
+ setTimeout(() => {
173
+ const href = (() => {
174
+ if (stateData.action === "login" && authResponse.error === "consent_required") {
175
+ return stateData.redirectUrl_consentRequiredCase;
176
+ }
136
177
 
137
- return stateData.redirectUrl;
138
- })();
178
+ return stateData.redirectUrl;
179
+ })();
180
+
181
+ debug966975.log(`(callback 0) location.href = "${href}";`);
182
+
183
+ location.href = href;
184
+ }, 0);
139
185
  break;
140
186
  }
141
187
 
188
+ debug966975.log(`Returning isHandled: ${isHandled ? "true" : "false"}`);
189
+
142
190
  return { isHandled };
143
191
  }
144
192
 
@@ -172,16 +220,31 @@ export function retrieveRedirectAuthResponseAndStateData(params: {
172
220
  }): { authResponse: AuthResponse; stateData: StateData.Redirect } | undefined {
173
221
  const { configId } = params;
174
222
 
223
+ debug966975.log(`>>> In retrieveRedirectAuthResponseAndStateData(${JSON.stringify({ configId })})`);
224
+
175
225
  const authResponses = readRedirectAuthResponses();
176
226
 
227
+ debug966975.log(`authResponses: ${JSON.stringify(authResponses)}`);
228
+
177
229
  let authResponseAndStateData:
178
230
  | { authResponse: AuthResponse; stateData: StateData.Redirect }
179
231
  | undefined = undefined;
180
232
 
181
233
  for (const authResponse of [...authResponses]) {
234
+ debug966975.log(`authResponse: ${JSON.stringify(authResponse)}`);
235
+
182
236
  const stateData = getStateData({ stateQueryParamValue: authResponse.state });
183
237
 
184
- assert(stateData !== undefined, "966975");
238
+ debug966975.log(`stateDate: ${JSON.stringify(stateData)}`);
239
+
240
+ try {
241
+ assert(stateData !== undefined, "966975");
242
+ } catch {
243
+ authResponses.splice(authResponses.indexOf(authResponse), 1);
244
+ debug966975.report();
245
+ continue;
246
+ }
247
+
185
248
  assert(stateData.context === "redirect", "474728");
186
249
 
187
250
  if (stateData.configId !== configId) {
@@ -194,9 +257,12 @@ export function retrieveRedirectAuthResponseAndStateData(params: {
194
257
  }
195
258
 
196
259
  if (authResponseAndStateData !== undefined) {
260
+ debug966975.log(`writeRedirectAuthResponses(${JSON.stringify({ authResponses })})`);
197
261
  writeRedirectAuthResponses({ authResponses });
198
262
  }
199
263
 
264
+ debug966975.log(`Returning ${JSON.stringify({ authResponseAndStateData })} <<<<<<<<<`);
265
+
200
266
  return authResponseAndStateData;
201
267
  }
202
268
 
@@ -36,6 +36,7 @@ export async function loginSilent(params: {
36
36
  | undefined;
37
37
 
38
38
  getExtraTokenParams: (() => Record<string, string | undefined>) | undefined;
39
+ autoLogin: boolean;
39
40
  }): Promise<ResultOfLoginSilent> {
40
41
  const {
41
42
  oidcClientTsUserManager,
@@ -43,17 +44,21 @@ export async function loginSilent(params: {
43
44
  configId,
44
45
  transformUrlBeforeRedirect_next,
45
46
  getExtraQueryParams,
46
- getExtraTokenParams
47
+ getExtraTokenParams,
48
+ autoLogin
47
49
  } = params;
48
50
 
49
51
  const dResult = new Deferred<ResultOfLoginSilent>();
50
52
 
51
53
  const timeoutDelayMs: number = (() => {
54
+ if (autoLogin) {
55
+ return 25_000;
56
+ }
57
+
52
58
  const downlinkAndRtt = getDownlinkAndRtt();
53
59
  const isDev = getIsDev();
54
60
 
55
61
  // Base delay is the minimum delay we should wait in any case
56
- //const BASE_DELAY_MS = 3000;
57
62
  const BASE_DELAY_MS = isDev ? 9_000 : 7_000;
58
63
 
59
64
  if (downlinkAndRtt === undefined) {
@@ -148,7 +148,7 @@ export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, u
148
148
  ) {
149
149
  console.warn(
150
150
  [
151
- "The OIDC refresh token shorter than the one of the access token.",
151
+ "The OIDC refresh token expirationTime is shorter than the one of the access token.",
152
152
  "This is very unusual and probably a misconfiguration."
153
153
  ].join(" ")
154
154
  );
@@ -0,0 +1,42 @@
1
+ export function getUserEnvironmentInfo(): string {
2
+ function safeGet<T>(getter: () => T, fallback: string = "Unknown"): string {
3
+ try {
4
+ const value = getter();
5
+ return value != null ? String(value) : fallback;
6
+ } catch {
7
+ return fallback;
8
+ }
9
+ }
10
+
11
+ const ua = safeGet(() => navigator.userAgent);
12
+ const platform = safeGet(() => navigator.platform);
13
+ const language = safeGet(() => navigator.language || (navigator as any).userLanguage);
14
+ const screenSize = safeGet(() => `${screen.width}x${screen.height}`);
15
+ const timezone = safeGet(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
16
+
17
+ const browser: string = (() => {
18
+ if (ua.includes("Firefox/")) return "Firefox";
19
+ if (ua.includes("Edg/")) return "Edge";
20
+ if (ua.includes("Chrome/") && !ua.includes("Edg/")) return "Chrome";
21
+ if (ua.includes("Safari/") && !ua.includes("Chrome/")) return "Safari";
22
+ if (ua.includes("OPR/") || ua.includes("Opera/")) return "Opera";
23
+ return "Unknown";
24
+ })();
25
+
26
+ const os: string = (() => {
27
+ if (platform.startsWith("Win")) return "Windows";
28
+ if (platform.startsWith("Mac")) return "macOS";
29
+ if (platform.startsWith("Linux")) return "Linux";
30
+ if (/Android/.test(ua)) return "Android";
31
+ if (/iPhone|iPad|iPod/.test(ua)) return "iOS";
32
+ return "Unknown";
33
+ })();
34
+
35
+ return `Browser: ${browser}
36
+ OS: ${os}
37
+ Platform: ${platform}
38
+ Language: ${language}
39
+ Screen: ${screenSize}
40
+ Timezone: ${timezone}
41
+ User Agent: ${ua}`;
42
+ }
@@ -0,0 +1 @@
1
+ export declare function getUserEnvironmentInfo(): string;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getUserEnvironmentInfo = getUserEnvironmentInfo;
4
+ function getUserEnvironmentInfo() {
5
+ function safeGet(getter, fallback) {
6
+ if (fallback === void 0) { fallback = "Unknown"; }
7
+ try {
8
+ var value = getter();
9
+ return value != null ? String(value) : fallback;
10
+ }
11
+ catch (_a) {
12
+ return fallback;
13
+ }
14
+ }
15
+ var ua = safeGet(function () { return navigator.userAgent; });
16
+ var platform = safeGet(function () { return navigator.platform; });
17
+ var language = safeGet(function () { return navigator.language || navigator.userLanguage; });
18
+ var screenSize = safeGet(function () { return "".concat(screen.width, "x").concat(screen.height); });
19
+ var timezone = safeGet(function () { return Intl.DateTimeFormat().resolvedOptions().timeZone; });
20
+ var browser = (function () {
21
+ if (ua.includes("Firefox/"))
22
+ return "Firefox";
23
+ if (ua.includes("Edg/"))
24
+ return "Edge";
25
+ if (ua.includes("Chrome/") && !ua.includes("Edg/"))
26
+ return "Chrome";
27
+ if (ua.includes("Safari/") && !ua.includes("Chrome/"))
28
+ return "Safari";
29
+ if (ua.includes("OPR/") || ua.includes("Opera/"))
30
+ return "Opera";
31
+ return "Unknown";
32
+ })();
33
+ var os = (function () {
34
+ if (platform.startsWith("Win"))
35
+ return "Windows";
36
+ if (platform.startsWith("Mac"))
37
+ return "macOS";
38
+ if (platform.startsWith("Linux"))
39
+ return "Linux";
40
+ if (/Android/.test(ua))
41
+ return "Android";
42
+ if (/iPhone|iPad|iPod/.test(ua))
43
+ return "iOS";
44
+ return "Unknown";
45
+ })();
46
+ return "Browser: ".concat(browser, "\nOS: ").concat(os, "\nPlatform: ").concat(platform, "\nLanguage: ").concat(language, "\nScreen: ").concat(screenSize, "\nTimezone: ").concat(timezone, "\nUser Agent: ").concat(ua);
47
+ }
48
+ //# sourceMappingURL=getUserEnvironmentInfo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getUserEnvironmentInfo.js","sourceRoot":"","sources":["../src/tools/getUserEnvironmentInfo.ts"],"names":[],"mappings":";;AAAA,wDAyCC;AAzCD,SAAgB,sBAAsB;IAClC,SAAS,OAAO,CAAI,MAAe,EAAE,QAA4B;QAA5B,yBAAA,EAAA,oBAA4B;QAC7D,IAAI,CAAC;YACD,IAAM,KAAK,GAAG,MAAM,EAAE,CAAC;YACvB,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACpD,CAAC;QAAC,WAAM,CAAC;YACL,OAAO,QAAQ,CAAC;QACpB,CAAC;IACL,CAAC;IAED,IAAM,EAAE,GAAG,OAAO,CAAC,cAAM,OAAA,SAAS,CAAC,SAAS,EAAnB,CAAmB,CAAC,CAAC;IAC9C,IAAM,QAAQ,GAAG,OAAO,CAAC,cAAM,OAAA,SAAS,CAAC,QAAQ,EAAlB,CAAkB,CAAC,CAAC;IACnD,IAAM,QAAQ,GAAG,OAAO,CAAC,cAAM,OAAA,SAAS,CAAC,QAAQ,IAAK,SAAiB,CAAC,YAAY,EAArD,CAAqD,CAAC,CAAC;IACtF,IAAM,UAAU,GAAG,OAAO,CAAC,cAAM,OAAA,UAAG,MAAM,CAAC,KAAK,cAAI,MAAM,CAAC,MAAM,CAAE,EAAlC,CAAkC,CAAC,CAAC;IACrE,IAAM,QAAQ,GAAG,OAAO,CAAC,cAAM,OAAA,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAQ,EAAhD,CAAgD,CAAC,CAAC;IAEjF,IAAM,OAAO,GAAW,CAAC;QACrB,IAAI,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC;YAAE,OAAO,SAAS,CAAC;QAC9C,IAAI,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,MAAM,CAAC;QACvC,IAAI,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,QAAQ,CAAC;QACpE,IAAI,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,OAAO,QAAQ,CAAC;QACvE,IAAI,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,OAAO,CAAC;QACjE,OAAO,SAAS,CAAC;IACrB,CAAC,CAAC,EAAE,CAAC;IAEL,IAAM,EAAE,GAAW,CAAC;QAChB,IAAI,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QACjD,IAAI,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,OAAO,OAAO,CAAC;QAC/C,IAAI,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,OAAO,CAAC;QACjD,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,OAAO,SAAS,CAAC;QACzC,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,OAAO,KAAK,CAAC;QAC9C,OAAO,SAAS,CAAC;IACrB,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,mBAAY,OAAO,mBACxB,EAAE,yBACI,QAAQ,yBACR,QAAQ,uBACV,UAAU,yBACR,QAAQ,2BACN,EAAE,CAAE,CAAC;AACnB,CAAC"}