keycloakify 10.0.0-rc.48 → 10.0.0-rc.49
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/bin/526.index.js +15 -3
- package/bin/932.index.js +5 -1
- package/login/lib/useUserProfileForm.js +175 -149
- package/login/lib/useUserProfileForm.js.map +1 -1
- package/package.json +6 -2
- package/src/bin/initialize-email-theme.ts +1 -0
- package/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl +11 -1
- package/src/bin/shared/promptKeycloakVersion.ts +6 -1
- package/src/bin/start-keycloak/myrealm-realm-18.json +2155 -0
- package/src/bin/start-keycloak/myrealm-realm-19.json +2186 -0
- package/src/bin/start-keycloak/myrealm-realm-20.json +2197 -0
- package/src/bin/start-keycloak/myrealm-realm-21.json +2201 -0
- package/src/bin/start-keycloak/myrealm-realm-23.json +20 -12
- package/src/bin/start-keycloak/myrealm-realm-24.json +10 -10
- package/src/bin/start-keycloak/myrealm-realm-25.json +2400 -0
- package/src/bin/start-keycloak/start-keycloak.ts +14 -2
- package/src/login/lib/useUserProfileForm.tsx +182 -180
- package/src/tools/formatNumber.ts +3 -2
- package/tools/formatNumber.js +2 -1
- package/tools/formatNumber.js.map +1 -1
@@ -159,7 +159,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|
159
159
|
);
|
160
160
|
|
161
161
|
const { keycloakVersion } = await promptKeycloakVersion({
|
162
|
-
startingFromMajor:
|
162
|
+
startingFromMajor: 18,
|
163
|
+
excludeMajorVersions: [22],
|
163
164
|
cacheDirPath: buildContext.cacheDirPath
|
164
165
|
});
|
165
166
|
|
@@ -231,6 +232,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|
231
232
|
|
232
233
|
const realmJsonFilePath = await (async () => {
|
233
234
|
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
235
|
+
if (cliCommandOptions.realmJsonFilePath === "none") {
|
236
|
+
return undefined;
|
237
|
+
}
|
238
|
+
|
234
239
|
console.log(
|
235
240
|
chalk.green(
|
236
241
|
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
|
@@ -312,6 +317,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|
312
317
|
|
313
318
|
await extractThemeResourcesFromJar();
|
314
319
|
|
320
|
+
const jarFilePath_cacheDir = pathJoin(buildContext.cacheDirPath, jarFileBasename);
|
321
|
+
|
322
|
+
fs.copyFileSync(jarFilePath, jarFilePath_cacheDir);
|
323
|
+
|
315
324
|
try {
|
316
325
|
child_process.execSync(`docker rm --force ${containerName}`, {
|
317
326
|
stdio: "ignore"
|
@@ -332,7 +341,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|
332
341
|
"-v",
|
333
342
|
`${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json`
|
334
343
|
]),
|
335
|
-
...[
|
344
|
+
...[
|
345
|
+
"-v",
|
346
|
+
`${jarFilePath_cacheDir}:/opt/keycloak/providers/keycloak-theme.jar`
|
347
|
+
],
|
336
348
|
...(keycloakMajorVersionNumber <= 20
|
337
349
|
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
|
338
350
|
: []),
|
@@ -130,168 +130,137 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
|
130
130
|
const initialState = useMemo((): internal.State => {
|
131
131
|
// NOTE: We don't use te kcContext.profile.attributes directly because
|
132
132
|
// they don't includes the password and password confirm fields and we want to add them.
|
133
|
-
//
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
"attributesByName" in kcContext.profile &&
|
144
|
-
Object.keys(kcContext.profile.attributesByName).length !== 0
|
145
|
-
) {
|
146
|
-
break retrocompat_patch;
|
147
|
-
}
|
148
|
-
|
149
|
-
if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
|
150
|
-
//NOTE: Handle legacy register.ftl page
|
151
|
-
return (["firstName", "lastName", "email", "username"] as const)
|
152
|
-
.filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
|
153
|
-
.map(name =>
|
154
|
-
id<Attribute>({
|
155
|
-
name: name,
|
156
|
-
displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
|
157
|
-
required: true,
|
158
|
-
value: (kcContext.register as any).formData[name] ?? "",
|
159
|
-
html5DataAnnotations: {},
|
160
|
-
readOnly: false,
|
161
|
-
validators: {},
|
162
|
-
annotations: {},
|
163
|
-
autocomplete: (() => {
|
164
|
-
switch (name) {
|
165
|
-
case "email":
|
166
|
-
return "email";
|
167
|
-
case "username":
|
168
|
-
return "username";
|
169
|
-
default:
|
170
|
-
return undefined;
|
171
|
-
}
|
172
|
-
})()
|
173
|
-
})
|
174
|
-
);
|
175
|
-
}
|
133
|
+
// We also want to apply some retro-compatibility and consistency patches.
|
134
|
+
const attributes: Attribute[] = (() => {
|
135
|
+
mock_user_profile_attributes_for_older_keycloak_versions: {
|
136
|
+
if (
|
137
|
+
"profile" in kcContext &&
|
138
|
+
"attributesByName" in kcContext.profile &&
|
139
|
+
Object.keys(kcContext.profile.attributesByName).length !== 0
|
140
|
+
) {
|
141
|
+
break mock_user_profile_attributes_for_older_keycloak_versions;
|
142
|
+
}
|
176
143
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
144
|
+
if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
|
145
|
+
//NOTE: Handle legacy register.ftl page
|
146
|
+
return (["firstName", "lastName", "email", "username"] as const)
|
147
|
+
.filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
|
148
|
+
.map(name =>
|
149
|
+
id<Attribute>({
|
150
|
+
name: name,
|
151
|
+
displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
|
152
|
+
required: true,
|
153
|
+
value: (kcContext.register as any).formData[name] ?? "",
|
154
|
+
html5DataAnnotations: {},
|
155
|
+
readOnly: false,
|
156
|
+
validators: {},
|
157
|
+
annotations: {},
|
158
|
+
autocomplete: (() => {
|
159
|
+
switch (name) {
|
160
|
+
case "email":
|
161
|
+
return "email";
|
162
|
+
case "username":
|
163
|
+
return "username";
|
164
|
+
default:
|
165
|
+
return undefined;
|
166
|
+
}
|
167
|
+
})()
|
168
|
+
})
|
169
|
+
);
|
170
|
+
}
|
204
171
|
|
205
|
-
|
206
|
-
|
207
|
-
|
172
|
+
if ("user" in kcContext && kcContext.user instanceof Object) {
|
173
|
+
//NOTE: Handle legacy login-update-profile.ftl
|
174
|
+
return (["username", "email", "firstName", "lastName"] as const)
|
175
|
+
.filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
|
176
|
+
.map(name =>
|
208
177
|
id<Attribute>({
|
209
|
-
name:
|
210
|
-
displayName: id<`\${${MessageKey}}`>(`\${
|
178
|
+
name: name,
|
179
|
+
displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
|
211
180
|
required: true,
|
212
|
-
value: (kcContext
|
181
|
+
value: (kcContext as any).user[name] ?? "",
|
213
182
|
html5DataAnnotations: {},
|
214
183
|
readOnly: false,
|
215
184
|
validators: {},
|
216
185
|
annotations: {},
|
217
|
-
autocomplete:
|
186
|
+
autocomplete: (() => {
|
187
|
+
switch (name) {
|
188
|
+
case "email":
|
189
|
+
return "email";
|
190
|
+
case "username":
|
191
|
+
return "username";
|
192
|
+
default:
|
193
|
+
return undefined;
|
194
|
+
}
|
195
|
+
})()
|
218
196
|
})
|
219
|
-
|
220
|
-
|
197
|
+
);
|
198
|
+
}
|
221
199
|
|
222
|
-
|
200
|
+
if ("email" in kcContext && kcContext.email instanceof Object) {
|
201
|
+
//NOTE: Handle legacy update-email.ftl
|
202
|
+
return [
|
203
|
+
id<Attribute>({
|
204
|
+
name: "email",
|
205
|
+
displayName: id<`\${${MessageKey}}`>(`\${email}`),
|
206
|
+
required: true,
|
207
|
+
value: (kcContext.email as any).value ?? "",
|
208
|
+
html5DataAnnotations: {},
|
209
|
+
readOnly: false,
|
210
|
+
validators: {},
|
211
|
+
annotations: {},
|
212
|
+
autocomplete: "email"
|
213
|
+
})
|
214
|
+
];
|
223
215
|
}
|
224
216
|
|
225
|
-
|
226
|
-
|
227
|
-
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } =
|
228
|
-
attribute_pre_group_patch as Attribute & {
|
229
|
-
group: string;
|
230
|
-
groupDisplayHeader?: string;
|
231
|
-
groupDisplayDescription?: string;
|
232
|
-
groupAnnotations: Record<string, string>;
|
233
|
-
};
|
234
|
-
|
235
|
-
return id<Attribute>({
|
236
|
-
...rest,
|
237
|
-
group: {
|
238
|
-
name: group,
|
239
|
-
displayHeader: groupDisplayHeader,
|
240
|
-
displayDescription: groupDisplayDescription,
|
241
|
-
html5DataAnnotations: {}
|
242
|
-
}
|
243
|
-
});
|
244
|
-
}
|
217
|
+
assert(false, "Unable to mock user profile from the current kcContext");
|
218
|
+
}
|
245
219
|
|
246
|
-
|
247
|
-
|
248
|
-
})();
|
220
|
+
return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
|
221
|
+
})();
|
249
222
|
|
250
|
-
|
251
|
-
|
223
|
+
// Retro-compatibility and consistency patches
|
224
|
+
attributes.forEach(attribute => {
|
225
|
+
patch_legacy_group: {
|
226
|
+
if (typeof attribute.group !== "string") {
|
227
|
+
break patch_legacy_group;
|
228
|
+
}
|
252
229
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
230
|
+
const { group, groupDisplayHeader, groupDisplayDescription /*, groupAnnotations*/ } = attribute as Attribute & {
|
231
|
+
group: string;
|
232
|
+
groupDisplayHeader?: string;
|
233
|
+
groupDisplayDescription?: string;
|
234
|
+
groupAnnotations: Record<string, string>;
|
235
|
+
};
|
257
236
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
237
|
+
delete attribute.group;
|
238
|
+
// @ts-expect-error
|
239
|
+
delete attribute.groupDisplayHeader;
|
240
|
+
// @ts-expect-error
|
241
|
+
delete attribute.groupDisplayDescription;
|
242
|
+
// @ts-expect-error
|
243
|
+
delete attribute.groupAnnotations;
|
263
244
|
|
264
|
-
|
265
|
-
|
266
|
-
name: "password",
|
267
|
-
displayName: id<`\${${MessageKey}}`>("${password}"),
|
268
|
-
required: true,
|
269
|
-
readOnly: false,
|
270
|
-
validators: {},
|
271
|
-
annotations: {},
|
272
|
-
autocomplete: "new-password",
|
273
|
-
html5DataAnnotations: {},
|
274
|
-
// NOTE: Compat with Keycloak version prior to 24
|
275
|
-
...({ groupAnnotations: {} } as {})
|
276
|
-
},
|
277
|
-
{
|
278
|
-
name: "password-confirm",
|
279
|
-
displayName: id<`\${${MessageKey}}`>("${passwordConfirm}"),
|
280
|
-
required: true,
|
281
|
-
readOnly: false,
|
282
|
-
validators: {},
|
283
|
-
annotations: {},
|
284
|
-
html5DataAnnotations: {},
|
285
|
-
autocomplete: "new-password",
|
286
|
-
// NOTE: Compat with Keycloak version prior to 24
|
287
|
-
...({ groupAnnotations: {} } as {})
|
288
|
-
}
|
289
|
-
);
|
245
|
+
if (group === "") {
|
246
|
+
break patch_legacy_group;
|
290
247
|
}
|
248
|
+
|
249
|
+
attribute.group = {
|
250
|
+
name: group,
|
251
|
+
displayHeader: groupDisplayHeader,
|
252
|
+
displayDescription: groupDisplayDescription,
|
253
|
+
html5DataAnnotations: {}
|
254
|
+
};
|
255
|
+
}
|
256
|
+
|
257
|
+
// Attributes with options rendered by default as select inputs
|
258
|
+
if (attribute.validators.options !== undefined && attribute.annotations.inputType === undefined) {
|
259
|
+
attribute.annotations.inputType = "select";
|
291
260
|
}
|
292
261
|
|
293
|
-
//
|
294
|
-
|
262
|
+
// Consistency patch on values/value property
|
263
|
+
{
|
295
264
|
if (getIsMultivaluedSingleField({ attribute })) {
|
296
265
|
attribute.multivalued = true;
|
297
266
|
}
|
@@ -303,65 +272,98 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
|
303
272
|
attribute.value ??= attribute.values?.[0];
|
304
273
|
delete attribute.values;
|
305
274
|
}
|
306
|
-
}
|
275
|
+
}
|
276
|
+
});
|
307
277
|
|
308
|
-
|
309
|
-
|
278
|
+
add_password_and_password_confirm: {
|
279
|
+
if (!kcContext.passwordRequired) {
|
280
|
+
break add_password_and_password_confirm;
|
281
|
+
}
|
310
282
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
283
|
+
attributes.forEach((attribute, i) => {
|
284
|
+
if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) {
|
285
|
+
// NOTE: We want to add password and password-confirm after the field that identifies the user.
|
286
|
+
// It's either email or username.
|
287
|
+
return;
|
288
|
+
}
|
316
289
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
290
|
+
attributes.splice(
|
291
|
+
i + 1,
|
292
|
+
0,
|
293
|
+
{
|
294
|
+
name: "password",
|
295
|
+
displayName: id<`\${${MessageKey}}`>("${password}"),
|
296
|
+
required: true,
|
297
|
+
readOnly: false,
|
298
|
+
validators: {},
|
299
|
+
annotations: {},
|
300
|
+
autocomplete: "new-password",
|
301
|
+
html5DataAnnotations: {}
|
302
|
+
},
|
303
|
+
{
|
304
|
+
name: "password-confirm",
|
305
|
+
displayName: id<`\${${MessageKey}}`>("${passwordConfirm}"),
|
306
|
+
required: true,
|
307
|
+
readOnly: false,
|
308
|
+
validators: {},
|
309
|
+
annotations: {},
|
310
|
+
html5DataAnnotations: {},
|
311
|
+
autocomplete: "new-password"
|
321
312
|
}
|
313
|
+
);
|
314
|
+
});
|
315
|
+
}
|
322
316
|
|
323
|
-
|
317
|
+
const initialFormFieldState: {
|
318
|
+
attribute: Attribute;
|
319
|
+
valueOrValues: string | string[];
|
320
|
+
}[] = [];
|
324
321
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
322
|
+
for (const attribute of attributes) {
|
323
|
+
handle_multi_valued_attribute: {
|
324
|
+
if (!attribute.multivalued) {
|
325
|
+
break handle_multi_valued_attribute;
|
326
|
+
}
|
329
327
|
|
330
|
-
|
328
|
+
const values = attribute.values?.length ? attribute.values : [""];
|
331
329
|
|
332
|
-
|
333
|
-
|
334
|
-
|
330
|
+
apply_validator_min_range: {
|
331
|
+
if (getIsMultivaluedSingleField({ attribute })) {
|
332
|
+
break apply_validator_min_range;
|
333
|
+
}
|
335
334
|
|
336
|
-
|
335
|
+
const validator = attribute.validators.multivalued;
|
337
336
|
|
338
|
-
|
339
|
-
|
340
|
-
|
337
|
+
if (validator === undefined) {
|
338
|
+
break apply_validator_min_range;
|
339
|
+
}
|
341
340
|
|
342
|
-
|
341
|
+
const { min: minStr } = validator;
|
343
342
|
|
344
|
-
|
345
|
-
|
346
|
-
}
|
343
|
+
if (!minStr) {
|
344
|
+
break apply_validator_min_range;
|
347
345
|
}
|
348
346
|
|
349
|
-
|
350
|
-
attribute,
|
351
|
-
valueOrValues: values
|
352
|
-
});
|
347
|
+
const min = parseInt(`${minStr}`);
|
353
348
|
|
354
|
-
|
349
|
+
for (let index = values.length; index < min; index++) {
|
350
|
+
values.push("");
|
351
|
+
}
|
355
352
|
}
|
356
353
|
|
357
|
-
|
354
|
+
initialFormFieldState.push({
|
358
355
|
attribute,
|
359
|
-
valueOrValues:
|
356
|
+
valueOrValues: values
|
360
357
|
});
|
358
|
+
|
359
|
+
continue;
|
361
360
|
}
|
362
361
|
|
363
|
-
|
364
|
-
|
362
|
+
initialFormFieldState.push({
|
363
|
+
attribute,
|
364
|
+
valueOrValues: attribute.value ?? ""
|
365
|
+
});
|
366
|
+
}
|
365
367
|
|
366
368
|
const initialState: internal.State = {
|
367
369
|
formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
|
@@ -1,4 +1,4 @@
|
|
1
|
-
export const formatNumber = (input: string, format: string)
|
1
|
+
export const formatNumber = (input: string, format: string) => {
|
2
2
|
if (!input) {
|
3
3
|
return "";
|
4
4
|
}
|
@@ -20,7 +20,8 @@ export const formatNumber = (input: string, format: string): string => {
|
|
20
20
|
let rawValue = input.replace(/\D+/g, "");
|
21
21
|
|
22
22
|
// make sure the value is a number
|
23
|
-
|
23
|
+
// @ts-expect-error: It's Keycloak's code, we trust it.
|
24
|
+
if (parseInt(rawValue) != rawValue) {
|
24
25
|
return "";
|
25
26
|
}
|
26
27
|
|
package/tools/formatNumber.js
CHANGED
@@ -12,7 +12,8 @@ export const formatNumber = (input, format) => {
|
|
12
12
|
// keep only digits
|
13
13
|
let rawValue = input.replace(/\D+/g, "");
|
14
14
|
// make sure the value is a number
|
15
|
-
|
15
|
+
// @ts-expect-error: It's Keycloak's code, we trust it.
|
16
|
+
if (parseInt(rawValue) != rawValue) {
|
16
17
|
return "";
|
17
18
|
}
|
18
19
|
// make sure the number of digits does not exceed the maximum size
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"formatNumber.js","sourceRoot":"","sources":["../src/tools/formatNumber.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAa,EAAE,MAAc,
|
1
|
+
{"version":3,"file":"formatNumber.js","sourceRoot":"","sources":["../src/tools/formatNumber.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAa,EAAE,MAAc,EAAE,EAAE;IAC1D,IAAI,CAAC,KAAK,EAAE;QACR,OAAO,EAAE,CAAC;KACb;IAED,4EAA4E;IAC5E,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAE5C,IAAI,CAAC,YAAY,EAAE;QACf,OAAO,EAAE,CAAC;KACb;IAED,0FAA0F;IAC1F,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAC/B,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EACnE,CAAC,CACJ,CAAC;IAEF,mBAAmB;IACnB,IAAI,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAEzC,kCAAkC;IAClC,uDAAuD;IACvD,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,EAAE;QAChC,OAAO,EAAE,CAAC;KACb;IAED,kEAAkE;IAClE,IAAI,QAAQ,CAAC,MAAM,GAAG,OAAO,EAAE;QAC3B,QAAQ,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;KAC7C;IAED,kEAAkE;IAClE,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAEhF,mFAAmF;IACnF,IAAI,MAAM,GAAG,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAElD,oDAAoD;IACpD,IAAI,CAAC,MAAM,EAAE;QACT,OAAO,KAAK,CAAC;KAChB;IAED,IAAI,MAAM,GAAG,MAAM,CAAC;IAEpB,oEAAoE;IACpE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QAC1C,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KAC3D;IAED,OAAO,MAAM,CAAC;AAClB,CAAC,CAAC"}
|