keycloakify 11.5.3 → 11.6.0

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.
Files changed (36) hide show
  1. package/bin/{682.index.js → 153.index.js} +300 -52
  2. package/bin/356.index.js +48 -25
  3. package/bin/{573.index.js → 880.index.js} +155 -9
  4. package/bin/main.js +6 -5
  5. package/bin/start-keycloak/realmConfig/{ParsedRealmJson.d.ts → ParsedRealmJson/ParsedRealmJson.d.ts} +2 -3
  6. package/bin/start-keycloak/realmConfig/ParsedRealmJson/index.d.ts +3 -0
  7. package/bin/start-keycloak/realmConfig/ParsedRealmJson/readRealmJsonFile.d.ts +4 -0
  8. package/bin/start-keycloak/realmConfig/ParsedRealmJson/writeRealmJsonFile.d.ts +6 -0
  9. package/bin/start-keycloak/realmConfig/defaultConfig/defaultConfig.d.ts +1 -4
  10. package/bin/tools/Stringifyable.d.ts +13 -0
  11. package/bin/tools/canonicalStringify.d.ts +5 -0
  12. package/bin/tools/createObjectThatThrowsIfAccessed.d.ts +21 -0
  13. package/package.json +18 -5
  14. package/src/bin/keycloakify/generateResources/generateResources.ts +162 -6
  15. package/src/bin/main.ts +4 -3
  16. package/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts +63 -24
  17. package/src/bin/start-keycloak/realmConfig/{ParsedRealmJson.ts → ParsedRealmJson/ParsedRealmJson.ts} +1 -19
  18. package/src/bin/start-keycloak/realmConfig/ParsedRealmJson/index.ts +3 -0
  19. package/src/bin/start-keycloak/realmConfig/ParsedRealmJson/readRealmJsonFile.ts +20 -0
  20. package/src/bin/start-keycloak/realmConfig/ParsedRealmJson/writeRealmJsonFile.ts +29 -0
  21. package/src/bin/start-keycloak/realmConfig/defaultConfig/defaultConfig.ts +3 -4
  22. package/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-18.json +51 -33
  23. package/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-19.json +48 -30
  24. package/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-20.json +50 -32
  25. package/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-21.json +29 -11
  26. package/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-22.json +2201 -0
  27. package/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-23.json +25 -7
  28. package/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-24.json +26 -8
  29. package/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-25.json +26 -8
  30. package/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-26.json +11 -11
  31. package/src/bin/start-keycloak/realmConfig/prepareRealmConfig.ts +1 -1
  32. package/src/bin/start-keycloak/realmConfig/realmConfig.ts +15 -19
  33. package/src/bin/start-keycloak/start-keycloak.ts +131 -36
  34. package/src/bin/tools/Stringifyable.ts +99 -0
  35. package/src/bin/tools/canonicalStringify.ts +164 -0
  36. package/src/bin/tools/createObjectThatThrowsIfAccessed.ts +90 -0
@@ -789,6 +789,24 @@
789
789
  "fullScopeAllowed": false,
790
790
  "nodeReRegistrationTimeout": 0,
