@atlasauth/cli 0.25.0 → 0.26.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 (546) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +26 -5
  3. package/dist/args.d.ts +631 -0
  4. package/dist/args.js +304 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +19 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/actions-gallery.d.ts +8 -0
  10. package/dist/commands/actions-gallery.js +197 -0
  11. package/dist/commands/actions-gallery.js.map +1 -0
  12. package/dist/commands/actions.d.ts +13 -0
  13. package/dist/commands/actions.js +135 -0
  14. package/dist/commands/actions.js.map +1 -0
  15. package/dist/commands/admin.d.ts +49 -0
  16. package/dist/commands/admin.js +353 -0
  17. package/dist/commands/admin.js.map +1 -0
  18. package/dist/commands/api-keys.d.ts +4 -0
  19. package/dist/commands/api-keys.js +96 -0
  20. package/dist/commands/api-keys.js.map +1 -0
  21. package/dist/commands/auth.d.ts +25 -0
  22. package/dist/commands/auth.js +102 -0
  23. package/dist/commands/auth.js.map +1 -0
  24. package/dist/commands/config.d.ts +128 -0
  25. package/dist/commands/config.js +1822 -0
  26. package/dist/commands/config.js.map +1 -0
  27. package/dist/commands/csp.d.ts +32 -0
  28. package/dist/commands/csp.js +180 -0
  29. package/dist/commands/csp.js.map +1 -0
  30. package/dist/commands/dev.d.ts +8 -0
  31. package/dist/commands/dev.js +84 -0
  32. package/dist/commands/dev.js.map +1 -0
  33. package/dist/commands/doctor-scan.d.ts +27 -0
  34. package/dist/commands/doctor-scan.js +221 -0
  35. package/dist/commands/doctor-scan.js.map +1 -0
  36. package/dist/commands/fga.d.ts +10 -0
  37. package/dist/commands/fga.js +146 -0
  38. package/dist/commands/fga.js.map +1 -0
  39. package/dist/commands/golive.d.ts +40 -0
  40. package/dist/commands/golive.js +297 -0
  41. package/dist/commands/golive.js.map +1 -0
  42. package/dist/commands/id-accounts.d.ts +13 -0
  43. package/dist/commands/id-accounts.js +195 -0
  44. package/dist/commands/id-accounts.js.map +1 -0
  45. package/dist/commands/import-better-auth.d.ts +3 -0
  46. package/dist/commands/import-better-auth.js +164 -0
  47. package/dist/commands/import-better-auth.js.map +1 -0
  48. package/dist/commands/import-jsonl.d.ts +50 -0
  49. package/dist/commands/import-jsonl.js +252 -0
  50. package/dist/commands/import-jsonl.js.map +1 -0
  51. package/dist/commands/import.d.ts +3 -0
  52. package/dist/commands/import.js +133 -0
  53. package/dist/commands/import.js.map +1 -0
  54. package/dist/commands/info.d.ts +75 -0
  55. package/dist/commands/info.js +362 -0
  56. package/dist/commands/info.js.map +1 -0
  57. package/dist/commands/init-next.d.ts +4 -0
  58. package/dist/commands/init-next.js +86 -0
  59. package/dist/commands/init-next.js.map +1 -0
  60. package/dist/commands/init.d.ts +3 -0
  61. package/dist/commands/init.js +525 -0
  62. package/dist/commands/init.js.map +1 -0
  63. package/dist/commands/integrations.d.ts +8 -0
  64. package/dist/commands/integrations.js +112 -0
  65. package/dist/commands/integrations.js.map +1 -0
  66. package/dist/commands/listen.d.ts +17 -0
  67. package/dist/commands/listen.js +223 -0
  68. package/dist/commands/listen.js.map +1 -0
  69. package/dist/commands/mcp.d.ts +38 -0
  70. package/dist/commands/mcp.js +111 -0
  71. package/dist/commands/mcp.js.map +1 -0
  72. package/dist/commands/migrate.d.ts +16 -0
  73. package/dist/commands/migrate.js +114 -0
  74. package/dist/commands/migrate.js.map +1 -0
  75. package/dist/commands/move-origin.d.ts +11 -0
  76. package/dist/commands/move-origin.js +95 -0
  77. package/dist/commands/move-origin.js.map +1 -0
  78. package/dist/commands/operator.d.ts +18 -0
  79. package/dist/commands/operator.js +238 -0
  80. package/dist/commands/operator.js.map +1 -0
  81. package/dist/commands/orgs.d.ts +4 -0
  82. package/dist/commands/orgs.js +72 -0
  83. package/dist/commands/orgs.js.map +1 -0
  84. package/dist/commands/pages.d.ts +14 -0
  85. package/dist/commands/pages.js +113 -0
  86. package/dist/commands/pages.js.map +1 -0
  87. package/dist/commands/partner-platforms.d.ts +10 -0
  88. package/dist/commands/partner-platforms.js +157 -0
  89. package/dist/commands/partner-platforms.js.map +1 -0
  90. package/dist/commands/project-commands.d.ts +38 -0
  91. package/dist/commands/project-commands.js +431 -0
  92. package/dist/commands/project-commands.js.map +1 -0
  93. package/dist/commands/referrals.d.ts +69 -0
  94. package/dist/commands/referrals.js +1056 -0
  95. package/dist/commands/referrals.js.map +1 -0
  96. package/dist/commands/resources.d.ts +44 -0
  97. package/dist/commands/resources.js +234 -0
  98. package/dist/commands/resources.js.map +1 -0
  99. package/dist/commands/sessions.d.ts +4 -0
  100. package/dist/commands/sessions.js +28 -0
  101. package/dist/commands/sessions.js.map +1 -0
  102. package/dist/commands/setup-guide.d.ts +3 -0
  103. package/dist/commands/setup-guide.js +38 -0
  104. package/dist/commands/setup-guide.js.map +1 -0
  105. package/dist/commands/shared.d.ts +38 -0
  106. package/dist/commands/shared.js +69 -0
  107. package/dist/commands/shared.js.map +1 -0
  108. package/dist/commands/status.d.ts +3 -0
  109. package/dist/commands/status.js +77 -0
  110. package/dist/commands/status.js.map +1 -0
  111. package/dist/commands/users.d.ts +4 -0
  112. package/dist/commands/users.js +111 -0
  113. package/dist/commands/users.js.map +1 -0
  114. package/dist/commands/webhooks.d.ts +34 -0
  115. package/dist/commands/webhooks.js +301 -0
  116. package/dist/commands/webhooks.js.map +1 -0
  117. package/dist/config-http.d.ts +38 -0
  118. package/dist/config-http.js +70 -0
  119. package/dist/config-http.js.map +1 -0
  120. package/dist/config.d.ts +64 -0
  121. package/dist/config.js +69 -0
  122. package/dist/config.js.map +1 -0
  123. package/dist/context.d.ts +89 -0
  124. package/dist/context.js +32 -0
  125. package/dist/context.js.map +1 -0
  126. package/dist/define-config.d.ts +9 -0
  127. package/dist/define-config.js +8 -0
  128. package/dist/define-config.js.map +1 -0
  129. package/dist/errors.d.ts +15 -0
  130. package/dist/errors.js +25 -0
  131. package/dist/errors.js.map +1 -0
  132. package/dist/help.d.ts +8 -0
  133. package/dist/help.js +926 -0
  134. package/dist/help.js.map +1 -0
  135. package/dist/import/csv.d.ts +6 -0
  136. package/dist/import/csv.js +63 -0
  137. package/dist/import/csv.js.map +1 -0
  138. package/dist/import/vendors.d.ts +57 -0
  139. package/dist/import/vendors.js +568 -0
  140. package/dist/import/vendors.js.map +1 -0
  141. package/dist/index.d.ts +33 -0
  142. package/dist/index.js +81 -0
  143. package/dist/index.js.map +1 -0
  144. package/dist/io.d.ts +21 -0
  145. package/dist/io.js +28 -0
  146. package/dist/io.js.map +1 -0
  147. package/dist/migrate/appwrite/plugin.d.ts +2 -0
  148. package/dist/migrate/appwrite/plugin.js +103 -0
  149. package/dist/migrate/appwrite/plugin.js.map +1 -0
  150. package/dist/migrate/auth0/plugin.d.ts +2 -0
  151. package/dist/migrate/auth0/plugin.js +126 -0
  152. package/dist/migrate/auth0/plugin.js.map +1 -0
  153. package/dist/migrate/authentik/plugin.d.ts +2 -0
  154. package/dist/migrate/authentik/plugin.js +135 -0
  155. package/dist/migrate/authentik/plugin.js.map +1 -0
  156. package/dist/migrate/azure-ad-b2c/plugin.d.ts +8 -0
  157. package/dist/migrate/azure-ad-b2c/plugin.js +165 -0
  158. package/dist/migrate/azure-ad-b2c/plugin.js.map +1 -0
  159. package/dist/migrate/better-auth/integration.d.ts +59 -0
  160. package/dist/migrate/better-auth/integration.js +81 -0
  161. package/dist/migrate/better-auth/integration.js.map +1 -0
  162. package/dist/migrate/better-auth/orchestrator.d.ts +43 -0
  163. package/dist/migrate/better-auth/orchestrator.js +525 -0
  164. package/dist/migrate/better-auth/orchestrator.js.map +1 -0
  165. package/dist/migrate/better-auth/output.d.ts +20 -0
  166. package/dist/migrate/better-auth/output.js +146 -0
  167. package/dist/migrate/better-auth/output.js.map +1 -0
  168. package/dist/migrate/better-auth/translate.d.ts +77 -0
  169. package/dist/migrate/better-auth/translate.js +1989 -0
  170. package/dist/migrate/better-auth/translate.js.map +1 -0
  171. package/dist/migrate/clerk/plugin.d.ts +2 -0
  172. package/dist/migrate/clerk/plugin.js +76 -0
  173. package/dist/migrate/clerk/plugin.js.map +1 -0
  174. package/dist/migrate/cognito/plugin.d.ts +5 -0
  175. package/dist/migrate/cognito/plugin.js +170 -0
  176. package/dist/migrate/cognito/plugin.js.map +1 -0
  177. package/dist/migrate/corbado/plugin.d.ts +2 -0
  178. package/dist/migrate/corbado/plugin.js +63 -0
  179. package/dist/migrate/corbado/plugin.js.map +1 -0
  180. package/dist/migrate/descope/plugin.d.ts +2 -0
  181. package/dist/migrate/descope/plugin.js +83 -0
  182. package/dist/migrate/descope/plugin.js.map +1 -0
  183. package/dist/migrate/entra-external-id/plugin.d.ts +8 -0
  184. package/dist/migrate/entra-external-id/plugin.js +12 -0
  185. package/dist/migrate/entra-external-id/plugin.js.map +1 -0
  186. package/dist/migrate/export/appwrite-config.d.ts +28 -0
  187. package/dist/migrate/export/appwrite-config.js +170 -0
  188. package/dist/migrate/export/appwrite-config.js.map +1 -0
  189. package/dist/migrate/export/appwrite.d.ts +51 -0
  190. package/dist/migrate/export/appwrite.js +288 -0
  191. package/dist/migrate/export/appwrite.js.map +1 -0
  192. package/dist/migrate/export/auth0-config.d.ts +29 -0
  193. package/dist/migrate/export/auth0-config.js +249 -0
  194. package/dist/migrate/export/auth0-config.js.map +1 -0
  195. package/dist/migrate/export/auth0.d.ts +62 -0
  196. package/dist/migrate/export/auth0.js +575 -0
  197. package/dist/migrate/export/auth0.js.map +1 -0
  198. package/dist/migrate/export/authentik-config.d.ts +45 -0
  199. package/dist/migrate/export/authentik-config.js +357 -0
  200. package/dist/migrate/export/authentik-config.js.map +1 -0
  201. package/dist/migrate/export/authentik.d.ts +110 -0
  202. package/dist/migrate/export/authentik.js +838 -0
  203. package/dist/migrate/export/authentik.js.map +1 -0
  204. package/dist/migrate/export/azure-config.d.ts +58 -0
  205. package/dist/migrate/export/azure-config.js +344 -0
  206. package/dist/migrate/export/azure-config.js.map +1 -0
  207. package/dist/migrate/export/azure.d.ts +85 -0
  208. package/dist/migrate/export/azure.js +373 -0
  209. package/dist/migrate/export/azure.js.map +1 -0
  210. package/dist/migrate/export/clerk-config.d.ts +26 -0
  211. package/dist/migrate/export/clerk-config.js +227 -0
  212. package/dist/migrate/export/clerk-config.js.map +1 -0
  213. package/dist/migrate/export/clerk.d.ts +46 -0
  214. package/dist/migrate/export/clerk.js +486 -0
  215. package/dist/migrate/export/clerk.js.map +1 -0
  216. package/dist/migrate/export/cognito-config.d.ts +25 -0
  217. package/dist/migrate/export/cognito-config.js +273 -0
  218. package/dist/migrate/export/cognito-config.js.map +1 -0
  219. package/dist/migrate/export/cognito.d.ts +103 -0
  220. package/dist/migrate/export/cognito.js +441 -0
  221. package/dist/migrate/export/cognito.js.map +1 -0
  222. package/dist/migrate/export/corbado-config.d.ts +20 -0
  223. package/dist/migrate/export/corbado-config.js +63 -0
  224. package/dist/migrate/export/corbado-config.js.map +1 -0
  225. package/dist/migrate/export/corbado.d.ts +37 -0
  226. package/dist/migrate/export/corbado.js +132 -0
  227. package/dist/migrate/export/corbado.js.map +1 -0
  228. package/dist/migrate/export/descope.d.ts +35 -0
  229. package/dist/migrate/export/descope.js +208 -0
  230. package/dist/migrate/export/descope.js.map +1 -0
  231. package/dist/migrate/export/fake-vendor.test-helper.d.ts +27 -0
  232. package/dist/migrate/export/fake-vendor.test-helper.js +41 -0
  233. package/dist/migrate/export/fake-vendor.test-helper.js.map +1 -0
  234. package/dist/migrate/export/firebase-config.d.ts +24 -0
  235. package/dist/migrate/export/firebase-config.js +215 -0
  236. package/dist/migrate/export/firebase-config.js.map +1 -0
  237. package/dist/migrate/export/firebase.d.ts +102 -0
  238. package/dist/migrate/export/firebase.js +464 -0
  239. package/dist/migrate/export/firebase.js.map +1 -0
  240. package/dist/migrate/export/frameworks/app-users.d.ts +42 -0
  241. package/dist/migrate/export/frameworks/app-users.js +161 -0
  242. package/dist/migrate/export/frameworks/app-users.js.map +1 -0
  243. package/dist/migrate/export/frameworks/authjs.d.ts +41 -0
  244. package/dist/migrate/export/frameworks/authjs.js +146 -0
  245. package/dist/migrate/export/frameworks/authjs.js.map +1 -0
  246. package/dist/migrate/export/frameworks/devise.d.ts +39 -0
  247. package/dist/migrate/export/frameworks/devise.js +218 -0
  248. package/dist/migrate/export/frameworks/devise.js.map +1 -0
  249. package/dist/migrate/export/frameworks/django.d.ts +45 -0
  250. package/dist/migrate/export/frameworks/django.js +315 -0
  251. package/dist/migrate/export/frameworks/django.js.map +1 -0
  252. package/dist/migrate/export/frameworks/lucia.d.ts +42 -0
  253. package/dist/migrate/export/frameworks/lucia.js +263 -0
  254. package/dist/migrate/export/frameworks/lucia.js.map +1 -0
  255. package/dist/migrate/export/frameworks/tables.d.ts +90 -0
  256. package/dist/migrate/export/frameworks/tables.js +325 -0
  257. package/dist/migrate/export/frameworks/tables.js.map +1 -0
  258. package/dist/migrate/export/frameworks/webauthn.d.ts +25 -0
  259. package/dist/migrate/export/frameworks/webauthn.js +130 -0
  260. package/dist/migrate/export/frameworks/webauthn.js.map +1 -0
  261. package/dist/migrate/export/frontegg-config.d.ts +29 -0
  262. package/dist/migrate/export/frontegg-config.js +238 -0
  263. package/dist/migrate/export/frontegg-config.js.map +1 -0
  264. package/dist/migrate/export/frontegg.d.ts +67 -0
  265. package/dist/migrate/export/frontegg.js +410 -0
  266. package/dist/migrate/export/frontegg.js.map +1 -0
  267. package/dist/migrate/export/fusionauth-config.d.ts +26 -0
  268. package/dist/migrate/export/fusionauth-config.js +226 -0
  269. package/dist/migrate/export/fusionauth-config.js.map +1 -0
  270. package/dist/migrate/export/fusionauth.d.ts +99 -0
  271. package/dist/migrate/export/fusionauth.js +642 -0
  272. package/dist/migrate/export/fusionauth.js.map +1 -0
  273. package/dist/migrate/export/hanko-config.d.ts +27 -0
  274. package/dist/migrate/export/hanko-config.js +265 -0
  275. package/dist/migrate/export/hanko-config.js.map +1 -0
  276. package/dist/migrate/export/hanko.d.ts +52 -0
  277. package/dist/migrate/export/hanko.js +605 -0
  278. package/dist/migrate/export/hanko.js.map +1 -0
  279. package/dist/migrate/export/keycloak-config.d.ts +19 -0
  280. package/dist/migrate/export/keycloak-config.js +219 -0
  281. package/dist/migrate/export/keycloak-config.js.map +1 -0
  282. package/dist/migrate/export/keycloak.d.ts +155 -0
  283. package/dist/migrate/export/keycloak.js +878 -0
  284. package/dist/migrate/export/keycloak.js.map +1 -0
  285. package/dist/migrate/export/kinde-descope-config.d.ts +38 -0
  286. package/dist/migrate/export/kinde-descope-config.js +172 -0
  287. package/dist/migrate/export/kinde-descope-config.js.map +1 -0
  288. package/dist/migrate/export/kinde.d.ts +57 -0
  289. package/dist/migrate/export/kinde.js +333 -0
  290. package/dist/migrate/export/kinde.js.map +1 -0
  291. package/dist/migrate/export/ldap.d.ts +53 -0
  292. package/dist/migrate/export/ldap.js +277 -0
  293. package/dist/migrate/export/ldap.js.map +1 -0
  294. package/dist/migrate/export/logto-config.d.ts +27 -0
  295. package/dist/migrate/export/logto-config.js +264 -0
  296. package/dist/migrate/export/logto-config.js.map +1 -0
  297. package/dist/migrate/export/logto.d.ts +150 -0
  298. package/dist/migrate/export/logto.js +809 -0
  299. package/dist/migrate/export/logto.js.map +1 -0
  300. package/dist/migrate/export/magic-config.d.ts +14 -0
  301. package/dist/migrate/export/magic-config.js +28 -0
  302. package/dist/migrate/export/magic-config.js.map +1 -0
  303. package/dist/migrate/export/magic.d.ts +31 -0
  304. package/dist/migrate/export/magic.js +93 -0
  305. package/dist/migrate/export/magic.js.map +1 -0
  306. package/dist/migrate/export/mini-yaml.d.ts +19 -0
  307. package/dist/migrate/export/mini-yaml.js +581 -0
  308. package/dist/migrate/export/mini-yaml.js.map +1 -0
  309. package/dist/migrate/export/nhost-config.d.ts +26 -0
  310. package/dist/migrate/export/nhost-config.js +360 -0
  311. package/dist/migrate/export/nhost-config.js.map +1 -0
  312. package/dist/migrate/export/nhost.d.ts +64 -0
  313. package/dist/migrate/export/nhost.js +304 -0
  314. package/dist/migrate/export/nhost.js.map +1 -0
  315. package/dist/migrate/export/okta-config.d.ts +30 -0
  316. package/dist/migrate/export/okta-config.js +205 -0
  317. package/dist/migrate/export/okta-config.js.map +1 -0
  318. package/dist/migrate/export/okta.d.ts +43 -0
  319. package/dist/migrate/export/okta.js +337 -0
  320. package/dist/migrate/export/okta.js.map +1 -0
  321. package/dist/migrate/export/onelogin.d.ts +22 -0
  322. package/dist/migrate/export/onelogin.js +123 -0
  323. package/dist/migrate/export/onelogin.js.map +1 -0
  324. package/dist/migrate/export/ory-config.d.ts +58 -0
  325. package/dist/migrate/export/ory-config.js +451 -0
  326. package/dist/migrate/export/ory-config.js.map +1 -0
  327. package/dist/migrate/export/ory.d.ts +81 -0
  328. package/dist/migrate/export/ory.js +555 -0
  329. package/dist/migrate/export/ory.js.map +1 -0
  330. package/dist/migrate/export/password-detect.d.ts +29 -0
  331. package/dist/migrate/export/password-detect.js +81 -0
  332. package/dist/migrate/export/password-detect.js.map +1 -0
  333. package/dist/migrate/export/pingone.d.ts +36 -0
  334. package/dist/migrate/export/pingone.js +218 -0
  335. package/dist/migrate/export/pingone.js.map +1 -0
  336. package/dist/migrate/export/plugin-kit.d.ts +25 -0
  337. package/dist/migrate/export/plugin-kit.js +82 -0
  338. package/dist/migrate/export/plugin-kit.js.map +1 -0
  339. package/dist/migrate/export/pocketbase-config.d.ts +28 -0
  340. package/dist/migrate/export/pocketbase-config.js +180 -0
  341. package/dist/migrate/export/pocketbase-config.js.map +1 -0
  342. package/dist/migrate/export/pocketbase.d.ts +52 -0
  343. package/dist/migrate/export/pocketbase.js +243 -0
  344. package/dist/migrate/export/pocketbase.js.map +1 -0
  345. package/dist/migrate/export/project-files.d.ts +15 -0
  346. package/dist/migrate/export/project-files.js +43 -0
  347. package/dist/migrate/export/project-files.js.map +1 -0
  348. package/dist/migrate/export/propelauth-config.d.ts +18 -0
  349. package/dist/migrate/export/propelauth-config.js +58 -0
  350. package/dist/migrate/export/propelauth-config.js.map +1 -0
  351. package/dist/migrate/export/propelauth-harness.test-helper.d.ts +62 -0
  352. package/dist/migrate/export/propelauth-harness.test-helper.js +109 -0
  353. package/dist/migrate/export/propelauth-harness.test-helper.js.map +1 -0
  354. package/dist/migrate/export/propelauth.d.ts +48 -0
  355. package/dist/migrate/export/propelauth.js +255 -0
  356. package/dist/migrate/export/propelauth.js.map +1 -0
  357. package/dist/migrate/export/record-set.d.ts +44 -0
  358. package/dist/migrate/export/record-set.js +98 -0
  359. package/dist/migrate/export/record-set.js.map +1 -0
  360. package/dist/migrate/export/source-lines.d.ts +17 -0
  361. package/dist/migrate/export/source-lines.js +43 -0
  362. package/dist/migrate/export/source-lines.js.map +1 -0
  363. package/dist/migrate/export/sso-metadata.d.ts +47 -0
  364. package/dist/migrate/export/sso-metadata.js +180 -0
  365. package/dist/migrate/export/sso-metadata.js.map +1 -0
  366. package/dist/migrate/export/stytch-config.d.ts +22 -0
  367. package/dist/migrate/export/stytch-config.js +102 -0
  368. package/dist/migrate/export/stytch-config.js.map +1 -0
  369. package/dist/migrate/export/stytch.d.ts +68 -0
  370. package/dist/migrate/export/stytch.js +510 -0
  371. package/dist/migrate/export/stytch.js.map +1 -0
  372. package/dist/migrate/export/supabase-config.d.ts +48 -0
  373. package/dist/migrate/export/supabase-config.js +408 -0
  374. package/dist/migrate/export/supabase-config.js.map +1 -0
  375. package/dist/migrate/export/supabase.d.ts +86 -0
  376. package/dist/migrate/export/supabase.js +524 -0
  377. package/dist/migrate/export/supabase.js.map +1 -0
  378. package/dist/migrate/export/supertokens-config.d.ts +20 -0
  379. package/dist/migrate/export/supertokens-config.js +150 -0
  380. package/dist/migrate/export/supertokens-config.js.map +1 -0
  381. package/dist/migrate/export/supertokens.d.ts +125 -0
  382. package/dist/migrate/export/supertokens.js +464 -0
  383. package/dist/migrate/export/supertokens.js.map +1 -0
  384. package/dist/migrate/export/table.d.ts +35 -0
  385. package/dist/migrate/export/table.js +248 -0
  386. package/dist/migrate/export/table.js.map +1 -0
  387. package/dist/migrate/export/translate-kit.d.ts +46 -0
  388. package/dist/migrate/export/translate-kit.js +148 -0
  389. package/dist/migrate/export/translate-kit.js.map +1 -0
  390. package/dist/migrate/export/userfront-config.d.ts +15 -0
  391. package/dist/migrate/export/userfront-config.js +49 -0
  392. package/dist/migrate/export/userfront-config.js.map +1 -0
  393. package/dist/migrate/export/userfront.d.ts +38 -0
  394. package/dist/migrate/export/userfront.js +189 -0
  395. package/dist/migrate/export/userfront.js.map +1 -0
  396. package/dist/migrate/export/vendor-http.d.ts +46 -0
  397. package/dist/migrate/export/vendor-http.js +138 -0
  398. package/dist/migrate/export/vendor-http.js.map +1 -0
  399. package/dist/migrate/export/workos-config.d.ts +23 -0
  400. package/dist/migrate/export/workos-config.js +125 -0
  401. package/dist/migrate/export/workos-config.js.map +1 -0
  402. package/dist/migrate/export/workos.d.ts +46 -0
  403. package/dist/migrate/export/workos.js +272 -0
  404. package/dist/migrate/export/workos.js.map +1 -0
  405. package/dist/migrate/export/zip.d.ts +8 -0
  406. package/dist/migrate/export/zip.js +66 -0
  407. package/dist/migrate/export/zip.js.map +1 -0
  408. package/dist/migrate/export/zitadel-config.d.ts +37 -0
  409. package/dist/migrate/export/zitadel-config.js +279 -0
  410. package/dist/migrate/export/zitadel-config.js.map +1 -0
  411. package/dist/migrate/export/zitadel.d.ts +82 -0
  412. package/dist/migrate/export/zitadel.js +447 -0
  413. package/dist/migrate/export/zitadel.js.map +1 -0
  414. package/dist/migrate/firebase/plugin.d.ts +2 -0
  415. package/dist/migrate/firebase/plugin.js +152 -0
  416. package/dist/migrate/firebase/plugin.js.map +1 -0
  417. package/dist/migrate/framework/builtins.d.ts +2 -0
  418. package/dist/migrate/framework/builtins.js +52 -0
  419. package/dist/migrate/framework/builtins.js.map +1 -0
  420. package/dist/migrate/framework/commands.d.ts +20 -0
  421. package/dist/migrate/framework/commands.js +58 -0
  422. package/dist/migrate/framework/commands.js.map +1 -0
  423. package/dist/migrate/framework/custom.d.ts +12 -0
  424. package/dist/migrate/framework/custom.js +96 -0
  425. package/dist/migrate/framework/custom.js.map +1 -0
  426. package/dist/migrate/framework/orchestrator.d.ts +113 -0
  427. package/dist/migrate/framework/orchestrator.js +532 -0
  428. package/dist/migrate/framework/orchestrator.js.map +1 -0
  429. package/dist/migrate/framework/plugin.d.ts +167 -0
  430. package/dist/migrate/framework/plugin.js +3 -0
  431. package/dist/migrate/framework/plugin.js.map +1 -0
  432. package/dist/migrate/framework/registry.d.ts +11 -0
  433. package/dist/migrate/framework/registry.js +44 -0
  434. package/dist/migrate/framework/registry.js.map +1 -0
  435. package/dist/migrate/framework/report.d.ts +17 -0
  436. package/dist/migrate/framework/report.js +103 -0
  437. package/dist/migrate/framework/report.js.map +1 -0
  438. package/dist/migrate/frameworks/plugins.d.ts +29 -0
  439. package/dist/migrate/frameworks/plugins.js +402 -0
  440. package/dist/migrate/frameworks/plugins.js.map +1 -0
  441. package/dist/migrate/frameworks/report.d.ts +92 -0
  442. package/dist/migrate/frameworks/report.js +207 -0
  443. package/dist/migrate/frameworks/report.js.map +1 -0
  444. package/dist/migrate/frameworks/translate-js.d.ts +5 -0
  445. package/dist/migrate/frameworks/translate-js.js +503 -0
  446. package/dist/migrate/frameworks/translate-js.js.map +1 -0
  447. package/dist/migrate/frameworks/translate-rails-django.d.ts +3 -0
  448. package/dist/migrate/frameworks/translate-rails-django.js +1391 -0
  449. package/dist/migrate/frameworks/translate-rails-django.js.map +1 -0
  450. package/dist/migrate/frontegg/plugin.d.ts +2 -0
  451. package/dist/migrate/frontegg/plugin.js +78 -0
  452. package/dist/migrate/frontegg/plugin.js.map +1 -0
  453. package/dist/migrate/fusionauth/plugin.d.ts +2 -0
  454. package/dist/migrate/fusionauth/plugin.js +109 -0
  455. package/dist/migrate/fusionauth/plugin.js.map +1 -0
  456. package/dist/migrate/hanko/plugin.d.ts +2 -0
  457. package/dist/migrate/hanko/plugin.js +100 -0
  458. package/dist/migrate/hanko/plugin.js.map +1 -0
  459. package/dist/migrate/keycloak/plugin.d.ts +2 -0
  460. package/dist/migrate/keycloak/plugin.js +148 -0
  461. package/dist/migrate/keycloak/plugin.js.map +1 -0
  462. package/dist/migrate/kinde/plugin.d.ts +2 -0
  463. package/dist/migrate/kinde/plugin.js +105 -0
  464. package/dist/migrate/kinde/plugin.js.map +1 -0
  465. package/dist/migrate/ldap/plugin.d.ts +2 -0
  466. package/dist/migrate/ldap/plugin.js +73 -0
  467. package/dist/migrate/ldap/plugin.js.map +1 -0
  468. package/dist/migrate/logto/plugin.d.ts +2 -0
  469. package/dist/migrate/logto/plugin.js +131 -0
  470. package/dist/migrate/logto/plugin.js.map +1 -0
  471. package/dist/migrate/magic/plugin.d.ts +2 -0
  472. package/dist/migrate/magic/plugin.js +56 -0
  473. package/dist/migrate/magic/plugin.js.map +1 -0
  474. package/dist/migrate/nhost/plugin.d.ts +2 -0
  475. package/dist/migrate/nhost/plugin.js +115 -0
  476. package/dist/migrate/nhost/plugin.js.map +1 -0
  477. package/dist/migrate/oidc/plugin.d.ts +2 -0
  478. package/dist/migrate/oidc/plugin.js +73 -0
  479. package/dist/migrate/oidc/plugin.js.map +1 -0
  480. package/dist/migrate/okta/plugin.d.ts +2 -0
  481. package/dist/migrate/okta/plugin.js +105 -0
  482. package/dist/migrate/okta/plugin.js.map +1 -0
  483. package/dist/migrate/onelogin/plugin.d.ts +2 -0
  484. package/dist/migrate/onelogin/plugin.js +61 -0
  485. package/dist/migrate/onelogin/plugin.js.map +1 -0
  486. package/dist/migrate/ory/plugin.d.ts +2 -0
  487. package/dist/migrate/ory/plugin.js +186 -0
  488. package/dist/migrate/ory/plugin.js.map +1 -0
  489. package/dist/migrate/ping-identity/plugin.d.ts +2 -0
  490. package/dist/migrate/ping-identity/plugin.js +100 -0
  491. package/dist/migrate/ping-identity/plugin.js.map +1 -0
  492. package/dist/migrate/pocketbase/plugin.d.ts +2 -0
  493. package/dist/migrate/pocketbase/plugin.js +99 -0
  494. package/dist/migrate/pocketbase/plugin.js.map +1 -0
  495. package/dist/migrate/propelauth/plugin.d.ts +2 -0
  496. package/dist/migrate/propelauth/plugin.js +65 -0
  497. package/dist/migrate/propelauth/plugin.js.map +1 -0
  498. package/dist/migrate/saml/plugin.d.ts +2 -0
  499. package/dist/migrate/saml/plugin.js +79 -0
  500. package/dist/migrate/saml/plugin.js.map +1 -0
  501. package/dist/migrate/stytch/plugin.d.ts +2 -0
  502. package/dist/migrate/stytch/plugin.js +97 -0
  503. package/dist/migrate/stytch/plugin.js.map +1 -0
  504. package/dist/migrate/supabase/plugin.d.ts +2 -0
  505. package/dist/migrate/supabase/plugin.js +149 -0
  506. package/dist/migrate/supabase/plugin.js.map +1 -0
  507. package/dist/migrate/supertokens/plugin.d.ts +2 -0
  508. package/dist/migrate/supertokens/plugin.js +114 -0
  509. package/dist/migrate/supertokens/plugin.js.map +1 -0
  510. package/dist/migrate/user-table/plugin.d.ts +2 -0
  511. package/dist/migrate/user-table/plugin.js +62 -0
  512. package/dist/migrate/user-table/plugin.js.map +1 -0
  513. package/dist/migrate/userfront/plugin.d.ts +2 -0
  514. package/dist/migrate/userfront/plugin.js +60 -0
  515. package/dist/migrate/userfront/plugin.js.map +1 -0
  516. package/dist/migrate/workos/plugin.d.ts +2 -0
  517. package/dist/migrate/workos/plugin.js +81 -0
  518. package/dist/migrate/workos/plugin.js.map +1 -0
  519. package/dist/migrate/zitadel/plugin.d.ts +2 -0
  520. package/dist/migrate/zitadel/plugin.js +114 -0
  521. package/dist/migrate/zitadel/plugin.js.map +1 -0
  522. package/dist/output.d.ts +41 -0
  523. package/dist/output.js +72 -0
  524. package/dist/output.js.map +1 -0
  525. package/dist/project.d.ts +64 -0
  526. package/dist/project.js +86 -0
  527. package/dist/project.js.map +1 -0
  528. package/dist/prompt.d.ts +15 -0
  529. package/dist/prompt.js +46 -0
  530. package/dist/prompt.js.map +1 -0
  531. package/dist/redact.d.ts +41 -0
  532. package/dist/redact.js +115 -0
  533. package/dist/redact.js.map +1 -0
  534. package/dist/run.d.ts +12 -0
  535. package/dist/run.js +169 -0
  536. package/dist/run.js.map +1 -0
  537. package/dist/secrets.d.ts +41 -0
  538. package/dist/secrets.js +142 -0
  539. package/dist/secrets.js.map +1 -0
  540. package/dist/serialize.d.ts +30 -0
  541. package/dist/serialize.js +131 -0
  542. package/dist/serialize.js.map +1 -0
  543. package/dist/version.d.ts +7 -0
  544. package/dist/version.js +21 -0
  545. package/dist/version.js.map +1 -0
  546. package/package.json +3 -3
