ensure no diff if config hasn't changed

This commit is contained in:
Joseph Garrone 2024-12-19 17:42:26 +01:00
parent 474a863708
commit f43544e134
17 changed files with 607 additions and 193 deletions

View File

@ -3,10 +3,9 @@ import child_process from "child_process";
import { SemVer } from "../src/bin/tools/SemVer";
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig";
import { cacheDirPath } from "./shared/cacheDirPath";
import { runPrettier } from "../src/bin/tools/runPrettier";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { writeRealmJsonFile } from "../src/bin/start-keycloak/realmConfig/ParsedRealmJson";
import { join as pathJoin } from "path";
import * as fs from "fs";
import chalk from "chalk";
(async () => {
@ -26,9 +25,7 @@ import chalk from "chalk";
realmName: "myrealm"
});
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
const filePath = pathJoin(
const realmJsonFilePath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
@ -38,12 +35,11 @@ import chalk from "chalk";
`realm-kc-${keycloakMajorVersionNumber}.json`
);
sourceCode = await runPrettier({
sourceCode,
filePath
await writeRealmJsonFile({
parsedRealmJson,
realmJsonFilePath,
keycloakMajorVersionNumber
});
fs.writeFileSync(filePath, Buffer.from(sourceCode, "utf8"));
console.log(chalk.green(`Realm config dumped to ${filePath}`));
console.log(chalk.green(`Realm config dumped to ${realmJsonFilePath}`));
})();

View File