791
791
  "protocolMappers": [
792
+ {
793
+ "id": "8fd0d584-7052-4d04-a615-d18a71050873",
794
+ "name": "allowed-origins",
795
+ "protocol": "openid-connect",
796
+ "protocolMapper": "oidc-hardcoded-claim-mapper",
797
+ "consentRequired": false,
798
+ "config": {
799
+ "introspection.token.claim": "true",
800
+ "userinfo.token.claim": "true",
801
+ "id.token.claim": "false",
802
+ "access.token.claim": "true",
803
+ "claim.name": "allowed-origins",
804
+ "jsonType.label": "JSON",
805
+ "access.tokenResponse.claim": "false",
806
+ "claim.value": "[\"*\"]",
807
+ "lightweight.claim": "true"
808
+ }
809
+ },
792
810
  {
793
811
  "id": "59cde7ae-2218-4a8e-83af-cad992c3a700",
794
812
  "name": "locale",
@@ -1401,14 +1419,14 @@
1401
1419
  "subComponents": {},
1402
1420
  "config": {
1403
1421
  "allowed-protocol-mapper-types": [
1404
- "saml-role-list-mapper",
1405
1422
  "oidc-sha256-pairwise-sub-mapper",
1406
- "oidc-usermodel-attribute-mapper",
1407
1423
  "saml-user-attribute-mapper",
1408
1424
  "oidc-full-name-mapper",
1425
+ "oidc-usermodel-property-mapper",
1426
+ "oidc-usermodel-attribute-mapper",
1409
1427
  "oidc-address-mapper",
1410
1428
  "saml-user-property-mapper",
1411
- "oidc-usermodel-property-mapper"
1429
+ "saml-role-list-mapper"
1412
1430
  ]
1413
1431
  }
1414
1432
  },
@@ -1477,14 +1495,14 @@
1477
1495
  "subComponents": {},
1478
1496
  "config": {
1479
1497
  "allowed-protocol-mapper-types": [
1480
- "saml-user-attribute-mapper",
1498
+ "oidc-usermodel-property-mapper",
1481
1499
  "saml-role-list-mapper",
1500
+ "saml-user-property-mapper",
1482
1501
  "oidc-usermodel-attribute-mapper",
1502
+ "saml-user-attribute-mapper",
1483
1503
  "oidc-address-mapper",
1484
- "saml-user-property-mapper",
1485
1504
  "oidc-full-name-mapper",
1486
- "oidc-sha256-pairwise-sub-mapper",
1487
- "oidc-usermodel-property-mapper"
1505
+ "oidc-sha256-pairwise-sub-mapper"
1488
1506
  ]
1489
1507
  }
1490
1508
  }
@@ -919,6 +919,24 @@
919
919
  "claim.name": "locale",
920
920
  "jsonType.label": "String"
921
921
  }
922
+ },
923
+ {
924
+ "id": "8fd0d584-7052-4d04-a615-d18a71050873",
925
+ "name": "allowed-origins",
926
+ "protocol": "openid-connect",
927
+ "protocolMapper": "oidc-hardcoded-claim-mapper",
928
+ "consentRequired": false,
929
+ "config": {
930
+ "introspection.token.claim": "true",
931
+ "userinfo.token.claim": "true",
932
+ "id.token.claim": "false",
933
+ "access.token.claim": "true",
934
+ "claim.name": "allowed-origins",
935
+ "jsonType.label": "JSON",
936
+ "access.tokenResponse.claim": "false",
937
+ "claim.value": "[\"*\"]",
938
+ "lightweight.claim": "true"
939
+ }
922
940
  }
923
941
  ],
924
942
  "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
@@ -1545,14 +1563,14 @@
1545
1563
  "subComponents": {},
1546
1564
  "config": {
1547
1565
  "allowed-protocol-mapper-types": [
1566
+ "oidc-full-name-mapper",
1548
1567
  "saml-role-list-mapper",
1568
+ "saml-user-attribute-mapper",
1569
+ "oidc-usermodel-attribute-mapper",
1549
1570
  "oidc-address-mapper",
1550
1571
  "oidc-usermodel-property-mapper",
1551
- "saml-user-attribute-mapper",
1552
1572
  "saml-user-property-mapper",
1553
- "oidc-sha256-pairwise-sub-mapper",
1554
- "oidc-usermodel-attribute-mapper",
1555
- "oidc-full-name-mapper"
1573
+ "oidc-sha256-pairwise-sub-mapper"
1556
1574
  ]
1557
1575
  }
1558
1576
  },
@@ -1584,14 +1602,14 @@
1584
1602
  "subComponents": {},
1585
1603
  "config": {
1586
1604
  "allowed-protocol-mapper-types": [
1605
+ "oidc-sha256-pairwise-sub-mapper",
1606
+ "oidc-address-mapper",
1587
1607
  "oidc-full-name-mapper",
1588
1608
  "oidc-usermodel-property-mapper",
1589
1609
  "saml-user-attribute-mapper",
1590
- "oidc-sha256-pairwise-sub-mapper",
1591
1610
  "saml-role-list-mapper",
1592
- "oidc-address-mapper",
1593
- "oidc-usermodel-attribute-mapper",
1594
- "saml-user-property-mapper"
1611
+ "saml-user-property-mapper",
1612
+ "oidc-usermodel-attribute-mapper"
1595
1613
  ]
1596
1614
  }
1597
1615
  },
@@ -964,6 +964,24 @@
964
964
  "claim.name": "locale",
965
965
  "jsonType.label": "String"
966
966
  }