@@ -0,0 +1,1822 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_CONFIG_FILES = void 0;
37
+ exports.config = config;
38
+ exports.resolveFormat = resolveFormat;
39
+ exports.planConfigImport = planConfigImport;
40
+ exports.diffFields = diffFields;
41
+ exports.printedFields = printedFields;
42
+ exports.readSecretsEnv = readSecretsEnv;
43
+ exports.unresolvedPlaceholders = unresolvedPlaceholders;
44
+ exports.configTypes = configTypes;
45
+ exports.loadCodeConfig = loadCodeConfig;
46
+ exports.planCommand = planCommand;
47
+ exports.applyCommand = applyCommand;
48
+ exports.printPlan = printPlan;
49
+ exports.interpolate = interpolate;
50
+ const node_fs_1 = require("node:fs");
51
+ const node_os_1 = require("node:os");
52
+ const node_path_1 = require("node:path");
53
+ const node_url_1 = require("node:url");
54
+ const args_1 = require("../args");
55
+ const config_http_1 = require("../config-http");
56
+ const errors_1 = require("../errors");
57
+ const serialize_1 = require("../serialize");
58
+ const backend_1 = require("@atlasauth/backend");
59
+ const shared_1 = require("./shared");
60
+ const secrets_1 = require("../secrets");
61
+ const redact_1 = require("../redact");
62
+ /** The refusal `rotate_secret` answers when the secret is the one already stored. */
63
+ const isSameSecretRefusal = (err) => err instanceof backend_1.AtlasApiError && err.status === 422 && /must differ/.test(err.message);
64
+ // ───────────────────────────── small helpers ─────────────────────────────
65
+ /** GET a `{ data: [...] }` list envelope, tolerating an absent/garbled body. */
66
+ async function listData(http, path) {
67
+ const res = await http.get(path);
68
+ return Array.isArray(res?.data) ? res.data : [];
69
+ }
70
+ /** Build a config object from a row, keeping only the named keys and dropping null/undefined. */
71
+ function pick(row, keys) {
72
+ const out = {};
73
+ for (const k of keys) {
74
+ const v = row[k];
75
+ if (v === null || v === undefined)
76
+ continue;
77
+ out[k] = v;
78
+ }
79
+ return out;
80
+ }
81
+ /**
82
+ * The write body: strip null/undefined. On a PATCH (`keepNull`) an explicit file
83
+ * `null` is kept so it clears a nullable field; an absent key stays unmanaged.
84
+ */
85
+ function compact(config, keepNull = false) {
86
+ const out = {};
87
+ if (config && typeof config === 'object' && !Array.isArray(config)) {
88
+ for (const [k, v] of Object.entries(config)) {
89
+ if (v === undefined || (v === null && !keepNull))
90
+ continue;
91
+ out[k] = v;
92
+ }
93
+ }
94
+ return out;
95
+ }
96
+ function enc(segment) {
97
+ return encodeURIComponent(segment);
98
+ }
99
+ // ───────────────────────────── the registry ─────────────────────────────
100
+ /**
101
+ * The single source of truth for what `atlas config` round-trips — the same
102
+ * resource set the Terraform provider manages. Each entry maps a resource type
103
+ * to its list/create/update/delete endpoints and its redacted projection.
104
+ * SECRETS ARE NEVER PART OF A PROJECTION: the management API never returns them
105
+ * on a read, so they never reach disk; a credential that must be (re)written is
106
+ * supplied at import time via a `${ENV_VAR}` placeholder in the file.
107
+ */
108
+ function registry() {
109
+ const specs = [
110
+ // 0. Email provider — first, because email sign-in strategies need one. Secrets
111
+ // (api_key, …) are write-only: the live side shows "(secret set)", never a value.
112
+ {
113
+ kind: 'singleton',
114
+ type: 'email_provider',
115
+ async fetchLive(http) {
116
+ let p;
117
+ try {
118
+ p = await http.get('/v1/messaging_providers/email');
119
+ }
120
+ catch (err) {
121
+ if ((0, config_http_1.isNotFound)(err))
122
+ return null;
123
+ throw err;
124
+ }
125
+ if (!p || p.configured === false)
126
+ return null;
127
+ const config = { ...obj((p.config ?? null)) };
128
+ const set = obj((p.secrets_set ?? null));
129
+ const names = Array.isArray(p.secrets_set) ? p.secrets_set : Object.keys(set).filter((k) => set[k] === true);
130
+ for (const k of names)
131
+ config[k] = SECRET_SET;
132
+ return pick({ ...p, config }, ['transport', 'from_address', 'config']);
133
+ },
134
+ async apply(http, config) {
135
+ await http.put('/v1/messaging_providers/email', compact(withoutMarkers(config)));
136
+ },
137
+ fileKeys: ['transport', 'from_address', 'mode', 'config'],
138
+ reportsSecrets: true,
139
+ },
140
+ // 1. Instance config (auth_config + allowed origins) — the §9.3 singleton.
141
+ {
142
+ kind: 'singleton',
143
+ type: 'instance',
144
+ fileKeys: ['allowed_origins', 'auth_config'],
145
+ async fetchLive(http) {
146
+ const inst = await http.get('/v1/instance');
147
+ const hints = obj(inst.allowed_origin_integrations ?? null);
148
+ const origins = originStrings(inst.allowed_origins ?? []).map((o) => (hints[o] ? { origin: o, integration: hints[o] } : o));
149
+ return { allowed_origins: origins, auth_config: stripOriginIntegrations(inst.auth_config ?? {}) };
150
+ },
151
+ async apply(http, config) {
152
+ const c = compact(config);
153
+ // Integration hints live in auth_config.originIntegrations server-side and a
154
+ // write replaces auth_config, so carry the live hints over for any origin the
155
+ // file names as a plain string (the plan ignores hints the file does not set).
156
+ const live = await http.get('/v1/instance');
157
+ const liveHints = obj(live.allowed_origin_integrations ?? null);
158
+ const authConfig = stripOriginIntegrations(c.auth_config ?? {});
159
+ const body = { auth_config: authConfig };
160
+ if (c.allowed_origins !== undefined) {
161
+ // A file without allowed_origins leaves the live origins alone (a partial
162
+ // document, e.g. one produced by `atlas migrate better-auth config`).
163
+ body.allowed_origins = (Array.isArray(c.allowed_origins) ? c.allowed_origins : []).map((e) => typeof e === 'string' && liveHints[e] ? { origin: e, integration: liveHints[e] } : e);
164
+ }
165
+ else {
166
+ const kept = obj(live.auth_config ?? null).originIntegrations;
167
+ if (kept !== undefined)
168
+ authConfig.originIntegrations = kept;
169
+ }
170
+ await http.patch('/v1/instance', body);
171
+ },
172
+ normalize(c) {
173
+ const d = obj(c);
174
+ const hints = {};
175
+ for (const e of Array.isArray(d.allowed_origins) ? d.allowed_origins : []) {
176
+ const o = obj(e);
177
+ if (o.integration)
178
+ hints[String(o.origin)] = o.integration;
179
+ }
180
+ const out = {};
181
+ if (d.allowed_origins !== undefined)
182
+ out.allowed_origins = originStrings(d.allowed_origins);
183
+ if (Object.keys(hints).length)
184
+ out.allowed_origin_integrations = hints;
185
+ if (d.auth_config !== undefined)
186
+ out.auth_config = stripOriginIntegrations(d.auth_config);
187
+ return out;
188
+ },
189
+ },
190
+ // 2. Org settings schema (P1-5) — a singleton JSON Schema; 404 when unset.
191
+ {
192
+ kind: 'singleton',
193
+ type: 'org_settings_schema',
194
+ fileKeys: ['schema'],
195
+ async fetchLive(http) {
196
+ try {
197
+ const s = await http.get('/v1/organization_settings_schema');
198
+ return { schema: s.schema ?? {} };
199
+ }
200
+ catch (err) {
201
+ if ((0, config_http_1.isNotFound)(err))
202
+ return null;
203
+ throw err;
204
+ }
205
+ },
206
+ async apply(http, config) {
207
+ const c = compact(config);
208
+ await http.put('/v1/organization_settings_schema', { schema: c.schema ?? {} });
209
+ },
210
+ },
211
+ // 3. Redirect URL allowlist — key = the (normalised) url; immutable, so no update.
212
+ {
213
+ kind: 'collection',
214
+ type: 'redirect_urls',
215
+ fileKeys: ['url'],
216
+ keyOf: (c) => String(obj(c).url ?? ''),
217
+ async fetchLive(http) {
218
+ const rows = await listData(http, '/v1/redirect_urls');
219
+ return rows.map((r) => ({ key: r.url, id: r.id, config: pick(r, ['url']) }));
220
+ },
221
+ async create(http, config) {
222
+ await http.post('/v1/redirect_urls', { url: obj(config).url });
223
+ },
224
+ async remove(http, live) {
225
+ await http.delete(`/v1/redirect_urls/${enc(live.id)}`);
226
+ },
227
+ },
228
+ // 4. OAuth clients (first-party / M2M apps) — key = name.
229
+ {
230
+ kind: 'collection',
231
+ type: 'oauth_clients',
232
+ fileKeys: [...OAUTH_CLIENT_KEYS, ...OAUTH_CLIENT_WRITE_KEYS],
233
+ keyOf: (c) => String(obj(c).name ?? ''),
234
+ async fetchLive(http) {
235
+ const rows = await listData(http, '/v1/oauth_clients');
236
+ return rows.map((r) => ({
237
+ key: String(r.name ?? ''),
238
+ id: String(r.id ?? ''),
239
+ config: pick(r, OAUTH_CLIENT_KEYS),
240
+ }));
241
+ },
242
+ async create(http, config) {
243
+ await http.post('/v1/oauth_clients', compact(config));
244
+ },
245
+ async update(http, live, config) {
246
+ await http.patch(`/v1/oauth_clients/${enc(live.id)}`, compact(config, true));
247
+ },
248
+ async remove(http, live) {
249
+ await http.delete(`/v1/oauth_clients/${enc(live.id)}`);
250
+ },
251
+ },
252
+ // 4b. Hooks — after the clients. before_user_create: url, timeout_ms, fail_closed,
253
+ // enabled; the signing secret is write-only ("(secret set)").
254
+ {
255
+ kind: 'singleton',
256
+ type: 'hooks',
257
+ async fetchLive(http) {
258
+ let h;
259
+ try {
260
+ h = await http.get('/v1/instance/hooks/before_user_create');
261
+ }
262
+ catch (err) {
263
+ if ((0, config_http_1.isNotFound)(err))
264
+ return null;
265
+ throw err;
266
+ }
267
+ if (!h || (h.url == null && h.enabled !== true))
268
+ return null;
269
+ const bu = pick(h, ['url', 'timeout_ms', 'fail_closed', 'enabled']);
270
+ if (h.signing_secret_set === true)
271
+ bu.signing_secret = SECRET_SET;
272
+ return { before_user_create: bu };
273
+ },
274
+ async apply(http, config, opts) {
275
+ const bu = compact(obj(withoutMarkers(config)).before_user_create ?? {}, true);
276
+ if (Object.keys(bu).length === 0)
277
+ return;
278
+ try {
279
+ await http.patch(`/v1/instance/hooks/before_user_create${opts.forceHook ? '?force=true' : ''}`, bu);
280
+ }
281
+ catch (err) {
282
+ const refused = err instanceof backend_1.AtlasApiError && err.status === 409 ? err.errors.find((e) => e.code === 'HOOK_TEST_FAILED') : undefined;
283
+ if (refused) {
284
+ throw new errors_1.CliError(`hooks.before_user_create: the change was refused because the hook test call failed (HOOK_TEST_FAILED): ${refused.message} ` +
285
+ 'The hook was left as it was. Deploy the route, or pass --force-hook to apply it anyway.');
286
+ }
287
+ throw err;
288
+ }
289
+ },
290
+ fileKeys: ['before_user_create'],
291
+ nestedKeys: { before_user_create: ['url', 'timeout_ms', 'fail_closed', 'enabled', 'signing_secret'] },
292
+ reportsSecrets: true,
293
+ },
294
+ // 5. Social / enterprise OAuth providers — key = the catalog provider slug.
295
+ {
296
+ kind: 'collection',
297
+ type: 'oauth_providers',
298
+ fileKeys: [...OAUTH_PROVIDER_KEYS, 'client_secret', 'values', 'settings'],
299
+ reportsSecrets: true,
300
+ defaulted: ['mode'],
301
+ writeOnly: ['values'],
302
+ warnings(config, live) {
303
+ const c = obj(config);
304
+ const lv = obj(live ?? null);
305
+ const inFile = typeof c.client_secret === 'string' && c.client_secret !== '';
306
+ const out = [];
307
+ if (c.mode === 'redirect' && !inFile && !lv.client_secret) {
308
+ out.push(`oauth_providers ${String(c.provider)}: mode redirect needs a client_secret, and neither the file nor the instance has one.`);
309
+ }
310
+ if ((c.mode ?? lv.mode) === 'native' && inFile) {
311
+ out.push(`oauth_providers ${String(c.provider)}: secret unused in native mode. Remove client_secret, or set mode: redirect.`);
312
+ }
313
+ return out;
314
+ },
315
+ keyOf: (c) => String(obj(c).provider ?? ''),
316
+ async fetchLive(http) {
317
+ const rows = await listData(http, '/v1/oauth_providers');
318
+ return rows
319
+ .filter((r) => r.configured === true)
320
+ .map((r) => ({
321
+ key: String(r.provider ?? ''),
322
+ id: String(r.provider ?? ''),
323
+ config: {
324
+ ...obj(pick(r, OAUTH_PROVIDER_KEYS)),
325
+ ...(r.setting_values && Object.keys(r.setting_values).length ? { settings: r.setting_values } : {}),
326
+ ...(r.has_secret === true ? { client_secret: SECRET_SET } : {}),
327
+ },
328
+ }));
329
+ },
330
+ create: putProvider,
331
+ update: (http, _live, config) => putProvider(http, config),
332
+ async remove(http, live) {
333
+ await http.delete(`/v1/oauth_providers/${enc(live.id)}`);
334
+ },
335
+ },
336
+ // 5b. Custom (generic) OAuth 2.0 / OIDC login providers — key = provider_key.
337
+ {
338
+ kind: 'collection',
339
+ type: 'custom_oauth_providers',
340
+ fileKeys: [...CUSTOM_PROVIDER_KEYS, ...CUSTOM_PROVIDER_WRITE_KEYS],
341
+ reportsSecrets: true,
342
+ keyOf: (c) => String(obj(c).provider_key ?? ''),
343
+ async fetchLive(http) {
344
+ const rows = await listData(http, '/v1/custom_oauth_providers');
345
+ return rows.map((r) => ({
346
+ key: String(r.provider_key ?? ''),
347
+ id: String(r.id ?? ''),
348
+ config: { ...obj(pick(r, CUSTOM_PROVIDER_KEYS)), ...(r.has_secret === true ? { client_secret: SECRET_SET } : {}) },
349
+ }));
350
+ },
351
+ async create(http, config) {
352
+ await http.post('/v1/custom_oauth_providers', compact(config));
353
+ },
354
+ async update(http, live, config) {
355
+ const body = compact(config, true);
356
+ delete body.provider_key; // immutable
357
+ await http.patch(`/v1/custom_oauth_providers/${enc(live.id)}`, body);
358
+ },
359
+ async remove(http, live) {
360
+ await http.delete(`/v1/custom_oauth_providers/${enc(live.id)}`);
361
+ },
362
+ },
363
+ // 6. Resource servers (API audiences) — key = identifier.
364
+ {
365
+ kind: 'collection',
366
+ type: 'resource_servers',
367
+ fileKeys: [...RESOURCE_SERVER_KEYS, 'publish_resource_metadata', 'access_token_format', 'refresh_token_absolute_ttl_seconds', 'refresh_token_idle_ttl_seconds', 'introspection_client_ids'],
368
+ keyOf: (c) => String(obj(c).identifier ?? ''),
369
+ async fetchLive(http) {
370
+ const rows = await listData(http, '/v1/resource_servers');
371
+ return rows.map((r) => ({
372
+ key: String(r.identifier ?? ''),
373
+ id: String(r.id ?? ''),
374
+ config: pick(r, RESOURCE_SERVER_KEYS),
375
+ }));
376
+ },
377
+ async create(http, config) {
378
+ const body = compact(config);
379
+ delete body.signing_alg; // read-only (reported, not settable)
380
+ await http.post('/v1/resource_servers', body);
381
+ },
382
+ async update(http, live, config) {
383
+ const body = compact(config, true);
384
+ delete body.identifier; // immutable
385
+ delete body.signing_alg; // read-only
386
+ await http.patch(`/v1/resource_servers/${enc(live.id)}`, body);
387
+ },
388
+ async remove(http, live) {
389
+ await http.delete(`/v1/resource_servers/${enc(live.id)}`);
390
+ },
391
+ },
392
+ // 7. JWT templates — key = name; claims is a flat template map.
393
+ {
394
+ kind: 'collection',
395
+ type: 'jwt_templates',
396
+ fileKeys: ['name', 'claims'],
397
+ keyOf: (c) => String(obj(c).name ?? ''),
398
+ async fetchLive(http) {
399
+ const rows = await listData(http, '/v1/jwt_templates');
400
+ return rows.map((r) => ({
401
+ key: r.name,
402
+ id: r.name,
403
+ config: { name: r.name, claims: r.claims ?? {} },
404
+ }));
405
+ },
406
+ async create(http, config) {
407
+ const c = obj(config);
408
+ await http.post('/v1/jwt_templates', { name: c.name, claims: c.claims ?? {} });
409
+ },
410
+ async update(http, live, config) {
411
+ await http.patch(`/v1/jwt_templates/${enc(live.id)}`, { claims: obj(config).claims ?? {} });
412
+ },
413
+ async remove(http, live) {
414
+ await http.delete(`/v1/jwt_templates/${enc(live.id)}`);
415
+ },
416
+ },
417
+ // 7b. Custom permissions — key = key. Before roles, which grant them. System ones skipped.
418
+ {
419
+ kind: 'collection',
420
+ type: 'permissions',
421
+ fileKeys: PERMISSION_KEYS,
422
+ keyOf: (c) => String(obj(c).key ?? ''),
423
+ async fetchLive(http) {
424
+ const rows = await listData(http, '/v1/permissions');
425
+ return rows
426
+ .filter((r) => r.is_system !== true)
427
+ .map((r) => ({ key: String(r.key ?? ''), id: String(r.id ?? ''), config: pick(r, PERMISSION_KEYS) }));
428
+ },
429
+ async create(http, config) {
430
+ await http.post('/v1/permissions', compact(config));
431
+ },
432
+ async update(http, live, config) {
433
+ const c = obj(config);
434
+ await http.patch(`/v1/permissions/${enc(live.id)}`, { name: c.name, description: c.description ?? null });
435
+ },
436
+ async remove(http, live) {
437
+ await http.delete(`/v1/permissions/${enc(live.id)}`);
438
+ },
439
+ },
440
+ // 8. Roles — key = key; label via PATCH, permission set via PUT. System roles skipped.
441
+ {
442
+ kind: 'collection',
443
+ type: 'roles',
444
+ fileKeys: [...ROLE_KEYS, 'visibility'],
445
+ keyOf: (c) => String(obj(c).key ?? ''),
446
+ async fetchLive(http) {
447
+ const rows = await listData(http, '/v1/roles');
448
+ return rows
449
+ .filter((r) => r.is_system !== true)
450
+ .map((r) => ({
451
+ key: String(r.key ?? ''),
452
+ id: String(r.id ?? ''),
453
+ config: pick(r, ROLE_KEYS),
454
+ }));
455
+ },
456
+ async create(http, config) {
457
+ await http.post('/v1/roles', compact(config));
458
+ },
459
+ async update(http, live, config) {
460
+ const c = obj(config);
461
+ await http.patch(`/v1/roles/${enc(live.id)}`, {
462
+ name: c.name,
463
+ description: c.description ?? null,
464
+ });
465
+ await http.put(`/v1/roles/${enc(live.id)}/permissions`, {
466
+ permissions: Array.isArray(c.permissions) ? c.permissions : [],
467
+ });
468
+ },
469
+ async remove(http, live) {
470
+ await http.delete(`/v1/roles/${enc(live.id)}`);
471
+ },
472
+ },
473
+ // 9. Webhook endpoints — key = url.
474
+ {
475
+ kind: 'collection',
476
+ type: 'webhook_endpoints',
477
+ fileKeys: [...WEBHOOK_KEYS, 'secret'],
478
+ // A dry-run rotation tells a new secret from the stored one without revealing either.
479
+ async secretChanged(http, live, config) {
480
+ try {
481
+ await http.post(`/v1/webhook_endpoints/${enc(live.id)}/rotate_secret?dry_run=true`, { secret: obj(config).secret });
482
+ return true;
483
+ }
484
+ catch (err) {
485
+ return isSameSecretRefusal(err) ? false : null;
486
+ }
487
+ },
488
+ keyOf: (c) => String(obj(c).url ?? ''),
489
+ async fetchLive(http) {
490
+ const rows = await listData(http, '/v1/webhook_endpoints');
491
+ return rows.map((r) => ({
492
+ key: String(r.url ?? ''),
493
+ id: String(r.id ?? ''),
494
+ config: pick(r, WEBHOOK_KEYS),
495
+ ...(typeof r.managed_by === 'string' && r.managed_by ? { managedBy: r.managed_by } : {}),
496
+ }));
497
+ },
498
+ async create(http, config) {
499
+ const c = obj(config);
500
+ await http.post('/v1/webhook_endpoints', {
501
+ url: c.url,
502
+ enabled_events: c.enabled_events ?? [],
503
+ // Sent on create so a paused endpoint is never live, even briefly.
504
+ ...(typeof c.active === 'boolean' ? { active: c.active } : {}),
505
+ ...(typeof c.secret === 'string' ? { secret: c.secret } : {}),
506
+ });
507
+ },
508
+ async update(http, live, config) {
509
+ // PATCH leaves the signing secret alone; only rotate_secret changes it.
510
+ const { secret, ...rest } = obj(config);
511
+ await http.patch(`/v1/webhook_endpoints/${enc(live.id)}`, compact(rest, true));
512
+ if (typeof secret !== 'string' || !secret)
513
+ return;
514
+ try {
515
+ await http.post(`/v1/webhook_endpoints/${enc(live.id)}/rotate_secret`, { secret });
516
+ }
517
+ catch (err) {
518
+ if (!isSameSecretRefusal(err))
519
+ throw err;
520
+ }
521
+ },
522
+ async remove(http, live) {
523
+ await http.delete(`/v1/webhook_endpoints/${enc(live.id)}`);
524
+ },
525
+ },
526
+ // 10. SSO connections — key = a stable identifier derived from the connection.
527
+ {
528
+ kind: 'collection',
529
+ type: 'sso_connections',
530
+ fileKeys: [...SSO_KEYS, ...SSO_WRITE_KEYS],
531
+ reportsSecrets: true,
532
+ writeOnly: ['saml_idp_certificate'],
533
+ keyOf: ssoKey,
534
+ async fetchLive(http) {
535
+ const rows = await listData(http, '/v1/sso_connections');
536
+ return rows.map((r) => {
537
+ const config = { ...obj(pick(r, SSO_KEYS)), ...(r.has_secret === true ? { oidc_client_secret: SECRET_SET, discourse_secret: SECRET_SET } : {}) };
538
+ return { key: ssoKey(config), id: String(r.id ?? ''), config };
539
+ });
540
+ },
541
+ async create(http, config) {
542
+ await http.post('/v1/sso_connections', compact(config));
543
+ },
544
+ async update(http, live, config) {
545
+ const body = compact(config, true);
546
+ delete body.type; // fixed at creation; PATCH does not take it
547
+ await http.patch(`/v1/sso_connections/${enc(live.id)}`, body);
548
+ },
549
+ async remove(http, live) {
550
+ await http.delete(`/v1/sso_connections/${enc(live.id)}`);
551
+ },
552
+ },
553
+ // 10b. SSO onboarding profiles (hosted setup links) — key = name. After the SSO
554
+ // connections; a profile may name an organization that already exists.
555
+ {
556
+ kind: 'collection',
557
+ type: 'sso_onboarding_profiles',
558
+ fileKeys: SSO_PROFILE_KEYS,
559
+ keyOf: (c) => String(obj(c).name ?? ''),
560
+ async fetchLive(http) {
561
+ let rows;
562
+ try {
563
+ rows = await listData(http, '/v1/sso_onboarding_profiles');
564
+ }
565
+ catch (err) {
566
+ if ((0, config_http_1.isNotFound)(err))
567
+ return [];
568
+ throw err;
569
+ }
570
+ return rows.map((r) => {
571
+ const config = pick(r, SSO_PROFILE_KEYS);
572
+ return { key: String(r.name ?? ''), id: String(r.id ?? ''), config };
573
+ });
574
+ },
575
+ async create(http, config) {
576
+ await http.post('/v1/sso_onboarding_profiles', compact(config));
577
+ },
578
+ async update(http, live, config) {
579
+ await http.patch(`/v1/sso_onboarding_profiles/${enc(live.id)}`, compact(config, true));
580
+ },
581
+ async remove(http, live) {
582
+ await http.delete(`/v1/sso_onboarding_profiles/${enc(live.id)}`);
583
+ },
584
+ },
585
+ // 11. Billing plans — key = slug.
586
+ {
587
+ kind: 'collection',
588
+ type: 'billing_plans',
589
+ fileKeys: [...BILLING_KEYS, 'grace_days', 'provider_price_id', 'annual_amount', 'seat_feature_id', 'promotion'],
590
+ keyOf: (c) => String(obj(c).slug ?? ''),
591
+ async fetchLive(http) {
592
+ const rows = await listData(http, '/v1/billing/plans');
593
+ return rows.map((r) => ({
594
+ key: String(r.slug ?? ''),
595
+ id: String(r.id ?? ''),
596
+ config: pick(r, BILLING_KEYS),
597
+ }));
598
+ },
599
+ async create(http, config) {
600
+ await http.post('/v1/billing/plans', compact(config));
601
+ },
602
+ async update(http, live, config) {
603
+ const body = compact(config, true);
604
+ delete body.slug; // immutable
605
+ await http.patch(`/v1/billing/plans/${enc(live.id)}`, body);
606
+ },
607
+ async remove(http, live) {
608
+ await http.delete(`/v1/billing/plans/${enc(live.id)}`);
609
+ },
610
+ },
611
+ // 12. Notification templates (P2-12) — key = name; replace via PUT.
612
+ {
613
+ kind: 'collection',
614
+ type: 'notification_templates',
615
+ fileKeys: NOTIFICATION_KEYS,
616
+ keyOf: (c) => String(obj(c).name ?? ''),
617
+ async fetchLive(http) {
618
+ const rows = await listData(http, '/v1/notification_templates');
619
+ return rows.map((r) => ({
620
+ key: String(r.name ?? ''),
621
+ id: String(r.name ?? ''),
622
+ config: pick(r, NOTIFICATION_KEYS),
623
+ }));
624
+ },
625
+ async create(http, config) {
626
+ await http.post('/v1/notification_templates', compact(config));
627
+ },
628
+ async update(http, live, config) {
629
+ const body = compact(config, true);
630
+ delete body.name; // the name is the URL key
631
+ await http.put(`/v1/notification_templates/${enc(live.id)}`, body);
632
+ },
633
+ async remove(http, live) {
634
+ await http.delete(`/v1/notification_templates/${enc(live.id)}`);
635
+ },
636
+ },
637
+ // 13. Email templates (§11.1) — key = name; only the CUSTOMISED overrides are config.
638
+ {
639
+ kind: 'collection',
640
+ type: 'email_templates',
641
+ fileKeys: EMAIL_KEYS,
642
+ keyOf: (c) => String(obj(c).name ?? ''),
643
+ async fetchLive(http) {
644
+ const rows = await listData(http, '/v1/email_templates');
645
+ return rows
646
+ .filter((r) => r.customised && r.override)
647
+ .map((r) => ({
648
+ key: r.name,
649
+ id: r.name,
650
+ config: pick({ name: r.name, ...(r.override ?? {}) }, EMAIL_KEYS),
651
+ }));
652
+ },
653
+ async create(http, config) {
654
+ await putEmail(http, config);
655
+ },
656
+ async update(http, _live, config) {
657
+ await putEmail(http, config);
658
+ },
659
+ async remove(http, live) {
660
+ await http.delete(`/v1/email_templates/${enc(live.id)}`);
661
+ },
662
+ },
663
+ // 14. Organization policies — key = organization_id; attaches to existing orgs.
664
+ {
665
+ kind: 'collection',
666
+ type: 'organization_policies',
667
+ fileKeys: ORG_POLICY_KEYS,
668
+ keyOf: (c) => String(obj(c).organization_id ?? ''),
669
+ async fetchLive(http) {
670
+ const orgs = await listData(http, '/v1/organizations');
671
+ const out = [];
672
+ for (const org of orgs) {
673
+ // There is no GET for org policy; an empty PATCH returns it unchanged.
674
+ const res = await http.patch(`/v1/organizations/${enc(org.id)}/policy`, {});
675
+ const policy = res?.policy ?? {};
676
+ const config = pick({ organization_id: org.id, ...policy }, ORG_POLICY_KEYS);
677
+ // Only orgs with a real (non-default) policy are config.
678
+ if (Object.keys(obj(config)).length > 1) {
679
+ out.push({ key: org.id, id: org.id, config });
680
+ }
681
+ }
682
+ return out;
683
+ },
684
+ async create(http, config) {
685
+ await patchOrgPolicy(http, config);
686
+ },
687
+ async update(http, _live, config) {
688
+ await patchOrgPolicy(http, config);
689
+ },
690
+ // No delete: pruning a policy would silently loosen an org's security, so
691
+ // org policies are never deleted by import (reported as skipped).
692
+ },
693
+ // 12. Branding — the hosted-page appearance; a partial merge on write (PATCH).
694
+ {
695
+ kind: 'singleton',
696
+ type: 'branding',
697
+ async fetchLive(http) {
698
+ let b;
699
+ try {
700
+ b = await http.get('/v1/branding');
701
+ }
702
+ catch (err) {
703
+ if ((0, config_http_1.isNotFound)(err))
704
+ return null;
705
+ throw err;
706
+ }
707
+ const { object: _o, resolved: _r, ...fields } = b ?? {};
708
+ const out = compact(fields);
709
+ return Object.keys(out).length ? out : null;
710
+ },
711
+ async apply(http, config) {
712
+ await http.patch('/v1/branding', compact(config, true));
713
+ },
714
+ },
715
+ // 13. Cookie-consent banner — last: it only needs the instance. PUT replaces the
716
+ // whole setting and needs script_url, so apply merges the file over the live one.
717
+ {
718
+ kind: 'singleton',
719
+ type: 'cookie_consent',
720
+ fileKeys: COOKIE_CONSENT_KEYS,
721
+ async fetchLive(http) {
722
+ let cc;
723
+ try {
724
+ cc = await http.get('/v1/instance/cookie_consent');
725
+ }
726
+ catch (err) {
727
+ if ((0, config_http_1.isNotFound)(err))
728
+ return null;
729
+ throw err;
730
+ }
731
+ const out = pick(cc ?? {}, COOKIE_CONSENT_KEYS);
732
+ return Object.keys(out).length ? out : null;
733
+ },
734
+ async apply(http, config) {
735
+ const live = await http.get('/v1/instance/cookie_consent');
736
+ const body = { ...pick(live ?? {}, COOKIE_CONSENT_KEYS), ...compact(config, true) };
737
+ if (body.script_url === undefined || body.script_url === null) {
738
+ throw new errors_1.CliError('cookie_consent needs a script_url (the consent manager script) when none is set on the instance.');
739
+ }
740
+ await http.put('/v1/instance/cookie_consent', body);
741
+ },
742
+ },
743
+ // 14. "Sign in with Atlas" button on the hosted pages. The file says whether it
744
+ // is on; `connection_id` (optional) links an SSO connection the instance
745
+ // already has instead of the preset one. Only `enabled` is exported or
746
+ // compared: the connection id is this instance's own.
747
+ {
748
+ kind: 'singleton',
749
+ type: 'sign_in_with_atlas',
750
+ fileKeys: ['enabled', 'connection_id'],
751
+ async fetchLive(http) {
752
+ let live;
753
+ try {
754
+ live = await http.get('/v1/instance/sign_in_with_atlas');
755
+ }
756
+ catch (err) {
757
+ if ((0, config_http_1.isNotFound)(err))
758
+ return null;
759
+ throw err;
760
+ }
761
+ return { enabled: live?.enabled === true };
762
+ },
763
+ normalize: (c) => ({ enabled: obj(c).enabled === true }),
764
+ async apply(http, config) {
765
+ const c = obj(config);
766
+ if (typeof c.enabled !== 'boolean')
767
+ throw new errors_1.CliError('sign_in_with_atlas needs enabled: true or false.');
768
+ await http.patch('/v1/instance/sign_in_with_atlas', {
769
+ enabled: c.enabled,
770
+ ...(c.connection_id !== undefined ? { connection_id: c.connection_id } : {}),
771
+ });
772
+ },
773
+ },
774
+ // 15. Staged signing-key rotation: `prepublish_days` (1..90) publishes each new key
775
+ // that many days before it signs; null or 0 turns it off. Only the policy, never keys.
776
+ {
777
+ kind: 'singleton',
778
+ type: 'signing_key_policy',
779
+ fileKeys: ['prepublish_days'],
780
+ async fetchLive(http) {
781
+ let live;
782
+ try {
783
+ live = await http.get('/v1/instance/signing_key_policy');
784
+ }
785
+ catch (err) {
786
+ if ((0, config_http_1.isNotFound)(err))
787
+ return null;
788
+ throw err;
789
+ }
790
+ return live?.prepublish_days ? { prepublish_days: live.prepublish_days } : null;
791
+ },
792
+ normalize: (c) => ({ prepublish_days: obj(c).prepublish_days || null }),
793
+ async apply(http, config) {
794
+ const d = obj(config).prepublish_days ?? null;
795
+ if (d !== null && (typeof d !== 'number' || !Number.isInteger(d) || d < 0 || d > 90)) {
796
+ throw new errors_1.CliError('signing_key_policy.prepublish_days must be a whole number of days from 1 to 90, or null / 0 to turn it off.');
797
+ }
798
+ await http.patch('/v1/instance/signing_key_policy', { prepublish_days: d || null });
799
+ },
800
+ },
801
+ ];
802
+ return specs;
803
+ }
804
+ const OAUTH_CLIENT_KEYS = [
805
+ 'name',
806
+ 'redirect_uris',
807
+ 'allowed_scopes',
808
+ 'grant_types',
809
+ 'token_endpoint_auth_method',
810
+ 'logo_url',
811
+ 'first_party',
812
+ 'post_logout_redirect_uris',
813
+ 'claims_redirect_uris',
814
+ 'backchannel_logout_uri',
815
+ 'backchannel_logout_session_required',
816
+ 'frontchannel_logout_uri',
817
+ 'frontchannel_logout_session_required',
818
+ 'skip_logout_confirmation',
819
+ 'refresh_touches_session',
820
+ 'require_verified_email',
821
+ 'require_pkce',
822
+ 'login_url',
823
+ 'consent_url',
824
+ 'subject_type',
825
+ 'sector_identifier_uri',
826
+ 'access_token_format',
827
+ 'token_lifetimes',
828
+ ];
829
+ /** What a file may also send on an OAuth client: accepted on write, never exported. */
830
+ const OAUTH_CLIENT_WRITE_KEYS = [
831
+ 'application_type',
832
+ 'client_secret',
833
+ 'secret_expires_in_seconds',
834
+ 'jwks',
835
+ 'jwks_uri',
836
+ 'tls_client_auth_subject_dn',
837
+ 'frontchannel_logout_session_required',
838
+ 'disabled',
839
+ 'metadata',
840
+ 'external_ref',
841
+ 'add_redirect_origins',
842
+ ];
843
+ const OAUTH_PROVIDER_KEYS = [
844
+ 'provider',
845
+ 'enabled',
846
+ 'allow_sign_in',
847
+ 'allow_sign_up',
848
+ 'scopes',
849
+ 'client_id',
850
+ 'mode',
851
+ ];
852
+ const RESOURCE_SERVER_KEYS = [
853
+ 'identifier',
854
+ 'name',
855
+ 'scopes',
856
+ 'token_ttl_seconds',
857
+ 'signing_alg',
858
+ ];
859
+ const ROLE_KEYS = ['key', 'name', 'description', 'permissions'];
860
+ const PERMISSION_KEYS = ['key', 'name', 'description'];
861
+ const CUSTOM_PROVIDER_KEYS = [
862
+ 'provider_key',
863
+ 'display_name',
864
+ 'icon_url',
865
+ 'protocol',
866
+ 'issuer',
867
+ 'discovery_url',
868
+ 'authorization_endpoint',
869
+ 'token_endpoint',
870
+ 'userinfo_endpoint',
871
+ 'jwks_uri',
872
+ 'scopes',
873
+ 'pkce',
874
+ 'authorization_params',
875
+ 'token_params',
876
+ 'token_endpoint_auth_method',
877
+ 'profile_source',
878
+ 'profile_mapping',
879
+ 'client_id',
880
+ 'enabled',
881
+ 'allow_sign_in',
882
+ 'allow_sign_up',
883
+ ];
884
+ const CUSTOM_PROVIDER_WRITE_KEYS = [
885
+ 'client_secret',
886
+ 'end_session_endpoint',
887
+ 'revocation_endpoint',
888
+ 'host_params_schema',
889
+ 'data_only',
890
+ 'scope_separator',
891
+ 'userinfo_auth',
892
+ 'client_assertion_audience',
893
+ 'logout',
894
+ 'idp_initiated_login',
895
+ ];
896
+ const WEBHOOK_KEYS = ['url', 'enabled_events', 'active'];
897
+ const SSO_KEYS = [
898
+ 'type',
899
+ 'status',
900
+ 'organization_id',
901
+ 'oidc_issuer',
902
+ 'oidc_client_id',
903
+ 'saml_idp_entity_id',
904
+ 'saml_idp_sso_url',
905
+ 'saml_sp_entity_id',
906
+ 'saml_allow_idp_initiated',
907
+ 'saml_sign_authn_requests',
908
+ 'saml_want_response_signed',
909
+ 'discourse_provider_url',
910
+ 'allowed_domains',
911
+ 'claim_role_mappings',
912
+ 'default_role_id',
913
+ ];
914
+ const SSO_WRITE_KEYS = [
915
+ 'oidc_client_secret',
916
+ 'saml_idp_certificate',
917
+ 'saml_single_logout',
918
+ 'saml_idp_slo_url',
919
+ 'saml_idp_slo_response_url',
920
+ 'saml_idp_slo_binding',
921
+ 'saml_idp_slo_soap_url',
922
+ 'saml_require_encrypted_assertions',
923
+ 'saml_acs_path',
924
+ 'require_consent_screen',
925
+ 'discourse_secret',
926
+ 'provisioning_mode',
927
+ 'manual_endpoints',
928
+ 'github_access',
929
+ 'attribute_mapping',
930
+ 'oidc_scopes',
931
+ 'profile_mapping',
932
+ 'token_endpoint_auth_method',
933
+ 'client_assertion_audience',
934
+ 'logout',
935
+ 'idp_initiated_login',
936
+ ];
937
+ const SSO_PROFILE_KEYS = ['name', 'allowed_connection_types', 'organization_id', 'company_name', 'allow_scim', 'intents', 'return_url'];
938
+ const COOKIE_CONSENT_KEYS = ['enabled', 'script_url', 'script_id', 'site_id', 'blocking_mode', 'extra_origins', 'settings_link', 'culture', 'host_aliases'];
939
+ const BILLING_KEYS = [
940
+ 'name',
941
+ 'slug',
942
+ 'stripe_price_id',
943
+ 'interval',
944
+ 'amount',
945
+ 'currency',
946
+ 'features',
947
+ 'limits',
948
+ 'audience',
949
+ 'active',
950
+ 'pricing_model',
951
+ 'trial_days',
952
+ 'stripe_meter_id',
953
+ 'usage_unit',
954
+ 'provider',
955
+ 'annual_price_id',
956
+ 'kind',
957
+ ];
958
+ const NOTIFICATION_KEYS = ['name', 'subject', 'body', 'category'];
959
+ const EMAIL_KEYS = ['name', 'subject', 'text'];
960
+ const ORG_POLICY_KEYS = [
961
+ 'organization_id',
962
+ 'requireMfa',
963
+ 'ssoRequired',
964
+ 'sessionIdleOverrideMs',
965
+ 'allowedSignInMethods',
966
+ 'ipAllowlist',
967
+ 'maxSessionAgeSeconds',
968
+ ];
969
+ /** Narrow a Config to a plain string-keyed record for field access. */
970
+ function obj(config) {
971
+ return config && typeof config === 'object' && !Array.isArray(config)
972
+ ? config
973
+ : {};
974
+ }
975
+ /** What a write-only secret looks like on the live side, and in a plan. */
976
+ const SECRET_SET = '(secret set)';
977
+ const isObj = (x) => x !== null && typeof x === 'object' && !Array.isArray(x);
978
+ /**
979
+ * Bring a file's secrets and the live document to one comparable shape. A secret
980
+ * value becomes "(secret set)", so a plan never holds it and it compares equal to
981
+ * the live marker. `unverifiable` lists the secrets set on both sides, or that the
982
+ * API never reports (`reported` false): Atlas cannot say whether they differ, so
983
+ * they are not drift, and the plan says so. A secret the API reports as unset stays
984
+ * a difference. An unresolved ${VAR} stays visible (apply refuses it), as does an
985
+ * empty value.
986
+ */
987
+ function reconcileSecrets(desired, live, reported, prefix = '') {
988
+ if (!isObj(desired))
989
+ return { desired, live, unverifiable: [] };
990
+ const d = { ...desired };
991
+ const l = isObj(live) ? { ...live } : {};
992
+ const unverifiable = [];
993
+ for (const [k, v] of Object.entries(desired)) {
994
+ const path = prefix ? `${prefix}.${k}` : k;
995
+ if (isObj(v)) {
996
+ const inner = reconcileSecrets(v, l[k], reported, path);
997
+ d[k] = inner.desired;
998
+ if (isObj(l[k]))
999
+ l[k] = inner.live;
1000
+ unverifiable.push(...inner.unverifiable);
1001
+ }
1002
+ else if ((0, redact_1.isSecretKey)(k) && typeof v === 'string' && v !== '' && v !== SECRET_SET && !(0, redact_1.isPlaceholder)(v)) {
1003
+ if (l[k] === SECRET_SET) {
1004
+ d[k] = SECRET_SET;
1005
+ unverifiable.push(path);
1006
+ }
1007
+ else if (l[k] === undefined || l[k] === null) {
1008
+ d[k] = SECRET_SET;
1009
+ if (!reported) {
1010
+ l[k] = SECRET_SET;
1011
+ unverifiable.push(path);
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+ return { desired: d, live: isObj(live) || unverifiable.length ? l : live, unverifiable };
1017
+ }
1018
+ /** The live document without the secret markers and server defaults the file says nothing about. */
1019
+ function withoutUnstated(live, desired, defaulted = []) {
1020
+ if (!isObj(live))
1021
+ return live;
1022
+ const want = isObj(desired) ? desired : {};
1023
+ const out = {};
1024
+ for (const [k, v] of Object.entries(live)) {
1025
+ if (!(k in want) && (v === SECRET_SET || defaulted.includes(k)))
1026
+ continue;
1027
+ out[k] = isObj(v) && isObj(want[k]) ? withoutUnstated(v, want[k]) : v;
1028
+ }
1029
+ return out;
1030
+ }
1031
+ /** How close two key names are (edit distance). */
1032
+ function distance(a, b) {
1033
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
1034
+ for (let i = 1; i <= a.length; i++) {
1035
+ const row = [i];
1036
+ for (let j = 1; j <= b.length; j++)
1037
+ row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1038
+ prev = row;
1039
+ }
1040
+ return prev[b.length];
1041
+ }
1042
+ function didYouMean(key, known) {
1043
+ const norm = (x) => x.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
1044
+ const best = [...known].sort((a, b) => distance(norm(key), a) - distance(norm(key), b))[0];
1045
+ return best && distance(norm(key), best) <= Math.max(2, Math.floor(key.length / 3)) ? ` (did you mean ${best}?)` : '';
1046
+ }
1047
+ /** The keys in one parsed resource file that no spec knows, each named with its nearest real key. */
1048
+ function unknownFileKeys(spec, doc) {
1049
+ const out = [];
1050
+ const check = (value, known, label) => {
1051
+ if (!isObj(value))
1052
+ return;
1053
+ for (const k of Object.keys(value))
1054
+ if (!known.includes(k))
1055
+ out.push(`${label}: ${k}${didYouMean(k, known)}`);
1056
+ };
1057
+ if (spec.kind === 'collection') {
1058
+ for (const entry of asArray(doc))
1059
+ check(entry, spec.fileKeys, `${spec.type} ${spec.keyOf(entry) || '(no key)'}`);
1060
+ }
1061
+ else if (spec.fileKeys) {
1062
+ check(doc, spec.fileKeys, spec.type);
1063
+ for (const [k, known] of Object.entries(spec.nestedKeys ?? {}))
1064
+ if (isObj(doc))
1065
+ check(doc[k], known, `${spec.type}.${k}`);
1066
+ }
1067
+ return out;
1068
+ }
1069
+ /** A document without any "(secret set)" marker: what an export writes and what apply sends. */
1070
+ function withoutMarkers(value) {
1071
+ if (Array.isArray(value))
1072
+ return value.map(withoutMarkers);
1073
+ if (value && typeof value === 'object') {
1074
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== SECRET_SET).map(([k, v]) => [k, withoutMarkers(v)]));
1075
+ }
1076
+ return value;
1077
+ }
1078
+ /** An origin list in either form (strings, or { origin, integration }) as plain strings. */
1079
+ function originStrings(list) {
1080
+ return (Array.isArray(list) ? list : []).map((e) => (typeof e === 'string' ? e : String(obj(e).origin ?? '')));
1081
+ }
1082
+ /** auth_config without the integration hints, which belong to allowed_origins. */
1083
+ function stripOriginIntegrations(authConfig) {
1084
+ const { originIntegrations: _hints, ...rest } = obj(authConfig);
1085
+ return rest;
1086
+ }
1087
+ /** A stable natural key for an SSO connection, which has no name field. */
1088
+ function ssoKey(config) {
1089
+ const c = obj(config);
1090
+ const type = String(c.type ?? 'sso');
1091
+ const id = c.oidc_issuer ?? c.saml_idp_entity_id ?? c.saml_sp_entity_id ?? c.discourse_provider_url ?? '';
1092
+ return `${type}:${String(id)}`;
1093
+ }
1094
+ /** Configure an OAuth provider: credentials/scopes (PUT), then scope + enabled toggles. */
1095
+ async function putProvider(http, config) {
1096
+ const c = obj(config);
1097
+ // Provider settings (a tenant, a domain, Apple's team/key ids) ride along in `values`.
1098
+ const values = { ...obj(c.values ?? null) };
1099
+ if (c.client_id !== undefined && c.client_id !== null)
1100
+ values.client_id = c.client_id;
1101
+ // A secret only exists here when the file carried a ${ENV} placeholder for it.
1102
+ if (c.client_secret !== undefined && c.client_secret !== null) {
1103
+ values.client_secret = c.client_secret;
1104
+ }
1105
+ const body = {};
1106
+ if (Object.keys(values).length)
1107
+ body.values = values;
1108
+ if (Array.isArray(c.scopes))
1109
+ body.scopes = c.scopes;
1110
+ // Credential-less providers (siwe, siws, siwb, siwf) take verifier settings instead.
1111
+ if (c.settings && typeof c.settings === 'object' && !Array.isArray(c.settings))
1112
+ body.settings = c.settings;
1113
+ if (c.mode === 'native' || c.mode === 'redirect')
1114
+ body.mode = c.mode;
1115
+ const provider = String(c.provider ?? '');
1116
+ if (Object.keys(body).length) {
1117
+ await http.put(`/v1/oauth_providers/${enc(provider)}`, body);
1118
+ }
1119
+ if (c.allow_sign_in !== undefined || c.allow_sign_up !== undefined) {
1120
+ await http.post(`/v1/oauth_providers/${enc(provider)}/scope`, {
1121
+ allow_sign_in: c.allow_sign_in ?? true,
1122
+ allow_sign_up: c.allow_sign_up ?? true,
1123
+ });
1124
+ }
1125
+ if (c.enabled !== undefined) {
1126
+ await http.post(`/v1/oauth_providers/${enc(provider)}/enabled`, { enabled: c.enabled });
1127
+ }
1128
+ }
1129
+ async function putEmail(http, config) {
1130
+ const c = obj(config);
1131
+ const body = {};
1132
+ if (c.subject !== undefined)
1133
+ body.subject = c.subject;
1134
+ if (c.text !== undefined)
1135
+ body.text = c.text;
1136
+ await http.put(`/v1/email_templates/${enc(String(c.name ?? ''))}`, body);
1137
+ }
1138
+ async function patchOrgPolicy(http, config) {
1139
+ const c = obj(config);
1140
+ const body = {};
1141
+ for (const k of ORG_POLICY_KEYS) {
1142
+ if (k === 'organization_id')
1143
+ continue;
1144
+ if (c[k] !== undefined && c[k] !== null)
1145
+ body[k] = c[k];
1146
+ }
1147
+ await http.patch(`/v1/organizations/${enc(String(c.organization_id ?? ''))}/policy`, body);
1148
+ }
1149
+ // ───────────────────────────── the command ─────────────────────────────
1150
+ /** `atlas config export|import` — Auth0 `deploy-cli`-style config-as-code. */
1151
+ async function config(ctx, args) {
1152
+ const sub = args.positionals[1];
1153
+ switch (sub) {
1154
+ case 'export':
1155
+ return exportConfig(ctx, args.values);
1156
+ case 'import':
1157
+ return importConfig(ctx, args.values);
1158
+ case 'plan':
1159
+ return planConfig(ctx, args.values, { dir: resolveDir(args.values), format: resolveFormat(args.values), env: readSecretsEnv(ctx, args.values) });
1160
+ case 'promote':
1161
+ return promoteConfig(ctx, args.values);
1162
+ default:
1163
+ throw new errors_1.CliError(sub
1164
+ ? `Unknown subcommand 'config ${sub}'. Try: export, import, plan, promote.`
1165
+ : 'config needs a subcommand: export, import, plan, promote.');
1166
+ }
1167
+ }
1168
+ function resolveFormat(values) {
1169
+ const raw = (0, args_1.str)(values, 'format');
1170
+ if (raw === undefined)
1171
+ return 'json';
1172
+ if (raw !== 'json' && raw !== 'yaml') {
1173
+ throw new errors_1.CliError(`--format must be 'json' or 'yaml', got '${raw}'.`);
1174
+ }
1175
+ return raw;
1176
+ }
1177
+ function resolveDir(values) {
1178
+ return (0, args_1.str)(values, 'dir') || './atlas-config';
1179
+ }
1180
+ /** Build the config HTTP client from the resolved profile credential. */
1181
+ function requireHttp(ctx, values, onUnknownFields) {
1182
+ const auth = (0, shared_1.resolveAuth)(ctx, values);
1183
+ const make = ctx.makeHttp ?? config_http_1.createConfigHttp;
1184
+ return make({ secretKey: auth.secretKey, apiUrl: auth.apiUrl, ...(onUnknownFields ? { onUnknownFields } : {}) });
1185
+ }
1186
+ /** Write every resource type's live config to `dir` (deterministic, secrets omitted); returns the file count. */
1187
+ async function writeExport(http, dir, format) {
1188
+ const ext = format === 'json' ? 'json' : 'yaml';
1189
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
1190
+ let fileCount = 0;
1191
+ for (const spec of registry()) {
1192
+ if (spec.kind === 'singleton') {
1193
+ const live = await spec.fetchLive(http);
1194
+ if (live === null)
1195
+ continue; // nothing to write (e.g. no schema registered)
1196
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, `${spec.type}.${ext}`), (0, serialize_1.serialize)(withoutMarkers(live), format));
1197
+ fileCount += 1;
1198
+ }
1199
+ else {
1200
+ const entries = (await spec.fetchLive(http)).filter((e) => !e.managedBy);
1201
+ // Deterministic: entries sorted by natural key, keys sorted inside each.
1202
+ const sorted = [...entries].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
1203
+ const doc = sorted.map((e) => (0, serialize_1.canonicalize)(withoutMarkers(e.config)));
1204
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, `${spec.type}.${ext}`), (0, serialize_1.serialize)(doc, format));
1205
+ fileCount += 1;
1206
+ }
1207
+ }
1208
+ return fileCount;
1209
+ }
1210
+ async function exportConfig(ctx, values) {
1211
+ const http = requireHttp(ctx, values);
1212
+ const dir = resolveDir(values);
1213
+ const format = resolveFormat(values);
1214
+ const fileCount = await writeExport(http, dir, format);
1215
+ ctx.io.out(`Exported ${fileCount} resource file(s) to ${dir} (${format}).`);
1216
+ ctx.io.out('Secrets are never exported — supply any credential at import via a ${ENV} placeholder.');
1217
+ return 0;
1218
+ }
1219
+ /**
1220
+ * Diff a config directory against the live instance. Nothing is written: the
1221
+ * returned changes carry their own `apply`. Used by `atlas config import` and by
1222
+ * `atlas migrate better-auth` (which shows the plan before applying it).
1223
+ */
1224
+ async function planConfigImport(http, opts) {
1225
+ const { dir, format, env, prune } = opts;
1226
+ const applyOpts = { forceHook: opts.forceHook === true };
1227
+ const ext = format === 'json' ? 'json' : 'yaml';
1228
+ if (!(0, node_fs_1.existsSync)(dir)) {
1229
+ throw new errors_1.CliError(`Config directory not found: ${dir}. Run 'atlas config export' first.`);
1230
+ }
1231
+ const present = new Set((0, node_fs_1.readdirSync)(dir));
1232
+ const unknown = registry().flatMap((spec) => present.has(`${spec.type}.${ext}`) ? unknownFileKeys(spec, (0, serialize_1.deserialize)((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, `${spec.type}.${ext}`), 'utf8'), format)) : []);
1233
+ if (unknown.length)
1234
+ throw new errors_1.CliError(`Unknown key(s) in the config:\n ${unknown.join('\n ')}`);
1235
+ const rotate = opts.rotateSecrets === true;
1236
+ const warnings = [];
1237
+ const unverifiableSecrets = [];
1238
+ const changes = [];
1239
+ let unchanged = 0;
1240
+ const skippedDeletes = [];
1241
+ const managed = [];
1242
+ for (const spec of registry()) {
1243
+ const file = `${spec.type}.${ext}`;
1244
+ // A type with no file is simply NOT MANAGED — leave live untouched.
1245
+ if (!present.has(file))
1246
+ continue;
1247
+ const text = (0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, file), 'utf8');
1248
+ const parsed = interpolate((0, serialize_1.deserialize)(text, format), env);
1249
+ if (spec.kind === 'singleton') {
1250
+ const desired = parsed;
1251
+ const liveRaw = await spec.fetchLive(http);
1252
+ const norm = spec.normalize ?? ((c) => c);
1253
+ const desiredN = norm(desired);
1254
+ // A section that only clears fields (all null) against nothing live is already satisfied.
1255
+ if (liveRaw === null && onlyNulls(desiredN)) {
1256
+ unchanged += 1;
1257
+ continue;
1258
+ }
1259
+ if (liveRaw === null) {
1260
+ const masked = reconcileSecrets(desiredN, undefined, true).desired;
1261
+ changes.push({ type: spec.type, action: 'create', key: '', after: masked, raw: desiredN, apply: () => spec.apply(http, desired, applyOpts) });
1262
+ continue;
1263
+ }
1264
+ const seen = withoutUnstated(comparable(norm(liveRaw), desiredN, prune), desiredN);
1265
+ const r = reconcileSecrets(desiredN, seen, spec.reportsSecrets === true);
1266
+ const forced = rotate ? r.unverifiable : [];
1267
+ if (!rotate)
1268
+ unverifiableSecrets.push(...r.unverifiable.map((path) => ({ type: spec.type, key: '', path })));
1269
+ if (forced.length === 0 && (0, serialize_1.deepEqual)(r.desired, r.live)) {
1270
+ unchanged += 1;
1271
+ continue;
1272
+ }
1273
+ changes.push({ type: spec.type, action: 'update', key: '', before: r.live, after: r.desired, raw: desiredN, changedSecrets: forced, apply: () => spec.apply(http, desired, applyOpts) });
1274
+ continue;
1275
+ }
1276
+ // Collection: diff desired-by-key against live-by-key.
1277
+ const desiredList = asArray(parsed);
1278
+ const desiredByKey = new Map();
1279
+ for (const cfg of desiredList)
1280
+ desiredByKey.set(spec.keyOf(cfg), cfg);
1281
+ const all = await spec.fetchLive(http);
1282
+ for (const e of all)
1283
+ if (e.managedBy)
1284
+ managed.push({ type: spec.type, key: e.key, managedBy: e.managedBy });
1285
+ const live = all.filter((e) => !e.managedBy);
1286
+ const liveByKey = new Map(live.map((e) => [e.key, e]));
1287
+ for (const [key, cfg] of desiredByKey) {
1288
+ const existing = liveByKey.get(key);
1289
+ warnings.push(...(spec.warnings?.(cfg, existing?.config) ?? []));
1290
+ if (!existing) {
1291
+ changes.push({ type: spec.type, action: 'create', key, after: reconcileSecrets(cfg, undefined, true).desired, raw: cfg, apply: () => spec.create(http, cfg) });
1292
+ continue;
1293
+ }
1294
+ const seen = withoutUnstated(comparable(existing.config, cfg, prune), cfg, spec.defaulted);
1295
+ const r = reconcileSecrets(cfg, seen, spec.reportsSecrets === true);
1296
+ const unverifiable = [...r.unverifiable];
1297
+ const liveSeen = obj(r.live);
1298
+ for (const k of spec.writeOnly ?? []) {
1299
+ if (obj(r.desired)[k] === undefined)
1300
+ continue;
1301
+ liveSeen[k] = obj(r.desired)[k];
1302
+ unverifiable.push(k);
1303
+ }
1304
+ let forced = rotate ? unverifiable : [];
1305
+ let unknownToApi = rotate ? [] : unverifiable;
1306
+ if (spec.secretChanged && unverifiable.length) {
1307
+ const changed = await spec.secretChanged(http, existing, cfg);
1308
+ if (changed !== null) {
1309
+ forced = changed ? unverifiable : [];
1310
+ unknownToApi = [];
1311
+ }
1312
+ }
1313
+ unverifiableSecrets.push(...unknownToApi.map((path) => ({ type: spec.type, key, path })));
1314
+ if (forced.length === 0 && (0, serialize_1.deepEqual)(r.desired, liveSeen)) {
1315
+ unchanged += 1;
1316
+ continue;
1317
+ }
1318
+ if (!spec.update) {
1319
+ throw new errors_1.CliError(`${spec.type} '${key}' changed but is immutable; recreate it instead.`);
1320
+ }
1321
+ const upd = spec.update;
1322
+ changes.push({ type: spec.type, action: 'update', key, before: liveSeen, after: r.desired, raw: cfg, changedSecrets: forced, apply: () => upd(http, existing, cfg) });
1323
+ }
1324
+ for (const entry of live) {
1325
+ if (desiredByKey.has(entry.key))
1326
+ continue;
1327
+ const change = { type: spec.type, action: 'delete', key: entry.key, before: entry.config };
1328
+ if (!spec.remove) {
1329
+ skippedDeletes.push(change);
1330
+ continue;
1331
+ }
1332
+ if (prune) {
1333
+ const rm = spec.remove;
1334
+ changes.push({ ...change, apply: () => rm(http, entry) });
1335
+ }
1336
+ else {
1337
+ skippedDeletes.push(change);
1338
+ }
1339
+ }
1340
+ }
1341
+ return { changes, unchanged, skippedDeletes, managed, warnings, unverifiableSecrets };
1342
+ }
1343
+ /** True for a non-empty object whose every leaf is `null` (a file that only clears fields). */
1344
+ function onlyNulls(x) {
1345
+ if (x === null)
1346
+ return true;
1347
+ if (typeof x !== 'object' || Array.isArray(x) || x === undefined)
1348
+ return false;
1349
+ const vals = Object.values(x);
1350
+ return vals.length > 0 && vals.every(onlyNulls);
1351
+ }
1352
+ /**
1353
+ * The live document as the plan compares it. With `--prune` the whole live
1354
+ * document (keys the file leaves unset show as drift). Without it only the keys
1355
+ * the file sets, recursively, so a freshly applied file plans clean. A desired
1356
+ * `null` means "unset", so it matches a live key that is absent.
1357
+ */
1358
+ function comparable(live, desired, prune) {
1359
+ if (prune)
1360
+ return live;
1361
+ if (!isObj(live) || !isObj(desired))
1362
+ return live;
1363
+ const out = {};
1364
+ for (const k of Object.keys(desired)) {
1365
+ const l = live[k];
1366
+ if (l === undefined || l === null) {
1367
+ if (desired[k] === null)
1368
+ out[k] = null;
1369
+ continue;
1370
+ }
1371
+ out[k] = comparable(l, desired[k], false);
1372
+ }
1373
+ return out;
1374
+ }
1375
+ /** The stored credential of a named profile (one profile per environment). */
1376
+ function profileHttp(ctx, name) {
1377
+ const stored = ctx.config.load().profiles[name];
1378
+ if (!stored?.secret_key) {
1379
+ throw new errors_1.CliError(`No profile named '${name}'. Run 'atlas login --profile ${name}' with that environment's secret key.`);
1380
+ }
1381
+ const secretKey = stored.secret_key;
1382
+ const apiUrl = (0, shared_1.envApiUrl)(ctx.env) || stored.api_url || backend_1.DEFAULT_API_URL;
1383
+ const make = ctx.makeHttp ?? config_http_1.createConfigHttp;
1384
+ return { http: make({ secretKey, apiUrl }), secretKey };
1385
+ }
1386
+ /** True for a production (live) secret key: `atlas_live_sk_…` or `sk_live_…`. */
1387
+ const isLiveKey = (key) => /(?:^|_)live_/.test(key);
1388
+ /**
1389
+ * `atlas config promote --from <profile> --to <profile>`: copy one environment's
1390
+ * configuration to another (development → staging → production) through the same
1391
+ * plan/apply engine as `config import`. Each environment is a profile holding that
1392
+ * instance's secret key. Secrets never travel: `${ENV}` placeholders stay as they
1393
+ * are and credentials are set per environment. `--dry-run` prints the plan only;
1394
+ * applying to a live (production) key needs `--yes`.
1395
+ */
1396
+ async function promoteConfig(ctx, values) {
1397
+ const from = (0, args_1.str)(values, 'from');
1398
+ const to = (0, args_1.str)(values, 'to');
1399
+ if (!from || !to)
1400
+ throw new errors_1.CliError('promote needs --from <profile> and --to <profile> (one profile per environment).');
1401
+ if (from === to)
1402
+ throw new errors_1.CliError('--from and --to name the same profile.');
1403
+ const format = resolveFormat(values);
1404
+ const dryRun = values['dry-run'] === true;
1405
+ const prune = values.prune === true;
1406
+ const source = profileHttp(ctx, from);
1407
+ const target = profileHttp(ctx, to);
1408
+ if (source.secretKey === target.secretKey) {
1409
+ throw new errors_1.CliError(`Profiles '${from}' and '${to}' hold the same secret key — they are the same environment.`);
1410
+ }
1411
+ if (!dryRun && isLiveKey(target.secretKey) && values.yes !== true) {
1412
+ throw new errors_1.CliError(`'${to}' is a live (production) environment. Review with --dry-run, then re-run with --yes to apply.`);
1413
+ }
1414
+ const dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'atlas-promote-'));
1415
+ try {
1416
+ await writeExport(source.http, dir, format);
1417
+ const plan = await planConfigImport(target.http, { dir, format, env: ctx.env, prune });
1418
+ ctx.io.out(`Promoting ${from} -> ${to}`);
1419
+ printPlan(ctx, plan.changes, plan.unchanged, plan.skippedDeletes, { dryRun, prune });
1420
+ printPlanNotes(ctx, plan);
1421
+ if (dryRun)
1422
+ return 0;
1423
+ for (const change of plan.changes)
1424
+ await change.apply();
1425
+ ctx.io.out(plan.changes.length ? `Applied ${plan.changes.length} change(s) to ${to}.` : 'No changes to apply.');
1426
+ return 0;
1427
+ }
1428
+ finally {
1429
+ (0, node_fs_1.rmSync)(dir, { recursive: true, force: true });
1430
+ }
1431
+ }
1432
+ async function importConfig(ctx, values) {
1433
+ const http = requireHttp(ctx, values);
1434
+ const dir = resolveDir(values);
1435
+ const format = resolveFormat(values);
1436
+ const dryRun = values['dry-run'] === true;
1437
+ const prune = values.prune === true;
1438
+ const plan = await planConfigImport(http, { dir, format, env: readSecretsEnv(ctx, values), prune, forceHook: values['force-hook'] === true, rotateSecrets: values['rotate-secret'] === true });
1439
+ printPlan(ctx, plan.changes, plan.unchanged, plan.skippedDeletes, { dryRun, prune });
1440
+ printPlanNotes(ctx, plan);
1441
+ if (dryRun)
1442
+ return 0;
1443
+ for (const change of plan.changes)
1444
+ await change.apply();
1445
+ if (plan.changes.length)
1446
+ ctx.io.out(`Applied ${plan.changes.length} change(s).`);
1447
+ else
1448
+ ctx.io.out('No changes to apply.');
1449
+ return 0;
1450
+ }
1451
+ /** Leaf-level differences between two documents, as dotted paths (arrays compare whole). */
1452
+ function diffFields(before, after, path = '') {
1453
+ if (isObj(before) && isObj(after)) {
1454
+ const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
1455
+ return keys.flatMap((k) => diffFields(before[k], after[k], path ? `${path}.${k}` : k));
1456
+ }
1457
+ if (before !== undefined && after !== undefined && (0, serialize_1.deepEqual)(before, after))
1458
+ return [];
1459
+ if (before === undefined && after === undefined)
1460
+ return [];
1461
+ return [{ path: path || '(document)', ...(before === undefined ? {} : { before }), ...(after === undefined ? {} : { after }) }];
1462
+ }
1463
+ const show = (v) => (v === undefined ? '(unset)' : typeof v === 'string' ? JSON.stringify(v) : JSON.stringify(v));
1464
+ /**
1465
+ * The only form a change's fields are printed or serialised in. A secret-named
1466
+ * string is `<redacted>` with a changed flag, whatever the resource; everything else
1467
+ * goes through the deep redactor.
1468
+ */
1469
+ function printedFields(c) {
1470
+ const reveal = (0, redact_1.revealingSecrets)();
1471
+ const diffs = diffFields(c.before, reveal ? (c.raw ?? c.after) : c.after);
1472
+ const rows = diffs.map((f) => {
1473
+ const leaf = f.path.split('.').pop();
1474
+ const textual = [f.before, f.after].every((v) => v === undefined || v === null || typeof v === 'string');
1475
+ if (!reveal && (0, redact_1.isSecretKey)(leaf) && textual && !(0, redact_1.isPlaceholder)(f.after))
1476
+ return { path: f.path, redacted: true, changed: true };
1477
+ return { path: f.path, ...(f.before === undefined ? {} : { before: (0, redact_1.redactDeep)(f.before, leaf) }), ...(f.after === undefined ? {} : { after: (0, redact_1.redactDeep)(f.after, leaf) }) };
1478
+ });
1479
+ for (const path of c.changedSecrets ?? [])
1480
+ if (!rows.some((r) => r.path === path))
1481
+ rows.push({ path, redacted: true, changed: true });
1482
+ return rows;
1483
+ }
1484
+ const printedLine = (f) => (f.redacted ? `${f.path}: ${redact_1.REDACTED} (changed)` : `${f.path}: ${show(f.before)} -> ${show(f.after)}`);
1485
+ /** The plan's advice: warnings, and the secrets Atlas cannot compare. */
1486
+ function printPlanNotes(ctx, plan) {
1487
+ for (const w of plan.warnings)
1488
+ ctx.io.out(`warning: ${w}`);
1489
+ for (const u of plan.unverifiableSecrets) {
1490
+ ctx.io.out(`note: ${u.type}${u.key ? ` ${u.key}` : ''} ${u.path}: Atlas does not return this value, so a change to it cannot be detected. Send it again with --rotate-secret.`);
1491
+ }
1492
+ }
1493
+ /**
1494
+ * `atlas config plan`: a Terraform-style, read-only diff of a config directory
1495
+ * (`atlas config export` output, committed to git) against the live instance.
1496
+ * Field-level for updates, `--json` for tooling, and `--exit-code` to exit 2
1497
+ * when there is drift so CI can gate on it. Never writes anything.
1498
+ */
1499
+ async function planConfig(ctx, values, source) {
1500
+ const http = requireHttp(ctx, values);
1501
+ const { dir, format, env } = source;
1502
+ const where = source.label ?? dir;
1503
+ const prune = values.prune === true;
1504
+ const plan = await planConfigImport(http, { dir, format, env, prune, rotateSecrets: values['rotate-secret'] === true });
1505
+ const deletes = prune ? [] : plan.skippedDeletes;
1506
+ const all = plan.changes;
1507
+ const rows = all.map((c) => ({
1508
+ type: c.type,
1509
+ key: c.key || null,
1510
+ action: c.action,
1511
+ fields: c.action === 'update' ? printedFields(c) : [],
1512
+ }));
1513
+ const pending = rows.length;
1514
+ if (values.json === true) {
1515
+ ctx.io.out(JSON.stringify({
1516
+ object: 'config_plan',
1517
+ changes: rows,
1518
+ unchanged: plan.unchanged,
1519
+ unmanaged_live: deletes.map((d) => ({ type: d.type, key: d.key || null })),
1520
+ managed_by_platform: plan.managed.map((m) => ({ type: m.type, key: m.key, managed_by: m.managedBy })),
1521
+ warnings: plan.warnings,
1522
+ unverifiable_secrets: plan.unverifiableSecrets.map((u) => ({ type: u.type, key: u.key || null, path: u.path })),
1523
+ has_changes: pending > 0,
1524
+ }, null, 2));
1525
+ }
1526
+ else {
1527
+ ctx.io.out(`Plan: ${pending} to change, ${plan.unchanged} unchanged${deletes.length ? `, ${deletes.length} live but not in config` : ''}.`);
1528
+ const sign = { create: '+', update: '~', delete: '-' };
1529
+ for (const c of [...all].sort(sortChange)) {
1530
+ const label = c.key ? `${c.type} ${c.key}` : c.type;
1531
+ ctx.io.out('');
1532
+ ctx.io.out(`${sign[c.action]} ${label} (${c.action})`);
1533
+ if (c.action === 'update') {
1534
+ for (const f of printedFields(c))
1535
+ ctx.io.out(` ${printedLine(f)}`);
1536
+ }
1537
+ }
1538
+ if (deletes.length) {
1539
+ ctx.io.out('');
1540
+ ctx.io.out(`Live but not in ${where} (kept; pass --prune to plan their deletion):`);
1541
+ for (const d of deletes)
1542
+ ctx.io.out(` ${d.type}${d.key ? ` ${d.key}` : ''}`);
1543
+ }
1544
+ if (plan.managed.length) {
1545
+ ctx.io.out('');
1546
+ ctx.io.out('Managed by a connected platform (not part of this file; never pruned):');
1547
+ for (const m of plan.managed)
1548
+ ctx.io.out(` ${m.type} ${m.key} (${m.managedBy})`);
1549
+ }
1550
+ if (plan.warnings.length || plan.unverifiableSecrets.length)
1551
+ ctx.io.out('');
1552
+ printPlanNotes(ctx, plan);
1553
+ if (pending === 0)
1554
+ ctx.io.out(`No drift: the instance matches ${where}.`);
1555
+ }
1556
+ return wantsCheck(values) && pending > 0 ? 2 : 0;
1557
+ }
1558
+ /** `--check` (CI) and `--exit-code` (Terraform's name) both mean: exit 2 when there is drift. */
1559
+ const wantsCheck = (values) => values['exit-code'] === true || values.check === true;
1560
+ /**
1561
+ * The environment `${VAR}` placeholders resolve against: the process env, then
1562
+ * `--secrets-file` (a dotenv file) on top. The file holds credentials, so it
1563
+ * must be readable by its owner only (mode 0600 or stricter); anything looser
1564
+ * is refused rather than read. POSIX only: Windows has no such mode bits.
1565
+ */
1566
+ function readSecretsEnv(ctx, values) {
1567
+ const file = (0, args_1.str)(values, 'secrets-file');
1568
+ if (!file)
1569
+ return ctx.env;
1570
+ let mode;
1571
+ try {
1572
+ mode = (0, node_fs_1.statSync)(file).mode;
1573
+ }
1574
+ catch {
1575
+ throw new errors_1.CliError(`--secrets-file not found: ${file}.`);
1576
+ }
1577
+ if (process.platform !== 'win32' && (mode & 0o077) !== 0) {
1578
+ throw new errors_1.CliError(`--secrets-file ${file} is readable by others (mode ${(mode & 0o777).toString(8)}). Run 'chmod 600 ${file}' and try again.`);
1579
+ }
1580
+ return { ...ctx.env, ...(0, secrets_1.parseDotenv)(ctx.readFile(file)) };
1581
+ }
1582
+ /** `${VAR}` placeholders still in a planned change (no env value and no --secrets-file entry). */
1583
+ function unresolvedPlaceholders(changes) {
1584
+ const names = new Set();
1585
+ for (const c of changes) {
1586
+ for (const m of JSON.stringify(c.after ?? null).matchAll(/\$\{([A-Z0-9_]+)\}/g))
1587
+ names.add(m[1]);
1588
+ }
1589
+ return [...names].sort();
1590
+ }
1591
+ /** The resource types a single config file may hold, in the order they are applied. */
1592
+ function configTypes() {
1593
+ return registry().map((s) => s.type);
1594
+ }
1595
+ /**
1596
+ * Resolve `atlas plan|apply [<path>]`: a directory of per-type files (as
1597
+ * `atlas config export` writes), or ONE file whose top-level keys are resource
1598
+ * types (`instance`, `webhook_endpoints`, …), split into a temp directory.
1599
+ * `cleanup` removes that temp directory.
1600
+ */
1601
+ /** Config files `atlas plan` / `atlas apply` pick up from the current directory when given no path. */
1602
+ exports.DEFAULT_CONFIG_FILES = ['atlas.config.ts', 'atlas.config.mts', 'atlas.config.mjs', 'atlas.config.js', 'atlas.yaml', 'atlas.yml', 'atlas.json'];
1603
+ const CODE_EXTS = new Set(['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs']);
1604
+ // The CLI compiles to CommonJS, where tsc turns import() into require(), which cannot load ESM or .ts,
1605
+ // so the real import() is reached through Function. A vm context with no import hook (a test runner)
1606
+ // cannot run that one, and gets the plain import() the runner itself transforms.
1607
+ const nativeImport = async (url) => {
1608
+ try {
1609
+ return await new Function('url', 'return import(url)')(url);
1610
+ }
1611
+ catch (err) {
1612
+ if (err.code !== 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING')
1613
+ throw err;
1614
+ return Promise.resolve(`${url}`).then(s => __importStar(require(s)));
1615
+ }
1616
+ };
1617
+ /**
1618
+ * Load an `atlas.config.ts` (or .js/.mjs): its default export is the config
1619
+ * object, or a function (sync or async) returning it. A .ts file runs on
1620
+ * Node's own type stripping (Node 22.18+ / 23.6+), so it adds no dependency.
1621
+ */
1622
+ async function loadCodeConfig(path) {
1623
+ let mod;
1624
+ try {
1625
+ mod = (await nativeImport((0, node_url_1.pathToFileURL)((0, node_path_1.resolve)(path)).href));
1626
+ }
1627
+ catch (err) {
1628
+ const msg = err.message;
1629
+ if (/\.ts|Unknown file extension|ERR_UNKNOWN_FILE_EXTENSION/.test(msg) && /\.[cm]?ts$/.test(path)) {
1630
+ throw new errors_1.CliError(`${path}: this Node (${process.version}) cannot load TypeScript directly. Use Node 22.18+ (or 23.6+), or rename it to atlas.config.mjs.`);
1631
+ }
1632
+ throw new errors_1.CliError(`${path}: ${msg}`);
1633
+ }
1634
+ const exported = mod.default;
1635
+ if (exported === undefined)
1636
+ throw new errors_1.CliError(`${path} has no default export. Write: export default defineConfig({ ... }).`);
1637
+ return typeof exported === 'function' ? await exported() : exported;
1638
+ }
1639
+ async function resolveSource(ctx, args) {
1640
+ const path = args.positionals[1] ??
1641
+ ((0, args_1.str)(args.values, 'dir') ? resolveDir(args.values) : (exports.DEFAULT_CONFIG_FILES.find((f) => (0, node_fs_1.existsSync)(f)) ?? resolveDir(args.values)));
1642
+ const env = readSecretsEnv(ctx, args.values);
1643
+ let isFile = false;
1644
+ try {
1645
+ isFile = (0, node_fs_1.statSync)(path).isFile();
1646
+ }
1647
+ catch {
1648
+ throw new errors_1.CliError(`Config not found: ${path}. Pass a config file (atlas.config.ts / atlas.yaml / atlas.json) or a directory from 'atlas config export'.`);
1649
+ }
1650
+ if (!isFile)
1651
+ return { dir: path, label: path, format: resolveFormat(args.values), env, cleanup: () => undefined };
1652
+ const ext = (0, node_path_1.extname)(path).toLowerCase();
1653
+ const format = ext === '.yaml' || ext === '.yml' ? 'yaml' : ext === '.json' ? 'json' : resolveFormat(args.values);
1654
+ // A round-trip through JSON keeps only plain data (no functions, Dates become strings), like a .json file.
1655
+ const doc = CODE_EXTS.has(ext) ? JSON.parse(JSON.stringify((await loadCodeConfig(path)) ?? null)) : (0, serialize_1.deserialize)(ctx.readFile(path), format);
1656
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
1657
+ throw new errors_1.CliError(`${path} must be an object keyed by resource type (${configTypes().join(', ')}).`);
1658
+ }
1659
+ const known = new Set(configTypes());
1660
+ const unknown = Object.keys(doc).filter((k) => !known.has(k));
1661
+ if (unknown.length) {
1662
+ throw new errors_1.CliError(`${path}: unknown resource type(s) ${unknown.join(', ')}. Known: ${configTypes().join(', ')}.`);
1663
+ }
1664
+ const dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'atlas-apply-'));
1665
+ for (const [type, value] of Object.entries(doc))
1666
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, `${type}.json`), (0, serialize_1.serialize)(value, 'json'));
1667
+ return { dir, label: path, format: 'json', env, cleanup: () => (0, node_fs_1.rmSync)(dir, { recursive: true, force: true }) };
1668
+ }
1669
+ /**
1670
+ * `atlas plan [<file|dir>]`: the read-only diff of `atlas config plan`, taking
1671
+ * a single config file as well. `--check` exits 2 on drift for CI.
1672
+ */
1673
+ async function planCommand(ctx, args) {
1674
+ const source = await resolveSource(ctx, args);
1675
+ try {
1676
+ return await planConfig(ctx, args.values, source);
1677
+ }
1678
+ finally {
1679
+ source.cleanup();
1680
+ }
1681
+ }
1682
+ /**
1683
+ * `atlas apply [<file|dir>]`: plan, show it, confirm, then apply in the fixed
1684
+ * resource order (see {@link configTypes}). `--yes` skips the question (needed
1685
+ * when there is no terminal); `--check` only plans and exits 2 on drift.
1686
+ * Refuses to apply while any `${VAR}` placeholder is unresolved, so a missing
1687
+ * secret is never sent as the literal text.
1688
+ */
1689
+ async function applyCommand(ctx, args) {
1690
+ const values = args.values;
1691
+ if (values.check === true || values['exit-code'] === true)
1692
+ return planCommand(ctx, args);
1693
+ const source = await resolveSource(ctx, args);
1694
+ try {
1695
+ const onUnknownFields = (n) => ctx.io.err(`atlas: warning: ${n.method} ${n.path} ignored field(s) ${n.fields}${n.suggestions ? ` (did you mean: ${n.suggestions})` : ''}. This instance is lenient; set auth_config.strictRequestBodies to refuse them.`);
1696
+ const kept = keepingSecrets(requireHttp(ctx, values, onUnknownFields), (0, args_1.str)(values, 'out') || (0, secrets_1.productDir)(ctx.env, 'atlas'));
1697
+ const prune = values.prune === true;
1698
+ const plan = await planConfigImport(kept.http, { dir: source.dir, format: source.format, env: source.env, prune, forceHook: values['force-hook'] === true, rotateSecrets: values['rotate-secret'] === true });
1699
+ printPlan(ctx, plan.changes, plan.unchanged, plan.skippedDeletes, { dryRun: false, prune });
1700
+ for (const c of plan.changes) {
1701
+ if (c.action !== 'update')
1702
+ continue;
1703
+ const fields = printedFields(c);
1704
+ if (fields.length)
1705
+ ctx.io.out(` ~ ${c.key ? `${c.type} ${c.key}` : c.type}:`);
1706
+ for (const f of fields)
1707
+ ctx.io.out(` ${printedLine(f)}`);
1708
+ }
1709
+ printPlanNotes(ctx, plan);
1710
+ if (plan.changes.length === 0) {
1711
+ ctx.io.out('No changes to apply.');
1712
+ return 0;
1713
+ }
1714
+ const missing = unresolvedPlaceholders(plan.changes);
1715
+ if (missing.length) {
1716
+ throw new errors_1.CliError(`Unresolved placeholder(s): ${missing.map((n) => `\${${n}}`).join(', ')}. Set them in the environment or in --secrets-file.`);
1717
+ }
1718
+ if (values.yes !== true) {
1719
+ const answer = (await ctx.prompt.text(`Apply ${plan.changes.length} change(s)? [y/N]`, { default: 'n' })).trim().toLowerCase();
1720
+ if (answer !== 'y' && answer !== 'yes') {
1721
+ ctx.io.out('Nothing applied.');
1722
+ return 1;
1723
+ }
1724
+ }
1725
+ for (const change of plan.changes)
1726
+ await change.apply();
1727
+ ctx.io.out(`Applied ${plan.changes.length} change(s).`);
1728
+ for (const path of kept.saved)
1729
+ ctx.io.out(`A new secret was written to ${path} (mode 0600). It is shown only once.`);
1730
+ return 0;
1731
+ }
1732
+ finally {
1733
+ source.cleanup();
1734
+ }
1735
+ }
1736
+ /**
1737
+ * The API, but a create that answers with a one-time secret (a new OAuth
1738
+ * client's `client_secret`, a new webhook endpoint's `secret`) writes it to a
1739
+ * 0600 dotenv file in `dir` (default `~/.config/atlas`, or `--out`) instead of
1740
+ * dropping it. The file names are the hosted-redirect env names.
1741
+ */
1742
+ function keepingSecrets(http, dir) {
1743
+ const saved = [];
1744
+ const wrapped = {
1745
+ get: (path) => http.get(path),
1746
+ put: (path, body) => http.put(path, body),
1747
+ patch: (path, body) => http.patch(path, body),
1748
+ delete: (path, body) => http.delete(path, body),
1749
+ async post(path, body) {
1750
+ const res = await http.post(path, body);
1751
+ const r = (res ?? {});
1752
+ const id = String(r.id ?? r.client_id ?? 'new').replace(/[^A-Za-z0-9_-]/g, '_');
1753
+ let entries = null;
1754
+ if (path === '/v1/oauth_clients' && typeof r.client_secret === 'string') {
1755
+ entries = { ATLAS_CLIENT_ID: String(r.client_id ?? ''), ATLAS_CLIENT_SECRET: r.client_secret };
1756
+ }
1757
+ else if (path === '/v1/webhook_endpoints' && typeof r.secret === 'string' && !body?.secret) {
1758
+ // A secret the file chose is already the caller's; only a minted one needs keeping.
1759
+ entries = { ATLAS_WEBHOOK_SECRET: r.secret };
1760
+ }
1761
+ if (entries) {
1762
+ const kind = path === '/v1/oauth_clients' ? 'oauth-client' : 'webhook';
1763
+ saved.push((0, secrets_1.writeSecretFile)(dir, `${kind}-${id}.env`, entries, { comment: `Written by atlas apply. ${kind} ${id}.` }).path);
1764
+ }
1765
+ return res;
1766
+ },
1767
+ };
1768
+ return { http: wrapped, saved };
1769
+ }
1770
+ function printPlan(ctx, changes, unchanged, skippedDeletes, opts) {
1771
+ const heading = opts.dryRun ? 'Plan (dry run — nothing applied):' : 'Plan:';
1772
+ ctx.io.out(heading);
1773
+ if (changes.length === 0) {
1774
+ ctx.io.out(' (no changes)');
1775
+ }
1776
+ const sign = { create: '+', update: '~', delete: '-' };
1777
+ for (const c of [...changes].sort(sortChange)) {
1778
+ const label = c.key ? `${c.type} ${c.key}` : c.type;
1779
+ ctx.io.out(` ${sign[c.action]} ${c.action} ${label}`);
1780
+ }
1781
+ if (!opts.prune && skippedDeletes.length) {
1782
+ ctx.io.out(` ${skippedDeletes.length} resource(s) exist live but not in config — pass --prune to delete.`);
1783
+ }
1784
+ ctx.io.out(` ${unchanged} unchanged.`);
1785
+ }
1786
+ function sortChange(a, b) {
1787
+ if (a.type !== b.type)
1788
+ return a.type < b.type ? -1 : 1;
1789
+ if (a.action !== b.action)
1790
+ return a.action < b.action ? -1 : 1;
1791
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
1792
+ }
1793
+ function asArray(value) {
1794
+ return Array.isArray(value) ? value : [];
1795
+ }
1796
+ /**
1797
+ * Replace `${ENV_VAR}` placeholders inside any string with the environment's
1798
+ * value — the one sanctioned way a secret enters an import without ever living
1799
+ * on disk. An unset variable is left verbatim so the plan surfaces it rather
1800
+ * than silently blanking a credential.
1801
+ */
1802
+ function interpolate(value, env, key) {
1803
+ if (typeof value === 'string') {
1804
+ return value.replace(/\$\{([A-Z0-9_]+)\}/g, (match, name) => {
1805
+ const resolved = env[name];
1806
+ if (resolved === undefined)
1807
+ return match;
1808
+ (0, redact_1.rememberResolved)(key, resolved);
1809
+ return resolved;
1810
+ });
1811
+ }
1812
+ if (Array.isArray(value))
1813
+ return value.map((v) => interpolate(v, env, key));
1814
+ if (value !== null && typeof value === 'object') {
1815
+ const out = {};
1816
+ for (const [k, v] of Object.entries(value))
1817
+ out[k] = interpolate(v, env, k);
1818
+ return out;
1819
+ }
1820
+ return value;
1821
+ }
1822
+ //# sourceMappingURL=config.js.map