@atlasauth/cli 0.12.0 → 0.13.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 (276) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/args.d.ts +15 -0
  3. package/dist/args.js +7 -0
  4. package/dist/args.js.map +1 -1
  5. package/dist/commands/config.js +71 -6
  6. package/dist/commands/config.js.map +1 -1
  7. package/dist/commands/dev.d.ts +8 -0
  8. package/dist/commands/dev.js +84 -0
  9. package/dist/commands/dev.js.map +1 -0
  10. package/dist/commands/pages.d.ts +14 -0
  11. package/dist/commands/pages.js +113 -0
  12. package/dist/commands/pages.js.map +1 -0
  13. package/dist/context.d.ts +1 -1
  14. package/dist/help.d.ts +1 -1
  15. package/dist/help.js +58 -2
  16. package/dist/help.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +3 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/migrate/appwrite/plugin.d.ts +2 -0
  21. package/dist/migrate/appwrite/plugin.js +103 -0
  22. package/dist/migrate/appwrite/plugin.js.map +1 -0
  23. package/dist/migrate/authentik/plugin.d.ts +2 -0
  24. package/dist/migrate/authentik/plugin.js +135 -0
  25. package/dist/migrate/authentik/plugin.js.map +1 -0
  26. package/dist/migrate/azure-ad-b2c/plugin.d.ts +8 -0
  27. package/dist/migrate/azure-ad-b2c/plugin.js +165 -0
  28. package/dist/migrate/azure-ad-b2c/plugin.js.map +1 -0
  29. package/dist/migrate/cognito/plugin.d.ts +5 -0
  30. package/dist/migrate/cognito/plugin.js +170 -0
  31. package/dist/migrate/cognito/plugin.js.map +1 -0
  32. package/dist/migrate/corbado/plugin.d.ts +2 -0
  33. package/dist/migrate/corbado/plugin.js +63 -0
  34. package/dist/migrate/corbado/plugin.js.map +1 -0
  35. package/dist/migrate/entra-external-id/plugin.d.ts +8 -0
  36. package/dist/migrate/entra-external-id/plugin.js +12 -0
  37. package/dist/migrate/entra-external-id/plugin.js.map +1 -0
  38. package/dist/migrate/export/appwrite-config.d.ts +28 -0
  39. package/dist/migrate/export/appwrite-config.js +170 -0
  40. package/dist/migrate/export/appwrite-config.js.map +1 -0
  41. package/dist/migrate/export/appwrite.d.ts +51 -0
  42. package/dist/migrate/export/appwrite.js +288 -0
  43. package/dist/migrate/export/appwrite.js.map +1 -0
  44. package/dist/migrate/export/authentik-config.d.ts +45 -0
  45. package/dist/migrate/export/authentik-config.js +357 -0
  46. package/dist/migrate/export/authentik-config.js.map +1 -0
  47. package/dist/migrate/export/authentik.d.ts +110 -0
  48. package/dist/migrate/export/authentik.js +838 -0
  49. package/dist/migrate/export/authentik.js.map +1 -0
  50. package/dist/migrate/export/azure-config.d.ts +58 -0
  51. package/dist/migrate/export/azure-config.js +344 -0
  52. package/dist/migrate/export/azure-config.js.map +1 -0
  53. package/dist/migrate/export/azure.d.ts +85 -0
  54. package/dist/migrate/export/azure.js +373 -0
  55. package/dist/migrate/export/azure.js.map +1 -0
  56. package/dist/migrate/export/cognito-config.d.ts +25 -0
  57. package/dist/migrate/export/cognito-config.js +273 -0
  58. package/dist/migrate/export/cognito-config.js.map +1 -0
  59. package/dist/migrate/export/cognito.d.ts +103 -0
  60. package/dist/migrate/export/cognito.js +441 -0
  61. package/dist/migrate/export/cognito.js.map +1 -0
  62. package/dist/migrate/export/corbado-config.d.ts +20 -0
  63. package/dist/migrate/export/corbado-config.js +63 -0
  64. package/dist/migrate/export/corbado-config.js.map +1 -0
  65. package/dist/migrate/export/corbado.d.ts +37 -0
  66. package/dist/migrate/export/corbado.js +132 -0
  67. package/dist/migrate/export/corbado.js.map +1 -0
  68. package/dist/migrate/export/firebase-config.d.ts +24 -0
  69. package/dist/migrate/export/firebase-config.js +215 -0
  70. package/dist/migrate/export/firebase-config.js.map +1 -0
  71. package/dist/migrate/export/firebase.d.ts +102 -0
  72. package/dist/migrate/export/firebase.js +464 -0
  73. package/dist/migrate/export/firebase.js.map +1 -0
  74. package/dist/migrate/export/frontegg-config.d.ts +29 -0
  75. package/dist/migrate/export/frontegg-config.js +238 -0
  76. package/dist/migrate/export/frontegg-config.js.map +1 -0
  77. package/dist/migrate/export/frontegg.d.ts +67 -0
  78. package/dist/migrate/export/frontegg.js +410 -0
  79. package/dist/migrate/export/frontegg.js.map +1 -0
  80. package/dist/migrate/export/fusionauth-config.d.ts +26 -0
  81. package/dist/migrate/export/fusionauth-config.js +226 -0
  82. package/dist/migrate/export/fusionauth-config.js.map +1 -0
  83. package/dist/migrate/export/fusionauth.d.ts +99 -0
  84. package/dist/migrate/export/fusionauth.js +642 -0
  85. package/dist/migrate/export/fusionauth.js.map +1 -0
  86. package/dist/migrate/export/hanko-config.d.ts +27 -0
  87. package/dist/migrate/export/hanko-config.js +265 -0
  88. package/dist/migrate/export/hanko-config.js.map +1 -0
  89. package/dist/migrate/export/hanko.d.ts +52 -0
  90. package/dist/migrate/export/hanko.js +605 -0
  91. package/dist/migrate/export/hanko.js.map +1 -0
  92. package/dist/migrate/export/keycloak-config.d.ts +19 -0
  93. package/dist/migrate/export/keycloak-config.js +219 -0
  94. package/dist/migrate/export/keycloak-config.js.map +1 -0
  95. package/dist/migrate/export/keycloak.d.ts +155 -0
  96. package/dist/migrate/export/keycloak.js +878 -0
  97. package/dist/migrate/export/keycloak.js.map +1 -0
  98. package/dist/migrate/export/ldap.d.ts +53 -0
  99. package/dist/migrate/export/ldap.js +277 -0
  100. package/dist/migrate/export/ldap.js.map +1 -0
  101. package/dist/migrate/export/logto-config.d.ts +27 -0
  102. package/dist/migrate/export/logto-config.js +264 -0
  103. package/dist/migrate/export/logto-config.js.map +1 -0
  104. package/dist/migrate/export/logto.d.ts +150 -0
  105. package/dist/migrate/export/logto.js +809 -0
  106. package/dist/migrate/export/logto.js.map +1 -0
  107. package/dist/migrate/export/magic-config.d.ts +14 -0
  108. package/dist/migrate/export/magic-config.js +28 -0
  109. package/dist/migrate/export/magic-config.js.map +1 -0
  110. package/dist/migrate/export/magic.d.ts +31 -0
  111. package/dist/migrate/export/magic.js +93 -0
  112. package/dist/migrate/export/magic.js.map +1 -0
  113. package/dist/migrate/export/mini-yaml.d.ts +19 -0
  114. package/dist/migrate/export/mini-yaml.js +581 -0
  115. package/dist/migrate/export/mini-yaml.js.map +1 -0
  116. package/dist/migrate/export/nhost-config.d.ts +26 -0
  117. package/dist/migrate/export/nhost-config.js +360 -0
  118. package/dist/migrate/export/nhost-config.js.map +1 -0
  119. package/dist/migrate/export/nhost.d.ts +64 -0
  120. package/dist/migrate/export/nhost.js +304 -0
  121. package/dist/migrate/export/nhost.js.map +1 -0
  122. package/dist/migrate/export/onelogin.d.ts +22 -0
  123. package/dist/migrate/export/onelogin.js +123 -0
  124. package/dist/migrate/export/onelogin.js.map +1 -0
  125. package/dist/migrate/export/ory-config.d.ts +58 -0
  126. package/dist/migrate/export/ory-config.js +451 -0
  127. package/dist/migrate/export/ory-config.js.map +1 -0
  128. package/dist/migrate/export/ory.d.ts +81 -0
  129. package/dist/migrate/export/ory.js +555 -0
  130. package/dist/migrate/export/ory.js.map +1 -0
  131. package/dist/migrate/export/password-detect.d.ts +29 -0
  132. package/dist/migrate/export/password-detect.js +81 -0
  133. package/dist/migrate/export/password-detect.js.map +1 -0
  134. package/dist/migrate/export/pingone.d.ts +36 -0
  135. package/dist/migrate/export/pingone.js +218 -0
  136. package/dist/migrate/export/pingone.js.map +1 -0
  137. package/dist/migrate/export/pocketbase-config.d.ts +28 -0
  138. package/dist/migrate/export/pocketbase-config.js +180 -0
  139. package/dist/migrate/export/pocketbase-config.js.map +1 -0
  140. package/dist/migrate/export/pocketbase.d.ts +52 -0
  141. package/dist/migrate/export/pocketbase.js +243 -0
  142. package/dist/migrate/export/pocketbase.js.map +1 -0
  143. package/dist/migrate/export/project-files.d.ts +15 -0
  144. package/dist/migrate/export/project-files.js +43 -0
  145. package/dist/migrate/export/project-files.js.map +1 -0
  146. package/dist/migrate/export/propelauth-config.d.ts +18 -0
  147. package/dist/migrate/export/propelauth-config.js +58 -0
  148. package/dist/migrate/export/propelauth-config.js.map +1 -0
  149. package/dist/migrate/export/propelauth-harness.test-helper.d.ts +62 -0
  150. package/dist/migrate/export/propelauth-harness.test-helper.js +109 -0
  151. package/dist/migrate/export/propelauth-harness.test-helper.js.map +1 -0
  152. package/dist/migrate/export/propelauth.d.ts +48 -0
  153. package/dist/migrate/export/propelauth.js +255 -0
  154. package/dist/migrate/export/propelauth.js.map +1 -0
  155. package/dist/migrate/export/source-lines.d.ts +17 -0
  156. package/dist/migrate/export/source-lines.js +43 -0
  157. package/dist/migrate/export/source-lines.js.map +1 -0
  158. package/dist/migrate/export/sso-metadata.d.ts +47 -0
  159. package/dist/migrate/export/sso-metadata.js +180 -0
  160. package/dist/migrate/export/sso-metadata.js.map +1 -0
  161. package/dist/migrate/export/stytch-config.d.ts +22 -0
  162. package/dist/migrate/export/stytch-config.js +102 -0
  163. package/dist/migrate/export/stytch-config.js.map +1 -0
  164. package/dist/migrate/export/stytch.d.ts +68 -0
  165. package/dist/migrate/export/stytch.js +510 -0
  166. package/dist/migrate/export/stytch.js.map +1 -0
  167. package/dist/migrate/export/supabase-config.d.ts +48 -0
  168. package/dist/migrate/export/supabase-config.js +408 -0
  169. package/dist/migrate/export/supabase-config.js.map +1 -0
  170. package/dist/migrate/export/supabase.d.ts +86 -0
  171. package/dist/migrate/export/supabase.js +524 -0
  172. package/dist/migrate/export/supabase.js.map +1 -0
  173. package/dist/migrate/export/supertokens-config.d.ts +20 -0
  174. package/dist/migrate/export/supertokens-config.js +150 -0
  175. package/dist/migrate/export/supertokens-config.js.map +1 -0
  176. package/dist/migrate/export/supertokens.d.ts +125 -0
  177. package/dist/migrate/export/supertokens.js +464 -0
  178. package/dist/migrate/export/supertokens.js.map +1 -0
  179. package/dist/migrate/export/table.d.ts +35 -0
  180. package/dist/migrate/export/table.js +248 -0
  181. package/dist/migrate/export/table.js.map +1 -0
  182. package/dist/migrate/export/userfront-config.d.ts +15 -0
  183. package/dist/migrate/export/userfront-config.js +49 -0
  184. package/dist/migrate/export/userfront-config.js.map +1 -0
  185. package/dist/migrate/export/userfront.d.ts +38 -0
  186. package/dist/migrate/export/userfront.js +189 -0
  187. package/dist/migrate/export/userfront.js.map +1 -0
  188. package/dist/migrate/export/workos-config.d.ts +23 -0
  189. package/dist/migrate/export/workos-config.js +125 -0
  190. package/dist/migrate/export/workos-config.js.map +1 -0
  191. package/dist/migrate/export/workos.d.ts +46 -0
  192. package/dist/migrate/export/workos.js +272 -0
  193. package/dist/migrate/export/workos.js.map +1 -0
  194. package/dist/migrate/export/zitadel-config.d.ts +37 -0
  195. package/dist/migrate/export/zitadel-config.js +279 -0
  196. package/dist/migrate/export/zitadel-config.js.map +1 -0
  197. package/dist/migrate/export/zitadel.d.ts +82 -0
  198. package/dist/migrate/export/zitadel.js +447 -0
  199. package/dist/migrate/export/zitadel.js.map +1 -0
  200. package/dist/migrate/firebase/plugin.d.ts +2 -0
  201. package/dist/migrate/firebase/plugin.js +152 -0
  202. package/dist/migrate/firebase/plugin.js.map +1 -0
  203. package/dist/migrate/framework/builtins.js +35 -6
  204. package/dist/migrate/framework/builtins.js.map +1 -1
  205. package/dist/migrate/framework/orchestrator.js +3 -2
  206. package/dist/migrate/framework/orchestrator.js.map +1 -1
  207. package/dist/migrate/framework/plugin.d.ts +2 -0
  208. package/dist/migrate/frontegg/plugin.d.ts +2 -0
  209. package/dist/migrate/frontegg/plugin.js +78 -0
  210. package/dist/migrate/frontegg/plugin.js.map +1 -0
  211. package/dist/migrate/fusionauth/plugin.d.ts +2 -0
  212. package/dist/migrate/fusionauth/plugin.js +109 -0
  213. package/dist/migrate/fusionauth/plugin.js.map +1 -0
  214. package/dist/migrate/hanko/plugin.d.ts +2 -0
  215. package/dist/migrate/hanko/plugin.js +100 -0
  216. package/dist/migrate/hanko/plugin.js.map +1 -0
  217. package/dist/migrate/keycloak/plugin.d.ts +2 -0
  218. package/dist/migrate/keycloak/plugin.js +148 -0
  219. package/dist/migrate/keycloak/plugin.js.map +1 -0
  220. package/dist/migrate/ldap/plugin.d.ts +2 -0
  221. package/dist/migrate/ldap/plugin.js +73 -0
  222. package/dist/migrate/ldap/plugin.js.map +1 -0
  223. package/dist/migrate/logto/plugin.d.ts +2 -0
  224. package/dist/migrate/logto/plugin.js +131 -0
  225. package/dist/migrate/logto/plugin.js.map +1 -0
  226. package/dist/migrate/magic/plugin.d.ts +2 -0
  227. package/dist/migrate/magic/plugin.js +56 -0
  228. package/dist/migrate/magic/plugin.js.map +1 -0
  229. package/dist/migrate/nhost/plugin.d.ts +2 -0
  230. package/dist/migrate/nhost/plugin.js +115 -0
  231. package/dist/migrate/nhost/plugin.js.map +1 -0
  232. package/dist/migrate/oidc/plugin.d.ts +2 -0
  233. package/dist/migrate/oidc/plugin.js +73 -0
  234. package/dist/migrate/oidc/plugin.js.map +1 -0
  235. package/dist/migrate/onelogin/plugin.d.ts +2 -0
  236. package/dist/migrate/onelogin/plugin.js +61 -0
  237. package/dist/migrate/onelogin/plugin.js.map +1 -0
  238. package/dist/migrate/ory/plugin.d.ts +2 -0
  239. package/dist/migrate/ory/plugin.js +186 -0
  240. package/dist/migrate/ory/plugin.js.map +1 -0
  241. package/dist/migrate/ping-identity/plugin.d.ts +2 -0
  242. package/dist/migrate/ping-identity/plugin.js +100 -0
  243. package/dist/migrate/ping-identity/plugin.js.map +1 -0
  244. package/dist/migrate/pocketbase/plugin.d.ts +2 -0
  245. package/dist/migrate/pocketbase/plugin.js +99 -0
  246. package/dist/migrate/pocketbase/plugin.js.map +1 -0
  247. package/dist/migrate/propelauth/plugin.d.ts +2 -0
  248. package/dist/migrate/propelauth/plugin.js +65 -0
  249. package/dist/migrate/propelauth/plugin.js.map +1 -0
  250. package/dist/migrate/saml/plugin.d.ts +2 -0
  251. package/dist/migrate/saml/plugin.js +79 -0
  252. package/dist/migrate/saml/plugin.js.map +1 -0
  253. package/dist/migrate/stytch/plugin.d.ts +2 -0
  254. package/dist/migrate/stytch/plugin.js +97 -0
  255. package/dist/migrate/stytch/plugin.js.map +1 -0
  256. package/dist/migrate/supabase/plugin.d.ts +2 -0
  257. package/dist/migrate/supabase/plugin.js +149 -0
  258. package/dist/migrate/supabase/plugin.js.map +1 -0
  259. package/dist/migrate/supertokens/plugin.d.ts +2 -0
  260. package/dist/migrate/supertokens/plugin.js +114 -0
  261. package/dist/migrate/supertokens/plugin.js.map +1 -0
  262. package/dist/migrate/user-table/plugin.d.ts +2 -0
  263. package/dist/migrate/user-table/plugin.js +62 -0
  264. package/dist/migrate/user-table/plugin.js.map +1 -0
  265. package/dist/migrate/userfront/plugin.d.ts +2 -0
  266. package/dist/migrate/userfront/plugin.js +60 -0
  267. package/dist/migrate/userfront/plugin.js.map +1 -0
  268. package/dist/migrate/workos/plugin.d.ts +2 -0
  269. package/dist/migrate/workos/plugin.js +81 -0
  270. package/dist/migrate/workos/plugin.js.map +1 -0
  271. package/dist/migrate/zitadel/plugin.d.ts +2 -0
  272. package/dist/migrate/zitadel/plugin.js +114 -0
  273. package/dist/migrate/zitadel/plugin.js.map +1 -0
  274. package/dist/run.js +4 -0
  275. package/dist/run.js.map +1 -1
  276. package/package.json +6 -5