967
+ },
968
+ {
969
+ "id": "8fd0d584-7052-4d04-a615-d18a71050873",
970
+ "name": "allowed-origins",
971
+ "protocol": "openid-connect",
972
+ "protocolMapper": "oidc-hardcoded-claim-mapper",
973
+ "consentRequired": false,
974
+ "config": {
975
+ "introspection.token.claim": "true",
976
+ "userinfo.token.claim": "true",
977
+ "id.token.claim": "false",
978
+ "access.token.claim": "true",
979
+ "claim.name": "allowed-origins",
980
+ "jsonType.label": "JSON",
981
+ "access.tokenResponse.claim": "false",
982
+ "claim.value": "[\"*\"]",
983
+ "lightweight.claim": "true"
984
+ }
967
985
  }
968
986
  ],
969
987
  "defaultClientScopes": [
@@ -1618,14 +1636,14 @@
1618
1636
  "subComponents": {},
1619
1637
  "config": {
1620
1638
  "allowed-protocol-mapper-types": [
1621
- "saml-role-list-mapper",
1622
- "oidc-full-name-mapper",
1623
- "saml-user-property-mapper",
1624
- "saml-user-attribute-mapper",
1625
1639
  "oidc-usermodel-attribute-mapper",
1626
1640
  "oidc-address-mapper",
1641
+ "saml-role-list-mapper",
1642
+ "saml-user-property-mapper",
1627
1643
  "oidc-sha256-pairwise-sub-mapper",
1628
- "oidc-usermodel-property-mapper"
1644
+ "saml-user-attribute-mapper",
1645
+ "oidc-usermodel-property-mapper",
1646
+ "oidc-full-name-mapper"
1629
1647
  ]
1630
1648
  }
1631
1649
  },
@@ -1657,12 +1675,12 @@
1657
1675
  "allowed-protocol-mapper-types": [
1658
1676
  "oidc-address-mapper",
1659
1677
  "saml-user-attribute-mapper",
1660
- "oidc-full-name-mapper",
1661
1678
  "saml-role-list-mapper",
1679
+ "oidc-usermodel-property-mapper",
1662
1680
  "oidc-sha256-pairwise-sub-mapper",
1663
- "oidc-usermodel-attribute-mapper",
1664
1681
  "saml-user-property-mapper",
1665
- "oidc-usermodel-property-mapper"
1682
+ "oidc-usermodel-attribute-mapper",
1683
+ "oidc-full-name-mapper"
1666
1684
  ]
1667
1685
  }
1668
1686
  },
@@ -997,7 +997,7 @@
997
997
  "claim.value": "[\"*\"]",
998
998
  "userinfo.token.claim": "true",
999
999
  "id.token.claim": "false",
1000
- "lightweight.claim": "false",
1000
+ "lightweight.claim": "true",
1001
1001
  "access.token.claim": "true",
1002
1002
  "claim.name": "allowed-origins",
1003
1003
  "jsonType.label": "JSON",
@@ -1628,7 +1628,7 @@
1628
1628
  "smtpServer": {},
1629
1629
  "loginTheme": "keycloakify-starter",
1630
1630
  "accountTheme": "",
1631
- "adminTheme": "",
1631
+ "adminTheme": "keycloakify-starter",
1632
1632
  "emailTheme": "",
1633
1633
  "eventsEnabled": false,
1634
1634
  "eventsListeners": ["keycloakify-logging", "jboss-logging"],
@@ -1657,13 +1657,13 @@
1657
1657
  "subComponents": {},
1658
1658
  "config": {
1659
1659
  "allowed-protocol-mapper-types": [
1660
- "oidc-usermodel-property-mapper",
1661
- "saml-user-property-mapper",
1662
1660
  "oidc-address-mapper",
1663
1661
  "saml-user-attribute-mapper",
1664
- "saml-role-list-mapper",
1665
- "oidc-sha256-pairwise-sub-mapper",
1662
+ "oidc-usermodel-property-mapper",
1666
1663
  "oidc-usermodel-attribute-mapper",
1664
+ "saml-user-property-mapper",
1665
+ "oidc-sha256-pairwise-sub-mapper",
1666
+ "saml-role-list-mapper",
1667
1667
  "oidc-full-name-mapper"
1668
1668
  ]
1669
1669
  }
@@ -1694,14 +1694,14 @@
1694
1694
  "subComponents": {},