@ -1,8 +1,6 @@
import { z } from "zod";
import { assert, type Equals } from "tsafe/assert";
import { is } from "tsafe/is";
import { id } from "tsafe/id";
import * as fs from "fs";
export type ParsedRealmJson = {
realm: string;
@ -50,7 +48,7 @@ export type ParsedRealmJson = {
}[];
};
const zParsedRealmJson = (() => {
export const zParsedRealmJson = (() => {
type TargetType = ParsedRealmJson;
const zTargetType = z.object({
@ -118,19 +116,3 @@ const zParsedRealmJson = (() => {
return id<z.ZodType<TargetType>>(zTargetType);
})();
export function readRealmJsonFile(params: {
realmJsonFilePath: string;
}): ParsedRealmJson {
const { realmJsonFilePath } = params;
const parsedRealmJson = JSON.parse(
fs.readFileSync(realmJsonFilePath).toString("utf8")
) as unknown;
zParsedRealmJson.parse(parsedRealmJson);
assert(is<ParsedRealmJson>(parsedRealmJson));
return parsedRealmJson;
}

View File

@ -0,0 +1,3 @@
export type { ParsedRealmJson } from "./ParsedRealmJson";
export { readRealmJsonFile } from "./readRealmJsonFile";
export { writeRealmJsonFile } from "./writeRealmJsonFile";

View File

@ -0,0 +1,20 @@
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import * as fs from "fs";
import { type ParsedRealmJson, zParsedRealmJson } from "./ParsedRealmJson";
export function readRealmJsonFile(params: {
realmJsonFilePath: string;
}): ParsedRealmJson {
const { realmJsonFilePath } = params;
const parsedRealmJson = JSON.parse(
fs.readFileSync(realmJsonFilePath).toString("utf8")
) as unknown;
zParsedRealmJson.parse(parsedRealmJson);
assert(is<ParsedRealmJson>(parsedRealmJson));
return parsedRealmJson;
}

View File

@ -0,0 +1,29 @@
import * as fsPr from "fs/promises";
import { getIsPrettierAvailable, runPrettier } from "../../../tools/runPrettier";
import { canonicalStringify } from "../../../tools/canonicalStringify";
import type { ParsedRealmJson } from "./ParsedRealmJson";
import { getDefaultConfig } from "../defaultConfig";
export async function writeRealmJsonFile(params: {
realmJsonFilePath: string;
parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number;
}): Promise<void> {
const { realmJsonFilePath, parsedRealmJson, keycloakMajorVersionNumber } = params;
let sourceCode = canonicalStringify({
data: parsedRealmJson,
referenceData: getDefaultConfig({
keycloakMajorVersionNumber
})
});
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode: sourceCode,
filePath: realmJsonFilePath
});
}
await fsPr.writeFile(realmJsonFilePath, Buffer.from(sourceCode, "utf8"));
}

View File

@ -3,11 +3,10 @@ import { getThisCodebaseRootDirPath } from "../../../tools/getThisCodebaseRootDi
import * as fs from "fs";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import { type ParsedRealmJson, readRealmJsonFile } from "../ParsedRealmJson";
import { readRealmJsonFile } from "../ParsedRealmJson/readRealmJsonFile";
import type { ParsedRealmJson } from "../ParsedRealmJson/ParsedRealmJson";
export function getDefaultRealmJsonFilePath(params: {
keycloakMajorVersionNumber: number;
}) {
function getDefaultRealmJsonFilePath(params: { keycloakMajorVersionNumber: number }) {
const { keycloakMajorVersionNumber } = params;
return pathJoin(

View File

@ -756,6 +756,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1336,13 +1354,13 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper"
]
}
@ -1393,13 +1411,13 @@
"config": {
"allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper"
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper"
]
}
},
@ -1517,7 +1535,7 @@
"defaultLocale": "en",
"authenticationFlows": [
{
"id": "223ce532-2038-4f24-a606-2a5c73f7bd65",
"id": "f664efe4-102d-4ec1-bf11-11af67e3f178",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -1543,7 +1561,7 @@
]
},
{
"id": "57e47732-79cc-4d60-bee7-4f0b8fd44540",
"id": "8a5630c5-eca1-4b6a-8e59-459cb6c84535",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -1577,7 +1595,7 @@
]
},
{
"id": "c2735d89-60c0-45a4-9b3c-ae5df17df395",
"id": "c1a3eed3-25ce-44ae-93d1-f0b8148a0f8c",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1603,7 +1621,7 @@
]
},
{
"id": "11a5a507-2b9a-443f-961b-dffd66f4318d",
"id": "6eb188ad-1041-44dd-bf8f-37cae0d98bf1",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1629,7 +1647,7 @@
]
},
{
"id": "963bd753-6ea7-4d93-ab56-30f9ab59d597",
"id": "4ee215ac-f4e5-4edb-bf76-65dc9e211543",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1655,7 +1673,7 @@
]
},
{
"id": "1db6a489-a3b4-44c4-b480-1d1e8c123d20",
"id": "5a1eac7e-06a0-46d8-b9ae-1f2c934331f9",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -1681,7 +1699,7 @@
]
},
{
"id": "7a38f32d-4f34-450f-8f03-64802d7cb8f1",
"id": "ed165166-4521-4a62-b185-c4b51643cbb1",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -1707,7 +1725,7 @@
]
},
{
"id": "0df88739-3739-4d70-8893-47c546f19003",
"id": "4788fb1f-fd81-4f5d-9abe-4199dd641c1e",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -1734,7 +1752,7 @@
]
},
{
"id": "35025424-e291-4c54-8a29-70aadba549ce",
"id": "d778a70f-f472-4dd3-ac40-cb5612ddc171",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -1760,7 +1778,7 @@
]
},
{
"id": "1813b7f2-c3c2-4b92-8ffc-9ff2d12186c6",
"id": "9c1ea8ea-7c23-4e60-b02d-1900d9dc4109",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -1802,7 +1820,7 @@
]
},
{
"id": "954283ac-f1c2-40b6-a39f-bf23ff9f3ce8",
"id": "0ebdf418-d57d-4318-9359-7bd0cb2381f2",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -1844,7 +1862,7 @@
]
},
{
"id": "52a789ce-2cad-4f0f-93b2-295b7fd519f0",
"id": "5cc89293-c72e-4c5e-b31c-15558588a60d",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -1878,7 +1896,7 @@
]
},
{
"id": "5a6a71e1-9105-45b6-b5f0-52538461357b",
"id": "5ae5a321-ccac-449e-9c19-d6dc22ab8085",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -1896,7 +1914,7 @@
]
},
{
"id": "8392b6e7-bdbf-4d7f-97b6-885761c200db",
"id": "7737fdd1-0875-47e6-977b-12561cddfdc3",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -1923,7 +1941,7 @@
]
},
{
"id": "52136d70-8d08-42ea-b04b-cf40ea2807aa",
"id": "90f975c3-9826-461f-88ca-27c697aff86b",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -1949,7 +1967,7 @@
]
},
{
"id": "26bbc7e6-ef01-4cdb-9dba-520e2f3f8993",
"id": "ce2722d5-9f4f-41a2-8f81-e01f7b6cee57",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -1975,7 +1993,7 @@
]
},
{
"id": "f0887979-04eb-4033-8f19-0ffd8c8b7f6a",
"id": "31b5bfa7-98ad-47a2-b8e6-0669022cd8cb",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -1994,7 +2012,7 @@
]
},
{
"id": "a3b7b94b-bfbf-4760-a8c9-7d9cd98d262e",
"id": "bf8a950b-be3b-4e44-8602-64e0bba492eb",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -2036,7 +2054,7 @@
]
},
{
"id": "dc68a665-2e51-4a22-aaad-bd693ddc77cc",
"id": "e3519800-971b-4b1d-b64e-3983ccd02dea",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -2078,7 +2096,7 @@
]
},
{
"id": "ae6b73aa-1318-4ae8-a3d9-d01b5e7d957e",
"id": "9d5a33a2-e777-4beb-95de-b84812f69c56",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -2098,14 +2116,14 @@
],
"authenticatorConfig": [
{
"id": "0c18de7f-0714-41f4-9a3f-ed4edd53ae9c",
"id": "4901c91d-59bd-4727-b585-8e4e44828d0a",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "65b3c8bb-34a4-4d19-b578-245dc8ff53ea",
"id": "5062a078-83a7-4933-b0d5-3f75cc2a5003",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"

View File

@ -764,6 +764,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1344,14 +1362,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper"
"oidc-usermodel-property-mapper"
]
}
},
@ -1400,14 +1418,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper"
"saml-role-list-mapper"
]
}
},
@ -1525,7 +1543,7 @@
"defaultLocale": "en",
"authenticationFlows": [
{
"id": "1f4d4e13-1591-4751-8985-17886a8c98a9",
"id": "8ccfe057-5ce6-499b-9fae-3cd89b62bf01",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -1551,7 +1569,7 @@
]
},
{
"id": "126f07c3-1bcb-4a02-bf16-bb44674bf55d",
"id": "f3b9ab2e-41c2-4e73-876b-e2c275d6d14e",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -1585,7 +1603,7 @@
]
},
{
"id": "eb3a08c8-5f99-49b6-b02b-16b62571f273",
"id": "df1329cc-777c-42d8-aa2f-c5d5ddaaf5a4",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1611,7 +1629,7 @@
]
},
{
"id": "3dc19838-5025-4bbb-b569-b574bd5a8d90",
"id": "f78a4cbc-66ff-4caa-8066-67aff94946f4",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1637,7 +1655,7 @@
]
},
{
"id": "70d6fd40-d740-4dae-b0e6-350f8e9d4a1c",
"id": "4b20995b-5553-45db-86b0-05c3fe14edb1",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1663,7 +1681,7 @@
]
},
{
"id": "6e24dcb3-5818-483c-8e44-883858171901",
"id": "0a7cc6b7-e427-4f72-b44e-a02133241bad",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -1689,7 +1707,7 @@
]
},
{
"id": "ac6254cd-403b-457b-b308-22a2a0e4f99d",
"id": "e24e73c0-dd51-4fdc-a916-284f11f38487",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -1715,7 +1733,7 @@
]
},
{
"id": "485e74e6-9b3e-4b2c-a9b9-927802dc4f06",
"id": "37ee5a12-01c2-41b0-aafa-e9c6661ff544",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -1742,7 +1760,7 @@
]
},
{
"id": "ff9bb879-1d6a-4d1c-9836-1e4fab6f8997",
"id": "8902a1a7-c2ee-4648-869f-dd5ef89184fc",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -1768,7 +1786,7 @@
]
},
{
"id": "af8b2470-d581-401c-9984-762b966ebcc2",
"id": "77c78eed-4bcd-4779-b39f-10135be84946",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -1810,7 +1828,7 @@
]
},
{
"id": "414dbda4-eb3f-4baa-b23a-d3423af1eae6",
"id": "c6398883-01e6-47a1-bb97-c09f2983155d",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -1852,7 +1870,7 @@
]
},
{
"id": "1cae0c4b-8dfb-4f5d-a781-e74d0a13c940",
"id": "78ab5fb8-f35b-4053-b264-94b208000b13",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -1886,7 +1904,7 @@
]
},
{
"id": "e798b655-7d85-4b6b-aee7-1448a3e1e0ea",
"id": "959e154b-034e-413d-9b19-211e7d9ba33d",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -1904,7 +1922,7 @@
]
},
{
"id": "eb94b723-1041-426a-87bf-f7b4bd2f485d",
"id": "001e253d-bdbd-41e2-81c7-1c7b239feeb1",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -1931,7 +1949,7 @@
]
},
{
"id": "452d1d5f-7632-44d7-bc89-77ff2b209b3e",
"id": "45481bb0-18fe-4a26-a77c-35a5afe58436",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -1957,7 +1975,7 @@
]
},
{
"id": "7c1b9e8f-6b57-49d1-a9a7-494862f93c0f",
"id": "bb47b847-5a55-4c08-909e-9f6f8d8a0636",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -1983,7 +2001,7 @@
]
},
{
"id": "2b38f34a-1739-499e-bb24-1dff96f32009",
"id": "77e6e169-05b7-4b89-af00-09cfe1604eed",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -2002,7 +2020,7 @@
]
},
{
"id": "d26ae72b-a933-44dc-9927-1c82757004b2",
"id": "aef03fe8-1a70-40c3-879f-25588f75c119",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -2044,7 +2062,7 @@
]
},
{
"id": "222ee8d6-1892-4768-9ada-720274b6bf9a",
"id": "990abff7-e2ba-4217-984e-8890cbc2b3a9",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -2086,7 +2104,7 @@
]
},
{
"id": "e8b4d92c-27c1-4a9b-9b16-7ceb810fa230",
"id": "d9894cf6-2f99-493e-ac47-853f54bfc9c6",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -2106,14 +2124,14 @@
],
"authenticatorConfig": [
{
"id": "e5847a0b-855d-4d93-85fd-94714be3ed92",
"id": "101ed8ff-4383-4539-aa52-2d1e69698b78",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "a2a18aa4-bd4c-4c2a-9286-e9d6c64f4812",
"id": "049042a5-3551-4c16-81a1-64d86f5aa1e5",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"

View File

@ -775,6 +775,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1355,14 +1373,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"saml-user-attribute-mapper"
"saml-user-attribute-mapper",
"saml-role-list-mapper",
"saml-user-property-mapper"
]
}
},
@ -1411,14 +1429,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper"
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper"
]
}
},
@ -1536,7 +1554,7 @@
"defaultLocale": "en",
"authenticationFlows": [
{
"id": "c40791b4-4d59-4df2-bebd-2b71e793704f",
"id": "30a878f0-57aa-4d20-bab0-6cf1d7317a5c",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -1562,7 +1580,7 @@
]
},
{
"id": "8813b6d1-8b88-4672-b29b-8420ce3f3975",
"id": "d386affe-d1fe-472a-bee6-54105d0101f5",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -1596,7 +1614,7 @@
]
},
{
"id": "a9937c40-a1ee-4c57-adf7-ede0a9983953",
"id": "77b95bc0-bd0c-46b7-8240-3182023e9d50",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1622,7 +1640,7 @@
]
},
{
"id": "2d494b5a-eb73-40d0-94d3-a8d8024a7db4",
"id": "bc96d3d6-29a1-42af-a63e-bb67a8c6d78f",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1648,7 +1666,7 @@
]
},
{
"id": "2e977f5a-8110-412b-b704-3e15164dbb1b",
"id": "7697ca74-5c2b-45ab-9335-e0f6dec59b5c",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1674,7 +1692,7 @@
]
},
{
"id": "6f171b4b-8723-4e6d-bb1e-6b4293a7bb3f",
"id": "534cb120-f600-4f40-9707-7b781bdbce48",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -1700,7 +1718,7 @@
]
},
{
"id": "2dbb7f27-757d-4178-8217-4a24fdb0163c",
"id": "f884b048-b223-4ed6-ae16-e49a4255131e",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -1726,7 +1744,7 @@
]
},
{
"id": "7295aaf7-acf4-4b78-8186-d2415ea4ede0",
"id": "61c7966c-ad72-49f5-84dd-376152348092",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -1753,7 +1771,7 @@
]
},
{
"id": "e0d34d7c-7bbb-4847-8864-fbd97a1f3e89",
"id": "72412d0f-dd1b-49fe-bb0b-9dad99eb0491",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -1779,7 +1797,7 @@
]
},
{
"id": "5f3d0fb0-d95e-4841-89d3-a27d0cdbbcb4",
"id": "6b76613e-0d39-440d-aab4-98eaffb1e96a",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -1821,7 +1839,7 @@
]
},
{
"id": "c246380d-af25-4151-ab19-1f1e5b553008",
"id": "0ff60395-fa89-41be-ad22-fab339e67c49",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -1863,7 +1881,7 @@
]
},
{
"id": "abacf398-0f1f-4f28-a310-8d306d588048",
"id": "bbb3ece7-7dbf-4aba-80c3-dde4b9cdd0b6",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -1897,7 +1915,7 @@
]
},
{
"id": "a0f87683-619a-44d4-8b4f-4b053bba2346",
"id": "f5f2c0f6-7dbf-4978-845e-6cacac23aa13",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -1915,7 +1933,7 @@
]
},
{
"id": "e8820c7c-22a7-4618-beb7-3e09be72c00c",
"id": "cf463104-19e2-41a8-8a53-d3dd30b75344",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -1942,7 +1960,7 @@
]
},
{
"id": "cac00c38-ee44-44c9-b95e-cc755bab36ef",
"id": "b99b60dc-41ad-487d-be69-a2eefa954a9d",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -1968,7 +1986,7 @@
]
},
{
"id": "688cde36-507e-4a68-afdf-18ec4ad626a7",
"id": "18731296-2c96-4f98-a884-027e629e4f9d",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -1994,7 +2012,7 @@
]
},
{
"id": "e058697c-f450-4f14-ae64-04e9299fa24f",
"id": "9a9dce17-5425-4fd5-b3b8-81410e1dbce4",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -2013,7 +2031,7 @@
]
},
{
"id": "ad768088-32c9-4979-90dd-61bf111fd72e",
"id": "d0a24e08-cb69-4949-9518-50ae7a96ee49",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -2055,7 +2073,7 @@
]
},
{
"id": "47d4b090-f965-4588-b5bc-029ccb59876f",
"id": "6a9aa554-afba-487f-9c82-e94c81c15b3b",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -2097,7 +2115,7 @@
]
},
{
"id": "1f68feec-7f99-4c49-afe6-45d46684ca21",
"id": "e0361d46-eab4-41a6-bb2e-1dc6a5a6b073",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -2117,14 +2135,14 @@
],
"authenticatorConfig": [
{
"id": "bd7365c7-842b-4bc6-a4ca-498cf025c210",
"id": "053d6017-e54c-418a-abe7-44dd4752eacb",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "b929192d-f650-4a09-9701-3d3216547552",
"id": "8b545cf4-ab9e-4226-b3c0-d7ac773eae2f",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"

View File

@ -408,9 +408,9 @@
"otpPolicyPeriod": 30,
"otpPolicyCodeReusable": false,
"otpSupportedApplications": [
"totpAppGoogleName",
"totpAppFreeOTPName",
"totpAppMicrosoftAuthenticatorName"
"totpAppMicrosoftAuthenticatorName",
"totpAppGoogleName"
],
"webAuthnPolicyRpEntityName": "keycloak",
"webAuthnPolicySignatureAlgorithms": ["ES256"],
@ -779,6 +779,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1359,13 +1377,13 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper"
]
}
@ -1415,14 +1433,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-address-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper"
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper"
]
}
},