@@ -0,0 +1,878 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readKeycloakExport = readKeycloakExport;
4
+ exports.keycloakBase = keycloakBase;
5
+ exports.keycloakAdminHttp = keycloakAdminHttp;
6
+ exports.fetchKeycloakRealm = fetchKeycloakRealm;
7
+ exports.mapKeycloak = mapKeycloak;
8
+ exports.mapKeycloakUser = mapKeycloakUser;
9
+ exports.keycloakPassword = keycloakPassword;
10
+ exports.keycloakOtp = keycloakOtp;
11
+ exports.keycloakRecoveryCodes = keycloakRecoveryCodes;
12
+ exports.keycloakPasskey = keycloakPasskey;
13
+ exports.isBuiltInRole = isBuiltInRole;
14
+ exports.isBuiltInClient = isBuiltInClient;
15
+ exports.clientRoleKey = clientRoleKey;
16
+ exports.groupKey = groupKey;
17
+ exports.base32 = base32;
18
+ exports.base64url = base64url;
19
+ exports.exportKeycloak = exportKeycloak;
20
+ const record_set_1 = require("./record-set");
21
+ const translate_kit_1 = require("./translate-kit");
22
+ const vendor_http_1 = require("./vendor-http");
23
+ /** Realm roles every realm has; never imported as app roles. */
24
+ const BUILT_IN_ROLES = new Set(['offline_access', 'uma_authorization']);
25
+ /** Clients every realm has (their roles are Keycloak's own admin and account permissions). */
26
+ const BUILT_IN_CLIENTS = new Set(['account', 'account-console', 'admin-cli', 'broker', 'realm-management', 'security-admin-console']);
27
+ /** Brokered identity providers that are enterprise connections, not social sign-in. */
28
+ const ENTERPRISE = new Set(['saml', 'oidc', 'keycloak-oidc']);
29
+ /** Keycloak social provider ids that `socialKey` does not already know. */
30
+ const KC_SOCIAL = { 'linkedin-openid-connect': 'linkedin' };
31
+ /** User attributes the user record carries itself. */
32
+ const PHONE_ATTRS = ['phoneNumber', 'phone_number', 'phone', 'mobile'];
33
+ const CONSUMED_ATTRS = new Set([...PHONE_ATTRS, 'phoneNumberVerified', 'phone_number_verified', 'picture']);
34
+ // ── reading a realm export ──────────────────────────────────────────────────
35
+ /**
36
+ * Assemble a snapshot from the JSON documents of a realm export: one realm
37
+ * file (`--file`, a realm object or an array of realms), or a `--dir` export
38
+ * (`<realm>-realm.json` plus `<realm>-users-<n>.json` / `<realm>-federated-users-<n>.json`
39
+ * chunks, each `{ realm, users }`). `realmName` picks a realm when there are several.
40
+ */
41
+ function readKeycloakExport(docs, realmName) {
42
+ const realms = [];
43
+ const chunks = [];
44
+ for (const doc of docs) {
45
+ for (const d of Array.isArray(doc) ? doc.filter(record_set_1.isObj) : (0, record_set_1.isObj)(doc) ? [doc] : []) {
46
+ // A users chunk carries only `realm` (the name) and the user lists.
47
+ if (typeof d.realm === 'string' && Object.keys(d).every((k) => ['realm', 'users', 'federatedUsers'].includes(k)))
48
+ chunks.push(d);
49
+ else if (typeof d.realm === 'string')
50
+ realms.push(d);
51
+ }
52
+ }
53
+ if (!realms.length)
54
+ throw new Error('No realm found in the Keycloak export (expected the JSON written by kc.sh export).');
55
+ let realm;
56
+ if (realmName)
57
+ realm = realms.find((r) => r.realm === realmName);
58
+ else if (realms.length === 1)
59
+ realm = realms[0];
60
+ else {
61
+ // A full-server export also holds master: the one other realm is the app's.
62
+ const apps = realms.filter((r) => r.realm !== 'master');
63
+ if (apps.length === 1)
64
+ realm = apps[0];
65
+ }
66
+ if (!realm) {
67
+ throw new Error(realmName
68
+ ? `Realm "${realmName}" is not in the export (found: ${realms.map((r) => String(r.realm)).join(', ')}).`
69
+ : `The export holds several realms (${realms.map((r) => String(r.realm)).join(', ')}): name one with --set keycloak-realm=<realm>.`);
70
+ }
71
+ const name = String(realm.realm);
72
+ const mine = chunks.filter((c) => c.realm === name);
73
+ const { users, federatedUsers, ...rest } = realm;
74
+ return {
75
+ realm: rest,
76
+ users: [...(0, record_set_1.objs)(users), ...mine.flatMap((c) => (0, record_set_1.objs)(c.users))],
77
+ federatedUsers: [...(0, record_set_1.objs)(federatedUsers), ...mine.flatMap((c) => (0, record_set_1.objs)(c.federatedUsers))],
78
+ groups: (0, record_set_1.objs)(realm.groups),
79
+ organizations: (0, record_set_1.objs)(realm.organizations),
80
+ invitations: [],
81
+ source: 'file',
82
+ };
83
+ }
84
+ /** `https://sso.example.com/` → `https://sso.example.com`; a bare host gets https. */
85
+ function keycloakBase(raw) {
86
+ const s = raw.trim().replace(/\/+$/, '');
87
+ return /^https?:\/\//.test(s) ? s : `https://${s}`;
88
+ }
89
+ /**
90
+ * The Admin REST API (`{base}/admin/realms`) with an admin access token, fetched
91
+ * on first use and again shortly before it expires. Paths start with `/<realm>`.
92
+ */
93
+ function keycloakAdminHttp(o) {
94
+ const base = keycloakBase(o.baseUrl);
95
+ const fetchImpl = o.fetchImpl ?? fetch;
96
+ const now = o.now ?? Date.now;
97
+ const byUser = !!(o.username && o.password);
98
+ if (!byUser && !(o.clientId && o.clientSecret))
99
+ throw new Error('Keycloak Admin API needs an admin client (client id + secret) or an admin user (username + password).');
100
+ const authRealm = o.authRealm ?? (byUser ? 'master' : o.realm);
101
+ const tokenPath = `/realms/${encodeURIComponent(authRealm)}/protocol/openid-connect/token`;
102
+ let cached = null;
103
+ const token = async () => {
104
+ if (cached && cached.until > now())
105
+ return cached.value;
106
+ const form = new URLSearchParams(byUser
107
+ ? { grant_type: 'password', client_id: o.clientId ?? 'admin-cli', username: o.username, password: o.password }
108
+ : { grant_type: 'client_credentials', client_id: o.clientId, client_secret: o.clientSecret });
109
+ if (byUser && o.clientSecret)
110
+ form.set('client_secret', o.clientSecret);
111
+ let res;
112
+ try {
113
+ res = await fetchImpl(`${base}${tokenPath}`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' }, body: form.toString() });
114
+ }
115
+ catch (err) {
116
+ throw new vendor_http_1.VendorApiError('Keycloak', 0, tokenPath, err instanceof Error ? err.message : String(err));
117
+ }
118
+ const body = (await res.json().catch(() => null));
119
+ if (res.status !== 200 || typeof body?.access_token !== 'string') {
120
+ const detail = typeof body?.error_description === 'string' ? body.error_description : typeof body?.error === 'string' ? body.error : '';
121
+ throw new vendor_http_1.VendorApiError('Keycloak', res.status, tokenPath, detail);
122
+ }
123
+ const ttl = typeof body.expires_in === 'number' ? body.expires_in : 60;
124
+ cached = { value: body.access_token, until: now() + Math.max(ttl - 30, 5) * 1000 };
125
+ return cached.value;
126
+ };
127
+ return new vendor_http_1.VendorHttp({
128
+ vendor: 'Keycloak',
129
+ baseUrl: `${base}/admin/realms`,
130
+ headers: async () => ({ authorization: `Bearer ${await token()}` }),
131
+ fetchImpl,
132
+ sleep: o.sleep,
133
+ now,
134
+ });
135
+ }
136
+ /** Read a realm through the Admin REST API into the realm-export shape. */
137
+ async function fetchKeycloakRealm(http, o) {
138
+ const r = `/${encodeURIComponent(o.realm)}`;
139
+ const max = o.pageSize ?? 100;
140
+ const progress = o.onProgress ?? (() => undefined);
141
+ const warn = o.warn ?? (() => undefined);
142
+ const list = async (path) => (0, record_set_1.objs)((await http.get(path)).body);
143
+ /** A listing the token may not be allowed to read, or the server too old to have. */
144
+ const optional = async (path, what) => {
145
+ try {
146
+ return await list(path);
147
+ }
148
+ catch (err) {
149
+ if (!(err instanceof vendor_http_1.VendorApiError) || ![403, 404].includes(err.status))
150
+ throw err;
151
+ if (err.status === 403)
152
+ warn(`${what} could not be read with this admin credential (403) and were skipped.`);
153
+ return [];
154
+ }
155
+ };
156
+ const paged = async (path) => {
157
+ const out = [];
158
+ const sep = path.includes('?') ? '&' : '?';
159
+ for (let first = 0;; first += max) {
160
+ const page = await list(`${path}${sep}first=${first}&max=${max}`);
161
+ out.push(...page);
162
+ if (page.length < max)
163
+ return out;
164
+ }
165
+ };
166
+ progress('realm');
167
+ const realm = (await http.get(r)).body;
168
+ if (!(0, record_set_1.isObj)(realm))
169
+ throw new Error(`Keycloak returned no realm "${o.realm}".`);
170
+ realm.identityProviders = await optional(`${r}/identity-provider/instances`, 'Identity providers');
171
+ const clients = await optional(`${r}/clients`, 'Clients');
172
+ realm.clients = clients;
173
+ const clientRoles = {};
174
+ for (const c of clients) {
175
+ const clientId = (0, record_set_1.str)(c.clientId);
176
+ if (!clientId || !(0, record_set_1.str)(c.id) || isBuiltInClient(clientId, o.realm))
177
+ continue;
178
+ const roles = await optional(`${r}/clients/${encodeURIComponent((0, record_set_1.str)(c.id))}/roles`, `Roles of client ${clientId}`);
179
+ if (roles.length)
180
+ clientRoles[clientId] = roles;
181
+ }
182
+ realm.roles = { realm: await optional(`${r}/roles?briefRepresentation=false`, 'Realm roles'), client: clientRoles };
183
+ progress('users');
184
+ const groups = new Map();
185
+ const users = await paged(`${r}/users?briefRepresentation=false`);
186
+ for (const u of users) {
187
+ const id = (0, record_set_1.str)(u.id);
188
+ if (!id || (0, record_set_1.str)(u.serviceAccountClientId))
189
+ continue;
190
+ const base = `${r}/users/${encodeURIComponent(id)}`;
191
+ u.federatedIdentities = await list(`${base}/federated-identity`);
192
+ const memberOf = await list(`${base}/groups?briefRepresentation=false`);
193
+ u.groups = memberOf.map((g) => (0, record_set_1.str)(g.path)).filter((p) => !!p);
194
+ for (const g of memberOf)
195
+ if ((0, record_set_1.str)(g.path))
196
+ groups.set((0, record_set_1.str)(g.path), g);
197
+ const mappings = (await http.get(`${base}/role-mappings`)).body;
198
+ u.realmRoles = (0, record_set_1.objs)((0, record_set_1.isObj)(mappings) ? mappings.realmMappings : undefined).map((m) => (0, record_set_1.str)(m.name)).filter((n) => !!n);
199
+ const byClient = {};
200
+ for (const [key, m] of Object.entries((0, record_set_1.isObj)(mappings) && (0, record_set_1.isObj)(mappings.clientMappings) ? mappings.clientMappings : {})) {
201
+ if (!(0, record_set_1.isObj)(m))
202
+ continue;
203
+ const names = (0, record_set_1.objs)(m.mappings).map((x) => (0, record_set_1.str)(x.name)).filter((n) => !!n);
204
+ if (names.length)
205
+ byClient[(0, record_set_1.str)(m.client) ?? key] = names;
206
+ }
207
+ u.clientRoles = byClient;
208
+ u.credentials = await list(`${base}/credentials`);
209
+ }
210
+ // Roles reach a member through every ancestor group: read the parents nobody is a direct member of.
211
+ for (const path of [...groups.keys()]) {
212
+ const parts = path.split('/').filter(Boolean);
213
+ for (let i = parts.length - 1; i > 0; i--) {
214
+ const parent = `/${parts.slice(0, i).join('/')}`;
215
+ if (groups.has(parent))
216
+ continue;
217
+ try {
218
+ const { body } = await http.get(`${r}/group-by-path/${parts.slice(0, i).map(encodeURIComponent).join('/')}`);
219
+ if ((0, record_set_1.isObj)(body))
220
+ groups.set(parent, { ...body, path: parent, subGroups: [] });
221
+ }
222
+ catch (err) {
223
+ if (!(err instanceof vendor_http_1.VendorApiError))
224
+ throw err;
225
+ }
226
+ }
227
+ }
228
+ const organizations = [];
229
+ const invitations = [];
230
+ if (realm.organizationsEnabled === true) {
231
+ progress('organizations');
232
+ for (const org of await optional(`${r}/organizations?briefRepresentation=false&first=0&max=100000`, 'Organizations')) {
233
+ const id = (0, record_set_1.str)(org.id);
234
+ if (!id)
235
+ continue;
236
+ const path = `${r}/organizations/${encodeURIComponent(id)}`;
237
+ org.members = await paged(`${path}/members`);
238
+ organizations.push(org);
239
+ // Invitations have an admin endpoint from Keycloak 26.2 on; older servers answer 404.
240
+ for (const inv of await optional(`${path}/invitations?first=0&max=100000`, `Invitations of ${(0, record_set_1.str)(org.name) ?? id}`))
241
+ invitations.push({ organizationId: id, ...inv });
242
+ }
243
+ }
244
+ return { realm, users, federatedUsers: [], groups: [...groups.values()], organizations, invitations, source: 'api', baseUrl: o.baseUrl ? keycloakBase(o.baseUrl) : undefined };
245
+ }
246
+ // ── mapping ─────────────────────────────────────────────────────────────────
247
+ /** Map a snapshot (realm export or API) onto canonical records. */
248
+ function mapKeycloak(snap, opts = {}) {
249
+ const set = new record_set_1.RecordSet();
250
+ const realm = snap.realm;
251
+ const realmName = (0, record_set_1.str)(realm.realm) ?? 'realm';
252
+ const groupsAs = opts.groupsAs ?? 'roles';
253
+ const groups = indexGroups(snap.groups);
254
+ // ── Keycloak organizations (25+) ──
255
+ const orgIds = new Set();
256
+ const orgOfIdp = new Map();
257
+ for (const o of snap.organizations) {
258
+ const id = (0, record_set_1.str)(o.id);
259
+ const name = (0, record_set_1.str)(o.name);
260
+ if (!id || !name)
261
+ continue;
262
+ orgIds.add(id);
263
+ for (const idp of (0, record_set_1.objs)(o.identityProviders))
264
+ if ((0, record_set_1.str)(idp.alias))
265
+ orgOfIdp.set((0, record_set_1.str)(idp.alias), id);
266
+ set.add('organization', (0, record_set_1.compact)({
267
+ id,
268
+ name,
269
+ slug: (0, record_set_1.str)(o.alias),
270
+ domains: (0, record_set_1.objs)(o.domains).map((d) => (0, record_set_1.str)(d.name)?.toLowerCase()).filter((d) => !!d),
271
+ public_metadata: (0, record_set_1.bag)((0, record_set_1.compact)({ description: (0, record_set_1.str)(o.description), redirect_url: (0, record_set_1.str)(o.redirectUrl), ...unwrapAttributes(o.attributes) })),
272
+ private_metadata: o.enabled === false ? { keycloak_enabled: false } : undefined,
273
+ }));
274
+ }
275
+ if (orgIds.size)
276
+ set.add('role', { key: 'member', name: 'Member', scope: 'organization' }, 'org:member');
277
+ // ── groups as organizations ──
278
+ if (groupsAs === 'organizations') {
279
+ for (const g of groups.values()) {
280
+ set.add('organization', (0, record_set_1.compact)({ id: g.id, name: g.name, slug: g.path.split('/').filter(Boolean).map((s) => (0, translate_kit_1.keyOf)(s).replace(/_/g, '-')).join('-'), public_metadata: (0, record_set_1.bag)({ keycloak_group_path: g.path, ...unwrapAttributes(g.attributes) }) }));
281
+ }
282
+ if (groups.size)
283
+ set.add('role', { key: 'member', name: 'Member', scope: 'organization' }, 'org:member');
284
+ }
285
+ // ── roles: realm roles, client roles, groups ──
286
+ const roles = (0, record_set_1.isObj)(realm.roles) ? realm.roles : {};
287
+ let composite = false;
288
+ for (const role of (0, record_set_1.objs)(roles.realm)) {
289
+ const name = (0, record_set_1.str)(role.name);
290
+ if (!name || isBuiltInRole(name))
291
+ continue;
292
+ if (role.composite === true)
293
+ composite = true;
294
+ set.add('role', (0, record_set_1.compact)({ key: (0, translate_kit_1.keyOf)(name), name, description: (0, record_set_1.str)(role.description), scope: 'app' }), (0, translate_kit_1.keyOf)(name));
295
+ }
296
+ for (const [clientId, list] of Object.entries((0, record_set_1.isObj)(roles.client) ? roles.client : {})) {
297
+ if (isBuiltInClient(clientId, realmName))
298
+ continue;
299
+ for (const role of (0, record_set_1.objs)(list)) {
300
+ const name = (0, record_set_1.str)(role.name);
301
+ if (!name)
302
+ continue;
303
+ if (role.composite === true)
304
+ composite = true;
305
+ set.add('role', (0, record_set_1.compact)({ key: clientRoleKey(clientId, name), name: `${clientId}: ${name}`, description: (0, record_set_1.str)(role.description), scope: 'app' }), clientRoleKey(clientId, name));
306
+ }
307
+ }
308
+ if (groupsAs === 'roles') {
309
+ for (const g of groups.values())
310
+ set.add('role', { key: groupKey(g.path), name: g.path, description: 'Keycloak group', scope: 'app' }, groupKey(g.path));
311
+ }
312
+ if (composite)
313
+ set.warn('Composite roles were imported as plain roles: a user holds the roles assigned to them and their groups, not the roles a composite role includes. Assign those in Atlas, or give the composite role the permissions it stood for.');
314
+ // ── identity providers ──
315
+ const idps = new Map();
316
+ for (const idp of (0, record_set_1.objs)(realm.identityProviders))
317
+ if ((0, record_set_1.str)(idp.alias))
318
+ idps.set((0, record_set_1.str)(idp.alias), idp);
319
+ const connections = new Set();
320
+ for (const [alias, idp] of idps) {
321
+ const providerId = (0, record_set_1.str)(idp.providerId) ?? '';
322
+ if (!ENTERPRISE.has(providerId))
323
+ continue;
324
+ const orgId = (0, record_set_1.str)(idp.organizationId) ?? orgOfIdp.get(alias);
325
+ const connection = ssoConnection(idp, {
326
+ realm: realmName,
327
+ baseUrl: snap.baseUrl,
328
+ organizationId: orgId && orgIds.has(orgId) ? orgId : undefined,
329
+ domains: [
330
+ ...(0, record_set_1.strs)((0, record_set_1.isObj)(idp.config) ? [idp.config['kc.org.domain']] : []),
331
+ ...snap.organizations.filter((o) => (0, record_set_1.str)(o.id) === orgId).flatMap((o) => (0, record_set_1.objs)(o.domains).filter((d) => !(0, record_set_1.str)(d.identityProviderAlias) || (0, record_set_1.str)(d.identityProviderAlias) === alias).map((d) => (0, record_set_1.str)(d.name))),
332
+ ],
333
+ warn: (m) => set.warn(m),
334
+ });
335
+ set.add('sso_connection', connection);
336
+ connections.add(alias);
337
+ }
338
+ // ── users ──
339
+ const rpIds = {
340
+ webauthn: (0, record_set_1.str)(realm.webAuthnPolicyRpId) ?? opts.rpId,
341
+ passwordless: (0, record_set_1.str)(realm.webAuthnPolicyPasswordlessRpId) ?? (0, record_set_1.str)(realm.webAuthnPolicyRpId) ?? opts.rpId,
342
+ };
343
+ const userIdByName = new Map();
344
+ const counts = { password: 0, federated: 0, otpWithoutSecret: 0, temporary: 0 };
345
+ const badHashes = new Map();
346
+ for (const u of snap.users) {
347
+ if ((0, record_set_1.str)(u.serviceAccountClientId)) {
348
+ set.omit('user', 'Keycloak service-account user (a client credential, not a person)');
349
+ continue;
350
+ }
351
+ const user = mapKeycloakUser(u, realm);
352
+ if (!user) {
353
+ set.omit('user', 'no id, email or username');
354
+ continue;
355
+ }
356
+ const id = user.id;
357
+ if ((0, record_set_1.str)(u.username))
358
+ userIdByName.set((0, record_set_1.str)(u.username).toLowerCase(), id);
359
+ if ((0, record_set_1.str)(u.federationLink))
360
+ counts.federated++;
361
+ if ((0, record_set_1.strs)(u.requiredActions).includes('UPDATE_PASSWORD'))
362
+ counts.temporary++;
363
+ const creds = (0, record_set_1.objs)(u.credentials);
364
+ // Keycloak lists credentials by priority: the first password is the one it checks.
365
+ const pw = creds.find((c) => (0, record_set_1.str)(c.type) === 'password');
366
+ if (pw) {
367
+ if (snap.source === 'api')
368
+ counts.password++;
369
+ else {
370
+ const r = keycloakPassword(pw);
371
+ if ('hash' in r)
372
+ user.password = r.hash;
373
+ else
374
+ badHashes.set(r.problem, (badHashes.get(r.problem) ?? 0) + 1);
375
+ }
376
+ }
377
+ user.roles = userRoleKeys(u, groups, realmName, groupsAs === 'roles');
378
+ set.add('user', (0, record_set_1.compact)(user));
379
+ for (const fi of (0, record_set_1.objs)(u.federatedIdentities)) {
380
+ const identity = mapFederatedIdentity(fi, id, idps, connections);
381
+ if (identity)
382
+ set.add('identity', identity, `${identity.provider}:${identity.provider_user_id}`);
383
+ }
384
+ let hasOtp = false;
385
+ for (const c of creds) {
386
+ const type = (0, record_set_1.str)(c.type) ?? '';
387
+ if (type === 'otp' || type === 'totp' || type === 'hotp') {
388
+ hasOtp = true;
389
+ if (snap.source === 'api')
390
+ continue;
391
+ const f = keycloakOtp(c);
392
+ if ('factor' in f)
393
+ set.add('mfa_factor', { user_id: id, ...f.factor }, `otp:${id}:${(0, record_set_1.str)(c.id) ?? set.count('mfa_factor')}`);
394
+ else
395
+ set.omit('mfa_factor', f.problem);
396
+ }
397
+ else if (type === 'recovery-authn-codes') {
398
+ if (snap.source === 'api') {
399
+ set.omit('mfa_factor', 'recovery codes: the Admin API does not return their hashes (use a realm export)');
400
+ continue;
401
+ }
402
+ const f = keycloakRecoveryCodes(c);
403
+ if (f)
404
+ set.add('mfa_factor', { user_id: id, ...f }, `recovery:${id}`);
405
+ else
406
+ set.omit('mfa_factor', 'recovery codes: none left');
407
+ }
408
+ else if (type === 'webauthn' || type === 'webauthn-passwordless') {
409
+ const rp = type === 'webauthn' ? rpIds.webauthn : rpIds.passwordless;
410
+ const p = keycloakPasskey(c, rp);
411
+ if ('passkey' in p)
412
+ set.add('passkey', { user_id: id, ...p.passkey }, `passkey:${p.passkey.credential_id}`);
413
+ else
414
+ set.omit('passkey', p.problem);
415
+ }
416
+ }
417
+ if (hasOtp && snap.source === 'api')
418
+ counts.otpWithoutSecret++;
419
+ else if (u.totp === true && !hasOtp && snap.source === 'api')
420
+ counts.otpWithoutSecret++;
421
+ if (groupsAs === 'organizations') {
422
+ for (const path of (0, record_set_1.strs)(u.groups)) {
423
+ const g = groups.get(path);
424
+ if (g)
425
+ set.add('membership', { organization_id: g.id, user_id: id, role: 'member' }, `${g.id}:${id}`);
426
+ }
427
+ }
428
+ }
429
+ // ── organization members and invitations ──
430
+ for (const o of snap.organizations) {
431
+ const orgId = (0, record_set_1.str)(o.id);
432
+ if (!orgId || !orgIds.has(orgId))
433
+ continue;
434
+ for (const m of (0, record_set_1.objs)(o.members)) {
435
+ const uid = (0, record_set_1.str)(m.id) ?? ((0, record_set_1.str)(m.username) ? userIdByName.get((0, record_set_1.str)(m.username).toLowerCase()) : undefined);
436
+ if (!uid) {
437
+ set.omit('membership', 'organization member not among the exported users');
438
+ continue;
439
+ }
440
+ set.add('membership', { organization_id: orgId, user_id: uid, role: 'member' }, `${orgId}:${uid}`);
441
+ }
442
+ }
443
+ for (const inv of snap.invitations) {
444
+ const id = (0, record_set_1.str)(inv.id);
445
+ const email = (0, record_set_1.str)(inv.email)?.toLowerCase();
446
+ const orgId = (0, record_set_1.str)(inv.organizationId);
447
+ if (!id || !email)
448
+ continue;
449
+ const status = (0, record_set_1.str)(inv.status)?.toUpperCase();
450
+ set.add('invitation', (0, record_set_1.compact)({
451
+ id,
452
+ organization_id: orgId && orgIds.has(orgId) ? orgId : undefined,
453
+ email,
454
+ role: 'member',
455
+ status: status === 'EXPIRED' ? 'expired' : 'pending',
456
+ // Keycloak keeps these as epoch seconds.
457
+ expires_at: typeof inv.expiresAt === 'number' && inv.expiresAt > 0 ? inv.expiresAt * 1000 : undefined,
458
+ created_at: typeof inv.sentDate === 'number' && inv.sentDate > 0 ? inv.sentDate * 1000 : undefined,
459
+ }));
460
+ }
461
+ if (snap.federatedUsers.length)
462
+ set.omit('user', 'kept by a user-storage provider without import (no local profile): they move by lazy migration through Keycloak', snap.federatedUsers.length);
463
+ // ── what could not move, in plain words ──
464
+ if (counts.password) {
465
+ set.warn(`${counts.password} user(s) have a Keycloak password, but the Admin API never returns password hashes. Export the realm with kc.sh export (the only path that carries hashes) or turn on the keycloak lazy-migration preset: each password is then checked against Keycloak on its first Atlas sign-in.`);
466
+ }
467
+ if (counts.otpWithoutSecret) {
468
+ set.warn(`${counts.otpWithoutSecret} user(s) use an authenticator app; the Admin API does not return OTP secrets. A realm export (kc.sh export) carries them; otherwise those users enrol again in Atlas.`);
469
+ }
470
+ if (counts.federated) {
471
+ set.warn(`${counts.federated} user(s) are federated from LDAP / Kerberos (federationLink): Keycloak stores no password for them. Use the keycloak lazy-migration preset (Keycloak checks the password against the directory).`);
472
+ }
473
+ if (counts.temporary)
474
+ set.warn(`${counts.temporary} user(s) must change their password at their next Keycloak sign-in (UPDATE_PASSWORD): their current password was set by an administrator.`);
475
+ for (const [problem, n] of badHashes)
476
+ set.warn(`${n} user(s) were imported without a password (${problem}); they reset it, or use lazy migration.`);
477
+ if (snap.users.some((u) => (0, record_set_1.objs)(u.credentials).some((c) => (0, record_set_1.str)(c.type)?.startsWith('webauthn'))) && !rpIds.passwordless && !rpIds.webauthn) {
478
+ set.warn('The realm sets no WebAuthn relying-party id (Keycloak then uses its own host name): pass --set keycloak-rp-id=<host> to carry passkeys.');
479
+ }
480
+ set.warn('Keycloak sessions are not exportable: the keycloak bridge preset exchanges Keycloak access and ID tokens for Atlas sessions instead.');
481
+ const otpAlgorithm = (0, record_set_1.str)(realm.otpPolicyAlgorithm);
482
+ set.manifest = {
483
+ origin: snap.baseUrl ? `${snap.baseUrl}/realms/${realmName}` : `realm ${realmName}`,
484
+ sourceVersion: (0, record_set_1.str)(realm.keycloakVersion) ?? null,
485
+ rpId: rpIds.passwordless ?? rpIds.webauthn ?? null,
486
+ providers: [...new Set(set.list('identity').map((i) => i.provider))].sort(),
487
+ settings: (0, record_set_1.compact)({
488
+ realm: realmName,
489
+ source: snap.source === 'file' ? 'realm_export' : 'admin_api',
490
+ groups_as: groupsAs,
491
+ password_policy: (0, record_set_1.str)(realm.passwordPolicy),
492
+ otp: (0, record_set_1.str)(realm.otpPolicyType)
493
+ ? { type: (0, record_set_1.str)(realm.otpPolicyType), algorithm: otpAlgorithm ? otpAlgorithmOf(otpAlgorithm) : 'SHA1', digits: realm.otpPolicyDigits ?? 6, period: realm.otpPolicyPeriod ?? 30 }
494
+ : undefined,
495
+ }),
496
+ };
497
+ return set;
498
+ }
499
+ /** One Keycloak user → a user record without password and roles (null when it has nothing to sign in with). */
500
+ function mapKeycloakUser(u, realm = {}) {
501
+ const id = (0, record_set_1.str)(u.id);
502
+ if (!id)
503
+ return null;
504
+ const email = (0, record_set_1.str)(u.email)?.toLowerCase();
505
+ const username = (0, record_set_1.str)(u.username)?.toLowerCase();
506
+ const attrs = unwrapAttributes(u.attributes);
507
+ const phoneRaw = PHONE_ATTRS.map((k) => attrs[k]).find((v) => typeof v === 'string');
508
+ const phone = (0, record_set_1.e164)(phoneRaw);
509
+ const phoneVerified = attrs.phoneNumberVerified === 'true' || attrs.phone_number_verified === 'true';
510
+ const custom = Object.fromEntries(Object.entries(attrs).filter(([k]) => !CONSUMED_ATTRS.has(k)));
511
+ const actions = (0, record_set_1.strs)(u.requiredActions);
512
+ const user = (0, record_set_1.compact)({
513
+ id,
514
+ emails: email ? [{ email, verified: u.emailVerified === true, primary: true }] : [],
515
+ phones: phone ? [{ phone, verified: phoneVerified, primary: !email }] : [],
516
+ // With "email as username" the username is the address: keep it only when it is a real username.
517
+ username: username && username !== email && realm.registrationEmailAsUsername !== true ? username : undefined,
518
+ first_name: (0, record_set_1.str)(u.firstName),
519
+ last_name: (0, record_set_1.str)(u.lastName),
520
+ image_url: typeof attrs.picture === 'string' && /^https:\/\//.test(attrs.picture) ? attrs.picture : undefined,
521
+ banned: u.enabled === false ? true : undefined,
522
+ ban_reason: u.enabled === false ? 'Disabled in Keycloak' : undefined,
523
+ private_metadata: (0, record_set_1.bag)({
524
+ ...custom,
525
+ ...(actions.length ? { keycloak_required_actions: actions } : {}),
526
+ ...((0, record_set_1.str)(u.federationLink) ? { keycloak_federation_link: (0, record_set_1.str)(u.federationLink) } : {}),
527
+ }),
528
+ created_at: typeof u.createdTimestamp === 'number' ? u.createdTimestamp : undefined,
529
+ });
530
+ return user.emails?.length || user.phones?.length || user.username ? user : null;
531
+ }
532
+ // ── credentials ─────────────────────────────────────────────────────────────
533
+ /** PBKDF2 provider ids → digest (`pbkdf2` is PBKDF2WithHmacSHA1). */
534
+ const PBKDF2 = { pbkdf2: 'sha1', 'pbkdf2-sha1': 'sha1', 'pbkdf2-sha256': 'sha256', 'pbkdf2-sha512': 'sha512' };
535
+ /** Keycloak's Argon2 version names → the PHC `v=` value. */
536
+ const ARGON2_VERSIONS = { '1.3': 19, '1.0': 16 };
537
+ /**
538
+ * A Keycloak password credential → an Atlas password hash. Keycloak 9+ keeps
539
+ * `secretData` `{ value, salt }` and `credentialData` `{ algorithm,
540
+ * hashIterations, additionalParameters }` as JSON strings; earlier exports
541
+ * (and hand-written import files) put `hashedSaltedValue`, `salt`,
542
+ * `hashIterations` and `algorithm` on the credential itself. Salts and values
543
+ * are standard base64.
544
+ */
545
+ function keycloakPassword(cred) {
546
+ const secret = jsonObj(cred.secretData);
547
+ const data = jsonObj(cred.credentialData);
548
+ const value = (0, record_set_1.str)(secret.value) ?? (0, record_set_1.str)(cred.hashedSaltedValue);
549
+ const salt = (0, record_set_1.str)(secret.salt) ?? (0, record_set_1.str)(cred.salt);
550
+ const algorithm = ((0, record_set_1.str)(data.algorithm) ?? (0, record_set_1.str)(cred.algorithm))?.toLowerCase();
551
+ const iterations = Number(data.hashIterations ?? cred.hashIterations);
552
+ if (!value)
553
+ return { problem: 'no stored password value' };
554
+ // Community bcrypt providers store the whole digest as the value.
555
+ if (/^\$2[aby]\$\d{2}\$/.test(value))
556
+ return { hash: { algorithm: 'bcrypt', hash: value } };
557
+ if (!algorithm)
558
+ return { problem: 'the password credential names no algorithm' };
559
+ const digest = PBKDF2[algorithm];
560
+ if (digest) {
561
+ if (!salt || !Number.isInteger(iterations) || iterations < 1)
562
+ return { problem: `${algorithm} without a salt or iteration count` };
563
+ // PBEKeySpec over the password's UTF-8 bytes; the key length is the decoded value's (512 bits, or 256 for pbkdf2-sha256 from Keycloak 24 on).
564
+ return { hash: { algorithm: 'pbkdf2', hash: value, salt, params: { digest, iterations, encoding: 'base64', salt_encoding: 'base64' } } };
565
+ }
566
+ if (algorithm === 'argon2') {
567
+ const extra = (0, record_set_1.isObj)(data.additionalParameters) ? data.additionalParameters : {};
568
+ const first = (k) => (0, record_set_1.str)(Array.isArray(extra[k]) ? extra[k][0] : extra[k]);
569
+ const type = first('type') ?? 'id';
570
+ const version = ARGON2_VERSIONS[first('version') ?? '1.3'];
571
+ const memory = Number(first('memory') ?? 7168);
572
+ const parallelism = Number(first('parallelism') ?? 1);
573
+ const t = Number.isInteger(iterations) && iterations > 0 ? iterations : 5;
574
+ if (!salt || !['id', 'i', 'd'].includes(type) || version === undefined || !Number.isInteger(memory) || !Number.isInteger(parallelism)) {
575
+ return { problem: 'an Argon2 credential with parameters Keycloak never writes' };
576
+ }
577
+ // Keycloak's Argon2PasswordHashProvider (BouncyCastle, UTF-8 password) as the
578
+ // PHC string the reference argon2 tool prints for the same inputs.
579
+ const nopad = (s) => s.replace(/=+$/, '');
580
+ return { hash: { algorithm: 'argon2', hash: `$argon2${type}$v=${version}$m=${memory},t=${t},p=${parallelism}$${nopad(salt)}$${nopad(value)}` } };
581
+ }
582
+ return { problem: `unsupported hash algorithm "${algorithm}"` };
583
+ }
584
+ /**
585
+ * A Keycloak OTP credential → a TOTP factor. The secret is a plain string whose
586
+ * UTF-8 BYTES are the HMAC key (OTPCredentialModel.getDecodedSecret); Keycloak
587
+ * shows their base32 in the QR code. With `secretEncoding: "BASE32"` (secrets
588
+ * imported from elsewhere) the value already is base32.
589
+ */
590
+ function keycloakOtp(cred) {
591
+ const secret = jsonObj(cred.secretData);
592
+ const data = jsonObj(cred.credentialData);
593
+ const raw = typeof secret.value === 'string' && secret.value ? secret.value : typeof cred.value === 'string' && cred.value ? cred.value : undefined;
594
+ const subType = ((0, record_set_1.str)(data.subType) ?? ((0, record_set_1.str)(cred.type) === 'hotp' ? 'hotp' : 'totp')).toLowerCase();
595
+ if (subType === 'hotp')
596
+ return { problem: 'HOTP (counter-based) authenticators have no Atlas counterpart; re-enrol in Atlas' };
597
+ if (!raw)
598
+ return { problem: 'OTP credential without its secret' };
599
+ const encoding = (0, record_set_1.str)(data.secretEncoding)?.toUpperCase();
600
+ const secretBase32 = encoding === 'BASE32' ? raw.toUpperCase().replace(/[\s=]/g, '') : base32(Buffer.from(raw, 'utf8'));
601
+ const digits = Number(data.digits ?? cred.digits ?? 6);
602
+ const period = Number(data.period ?? cred.period ?? 30);
603
+ return {
604
+ factor: (0, record_set_1.compact)({
605
+ type: 'totp',
606
+ secret_base32: secretBase32,
607
+ digits: Number.isInteger(digits) && digits > 0 ? digits : 6,
608
+ period: Number.isInteger(period) && period > 0 ? period : 30,
609
+ algorithm: otpAlgorithmOf((0, record_set_1.str)(data.algorithm) ?? (0, record_set_1.str)(cred.algorithm) ?? 'HmacSHA1'),
610
+ verified: true,
611
+ created_at: typeof cred.createdDate === 'number' ? cred.createdDate : undefined,
612
+ }),
613
+ };
614
+ }
615
+ /**
616
+ * Recovery codes (`recovery-authn-codes`): Keycloak keeps base64 SHA-512 of
617
+ * each remaining code (RecoveryAuthnCodesUtils.hashRawCode), in the order the
618
+ * user must use them; Atlas accepts any remaining one.
619
+ */
620
+ function keycloakRecoveryCodes(cred) {
621
+ const codes = (0, record_set_1.objs)(jsonObj(cred.secretData).codes)
622
+ .map((c) => (0, record_set_1.str)(c.encodedHashedValue))
623
+ .filter((h) => !!h);
624
+ if (!codes.length)
625
+ return null;
626
+ return (0, record_set_1.compact)({
627
+ type: 'backup_codes',
628
+ backup_code_hashes: codes.map((hash) => ({ algorithm: 'sha512', hash, params: { encoding: 'base64' } })),
629
+ verified: true,
630
+ created_at: typeof cred.createdDate === 'number' ? cred.createdDate : undefined,
631
+ });
632
+ }
633
+ /**
634
+ * A WebAuthn credential (`webauthn` second factor or `webauthn-passwordless`
635
+ * passkey). `credentialId` is standard base64 and `credentialPublicKey` the
636
+ * base64url COSE key (CredentialPublicKeyConverter); both become base64url.
637
+ */
638
+ function keycloakPasskey(cred, rpId) {
639
+ const data = jsonObj(cred.credentialData);
640
+ const credentialId = (0, record_set_1.str)(data.credentialId);
641
+ const publicKey = (0, record_set_1.str)(data.credentialPublicKey);
642
+ if (!credentialId || !publicKey)
643
+ return { problem: 'WebAuthn credential without its id or public key' };
644
+ if (!rpId)
645
+ return { problem: 'WebAuthn credential with no relying-party id (pass --set keycloak-rp-id=<host>)' };
646
+ const aaguid = (0, record_set_1.str)(data.aaguid);
647
+ const counter = Number(data.counter);
648
+ return {
649
+ passkey: (0, record_set_1.compact)({
650
+ credential_id: base64url(credentialId),
651
+ public_key: base64url(publicKey),
652
+ sign_count: Number.isInteger(counter) && counter >= 0 ? counter : undefined,
653
+ rp_id: rpId,
654
+ transports: (0, record_set_1.strs)(data.transports).sort(),
655
+ aaguid: aaguid && aaguid !== '00000000-0000-0000-0000-000000000000' ? aaguid : undefined,
656
+ name: (0, record_set_1.str)(cred.userLabel),
657
+ created_at: typeof cred.createdDate === 'number' ? cred.createdDate : undefined,
658
+ }),
659
+ };
660
+ }
661
+ // ── identities and connections ──────────────────────────────────────────────
662
+ function mapFederatedIdentity(fi, userId, idps, connections) {
663
+ const alias = (0, record_set_1.str)(fi.identityProvider);
664
+ const subject = (0, record_set_1.str)(fi.userId);
665
+ if (!alias || !subject)
666
+ return null;
667
+ const idp = idps.get(alias);
668
+ const providerId = (0, record_set_1.str)(idp?.providerId) ?? alias;
669
+ const userName = (0, record_set_1.str)(fi.userName);
670
+ const email = userName?.includes('@') ? userName.toLowerCase() : undefined;
671
+ if (connections.has(alias)) {
672
+ return (0, record_set_1.compact)({ user_id: userId, provider: alias, provider_user_id: subject, kind: providerId === 'saml' ? 'saml' : 'oidc', sso_connection_id: alias, email, profile: (0, record_set_1.bag)((0, record_set_1.compact)({ username: userName })) });
673
+ }
674
+ const provider = KC_SOCIAL[providerId] ?? (0, translate_kit_1.socialKey)(providerId) ?? alias;
675
+ return (0, record_set_1.compact)({
676
+ user_id: userId,
677
+ provider,
678
+ provider_user_id: subject,
679
+ kind: idp ? 'oauth' : undefined,
680
+ email,
681
+ profile: (0, record_set_1.bag)((0, record_set_1.compact)({ username: userName, keycloak_alias: alias !== provider ? alias : undefined })),
682
+ });
683
+ }
684
+ function ssoConnection(idp, o) {
685
+ const alias = (0, record_set_1.str)(idp.alias);
686
+ const name = (0, record_set_1.str)(idp.displayName) ?? alias;
687
+ const config = (0, record_set_1.isObj)(idp.config) ? idp.config : {};
688
+ const domains = [...new Set(o.domains.filter((d) => !!d).map((d) => d.toLowerCase()))];
689
+ const realmUrl = o.baseUrl ? `${o.baseUrl}/realms/${o.realm}` : undefined;
690
+ if ((0, record_set_1.str)(idp.providerId) === 'saml') {
691
+ const certs = ((0, record_set_1.str)(config.signingCertificate) ?? '').split(',').map((c) => c.trim()).filter(Boolean);
692
+ if (certs.length > 1)
693
+ o.warn(`SAML identity provider ${name}: it has ${certs.length} signing certificates; the first was imported, add the others in Atlas.`);
694
+ return (0, record_set_1.compact)({
695
+ id: alias,
696
+ name,
697
+ organization_id: o.organizationId,
698
+ protocol: 'saml',
699
+ domains,
700
+ enabled: idp.enabled !== false,
701
+ saml: (0, record_set_1.compact)({
702
+ idp_entity_id: (0, record_set_1.str)(config.idpEntityId),
703
+ idp_sso_url: (0, record_set_1.str)(config.singleSignOnServiceUrl),
704
+ idp_certificate: certs[0] ? pem(certs[0]) : undefined,
705
+ idp_metadata_url: (0, record_set_1.str)(config.metadataDescriptorUrl),
706
+ // The IdP knows Keycloak by these: they change to Atlas's values at cutover.
707
+ sp_entity_id: (0, record_set_1.str)(config.entityId) ?? realmUrl,
708
+ sp_acs_url: realmUrl ? `${realmUrl}/broker/${alias}/endpoint` : undefined,
709
+ }),
710
+ });
711
+ }
712
+ const secret = (0, record_set_1.str)(config.clientSecret);
713
+ // The Admin API masks secrets; a realm export keeps them, unless they live in a vault.
714
+ const realSecret = secret && !/^\*+$/.test(secret) && !secret.startsWith('${vault.') ? secret : undefined;
715
+ if (!realSecret)
716
+ o.warn(`OIDC identity provider ${name}: its client secret was not available (masked or in a vault); enter it on the SSO connection after the import.`);
717
+ const issuer = (0, record_set_1.str)(config.issuer);
718
+ return (0, record_set_1.compact)({
719
+ id: alias,
720
+ name,
721
+ organization_id: o.organizationId,
722
+ protocol: 'oidc',
723
+ domains,
724
+ enabled: idp.enabled !== false,
725
+ oidc: (0, record_set_1.compact)({
726
+ issuer,
727
+ discovery_url: (0, record_set_1.str)(config.metadataDescriptorUrl) ?? (issuer ? `${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration` : undefined),
728
+ client_id: (0, record_set_1.str)(config.clientId),
729
+ client_secret: realSecret,
730
+ scopes: ((0, record_set_1.str)(config.defaultScope) ?? '').split(/[\s,]+/).filter(Boolean),
731
+ }),
732
+ });
733
+ }
734
+ /** Flatten a group tree (or a flat list) by path. */
735
+ function indexGroups(groups, parent = '', out = new Map()) {
736
+ for (const g of groups) {
737
+ const name = (0, record_set_1.str)(g.name);
738
+ if (!name)
739
+ continue;
740
+ const path = (0, record_set_1.str)(g.path) ?? `${parent}/${name}`;
741
+ const clientRoles = {};
742
+ for (const [client, list] of Object.entries((0, record_set_1.isObj)(g.clientRoles) ? g.clientRoles : {}))
743
+ clientRoles[client] = (0, record_set_1.strs)(list);
744
+ out.set(path, { id: (0, record_set_1.str)(g.id) ?? path, name, path, realmRoles: (0, record_set_1.strs)(g.realmRoles), clientRoles, attributes: g.attributes });
745
+ indexGroups((0, record_set_1.objs)(g.subGroups), path, out);
746
+ }
747
+ return out;
748
+ }
749
+ /**
750
+ * A user's app-role keys: realm and client roles mapped to the user, plus the
751
+ * roles of every group they belong to and of that group's ancestors (Keycloak
752
+ * hands a member all of those), plus one role per group with groups-as-roles.
753
+ */
754
+ function userRoleKeys(u, groups, realm, groupRoles) {
755
+ const keys = new Set();
756
+ const addRealm = (name) => {
757
+ if (!isBuiltInRole(name))
758
+ keys.add((0, translate_kit_1.keyOf)(name));
759
+ };
760
+ const addClient = (client, names) => {
761
+ if (isBuiltInClient(client, realm))
762
+ return;
763
+ for (const n of names)
764
+ keys.add(clientRoleKey(client, n));
765
+ };
766
+ (0, record_set_1.strs)(u.realmRoles).forEach(addRealm);
767
+ for (const [client, names] of Object.entries((0, record_set_1.isObj)(u.clientRoles) ? u.clientRoles : {}))
768
+ addClient(client, (0, record_set_1.strs)(names));
769
+ for (const path of (0, record_set_1.strs)(u.groups)) {
770
+ if (groupRoles)
771
+ keys.add(groupKey(path));
772
+ const parts = path.split('/').filter(Boolean);
773
+ for (let i = 1; i <= parts.length; i++) {
774
+ const g = groups.get(`/${parts.slice(0, i).join('/')}`);
775
+ if (!g)
776
+ continue;
777
+ g.realmRoles.forEach(addRealm);
778
+ for (const [client, names] of Object.entries(g.clientRoles))
779
+ addClient(client, names);
780
+ }
781
+ }
782
+ return [...keys];
783
+ }
784
+ function isBuiltInRole(name) {
785
+ return BUILT_IN_ROLES.has(name) || name.startsWith('default-roles-');
786
+ }
787
+ function isBuiltInClient(clientId, realm) {
788
+ // The master realm has one `<realm>-realm` admin client per realm.
789
+ return BUILT_IN_CLIENTS.has(clientId) || (realm === 'master' && clientId.endsWith('-realm'));
790
+ }
791
+ /** `acme-web` + `billing` → `acme_web:billing`. */
792
+ function clientRoleKey(clientId, role) {
793
+ return `${(0, translate_kit_1.keyOf)(clientId)}:${(0, translate_kit_1.keyOf)(role)}`;
794
+ }
795
+ /** `/Engineering/Backend` → `group:engineering_backend`. */
796
+ function groupKey(path) {
797
+ return `group:${(0, translate_kit_1.keyOf)(path.split('/').filter(Boolean).join('_'))}`;
798
+ }
799
+ // ── small helpers ───────────────────────────────────────────────────────────
800
+ /** Keycloak keeps credential data as JSON strings inside the JSON. */
801
+ function jsonObj(v) {
802
+ if ((0, record_set_1.isObj)(v))
803
+ return v;
804
+ if (typeof v !== 'string' || !v.trim())
805
+ return {};
806
+ try {
807
+ const parsed = JSON.parse(v);
808
+ return (0, record_set_1.isObj)(parsed) ? parsed : {};
809
+ }
810
+ catch {
811
+ return {};
812
+ }
813
+ }
814
+ /** `{ dept: ["R&D"], tags: ["a", "b"] }` → `{ dept: "R&D", tags: ["a", "b"] }`. */
815
+ function unwrapAttributes(v) {
816
+ const out = {};
817
+ for (const [k, raw] of Object.entries((0, record_set_1.isObj)(v) ? v : {})) {
818
+ const list = Array.isArray(raw) ? raw.filter((x) => x !== null && x !== undefined && x !== '') : raw === null || raw === undefined || raw === '' ? [] : [raw];
819
+ if (list.length === 1)
820
+ out[k] = list[0];
821
+ else if (list.length > 1)
822
+ out[k] = list;
823
+ }
824
+ return out;
825
+ }
826
+ const OTP_ALGORITHMS = { hmacsha1: 'SHA1', hmacsha256: 'SHA256', hmacsha512: 'SHA512', sha1: 'SHA1', sha256: 'SHA256', sha512: 'SHA512' };
827
+ /** `HmacSHA256` → `SHA256` (SHA1 for anything unknown, Keycloak's default). */
828
+ function otpAlgorithmOf(raw) {
829
+ return OTP_ALGORITHMS[raw.toLowerCase().replace(/[^a-z0-9]/g, '')] ?? 'SHA1';
830
+ }
831
+ const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
832
+ /** RFC 4648 base32, upper case, no padding. */
833
+ function base32(bytes) {
834
+ let out = '';
835
+ let bits = 0;
836
+ let value = 0;
837
+ for (const b of bytes) {
838
+ value = (value << 8) | b;
839
+ bits += 8;
840
+ while (bits >= 5) {
841
+ out += B32.charAt((value >>> (bits - 5)) & 31);
842
+ bits -= 5;
843
+ }
844
+ value &= (1 << bits) - 1;
845
+ }
846
+ if (bits > 0)
847
+ out += B32.charAt((value << (5 - bits)) & 31);
848
+ return out;
849
+ }
850
+ /** Standard or URL-safe base64 (padded or not) → unpadded base64url. */
851
+ function base64url(s) {
852
+ return s.replace(/\s+/g, '').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
853
+ }
854
+ function pem(b64) {
855
+ const body = b64.replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
856
+ return `-----BEGIN CERTIFICATE-----\n${body.match(/.{1,64}/g).join('\n')}\n-----END CERTIFICATE-----`;
857
+ }
858
+ /** Export a realm from an export file (hashes and OTP secrets included) or the Admin API (no secrets). */
859
+ async function exportKeycloak(o) {
860
+ let snap;
861
+ if (o.exportDocs) {
862
+ snap = readKeycloakExport(o.exportDocs, o.realm);
863
+ if (o.api?.baseUrl)
864
+ snap.baseUrl = keycloakBase(o.api.baseUrl);
865
+ }
866
+ else {
867
+ if (!o.api || !o.realm)
868
+ throw new Error('Keycloak: name the realm and give an admin credential, or a realm export.');
869
+ const warnings = [];
870
+ snap = await fetchKeycloakRealm(keycloakAdminHttp({ ...o.api, realm: o.realm }), { realm: o.realm, baseUrl: o.api.baseUrl, onProgress: o.onProgress, warn: (m) => warnings.push(m) });
871
+ const set = mapKeycloak(snap, o);
872
+ for (const w of warnings)
873
+ set.warn(w);
874
+ return set;
875
+ }
876
+ return mapKeycloak(snap, o);
877
+ }
878
+ //# sourceMappingURL=keycloak.js.map