1695
1695
  "config": {
1696
1696
  "allowed-protocol-mapper-types": [
1697
- "saml-user-attribute-mapper",
1698
- "oidc-full-name-mapper",
1697
+ "oidc-usermodel-attribute-mapper",
1699
1698
  "oidc-sha256-pairwise-sub-mapper",
1700
- "saml-user-property-mapper",
1701
- "oidc-usermodel-property-mapper",
1702
1699
  "saml-role-list-mapper",
1703
1700
  "oidc-address-mapper",
1704
- "oidc-usermodel-attribute-mapper"
1701
+ "oidc-full-name-mapper",
1702
+ "saml-user-property-mapper",
1703
+ "oidc-usermodel-property-mapper",
1704
+ "saml-user-attribute-mapper"
1705
1705
  ]
1706
1706
  }
1707
1707
  },
@@ -333,7 +333,7 @@ function editAccountConsoleAndSecurityAdminConsole(params: {
333
333
  "claim.value": '["*"]',
334
334
  "userinfo.token.claim": "true",
335
335
  "id.token.claim": "false",
336
- "lightweight.claim": "false",
336
+ "lightweight.claim": "true",
337
337
  "access.token.claim": "true",
338
338
  "claim.name": "allowed-origins",
339
339
  "jsonType.label": "JSON",
@@ -1,6 +1,5 @@
1
1
  import type { BuildContext } from "../../shared/buildContext";
2
2
  import { assert } from "tsafe/assert";
3
- import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
4
3
  import { getDefaultConfig } from "./defaultConfig";
5
4
  import {
6
5
  prepareRealmConfig,
@@ -14,7 +13,11 @@ import {
14
13
  sep as pathSep
15
14
  } from "path";
16
15
  import { existsAsync } from "../../tools/fs.existsAsync";
17
- import { readRealmJsonFile, type ParsedRealmJson } from "./ParsedRealmJson";
16
+ import {
17
+ readRealmJsonFile,
18
+ writeRealmJsonFile,
19
+ type ParsedRealmJson
20
+ } from "./ParsedRealmJson";
18
21
  import {
19
22
  dumpContainerConfig,
20
23
  type BuildContextLike as BuildContextLike_dumpContainerConfig
@@ -80,22 +83,11 @@ export async function getRealmConfig(params: {
80
83
  }
81
84
  }
82
85
 
83
- const writeRealmJsonFile = async (params: { parsedRealmJson: ParsedRealmJson }) => {
84
- const { parsedRealmJson } = params;
85
-
86
- let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
87
-
88
- if (await getIsPrettierAvailable()) {
89
- sourceCode = await runPrettier({
90
- sourceCode,
91
- filePath: realmJsonFilePath
92
- });
93
- }
94
-
95
- fs.writeFileSync(realmJsonFilePath, sourceCode);
96
- };
97
-
98
- await writeRealmJsonFile({ parsedRealmJson });
86
+ await writeRealmJsonFile({
87
+ realmJsonFilePath,
88
+ parsedRealmJson,
89
+ keycloakMajorVersionNumber
90
+ });
99
91
 
100
92
  const { onRealmConfigChange } = (() => {
101
93
  const run = runExclusive.build(async () => {
@@ -119,7 +111,11 @@ export async function getRealmConfig(params: {
119
111
  return;
120
112
  }
121
113
 
122
- await writeRealmJsonFile({ parsedRealmJson });
114
+ await writeRealmJsonFile({
115
+ realmJsonFilePath,
116
+ parsedRealmJson,
117
+ keycloakMajorVersionNumber
118
+ });
123
119
 
124
120
  console.log(
125
121
  [
@@ -142,15 +142,96 @@ export async function command(params: {
142
142
  ].join("\n")
143
143
  );
144
144
 
145
- const { value: tag } = await cliSelect<string>({
146
- values: latestMajorTags
147
- }).catch(() => {
148
- process.exit(-1);
149
- });
145
+ const tag_userSelected = await (async () => {
146
+ let tag: string;
147
+
148
+ let latestMajorTags_copy = [...latestMajorTags];
149
+
150
+ while (true) {
151
+ const { value } = await cliSelect<string>({
152
+ values: latestMajorTags_copy
153
+ }).catch(() => {
154
+ process.exit(-1);
155
+ });
156
+
157
+ tag = value;
158
+
159
+ {
160
+ const doImplementAccountMpa =
161
+ buildContext.implementedThemeTypes.account.isImplemented &&
162
+ buildContext.implementedThemeTypes.account.type === "Multi-Page";
163
+
164
+ if (doImplementAccountMpa && tag.startsWith("22.")) {
165
+ console.log(
166
+ chalk.yellow(
167
+ `You are implementing a Multi-Page Account theme. Keycloak 22 is not supported, select another version`
168
+ )
169
+ );
170
+ latestMajorTags_copy = latestMajorTags_copy.filter(
171
+ tag => !tag.startsWith("22.")
172
+ );
173
+ continue;
174
+ }
175
+ }
150
176
 
151
- console.log(`→ ${tag}`);
177
+ const readMajor = (tag: string) => {
178
+ const major = parseInt(tag.split(".")[0]);
179
+ assert(!isNaN(major));
180
+ return major;
181
+ };
152
182
 
153
- return { dockerImageTag: tag };
183
+ {
184
+ const major = readMajor(tag);
185
+
186
+ const doImplementAdminTheme =
187
+ buildContext.implementedThemeTypes.admin.isImplemented;
188
+
189
+ const getIsSupported = (major: number) => major >= 23;
190
+
191
+ if (doImplementAdminTheme && !getIsSupported(major)) {
192
+ console.log(
193
+ chalk.yellow(
194
+ `You are implementing an Admin theme. Only Keycloak 23 and later are supported, select another version`
195
+ )
196
+ );
197
+ latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
198
+ getIsSupported(readMajor(tag))
199
+ );
200
+ continue;
201
+ }
202
+ }
203
+
204
+ {
205
+ const doImplementAccountSpa =
206
+ buildContext.implementedThemeTypes.account.isImplemented &&
207
+ buildContext.implementedThemeTypes.account.type === "Single-Page";
208
+
209
+ const major = readMajor(tag);
210
+
211
+ const getIsSupported = (major: number) => major >= 19;
212
+
213
+ if (doImplementAccountSpa && !getIsSupported(major)) {
214
+ console.log(
215
+ chalk.yellow(
216
+ `You are implementing a Single-Page Account theme. Only Keycloak 19 and later are supported, select another version`
217
+ )
218
+ );
219
+ latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
220
+ getIsSupported(readMajor(tag))
221
+ );
222
+ continue;
223
+ }
224
+ }
225
+
226
+ break;
227
+ }
228
+
229
+ return tag;
230
+ })();
231
+
232
+ console.log(`→ ${tag_userSelected}`);
233
+
234
+ return { dockerImageTag: tag_userSelected };
154
235
  })();
155
236
 
156
237
  const keycloakMajorVersionNumber = (() => {
@@ -623,42 +704,56 @@ export async function command(params: {
623
704
  }
624
705
  )
625
706
  .on("all", async (...[, filePath]) => {
626
- ignore_account_spa: {
627
- const doImplementAccountSpa =
628
- buildContext.implementedThemeTypes.account.isImplemented &&
629
- buildContext.implementedThemeTypes.account.type === "Single-Page";
630
-
631
- if (!doImplementAccountSpa) {
632
- break ignore_account_spa;
707
+ ignore_path_covered_by_hmr: {
708
+ if (filePath.endsWith(".properties")) {
709
+ break ignore_path_covered_by_hmr;
633
710
  }
634
711
 
635
- if (
636
- !isInside({
637
- dirPath: pathJoin(buildContext.themeSrcDirPath, "account"),
638
- filePath
639
- })
640
- ) {
641
- break ignore_account_spa;
712
+ if (!doStartDevServer) {
713
+ break ignore_path_covered_by_hmr;
642
714
  }
643
715
 
644
- return;
645
- }
646
-
647
- ignore_admin: {
648
- if (!buildContext.implementedThemeTypes.admin.isImplemented) {
649
- break ignore_admin;
716
+ ignore_account_spa: {
717
+ const doImplementAccountSpa =
718
+ buildContext.implementedThemeTypes.account.isImplemented &&
719
+ buildContext.implementedThemeTypes.account.type ===
720
+ "Single-Page";
721
+
722
+ if (!doImplementAccountSpa) {
723
+ break ignore_account_spa;
724
+ }
725
+
726
+ if (
727
+ !isInside({
728
+ dirPath: pathJoin(
729
+ buildContext.themeSrcDirPath,
730
+ "account"
731
+ ),
732
+ filePath
733
+ })
734
+ ) {
735
+ break ignore_account_spa;
736
+ }
737
+
738
+ return;
650
739
  }
651
740
 
652
- if (
653
- !isInside({
654
- dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
655
- filePath
656
- })
657
- ) {
658
- break ignore_admin;
741
+ ignore_admin: {
742
+ if (!buildContext.implementedThemeTypes.admin.isImplemented) {
743
+ break ignore_admin;
744
+ }
745
+
746
+ if (
747
+ !isInside({
748
+ dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
749
+ filePath
750
+ })
751
+ ) {
752
+ break ignore_admin;
753
+ }
754
+
755
+ return;
659
756
  }
660
-
661
- return;
662
757
  }
663
758
 
664
759
  console.log(`Detected changes in ${filePath}`);
@@ -0,0 +1,99 @@
1
+ import { z } from "zod";
2
+ import { same } from "evt/tools/inDepth/same";
3
+ import { assert, type Equals } from "tsafe/assert";
4
+ import { id } from "tsafe/id";
5
+
6
+ export type Stringifyable =
7
+ | StringifyableAtomic
8
+ | StringifyableObject
9
+ | StringifyableArray;
10
+
11
+ export type StringifyableAtomic = string | number | boolean | null;
12
+
13
+ // NOTE: Use Record<string, Stringifyable>
14
+ interface StringifyableObject {
15
+ [key: string]: Stringifyable;
16
+ }
17
+
18
+ // NOTE: Use Stringifyable[]
19
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
20
+ interface StringifyableArray extends Array<Stringifyable> {}
21
+
22
+ export const zStringifyableAtomic = (() => {
23
+ type TargetType = StringifyableAtomic;
24
+
25
+ const zTargetType = z.union([z.string(), z.number(), z.boolean(), z.null()]);
26
+
27
+ assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
28
+
29
+ return id<z.ZodType<TargetType>>(zTargetType);
30
+ })();
31
+
32
+ export const zStringifyable: z.ZodType<Stringifyable> = z
33
+ .any()
34
+ .superRefine((val, ctx) => {
35
+ const isStringifyable = same(JSON.parse(JSON.stringify(val)), val);
36
+ if (!isStringifyable) {
37
+ ctx.addIssue({
38
+ code: z.ZodIssueCode.custom,
39
+ message: "Not stringifyable"
40
+ });
41
+ }
42
+ });
43
+
44
+ export function getIsAtomic(
45
+ stringifyable: Stringifyable
46
+ ): stringifyable is StringifyableAtomic {
47
+ return (
48
+ ["string", "number", "boolean"].includes(typeof stringifyable) ||
49
+ stringifyable === null
50
+ );
51
+ }
52
+
53
+ export const { getValueAtPath } = (() => {
54
+ function getValueAtPath_rec(
55
+ stringifyable: Stringifyable,
56
+ path: (string | number)[]
57
+ ): Stringifyable | undefined {
58
+ if (path.length === 0) {
59
+ return stringifyable;
60
+ }
61
+
62
+ if (getIsAtomic(stringifyable)) {
63
+ return undefined;
64
+ }
65
+
66
+ const [first, ...rest] = path;
67
+
68
+ let dereferenced: Stringifyable | undefined;
69
+
70
+ if (stringifyable instanceof Array) {
71
+ if (typeof first !== "number") {
72
+ return undefined;
73
+ }
74
+
75
+ dereferenced = stringifyable[first];
76
+ } else {
77
+ if (typeof first !== "string") {
78
+ return undefined;
79
+ }
80
+
81
+ dereferenced = stringifyable[first];
82
+ }
83
+
84
+ if (dereferenced === undefined) {
85
+ return undefined;
86
+ }
87
+
88
+ return getValueAtPath_rec(dereferenced, rest);
89
+ }
90
+
91
+ function getValueAtPath(
92
+ stringifyableObjectOrArray: Record<string, Stringifyable> | Stringifyable[],
93
+ path: (string | number)[]
94
+ ): Stringifyable | undefined {
95
+ return getValueAtPath_rec(stringifyableObjectOrArray, path);
96
+ }
97
+
98
+ return { getValueAtPath };
99
+ })();