View File

@ -789,6 +789,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"lightweight.claim": "true"
}
},
{
"id": "59cde7ae-2218-4a8e-83af-cad992c3a700",
"name": "locale",
@ -1401,14 +1419,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper"
"saml-role-list-mapper"
]
}
},
@ -1477,14 +1495,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper"
"oidc-sha256-pairwise-sub-mapper"
]
}
}

View File

@ -919,6 +919,24 @@
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"lightweight.claim": "true"
}
}
],
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
@ -1545,14 +1563,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper"
"oidc-sha256-pairwise-sub-mapper"
]
}
},
@ -1584,14 +1602,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper"
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper"
]
}
},

View File

@ -964,6 +964,24 @@
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"lightweight.claim": "true"
}
}
],
"defaultClientScopes": [
@ -1618,14 +1636,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-role-list-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-role-list-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper"
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper"
]
}
},
@ -1657,12 +1675,12 @@
"allowed-protocol-mapper-types": [
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper"
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper"
]
}
},

View File

@ -1657,14 +1657,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"saml-user-property-mapper"
"oidc-full-name-mapper"
]
}
},
@ -1695,13 +1695,13 @@
"config": {
"allowed-protocol-mapper-types": [
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper"
"saml-user-attribute-mapper"
]
}
},

View File

@ -1,6 +1,5 @@
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
import { getDefaultConfig } from "./defaultConfig";
import {
prepareRealmConfig,
@ -14,7 +13,11 @@ import {
sep as pathSep
} from "path";
import { existsAsync } from "../../tools/fs.existsAsync";
import { readRealmJsonFile, type ParsedRealmJson } from "./ParsedRealmJson";
import {
readRealmJsonFile,
writeRealmJsonFile,
type ParsedRealmJson
} from "./ParsedRealmJson";
import {
dumpContainerConfig,
type BuildContextLike as BuildContextLike_dumpContainerConfig
@ -80,22 +83,11 @@ export async function getRealmConfig(params: {
}
}
const writeRealmJsonFile = async (params: { parsedRealmJson: ParsedRealmJson }) => {
const { parsedRealmJson } = params;
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode,
filePath: realmJsonFilePath
});
}
fs.writeFileSync(realmJsonFilePath, sourceCode);
};
await writeRealmJsonFile({ parsedRealmJson });
await writeRealmJsonFile({
realmJsonFilePath,
parsedRealmJson,
keycloakMajorVersionNumber
});
const { onRealmConfigChange } = (() => {
const run = runExclusive.build(async () => {
@ -119,7 +111,11 @@ export async function getRealmConfig(params: {
return;
}
await writeRealmJsonFile({ parsedRealmJson });
await writeRealmJsonFile({
realmJsonFilePath,
parsedRealmJson,
keycloakMajorVersionNumber
});
console.log(
[

View File

@ -0,0 +1,99 @@
import { z } from "zod";
import { same } from "evt/tools/inDepth/same";
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
export type Stringifyable =
| StringifyableAtomic
| StringifyableObject
| StringifyableArray;
export type StringifyableAtomic = string | number | boolean | null;
// NOTE: Use Record<string, Stringifyable>
interface StringifyableObject {
[key: string]: Stringifyable;
}
// NOTE: Use Stringifyable[]
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface StringifyableArray extends Array<Stringifyable> {}
export const zStringifyableAtomic = (() => {
type TargetType = StringifyableAtomic;
const zTargetType = z.union([z.string(), z.number(), z.boolean(), z.null()]);
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
export const zStringifyable: z.ZodType<Stringifyable> = z
.any()
.superRefine((val, ctx) => {
const isStringifyable = same(JSON.parse(JSON.stringify(val)), val);
if (!isStringifyable) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Not stringifyable"
});
}
});
export function getIsAtomic(
stringifyable: Stringifyable
): stringifyable is StringifyableAtomic {
return (
["string", "number", "boolean"].includes(typeof stringifyable) ||
stringifyable === null
);
}
export const { getValueAtPath } = (() => {
function getValueAtPath_rec(
stringifyable: Stringifyable,
path: (string | number)[]
): Stringifyable | undefined {
if (path.length === 0) {
return stringifyable;
}
if (getIsAtomic(stringifyable)) {
return undefined;
}
const [first, ...rest] = path;
let dereferenced: Stringifyable | undefined;
if (stringifyable instanceof Array) {
if (typeof first !== "number") {
return undefined;
}
dereferenced = stringifyable[first];
} else {
if (typeof first !== "string") {
return undefined;
}
dereferenced = stringifyable[first];
}
if (dereferenced === undefined) {
return undefined;
}
return getValueAtPath_rec(dereferenced, rest);
}
function getValueAtPath(
stringifyableObjectOrArray: Record<string, Stringifyable> | Stringifyable[],
path: (string | number)[]
): Stringifyable | undefined {
return getValueAtPath_rec(stringifyableObjectOrArray, path);
}
return { getValueAtPath };
})();

View File

@ -0,0 +1,164 @@
import { getIsAtomic, getValueAtPath, type Stringifyable } from "./Stringifyable";
export function canonicalStringify(params: {
data: Record<string, Stringifyable> | Stringifyable[];
referenceData: Record<string, Stringifyable> | Stringifyable[];
}): string {
const { data, referenceData } = params;
return JSON.stringify(
makeDeterministicCopy({
path: [],
data,
getCanonicalKeys: path => {
const referenceValue = (() => {
const path_patched: (string | number)[] = [];
for (let i = 0; i < path.length; i++) {
let value_i = getValueAtPath(referenceData, [
...path_patched,
path[i]
]);
if (value_i !== undefined) {
path_patched.push(path[i]);
continue;
}
if (typeof path[i] !== "number") {
return undefined;
}
value_i = getValueAtPath(referenceData, [...path_patched, 0]);
if (value_i !== undefined) {
path_patched.push(0);
continue;
}
return undefined;
}
return getValueAtPath(referenceData, path_patched);
})();
if (referenceValue === undefined) {
return undefined;
}
if (getIsAtomic(referenceValue)) {
return undefined;
}
if (referenceValue instanceof Array) {
return undefined;
}
return Object.keys(referenceValue);
}
}),
null,
2
);
}
function makeDeterministicCopy(params: {
path: (string | number)[];
data: Stringifyable;
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
}): Stringifyable {
const { path, data, getCanonicalKeys } = params;
if (getIsAtomic(data)) {
return data;
}
if (data instanceof Array) {
return makeDeterministicCopy_array({
path,
data,
getCanonicalKeys
});
}
return makeDeterministicCopy_record({
path,
data,
getCanonicalKeys
});
}
function makeDeterministicCopy_record(params: {
path: (string | number)[];
data: Record<string, Stringifyable>;
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
}): Record<string, Stringifyable> {
const { path, data, getCanonicalKeys } = params;
const keysOfAtomicValues: string[] = [];
const keysOfNonAtomicValues: string[] = [];
for (const [key, value] of Object.entries(data)) {
if (getIsAtomic(value)) {
keysOfAtomicValues.push(key);
} else {
keysOfNonAtomicValues.push(key);
}
}
keysOfAtomicValues.sort();
keysOfNonAtomicValues.sort();
const keys = [...keysOfAtomicValues, ...keysOfNonAtomicValues];
reorder_according_to_canonical: {
const canonicalKeys = getCanonicalKeys(path);
if (canonicalKeys === undefined) {
break reorder_according_to_canonical;
}
const keys_toPrepend: string[] = [];
for (const key of canonicalKeys) {
const indexOfKey = keys.indexOf(key);
if (indexOfKey === -1) {
continue;
}
keys.splice(indexOfKey, 1);
keys_toPrepend.push(key);
}
keys.unshift(...keys_toPrepend);
}
const result: Record<string, Stringifyable> = {};
for (const key of keys) {
result[key] = makeDeterministicCopy({
path: [...path, key],
data: data[key],
getCanonicalKeys
});
}
return result;
}
function makeDeterministicCopy_array(params: {
path: (string | number)[];
data: Stringifyable[];
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
}): Stringifyable[] {
const { path, data, getCanonicalKeys } = params;
return [...data].map((entry, i) =>
makeDeterministicCopy({
path: [...path, i],
data: entry,
getCanonicalKeys
})
);
}