@atlasauth/cli 0.25.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/args.d.ts +601 -0
- package/dist/args.js +289 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/actions-gallery.d.ts +8 -0
- package/dist/commands/actions-gallery.js +197 -0
- package/dist/commands/actions-gallery.js.map +1 -0
- package/dist/commands/actions.d.ts +13 -0
- package/dist/commands/actions.js +135 -0
- package/dist/commands/actions.js.map +1 -0
- package/dist/commands/admin.d.ts +49 -0
- package/dist/commands/admin.js +353 -0
- package/dist/commands/admin.js.map +1 -0
- package/dist/commands/api-keys.d.ts +4 -0
- package/dist/commands/api-keys.js +96 -0
- package/dist/commands/api-keys.js.map +1 -0
- package/dist/commands/auth.d.ts +25 -0
- package/dist/commands/auth.js +102 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/config.d.ts +104 -0
- package/dist/commands/config.js +1532 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/csp.d.ts +32 -0
- package/dist/commands/csp.js +180 -0
- package/dist/commands/csp.js.map +1 -0
- package/dist/commands/dev.d.ts +8 -0
- package/dist/commands/dev.js +84 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/fga.d.ts +10 -0
- package/dist/commands/fga.js +146 -0
- package/dist/commands/fga.js.map +1 -0
- package/dist/commands/golive.d.ts +40 -0
- package/dist/commands/golive.js +285 -0
- package/dist/commands/golive.js.map +1 -0
- package/dist/commands/id-accounts.d.ts +13 -0
- package/dist/commands/id-accounts.js +195 -0
- package/dist/commands/id-accounts.js.map +1 -0
- package/dist/commands/import-better-auth.d.ts +3 -0
- package/dist/commands/import-better-auth.js +164 -0
- package/dist/commands/import-better-auth.js.map +1 -0
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.js +123 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/info.d.ts +75 -0
- package/dist/commands/info.js +362 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +438 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/integrations.d.ts +8 -0
- package/dist/commands/integrations.js +112 -0
- package/dist/commands/integrations.js.map +1 -0
- package/dist/commands/listen.d.ts +17 -0
- package/dist/commands/listen.js +223 -0
- package/dist/commands/listen.js.map +1 -0
- package/dist/commands/mcp.d.ts +38 -0
- package/dist/commands/mcp.js +111 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/migrate.d.ts +16 -0
- package/dist/commands/migrate.js +114 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/move-origin.d.ts +11 -0
- package/dist/commands/move-origin.js +95 -0
- package/dist/commands/move-origin.js.map +1 -0
- package/dist/commands/operator.d.ts +18 -0
- package/dist/commands/operator.js +238 -0
- package/dist/commands/operator.js.map +1 -0
- package/dist/commands/orgs.d.ts +4 -0
- package/dist/commands/orgs.js +72 -0
- package/dist/commands/orgs.js.map +1 -0
- package/dist/commands/pages.d.ts +14 -0
- package/dist/commands/pages.js +113 -0
- package/dist/commands/pages.js.map +1 -0
- package/dist/commands/partner-platforms.d.ts +10 -0
- package/dist/commands/partner-platforms.js +157 -0
- package/dist/commands/partner-platforms.js.map +1 -0
- package/dist/commands/project-commands.d.ts +38 -0
- package/dist/commands/project-commands.js +361 -0
- package/dist/commands/project-commands.js.map +1 -0
- package/dist/commands/referrals.d.ts +69 -0
- package/dist/commands/referrals.js +1056 -0
- package/dist/commands/referrals.js.map +1 -0
- package/dist/commands/resources.d.ts +44 -0
- package/dist/commands/resources.js +234 -0
- package/dist/commands/resources.js.map +1 -0
- package/dist/commands/sessions.d.ts +4 -0
- package/dist/commands/sessions.js +28 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/shared.d.ts +38 -0
- package/dist/commands/shared.js +69 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +77 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/users.d.ts +4 -0
- package/dist/commands/users.js +100 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/webhooks.d.ts +34 -0
- package/dist/commands/webhooks.js +301 -0
- package/dist/commands/webhooks.js.map +1 -0
- package/dist/config-http.d.ts +29 -0
- package/dist/config-http.js +64 -0
- package/dist/config-http.js.map +1 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +69 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +72 -0
- package/dist/context.js +27 -0
- package/dist/context.js.map +1 -0
- package/dist/define-config.d.ts +9 -0
- package/dist/define-config.js +8 -0
- package/dist/define-config.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/help.d.ts +8 -0
- package/dist/help.js +868 -0
- package/dist/help.js.map +1 -0
- package/dist/import/csv.d.ts +6 -0
- package/dist/import/csv.js +63 -0
- package/dist/import/csv.js.map +1 -0
- package/dist/import/vendors.d.ts +57 -0
- package/dist/import/vendors.js +568 -0
- package/dist/import/vendors.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +21 -0
- package/dist/io.js +28 -0
- package/dist/io.js.map +1 -0
- package/dist/migrate/appwrite/plugin.d.ts +2 -0
- package/dist/migrate/appwrite/plugin.js +103 -0
- package/dist/migrate/appwrite/plugin.js.map +1 -0
- package/dist/migrate/auth0/plugin.d.ts +2 -0
- package/dist/migrate/auth0/plugin.js +126 -0
- package/dist/migrate/auth0/plugin.js.map +1 -0
- package/dist/migrate/authentik/plugin.d.ts +2 -0
- package/dist/migrate/authentik/plugin.js +135 -0
- package/dist/migrate/authentik/plugin.js.map +1 -0
- package/dist/migrate/azure-ad-b2c/plugin.d.ts +8 -0
- package/dist/migrate/azure-ad-b2c/plugin.js +165 -0
- package/dist/migrate/azure-ad-b2c/plugin.js.map +1 -0
- package/dist/migrate/better-auth/integration.d.ts +59 -0
- package/dist/migrate/better-auth/integration.js +81 -0
- package/dist/migrate/better-auth/integration.js.map +1 -0
- package/dist/migrate/better-auth/orchestrator.d.ts +43 -0
- package/dist/migrate/better-auth/orchestrator.js +525 -0
- package/dist/migrate/better-auth/orchestrator.js.map +1 -0
- package/dist/migrate/better-auth/output.d.ts +20 -0
- package/dist/migrate/better-auth/output.js +146 -0
- package/dist/migrate/better-auth/output.js.map +1 -0
- package/dist/migrate/better-auth/translate.d.ts +77 -0
- package/dist/migrate/better-auth/translate.js +1989 -0
- package/dist/migrate/better-auth/translate.js.map +1 -0
- package/dist/migrate/clerk/plugin.d.ts +2 -0
- package/dist/migrate/clerk/plugin.js +76 -0
- package/dist/migrate/clerk/plugin.js.map +1 -0
- package/dist/migrate/cognito/plugin.d.ts +5 -0
- package/dist/migrate/cognito/plugin.js +170 -0
- package/dist/migrate/cognito/plugin.js.map +1 -0
- package/dist/migrate/corbado/plugin.d.ts +2 -0
- package/dist/migrate/corbado/plugin.js +63 -0
- package/dist/migrate/corbado/plugin.js.map +1 -0
- package/dist/migrate/descope/plugin.d.ts +2 -0
- package/dist/migrate/descope/plugin.js +83 -0
- package/dist/migrate/descope/plugin.js.map +1 -0
- package/dist/migrate/entra-external-id/plugin.d.ts +8 -0
- package/dist/migrate/entra-external-id/plugin.js +12 -0
- package/dist/migrate/entra-external-id/plugin.js.map +1 -0
- package/dist/migrate/export/appwrite-config.d.ts +28 -0
- package/dist/migrate/export/appwrite-config.js +170 -0
- package/dist/migrate/export/appwrite-config.js.map +1 -0
- package/dist/migrate/export/appwrite.d.ts +51 -0
- package/dist/migrate/export/appwrite.js +288 -0
- package/dist/migrate/export/appwrite.js.map +1 -0
- package/dist/migrate/export/auth0-config.d.ts +29 -0
- package/dist/migrate/export/auth0-config.js +249 -0
- package/dist/migrate/export/auth0-config.js.map +1 -0
- package/dist/migrate/export/auth0.d.ts +62 -0
- package/dist/migrate/export/auth0.js +575 -0
- package/dist/migrate/export/auth0.js.map +1 -0
- package/dist/migrate/export/authentik-config.d.ts +45 -0
- package/dist/migrate/export/authentik-config.js +357 -0
- package/dist/migrate/export/authentik-config.js.map +1 -0
- package/dist/migrate/export/authentik.d.ts +110 -0
- package/dist/migrate/export/authentik.js +838 -0
- package/dist/migrate/export/authentik.js.map +1 -0
- package/dist/migrate/export/azure-config.d.ts +58 -0
- package/dist/migrate/export/azure-config.js +344 -0
- package/dist/migrate/export/azure-config.js.map +1 -0
- package/dist/migrate/export/azure.d.ts +85 -0
- package/dist/migrate/export/azure.js +373 -0
- package/dist/migrate/export/azure.js.map +1 -0
- package/dist/migrate/export/clerk-config.d.ts +26 -0
- package/dist/migrate/export/clerk-config.js +227 -0
- package/dist/migrate/export/clerk-config.js.map +1 -0
- package/dist/migrate/export/clerk.d.ts +46 -0
- package/dist/migrate/export/clerk.js +486 -0
- package/dist/migrate/export/clerk.js.map +1 -0
- package/dist/migrate/export/cognito-config.d.ts +25 -0
- package/dist/migrate/export/cognito-config.js +273 -0
- package/dist/migrate/export/cognito-config.js.map +1 -0
- package/dist/migrate/export/cognito.d.ts +103 -0
- package/dist/migrate/export/cognito.js +441 -0
- package/dist/migrate/export/cognito.js.map +1 -0
- package/dist/migrate/export/corbado-config.d.ts +20 -0
- package/dist/migrate/export/corbado-config.js +63 -0
- package/dist/migrate/export/corbado-config.js.map +1 -0
- package/dist/migrate/export/corbado.d.ts +37 -0
- package/dist/migrate/export/corbado.js +132 -0
- package/dist/migrate/export/corbado.js.map +1 -0
- package/dist/migrate/export/descope.d.ts +35 -0
- package/dist/migrate/export/descope.js +208 -0
- package/dist/migrate/export/descope.js.map +1 -0
- package/dist/migrate/export/fake-vendor.test-helper.d.ts +27 -0
- package/dist/migrate/export/fake-vendor.test-helper.js +41 -0
- package/dist/migrate/export/fake-vendor.test-helper.js.map +1 -0
- package/dist/migrate/export/firebase-config.d.ts +24 -0
- package/dist/migrate/export/firebase-config.js +215 -0
- package/dist/migrate/export/firebase-config.js.map +1 -0
- package/dist/migrate/export/firebase.d.ts +102 -0
- package/dist/migrate/export/firebase.js +464 -0
- package/dist/migrate/export/firebase.js.map +1 -0
- package/dist/migrate/export/frameworks/app-users.d.ts +42 -0
- package/dist/migrate/export/frameworks/app-users.js +161 -0
- package/dist/migrate/export/frameworks/app-users.js.map +1 -0
- package/dist/migrate/export/frameworks/authjs.d.ts +41 -0
- package/dist/migrate/export/frameworks/authjs.js +146 -0
- package/dist/migrate/export/frameworks/authjs.js.map +1 -0
- package/dist/migrate/export/frameworks/devise.d.ts +39 -0
- package/dist/migrate/export/frameworks/devise.js +218 -0
- package/dist/migrate/export/frameworks/devise.js.map +1 -0
- package/dist/migrate/export/frameworks/django.d.ts +45 -0
- package/dist/migrate/export/frameworks/django.js +315 -0
- package/dist/migrate/export/frameworks/django.js.map +1 -0
- package/dist/migrate/export/frameworks/lucia.d.ts +42 -0
- package/dist/migrate/export/frameworks/lucia.js +263 -0
- package/dist/migrate/export/frameworks/lucia.js.map +1 -0
- package/dist/migrate/export/frameworks/tables.d.ts +90 -0
- package/dist/migrate/export/frameworks/tables.js +325 -0
- package/dist/migrate/export/frameworks/tables.js.map +1 -0
- package/dist/migrate/export/frameworks/webauthn.d.ts +25 -0
- package/dist/migrate/export/frameworks/webauthn.js +130 -0
- package/dist/migrate/export/frameworks/webauthn.js.map +1 -0
- package/dist/migrate/export/frontegg-config.d.ts +29 -0
- package/dist/migrate/export/frontegg-config.js +238 -0
- package/dist/migrate/export/frontegg-config.js.map +1 -0
- package/dist/migrate/export/frontegg.d.ts +67 -0
- package/dist/migrate/export/frontegg.js +410 -0
- package/dist/migrate/export/frontegg.js.map +1 -0
- package/dist/migrate/export/fusionauth-config.d.ts +26 -0
- package/dist/migrate/export/fusionauth-config.js +226 -0
- package/dist/migrate/export/fusionauth-config.js.map +1 -0
- package/dist/migrate/export/fusionauth.d.ts +99 -0
- package/dist/migrate/export/fusionauth.js +642 -0
- package/dist/migrate/export/fusionauth.js.map +1 -0
- package/dist/migrate/export/hanko-config.d.ts +27 -0
- package/dist/migrate/export/hanko-config.js +265 -0
- package/dist/migrate/export/hanko-config.js.map +1 -0
- package/dist/migrate/export/hanko.d.ts +52 -0
- package/dist/migrate/export/hanko.js +605 -0
- package/dist/migrate/export/hanko.js.map +1 -0
- package/dist/migrate/export/keycloak-config.d.ts +19 -0
- package/dist/migrate/export/keycloak-config.js +219 -0
- package/dist/migrate/export/keycloak-config.js.map +1 -0
- package/dist/migrate/export/keycloak.d.ts +155 -0
- package/dist/migrate/export/keycloak.js +878 -0
- package/dist/migrate/export/keycloak.js.map +1 -0
- package/dist/migrate/export/kinde-descope-config.d.ts +38 -0
- package/dist/migrate/export/kinde-descope-config.js +172 -0
- package/dist/migrate/export/kinde-descope-config.js.map +1 -0
- package/dist/migrate/export/kinde.d.ts +57 -0
- package/dist/migrate/export/kinde.js +333 -0
- package/dist/migrate/export/kinde.js.map +1 -0
- package/dist/migrate/export/ldap.d.ts +53 -0
- package/dist/migrate/export/ldap.js +277 -0
- package/dist/migrate/export/ldap.js.map +1 -0
- package/dist/migrate/export/logto-config.d.ts +27 -0
- package/dist/migrate/export/logto-config.js +264 -0
- package/dist/migrate/export/logto-config.js.map +1 -0
- package/dist/migrate/export/logto.d.ts +150 -0
- package/dist/migrate/export/logto.js +809 -0
- package/dist/migrate/export/logto.js.map +1 -0
- package/dist/migrate/export/magic-config.d.ts +14 -0
- package/dist/migrate/export/magic-config.js +28 -0
- package/dist/migrate/export/magic-config.js.map +1 -0
- package/dist/migrate/export/magic.d.ts +31 -0
- package/dist/migrate/export/magic.js +93 -0
- package/dist/migrate/export/magic.js.map +1 -0
- package/dist/migrate/export/mini-yaml.d.ts +19 -0
- package/dist/migrate/export/mini-yaml.js +581 -0
- package/dist/migrate/export/mini-yaml.js.map +1 -0
- package/dist/migrate/export/nhost-config.d.ts +26 -0
- package/dist/migrate/export/nhost-config.js +360 -0
- package/dist/migrate/export/nhost-config.js.map +1 -0
- package/dist/migrate/export/nhost.d.ts +64 -0
- package/dist/migrate/export/nhost.js +304 -0
- package/dist/migrate/export/nhost.js.map +1 -0
- package/dist/migrate/export/okta-config.d.ts +30 -0
- package/dist/migrate/export/okta-config.js +205 -0
- package/dist/migrate/export/okta-config.js.map +1 -0
- package/dist/migrate/export/okta.d.ts +43 -0
- package/dist/migrate/export/okta.js +337 -0
- package/dist/migrate/export/okta.js.map +1 -0
- package/dist/migrate/export/onelogin.d.ts +22 -0
- package/dist/migrate/export/onelogin.js +123 -0
- package/dist/migrate/export/onelogin.js.map +1 -0
- package/dist/migrate/export/ory-config.d.ts +58 -0
- package/dist/migrate/export/ory-config.js +451 -0
- package/dist/migrate/export/ory-config.js.map +1 -0
- package/dist/migrate/export/ory.d.ts +81 -0
- package/dist/migrate/export/ory.js +555 -0
- package/dist/migrate/export/ory.js.map +1 -0
- package/dist/migrate/export/password-detect.d.ts +29 -0
- package/dist/migrate/export/password-detect.js +81 -0
- package/dist/migrate/export/password-detect.js.map +1 -0
- package/dist/migrate/export/pingone.d.ts +36 -0
- package/dist/migrate/export/pingone.js +218 -0
- package/dist/migrate/export/pingone.js.map +1 -0
- package/dist/migrate/export/plugin-kit.d.ts +25 -0
- package/dist/migrate/export/plugin-kit.js +82 -0
- package/dist/migrate/export/plugin-kit.js.map +1 -0
- package/dist/migrate/export/pocketbase-config.d.ts +28 -0
- package/dist/migrate/export/pocketbase-config.js +180 -0
- package/dist/migrate/export/pocketbase-config.js.map +1 -0
- package/dist/migrate/export/pocketbase.d.ts +52 -0
- package/dist/migrate/export/pocketbase.js +243 -0
- package/dist/migrate/export/pocketbase.js.map +1 -0
- package/dist/migrate/export/project-files.d.ts +15 -0
- package/dist/migrate/export/project-files.js +43 -0
- package/dist/migrate/export/project-files.js.map +1 -0
- package/dist/migrate/export/propelauth-config.d.ts +18 -0
- package/dist/migrate/export/propelauth-config.js +58 -0
- package/dist/migrate/export/propelauth-config.js.map +1 -0
- package/dist/migrate/export/propelauth-harness.test-helper.d.ts +62 -0
- package/dist/migrate/export/propelauth-harness.test-helper.js +109 -0
- package/dist/migrate/export/propelauth-harness.test-helper.js.map +1 -0
- package/dist/migrate/export/propelauth.d.ts +48 -0
- package/dist/migrate/export/propelauth.js +255 -0
- package/dist/migrate/export/propelauth.js.map +1 -0
- package/dist/migrate/export/record-set.d.ts +44 -0
- package/dist/migrate/export/record-set.js +98 -0
- package/dist/migrate/export/record-set.js.map +1 -0
- package/dist/migrate/export/source-lines.d.ts +17 -0
- package/dist/migrate/export/source-lines.js +43 -0
- package/dist/migrate/export/source-lines.js.map +1 -0
- package/dist/migrate/export/sso-metadata.d.ts +47 -0
- package/dist/migrate/export/sso-metadata.js +180 -0
- package/dist/migrate/export/sso-metadata.js.map +1 -0
- package/dist/migrate/export/stytch-config.d.ts +22 -0
- package/dist/migrate/export/stytch-config.js +102 -0
- package/dist/migrate/export/stytch-config.js.map +1 -0
- package/dist/migrate/export/stytch.d.ts +68 -0
- package/dist/migrate/export/stytch.js +510 -0
- package/dist/migrate/export/stytch.js.map +1 -0
- package/dist/migrate/export/supabase-config.d.ts +48 -0
- package/dist/migrate/export/supabase-config.js +408 -0
- package/dist/migrate/export/supabase-config.js.map +1 -0
- package/dist/migrate/export/supabase.d.ts +86 -0
- package/dist/migrate/export/supabase.js +524 -0
- package/dist/migrate/export/supabase.js.map +1 -0
- package/dist/migrate/export/supertokens-config.d.ts +20 -0
- package/dist/migrate/export/supertokens-config.js +150 -0
- package/dist/migrate/export/supertokens-config.js.map +1 -0
- package/dist/migrate/export/supertokens.d.ts +125 -0
- package/dist/migrate/export/supertokens.js +464 -0
- package/dist/migrate/export/supertokens.js.map +1 -0
- package/dist/migrate/export/table.d.ts +35 -0
- package/dist/migrate/export/table.js +248 -0
- package/dist/migrate/export/table.js.map +1 -0
- package/dist/migrate/export/translate-kit.d.ts +46 -0
- package/dist/migrate/export/translate-kit.js +148 -0
- package/dist/migrate/export/translate-kit.js.map +1 -0
- package/dist/migrate/export/userfront-config.d.ts +15 -0
- package/dist/migrate/export/userfront-config.js +49 -0
- package/dist/migrate/export/userfront-config.js.map +1 -0
- package/dist/migrate/export/userfront.d.ts +38 -0
- package/dist/migrate/export/userfront.js +189 -0
- package/dist/migrate/export/userfront.js.map +1 -0
- package/dist/migrate/export/vendor-http.d.ts +46 -0
- package/dist/migrate/export/vendor-http.js +138 -0
- package/dist/migrate/export/vendor-http.js.map +1 -0
- package/dist/migrate/export/workos-config.d.ts +23 -0
- package/dist/migrate/export/workos-config.js +125 -0
- package/dist/migrate/export/workos-config.js.map +1 -0
- package/dist/migrate/export/workos.d.ts +46 -0
- package/dist/migrate/export/workos.js +272 -0
- package/dist/migrate/export/workos.js.map +1 -0
- package/dist/migrate/export/zip.d.ts +8 -0
- package/dist/migrate/export/zip.js +66 -0
- package/dist/migrate/export/zip.js.map +1 -0
- package/dist/migrate/export/zitadel-config.d.ts +37 -0
- package/dist/migrate/export/zitadel-config.js +279 -0
- package/dist/migrate/export/zitadel-config.js.map +1 -0
- package/dist/migrate/export/zitadel.d.ts +82 -0
- package/dist/migrate/export/zitadel.js +447 -0
- package/dist/migrate/export/zitadel.js.map +1 -0
- package/dist/migrate/firebase/plugin.d.ts +2 -0
- package/dist/migrate/firebase/plugin.js +152 -0
- package/dist/migrate/firebase/plugin.js.map +1 -0
- package/dist/migrate/framework/builtins.d.ts +2 -0
- package/dist/migrate/framework/builtins.js +52 -0
- package/dist/migrate/framework/builtins.js.map +1 -0
- package/dist/migrate/framework/commands.d.ts +20 -0
- package/dist/migrate/framework/commands.js +58 -0
- package/dist/migrate/framework/commands.js.map +1 -0
- package/dist/migrate/framework/custom.d.ts +12 -0
- package/dist/migrate/framework/custom.js +96 -0
- package/dist/migrate/framework/custom.js.map +1 -0
- package/dist/migrate/framework/orchestrator.d.ts +113 -0
- package/dist/migrate/framework/orchestrator.js +532 -0
- package/dist/migrate/framework/orchestrator.js.map +1 -0
- package/dist/migrate/framework/plugin.d.ts +167 -0
- package/dist/migrate/framework/plugin.js +3 -0
- package/dist/migrate/framework/plugin.js.map +1 -0
- package/dist/migrate/framework/registry.d.ts +11 -0
- package/dist/migrate/framework/registry.js +44 -0
- package/dist/migrate/framework/registry.js.map +1 -0
- package/dist/migrate/framework/report.d.ts +17 -0
- package/dist/migrate/framework/report.js +103 -0
- package/dist/migrate/framework/report.js.map +1 -0
- package/dist/migrate/frameworks/plugins.d.ts +29 -0
- package/dist/migrate/frameworks/plugins.js +402 -0
- package/dist/migrate/frameworks/plugins.js.map +1 -0
- package/dist/migrate/frameworks/report.d.ts +92 -0
- package/dist/migrate/frameworks/report.js +207 -0
- package/dist/migrate/frameworks/report.js.map +1 -0
- package/dist/migrate/frameworks/translate-js.d.ts +5 -0
- package/dist/migrate/frameworks/translate-js.js +503 -0
- package/dist/migrate/frameworks/translate-js.js.map +1 -0
- package/dist/migrate/frameworks/translate-rails-django.d.ts +3 -0
- package/dist/migrate/frameworks/translate-rails-django.js +1391 -0
- package/dist/migrate/frameworks/translate-rails-django.js.map +1 -0
- package/dist/migrate/frontegg/plugin.d.ts +2 -0
- package/dist/migrate/frontegg/plugin.js +78 -0
- package/dist/migrate/frontegg/plugin.js.map +1 -0
- package/dist/migrate/fusionauth/plugin.d.ts +2 -0
- package/dist/migrate/fusionauth/plugin.js +109 -0
- package/dist/migrate/fusionauth/plugin.js.map +1 -0
- package/dist/migrate/hanko/plugin.d.ts +2 -0
- package/dist/migrate/hanko/plugin.js +100 -0
- package/dist/migrate/hanko/plugin.js.map +1 -0
- package/dist/migrate/keycloak/plugin.d.ts +2 -0
- package/dist/migrate/keycloak/plugin.js +148 -0
- package/dist/migrate/keycloak/plugin.js.map +1 -0
- package/dist/migrate/kinde/plugin.d.ts +2 -0
- package/dist/migrate/kinde/plugin.js +105 -0
- package/dist/migrate/kinde/plugin.js.map +1 -0
- package/dist/migrate/ldap/plugin.d.ts +2 -0
- package/dist/migrate/ldap/plugin.js +73 -0
- package/dist/migrate/ldap/plugin.js.map +1 -0
- package/dist/migrate/logto/plugin.d.ts +2 -0
- package/dist/migrate/logto/plugin.js +131 -0
- package/dist/migrate/logto/plugin.js.map +1 -0
- package/dist/migrate/magic/plugin.d.ts +2 -0
- package/dist/migrate/magic/plugin.js +56 -0
- package/dist/migrate/magic/plugin.js.map +1 -0
- package/dist/migrate/nhost/plugin.d.ts +2 -0
- package/dist/migrate/nhost/plugin.js +115 -0
- package/dist/migrate/nhost/plugin.js.map +1 -0
- package/dist/migrate/oidc/plugin.d.ts +2 -0
- package/dist/migrate/oidc/plugin.js +73 -0
- package/dist/migrate/oidc/plugin.js.map +1 -0
- package/dist/migrate/okta/plugin.d.ts +2 -0
- package/dist/migrate/okta/plugin.js +105 -0
- package/dist/migrate/okta/plugin.js.map +1 -0
- package/dist/migrate/onelogin/plugin.d.ts +2 -0
- package/dist/migrate/onelogin/plugin.js +61 -0
- package/dist/migrate/onelogin/plugin.js.map +1 -0
- package/dist/migrate/ory/plugin.d.ts +2 -0
- package/dist/migrate/ory/plugin.js +186 -0
- package/dist/migrate/ory/plugin.js.map +1 -0
- package/dist/migrate/ping-identity/plugin.d.ts +2 -0
- package/dist/migrate/ping-identity/plugin.js +100 -0
- package/dist/migrate/ping-identity/plugin.js.map +1 -0
- package/dist/migrate/pocketbase/plugin.d.ts +2 -0
- package/dist/migrate/pocketbase/plugin.js +99 -0
- package/dist/migrate/pocketbase/plugin.js.map +1 -0
- package/dist/migrate/propelauth/plugin.d.ts +2 -0
- package/dist/migrate/propelauth/plugin.js +65 -0
- package/dist/migrate/propelauth/plugin.js.map +1 -0
- package/dist/migrate/saml/plugin.d.ts +2 -0
- package/dist/migrate/saml/plugin.js +79 -0
- package/dist/migrate/saml/plugin.js.map +1 -0
- package/dist/migrate/stytch/plugin.d.ts +2 -0
- package/dist/migrate/stytch/plugin.js +97 -0
- package/dist/migrate/stytch/plugin.js.map +1 -0
- package/dist/migrate/supabase/plugin.d.ts +2 -0
- package/dist/migrate/supabase/plugin.js +149 -0
- package/dist/migrate/supabase/plugin.js.map +1 -0
- package/dist/migrate/supertokens/plugin.d.ts +2 -0
- package/dist/migrate/supertokens/plugin.js +114 -0
- package/dist/migrate/supertokens/plugin.js.map +1 -0
- package/dist/migrate/user-table/plugin.d.ts +2 -0
- package/dist/migrate/user-table/plugin.js +62 -0
- package/dist/migrate/user-table/plugin.js.map +1 -0
- package/dist/migrate/userfront/plugin.d.ts +2 -0
- package/dist/migrate/userfront/plugin.js +60 -0
- package/dist/migrate/userfront/plugin.js.map +1 -0
- package/dist/migrate/workos/plugin.d.ts +2 -0
- package/dist/migrate/workos/plugin.js +81 -0
- package/dist/migrate/workos/plugin.js.map +1 -0
- package/dist/migrate/zitadel/plugin.d.ts +2 -0
- package/dist/migrate/zitadel/plugin.js +114 -0
- package/dist/migrate/zitadel/plugin.js.map +1 -0
- package/dist/output.d.ts +41 -0
- package/dist/output.js +72 -0
- package/dist/output.js.map +1 -0
- package/dist/project.d.ts +64 -0
- package/dist/project.js +86 -0
- package/dist/project.js.map +1 -0
- package/dist/prompt.d.ts +15 -0
- package/dist/prompt.js +46 -0
- package/dist/prompt.js.map +1 -0
- package/dist/run.d.ts +12 -0
- package/dist/run.js +157 -0
- package/dist/run.js.map +1 -0
- package/dist/secrets.d.ts +41 -0
- package/dist/secrets.js +142 -0
- package/dist/secrets.js.map +1 -0
- package/dist/serialize.d.ts +30 -0
- package/dist/serialize.js +131 -0
- package/dist/serialize.js.map +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.js +21 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.translateDevise = translateDevise;
|
|
4
|
+
exports.translateDjango = translateDjango;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const report_1 = require("./report");
|
|
8
|
+
/**
|
|
9
|
+
* Config translators for the server-rendered frameworks — Devise (Rails) and
|
|
10
|
+
* Django (contrib.auth, django-allauth, python-social-auth) — reading the
|
|
11
|
+
* app's Ruby / Python source line by line. Nothing runs and no secrets file is
|
|
12
|
+
* read (`.env`, `credentials.yml.enc`): `ENV[...]`, `os.environ[...]` and
|
|
13
|
+
* django-environ lookups are carried by NAME as `${NAME}` placeholders.
|
|
14
|
+
*/
|
|
15
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'venv', '.venv', 'env', '.env', 'site-packages', '__pycache__', '.tox', 'vendor', 'tmp', 'log', 'public', 'dist', 'build', '.bundle', 'storage', 'coverage']);
|
|
16
|
+
function walk(root, accept, depth = 6, dir = root, out = []) {
|
|
17
|
+
let names;
|
|
18
|
+
try {
|
|
19
|
+
names = (0, node_fs_1.readdirSync)(dir).sort();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
for (const name of names) {
|
|
25
|
+
const path = (0, node_path_1.join)(dir, name);
|
|
26
|
+
let st;
|
|
27
|
+
try {
|
|
28
|
+
st = (0, node_fs_1.statSync)(path);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (st.isDirectory()) {
|
|
34
|
+
if (depth > 0 && !SKIP_DIRS.has(name) && !name.startsWith('.'))
|
|
35
|
+
walk(root, accept, depth - 1, path, out);
|
|
36
|
+
}
|
|
37
|
+
else if (accept(rel(root, path)))
|
|
38
|
+
out.push(path);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
const rel = (root, path) => (0, node_path_1.relative)(root, path).split(node_path_1.sep).join('/');
|
|
43
|
+
/** A setting's source text for a report note: shortened, and withheld when the name says secret. */
|
|
44
|
+
function snippet(name, text) {
|
|
45
|
+
if (/SECRET|KEY|PASSWORD|TOKEN|PEPPER|CREDENTIAL/i.test(name))
|
|
46
|
+
return 'value not shown';
|
|
47
|
+
return text.length > 40 ? `${text.slice(0, 37)}…` : text;
|
|
48
|
+
}
|
|
49
|
+
function upperSnake(id) {
|
|
50
|
+
return id.replace(/[^A-Za-z0-9]+/g, '_').replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
|
|
51
|
+
}
|
|
52
|
+
/** Scan a line, calling `on` for each character outside a '…' / "…" string literal. */
|
|
53
|
+
function scan(line, on) {
|
|
54
|
+
let quote = null;
|
|
55
|
+
for (let i = 0; i < line.length; i++) {
|
|
56
|
+
const c = line[i];
|
|
57
|
+
if (quote) {
|
|
58
|
+
if (c === '\\')
|
|
59
|
+
i++;
|
|
60
|
+
else if (c === quote)
|
|
61
|
+
quote = null;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (c === '"' || c === "'")
|
|
65
|
+
quote = c;
|
|
66
|
+
else if (on(c, i) === true)
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The line without its `#` comment (Ruby and Python). */
|
|
71
|
+
function stripComment(line) {
|
|
72
|
+
let end = line.length;
|
|
73
|
+
scan(line, (c, i) => {
|
|
74
|
+
if (c === '#') {
|
|
75
|
+
end = i;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return line.slice(0, end);
|
|
80
|
+
}
|
|
81
|
+
/** Split on top-level commas (outside brackets and strings). */
|
|
82
|
+
function splitTop(s) {
|
|
83
|
+
const out = [];
|
|
84
|
+
let depth = 0;
|
|
85
|
+
let start = 0;
|
|
86
|
+
scan(s, (c, i) => {
|
|
87
|
+
if ('([{'.includes(c))
|
|
88
|
+
depth++;
|
|
89
|
+
else if (')]}'.includes(c))
|
|
90
|
+
depth--;
|
|
91
|
+
else if (c === ',' && depth === 0) {
|
|
92
|
+
out.push(s.slice(start, i).trim());
|
|
93
|
+
start = i + 1;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const last = s.slice(start).trim();
|
|
97
|
+
if (last)
|
|
98
|
+
out.push(last);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** Logical Ruby statements: comments dropped, continuation lines (trailing `,` / `\` / open brackets) joined. */
|
|
102
|
+
function rubyStatements(text) {
|
|
103
|
+
const out = [];
|
|
104
|
+
const lines = text.split(/\r?\n/);
|
|
105
|
+
let pending = null;
|
|
106
|
+
let depth = 0;
|
|
107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
const code = stripComment(lines[i]).trim();
|
|
109
|
+
if (!code)
|
|
110
|
+
continue;
|
|
111
|
+
if (pending)
|
|
112
|
+
pending.code += ` ${code}`;
|
|
113
|
+
else
|
|
114
|
+
pending = { code, line: i + 1 };
|
|
115
|
+
scan(code, (c) => {
|
|
116
|
+
if ('([{'.includes(c))
|
|
117
|
+
depth++;
|
|
118
|
+
else if (')]}'.includes(c))
|
|
119
|
+
depth--;
|
|
120
|
+
});
|
|
121
|
+
if (depth > 0 || /[,\\]$/.test(code)) {
|
|
122
|
+
if (pending.code.endsWith('\\'))
|
|
123
|
+
pending.code = pending.code.slice(0, -1);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
out.push(pending);
|
|
127
|
+
pending = null;
|
|
128
|
+
depth = 0;
|
|
129
|
+
}
|
|
130
|
+
if (pending)
|
|
131
|
+
out.push(pending);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** The argument text of a `name arg, …` / `name(arg, …)` call at the start of `code`, or undefined. */
|
|
135
|
+
function callArgs(code, name) {
|
|
136
|
+
if (!code.startsWith(name))
|
|
137
|
+
return undefined;
|
|
138
|
+
const rest = code.slice(name.length);
|
|
139
|
+
if (/^\s*\(/.test(rest) && rest.trimEnd().endsWith(')')) {
|
|
140
|
+
const inner = rest.trim().slice(1, -1);
|
|
141
|
+
let depth = 0;
|
|
142
|
+
let balanced = true;
|
|
143
|
+
scan(inner, (c) => {
|
|
144
|
+
if ('([{'.includes(c))
|
|
145
|
+
depth++;
|
|
146
|
+
else if (')]}'.includes(c) && --depth < 0)
|
|
147
|
+
balanced = false;
|
|
148
|
+
});
|
|
149
|
+
if (balanced)
|
|
150
|
+
return inner.trim();
|
|
151
|
+
}
|
|
152
|
+
return /^\s/.test(rest) ? rest.trim() : undefined;
|
|
153
|
+
}
|
|
154
|
+
function rubyValue(raw, at) {
|
|
155
|
+
const e = raw.trim();
|
|
156
|
+
let m = /^(['"])(.*)\1$/.exec(e);
|
|
157
|
+
if (m && !(m[1] === '"' && m[2].includes('#{')))
|
|
158
|
+
return { kind: 'literal', value: m[2], at };
|
|
159
|
+
if (/^-?\d+(\.\d+)?$/.test(e))
|
|
160
|
+
return { kind: 'literal', value: Number(e), at };
|
|
161
|
+
if (e === 'true' || e === 'false')
|
|
162
|
+
return { kind: 'literal', value: e === 'true', at };
|
|
163
|
+
if ((m = /^:(\w+)$/.exec(e)))
|
|
164
|
+
return { kind: 'literal', value: m[1], at };
|
|
165
|
+
m = /^ENV(?:\[\s*['"]([A-Za-z_]\w*)['"]\s*\]|\.fetch\(\s*['"]([A-Za-z_]\w*)['"][^)]*\))$/.exec(e);
|
|
166
|
+
if (m)
|
|
167
|
+
return { kind: 'env', name: (m[1] ?? m[2]), at };
|
|
168
|
+
return { kind: 'opaque', text: e.length > 60 ? `${e.slice(0, 57)}…` : e, at };
|
|
169
|
+
}
|
|
170
|
+
const RUBY_UNITS = { second: 1, minute: 60, hour: 3600, day: report_1.DAY, week: 7 * report_1.DAY, fortnight: 14 * report_1.DAY, month: 30 * report_1.DAY, year: 365 * report_1.DAY };
|
|
171
|
+
/** `30.minutes`, `2.hours`, `1.day`, `4.weeks`, or plain seconds. */
|
|
172
|
+
function rubyDuration(e) {
|
|
173
|
+
const s = e.trim();
|
|
174
|
+
if (/^\d+$/.test(s))
|
|
175
|
+
return Number(s);
|
|
176
|
+
const m = /^(\d+(?:\.\d+)?)\.(second|minute|hour|day|week|fortnight|month|year)s?$/.exec(s);
|
|
177
|
+
return m ? Math.round(Number(m[1]) * RUBY_UNITS[m[2]]) : undefined;
|
|
178
|
+
}
|
|
179
|
+
/** Symbols in `[:a, :b]`, `%i[a b]` or `:a`. */
|
|
180
|
+
function rubySymbols(e) {
|
|
181
|
+
const s = e.trim();
|
|
182
|
+
const pct = /^%[iw][[(](.*)[\])]$/.exec(s);
|
|
183
|
+
if (pct)
|
|
184
|
+
return pct[1].split(/\s+/).filter(Boolean);
|
|
185
|
+
return [...s.matchAll(/:(\w+)/g)].map((m) => m[1]);
|
|
186
|
+
}
|
|
187
|
+
/** Keyword arguments (`key: value` or `:key => value`) and positional ones. */
|
|
188
|
+
function rubyArgs(args) {
|
|
189
|
+
const positional = [];
|
|
190
|
+
const keywords = new Map();
|
|
191
|
+
for (const a of args) {
|
|
192
|
+
const m = /^(\w+):\s+(.*)$/s.exec(a) ?? /^:(\w+)\s*=>\s*(.*)$/s.exec(a);
|
|
193
|
+
if (m)
|
|
194
|
+
keywords.set(m[1], m[2].trim());
|
|
195
|
+
else
|
|
196
|
+
positional.push(a);
|
|
197
|
+
}
|
|
198
|
+
return { positional, keywords };
|
|
199
|
+
}
|
|
200
|
+
function underscore(name) {
|
|
201
|
+
return name
|
|
202
|
+
.replace(/::/g, '/')
|
|
203
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
204
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
205
|
+
.toLowerCase();
|
|
206
|
+
}
|
|
207
|
+
function deviseModels(project) {
|
|
208
|
+
const models = [];
|
|
209
|
+
for (const path of walk((0, node_path_1.join)(project, 'app/models'), (r) => r.endsWith('.rb'))) {
|
|
210
|
+
const file = rel(project, path);
|
|
211
|
+
let model;
|
|
212
|
+
let className;
|
|
213
|
+
for (const st of rubyStatements((0, node_fs_1.readFileSync)(path, 'utf8'))) {
|
|
214
|
+
const cls = /^class\s+([\w:]+)/.exec(st.code);
|
|
215
|
+
if (cls)
|
|
216
|
+
className = cls[1];
|
|
217
|
+
const call = callArgs(st.code, 'devise');
|
|
218
|
+
if (call === undefined || !className)
|
|
219
|
+
continue;
|
|
220
|
+
model ??= { name: className, file, line: st.line, modules: new Map(), options: new Map() };
|
|
221
|
+
const { positional, keywords } = rubyArgs(splitTop(call));
|
|
222
|
+
for (const p of positional)
|
|
223
|
+
for (const sym of rubySymbols(p))
|
|
224
|
+
model.modules.set(sym, { file, line: st.line });
|
|
225
|
+
for (const [k, v] of keywords)
|
|
226
|
+
model.options.set(k, { expr: v, at: { file, line: st.line } });
|
|
227
|
+
}
|
|
228
|
+
if (model)
|
|
229
|
+
models.push(model);
|
|
230
|
+
}
|
|
231
|
+
// The `User` model is the primary one when there are several.
|
|
232
|
+
return models.sort((a, b) => Number(b.name === 'User') - Number(a.name === 'User'));
|
|
233
|
+
}
|
|
234
|
+
/** Settings with nothing to carry over (Rails / Warden plumbing, form behaviour Atlas's hosted pages own). */
|
|
235
|
+
const DEVISE_PLUMBING = {
|
|
236
|
+
parent_controller: 'Devise controllers are replaced by Atlas hosted pages.',
|
|
237
|
+
skip_session_storage: 'Rails session plumbing; Atlas keeps its own session.',
|
|
238
|
+
sign_out_via: 'Atlas signs out through its own endpoint (POST).',
|
|
239
|
+
'responder.error_status': 'Turbo form responses; Atlas hosted pages render their own.',
|
|
240
|
+
'responder.redirect_status': 'Turbo form responses; Atlas hosted pages render their own.',
|
|
241
|
+
navigational_formats: 'Controller format plumbing.',
|
|
242
|
+
http_authenticatable: 'HTTP Basic sign-in has no Atlas counterpart; use API keys or M2M tokens for machine clients.',
|
|
243
|
+
params_authenticatable: 'Controller plumbing.',
|
|
244
|
+
clean_up_csrf_token_on_authentication: 'Atlas hosted pages handle CSRF themselves.',
|
|
245
|
+
email_regexp: 'Atlas validates email addresses itself.',
|
|
246
|
+
scoped_views: 'Devise views are replaced by Atlas hosted pages (customise them in the Atlas dashboard).',
|
|
247
|
+
sign_out_all_scopes: 'Atlas has one user pool; sign-out ends the Atlas session.',
|
|
248
|
+
send_email_changed_notification: 'Atlas sends its own account emails; choose which in the email templates.',
|
|
249
|
+
send_password_change_notification: 'Atlas sends its own account emails; choose which in the email templates.',
|
|
250
|
+
reload_routes: 'Rails routing plumbing.',
|
|
251
|
+
omniauth_path_prefix: 'OmniAuth routes are replaced by Atlas social sign-in.',
|
|
252
|
+
router_name: 'Rails routing plumbing.',
|
|
253
|
+
};
|
|
254
|
+
function translateDevise(project) {
|
|
255
|
+
const initializer = 'config/initializers/devise.rb';
|
|
256
|
+
const initPath = (0, node_path_1.join)(project, initializer);
|
|
257
|
+
const statements = (0, node_fs_1.existsSync)(initPath) ? rubyStatements((0, node_fs_1.readFileSync)(initPath, 'utf8')) : [];
|
|
258
|
+
const models = deviseModels(project);
|
|
259
|
+
const model = models[0];
|
|
260
|
+
const setupLine = statements.find((s) => /^Devise\.setup\b/.test(s.code))?.line ?? 1;
|
|
261
|
+
const where = statements.length ? { file: initializer, line: setupLine } : model ? { file: model.file, line: model.line } : { file: '(none)', line: 0 };
|
|
262
|
+
const origin = statements.length ? `Devise config at ${initializer}:${setupLine}` : model ? `Devise model at ${model.file}:${model.line}` : 'Devise config (not found)';
|
|
263
|
+
const b = new report_1.TranslationBuilder(origin, where);
|
|
264
|
+
if (!statements.length && !model) {
|
|
265
|
+
b.add('manual', 'setup', 'devise', `No ${initializer} and no model calling devise were found under ${project}. Point the command at the Rails app's root folder.`);
|
|
266
|
+
return b.result();
|
|
267
|
+
}
|
|
268
|
+
const area = 'devise';
|
|
269
|
+
// The initializer: `config.x = …` settings and `config.omniauth :provider, …` calls.
|
|
270
|
+
const settings = new Map();
|
|
271
|
+
const omniauth = [];
|
|
272
|
+
for (const st of statements) {
|
|
273
|
+
const at = { file: initializer, line: st.line };
|
|
274
|
+
const oaArgs = callArgs(st.code, 'config.omniauth');
|
|
275
|
+
const oa = oaArgs === undefined ? undefined : splitTop(oaArgs);
|
|
276
|
+
if (oa?.length && /^:\w+$/.test(oa[0])) {
|
|
277
|
+
omniauth.push({ strategy: oa[0].slice(1), args: oa.slice(1), at });
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (/^config\.warden\b/.test(st.code)) {
|
|
281
|
+
b.add('manual', area, 'warden', 'Warden hooks and strategies run inside your app; move what they do to Atlas Actions (post-sign-in, pre-sign-up) or your own middleware.', at);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const set = /^config\.([\w.]+)\s*=\s*(.+)$/.exec(st.code);
|
|
285
|
+
if (set && !settings.has(set[1]))
|
|
286
|
+
settings.set(set[1], { expr: set[2].trim(), at });
|
|
287
|
+
}
|
|
288
|
+
const used = new Set();
|
|
289
|
+
const take = (name) => {
|
|
290
|
+
used.add(name);
|
|
291
|
+
return settings.get(name);
|
|
292
|
+
};
|
|
293
|
+
const mods = model?.modules ?? new Map();
|
|
294
|
+
const modAt = (name) => mods.get(name);
|
|
295
|
+
const modelAt = model ? { file: model.file, line: model.line } : where;
|
|
296
|
+
const modelName = model?.name ?? 'User';
|
|
297
|
+
// Sign-in methods.
|
|
298
|
+
if (mods.has('database_authenticatable') || mods.has('two_factor_authenticatable')) {
|
|
299
|
+
b.set('strategies.password', true);
|
|
300
|
+
b.add('mapped', 'models', mods.has('database_authenticatable') ? ':database_authenticatable' : ':two_factor_authenticatable', 'Email and password sign-in on. bcrypt hashes (encrypted_password) carry over in the export.', modAt('database_authenticatable') ?? modAt('two_factor_authenticatable'), 'auth_config.strategies.password');
|
|
301
|
+
}
|
|
302
|
+
const keys = take('authentication_keys');
|
|
303
|
+
if (keys) {
|
|
304
|
+
const list = rubySymbols(keys.expr);
|
|
305
|
+
if (list.some((k) => k === 'username' || k === 'login')) {
|
|
306
|
+
b.set('strategies.username', true);
|
|
307
|
+
b.add(list.includes('login') ? 'differs' : 'mapped', area, 'authentication_keys', list.includes('login') ? 'A virtual :login (username or email): Atlas signs in with either in the same field when username sign-in is on.' : 'Username sign-in on.', keys.at, 'auth_config.strategies.username');
|
|
308
|
+
}
|
|
309
|
+
else
|
|
310
|
+
b.add(list.length === 1 && list[0] === 'email' ? 'mapped' : 'manual', area, 'authentication_keys', list.length === 1 && list[0] === 'email' ? 'Sign in with email.' : `Sign-in keys ${list.join(', ')}: Atlas signs in with email, username or phone; other keys need a custom flow.`, keys.at);
|
|
311
|
+
}
|
|
312
|
+
for (const name of ['case_insensitive_keys', 'strip_whitespace_keys']) {
|
|
313
|
+
const s = take(name);
|
|
314
|
+
if (s)
|
|
315
|
+
b.add('mapped', area, name, 'Atlas trims and compares email addresses case-insensitively.', s.at);
|
|
316
|
+
}
|
|
317
|
+
const reg = modAt('registerable');
|
|
318
|
+
if (model && !reg) {
|
|
319
|
+
b.set('signUpMode', 'restricted');
|
|
320
|
+
b.add('mapped', 'models', ':registerable', `${modelName} is not :registerable, so users cannot sign themselves up: Atlas sign-up is restricted (invitations and the allowlist still work).`, modelAt, 'auth_config.signUpMode');
|
|
321
|
+
}
|
|
322
|
+
else if (reg)
|
|
323
|
+
b.add('mapped', 'models', ':registerable', 'Self sign-up stays open.', reg);
|
|
324
|
+
// Passwords.
|
|
325
|
+
const stretches = take('stretches');
|
|
326
|
+
if (stretches)
|
|
327
|
+
b.add('mapped', area, 'stretches', 'The bcrypt cost is stored in each hash; Atlas verifies any cost and rehashes on first sign-in.', stretches.at);
|
|
328
|
+
const pepper = take('pepper');
|
|
329
|
+
if (pepper)
|
|
330
|
+
b.add('mapped', area, 'pepper', 'The export carries the pepper with each bcrypt hash, so passwords verify unchanged. The value is never written to the config files.', pepper.at);
|
|
331
|
+
const length = take('password_length');
|
|
332
|
+
if (length) {
|
|
333
|
+
const m = /^(\d+)\s*\.\.\.?\s*(\d+)$/.exec(length.expr);
|
|
334
|
+
if (m) {
|
|
335
|
+
b.minPasswordLength(Number(m[1]), area, 'password_length', length.at, 'Devise');
|
|
336
|
+
const max = Number(m[2]);
|
|
337
|
+
b.set('passwordPolicy.maxLength', (0, report_1.clamp)(max, 8, 256));
|
|
338
|
+
if (max > 256)
|
|
339
|
+
b.add('differs', area, 'password_length (max)', `Atlas caps passwords at 256 characters (Devise: ${max}).`, length.at, 'auth_config.passwordPolicy.maxLength');
|
|
340
|
+
}
|
|
341
|
+
else
|
|
342
|
+
b.add('manual', area, 'password_length', `Cannot read "${length.expr}" statically; set auth_config.passwordPolicy.minLength yourself.`, length.at);
|
|
343
|
+
}
|
|
344
|
+
if (modAt('validatable'))
|
|
345
|
+
b.add('mapped', 'models', ':validatable', 'Atlas checks the email format and the password length (password_length) too.', modAt('validatable'));
|
|
346
|
+
const pwned = modAt('pwned_password') ?? gemAt(project, 'devise-pwned_password');
|
|
347
|
+
if (pwned) {
|
|
348
|
+
b.set('passwordPolicy.checkBreaches', true);
|
|
349
|
+
b.add('mapped', modAt('pwned_password') ? 'models' : 'Gemfile', modAt('pwned_password') ? ':pwned_password' : 'devise-pwned_password', 'New passwords are checked against breached-password data.', pwned, 'auth_config.passwordPolicy.checkBreaches');
|
|
350
|
+
}
|
|
351
|
+
const encryptor = take('encryptor');
|
|
352
|
+
const encryptable = modAt('encryptable');
|
|
353
|
+
if (encryptable || encryptor) {
|
|
354
|
+
const scope = underscore(modelName.split('::').pop());
|
|
355
|
+
b.lazy = { preset: 'devise_json', config: scope === 'user' ? {} : { scope, path: `/${scope}s/sign_in` } };
|
|
356
|
+
b.add('manual', encryptable ? 'models' : area, encryptable ? ':encryptable' : 'encryptor', `devise-encryptable digests${encryptor ? ` (${encryptor.expr})` : ''} do not carry over. Turn on lazy migration (preset devise_json, your app's base URL): Atlas checks each password against your Rails sign-in once, then stores its own hash. Users who never sign in during the window reset their password.`, encryptable ?? encryptor.at, 'lazy migration devise_json');
|
|
357
|
+
}
|
|
358
|
+
// Recovery.
|
|
359
|
+
if (modAt('recoverable')) {
|
|
360
|
+
b.set('accountUx.passwordReset', 'link');
|
|
361
|
+
b.add('mapped', 'models', ':recoverable', 'Password reset by emailed link. Atlas sends it through your email provider; customise the "password reset" template instead of the Devise mailer view.', modAt('recoverable'), 'auth_config.accountUx.passwordReset');
|
|
362
|
+
}
|
|
363
|
+
const resetWithin = take('reset_password_within');
|
|
364
|
+
if (resetWithin) {
|
|
365
|
+
const s = rubyDuration(resetWithin.expr);
|
|
366
|
+
if (s !== undefined) {
|
|
367
|
+
const minutes = (0, report_1.clamp)(Math.ceil(s / 60), 1, 1440);
|
|
368
|
+
if (b.get('verification.magicLink.ttlMinutes') === undefined)
|
|
369
|
+
b.set('verification.magicLink.ttlMinutes', minutes);
|
|
370
|
+
b.add('differs', area, 'reset_password_within', `Reset links use the email-link lifetime (verification.magicLink.ttlMinutes, up to 1 day), shared with sign-in links. Devise: ${(0, report_1.humanDuration)(s)}. Reset links sent before the switch stop working.`, resetWithin.at, 'auth_config.verification.magicLink.ttlMinutes');
|
|
371
|
+
}
|
|
372
|
+
else
|
|
373
|
+
b.add('manual', area, 'reset_password_within', `Cannot read "${resetWithin.expr}" statically.`, resetWithin.at);
|
|
374
|
+
}
|
|
375
|
+
const signInAfterReset = take('sign_in_after_reset_password');
|
|
376
|
+
if (signInAfterReset) {
|
|
377
|
+
const v = signInAfterReset.expr === 'true';
|
|
378
|
+
b.set('signInAfterReset', v);
|
|
379
|
+
b.add('mapped', area, 'sign_in_after_reset_password', v ? 'A completed reset signs the user in.' : 'A completed reset ends at the sign-in screen.', signInAfterReset.at, 'auth_config.signInAfterReset');
|
|
380
|
+
}
|
|
381
|
+
// Email confirmation.
|
|
382
|
+
const confirmable = modAt('confirmable');
|
|
383
|
+
const unconfirmed = take('allow_unconfirmed_access_for');
|
|
384
|
+
const confirmWithin = take('confirm_within');
|
|
385
|
+
const reconfirmable = take('reconfirmable');
|
|
386
|
+
if (confirmable) {
|
|
387
|
+
b.set('requireEmailVerification', true);
|
|
388
|
+
b.set('accountUx.emailVerification', 'link');
|
|
389
|
+
b.add('mapped', 'models', ':confirmable', 'Email verification required, by emailed link. confirmed_at carries over: confirmed users stay verified.', confirmable, 'auth_config.requireEmailVerification');
|
|
390
|
+
if (unconfirmed) {
|
|
391
|
+
const s = rubyDuration(unconfirmed.expr);
|
|
392
|
+
b.add(s === 0 ? 'mapped' : 'differs', area, 'allow_unconfirmed_access_for', s === 0 ? 'No grace period: unconfirmed users cannot sign in.' : 'Atlas has no grace period: users confirm their email before their first session.', unconfirmed.at, 'auth_config.requireEmailVerification');
|
|
393
|
+
}
|
|
394
|
+
if (confirmWithin) {
|
|
395
|
+
const s = rubyDuration(confirmWithin.expr);
|
|
396
|
+
if (s !== undefined && b.get('verification.magicLink.ttlMinutes') === undefined)
|
|
397
|
+
b.set('verification.magicLink.ttlMinutes', (0, report_1.clamp)(Math.ceil(s / 60), 1, 1440));
|
|
398
|
+
b.add(s !== undefined && s <= report_1.DAY ? 'mapped' : 'differs', area, 'confirm_within', `Verification links last up to 1 day in Atlas (verification.magicLink.ttlMinutes)${s !== undefined ? `; Devise: ${(0, report_1.humanDuration)(s)}` : ''}. Users can ask for a new link.`, confirmWithin.at, 'auth_config.verification.magicLink.ttlMinutes');
|
|
399
|
+
}
|
|
400
|
+
if (reconfirmable)
|
|
401
|
+
b.add('mapped', area, 'reconfirmable', 'A changed email is verified before it replaces the old one.', reconfirmable.at);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
if (model) {
|
|
405
|
+
b.set('requireEmailVerification', false);
|
|
406
|
+
b.add('mapped', 'models', ':confirmable', `${modelName} is not :confirmable: email verification stays optional. The export marks emails unverified unless you pass --set trust-unconfirmable=true.`, modelAt, 'auth_config.requireEmailVerification');
|
|
407
|
+
}
|
|
408
|
+
for (const [name, s] of [['allow_unconfirmed_access_for', unconfirmed], ['confirm_within', confirmWithin], ['reconfirmable', reconfirmable]]) {
|
|
409
|
+
if (s)
|
|
410
|
+
b.add('mapped', area, name, `No effect in Devise: ${modelName} is not :confirmable.`, s.at);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Lockout.
|
|
414
|
+
const lockable = modAt('lockable');
|
|
415
|
+
const lockSettings = ['lock_strategy', 'unlock_strategy', 'maximum_attempts', 'unlock_in', 'unlock_keys', 'last_attempt_warning'].map((n) => [n, take(n)]);
|
|
416
|
+
if (lockable) {
|
|
417
|
+
const max = settings.get('maximum_attempts')?.expr ?? '20';
|
|
418
|
+
const unlockIn = rubyDuration(settings.get('unlock_in')?.expr ?? '1.hour');
|
|
419
|
+
const strategy = settings.get('lock_strategy')?.expr;
|
|
420
|
+
if (strategy === ':none')
|
|
421
|
+
b.add('differs', area, 'lock_strategy', "Devise never locks accounts. Atlas's brute-force protection is on by default; turn it off with attack_protection.brute_force if you really want none.", settings.get('lock_strategy').at, 'auth_config.attackProtection.bruteForce');
|
|
422
|
+
else {
|
|
423
|
+
b.set('attackProtection.bruteForce.enabled', true);
|
|
424
|
+
b.add('differs', 'models', ':lockable', `Atlas's brute-force protection locks sign-in progressively after repeated failures and unlocks on its own; it has no fixed attempt count or unlock email. Devise: lock after ${max} failures${unlockIn !== undefined ? `, unlock after ${(0, report_1.humanDuration)(unlockIn)}` : ''}. locked_at carries over: a locked user stays locked.`, lockable, 'auth_config.attackProtection.bruteForce');
|
|
425
|
+
}
|
|
426
|
+
for (const [name, s] of lockSettings)
|
|
427
|
+
if (s && !(name === 'lock_strategy' && strategy === ':none'))
|
|
428
|
+
b.add('differs', area, name, 'Covered by the :lockable entry: Atlas uses progressive lockout tiers instead.', s.at, 'auth_config.attackProtection.bruteForce');
|
|
429
|
+
}
|
|
430
|
+
else
|
|
431
|
+
for (const [name, s] of lockSettings)
|
|
432
|
+
if (s)
|
|
433
|
+
b.add('mapped', area, name, `No effect in Devise: ${modelName} is not :lockable. Atlas's brute-force protection stays on (its default).`, s.at);
|
|
434
|
+
// Session lifetime: :timeoutable is an idle timeout, :rememberable the remember-me cookie.
|
|
435
|
+
const timeoutable = modAt('timeoutable');
|
|
436
|
+
const rememberable = modAt('rememberable');
|
|
437
|
+
const timeoutIn = take('timeout_in');
|
|
438
|
+
const rememberFor = take('remember_for');
|
|
439
|
+
const extend = take('extend_remember_period');
|
|
440
|
+
const sessionArea = 'sessions';
|
|
441
|
+
let idle;
|
|
442
|
+
if (timeoutable) {
|
|
443
|
+
idle = timeoutIn ? rubyDuration(timeoutIn.expr) : 30 * 60;
|
|
444
|
+
if (idle === undefined)
|
|
445
|
+
b.add('manual', area, 'timeout_in', `Cannot read "${timeoutIn.expr}" statically; set auth_config.session.idleMs yourself.`, timeoutIn.at);
|
|
446
|
+
else
|
|
447
|
+
b.session(idle, true, sessionArea, timeoutIn ? 'timeout_in' : 'timeout_in (default 30 minutes)', timeoutIn?.at ?? timeoutable, 'Devise');
|
|
448
|
+
}
|
|
449
|
+
else if (timeoutIn)
|
|
450
|
+
b.add('mapped', area, 'timeout_in', `No effect in Devise: ${modelName} is not :timeoutable.`, timeoutIn.at);
|
|
451
|
+
if (rememberable) {
|
|
452
|
+
const s = rememberFor ? rubyDuration(rememberFor.expr) : 14 * report_1.DAY;
|
|
453
|
+
const setting = rememberFor ? 'remember_for' : 'remember_for (default 2 weeks)';
|
|
454
|
+
if (s === undefined)
|
|
455
|
+
b.add('manual', area, 'remember_for', `Cannot read "${rememberFor.expr}" statically; set auth_config.session.absoluteMs yourself.`, rememberFor.at);
|
|
456
|
+
else if (idle !== undefined) {
|
|
457
|
+
const abs = (0, report_1.clamp)(s, Math.max(report_1.MIN_IDLE_S, idle), report_1.MAX_ABSOLUTE_S);
|
|
458
|
+
b.set('session.absoluteMs', abs * 1000);
|
|
459
|
+
b.add('differs', sessionArea, setting, `Sessions end after ${(0, report_1.humanDuration)(abs)} at most${abs !== s ? ` (Devise: ${(0, report_1.humanDuration)(s)}; Atlas allows up to 90 days)` : ''}. Devise skips the idle timeout for users who ticked "remember me"; Atlas applies it to every session.`, rememberFor?.at ?? rememberable, 'auth_config.session.absoluteMs');
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
b.session(s, extend?.expr === 'true', sessionArea, setting, rememberFor?.at ?? rememberable, 'Devise');
|
|
463
|
+
b.add('differs', 'models', ':rememberable', 'Devise keeps users signed in past the browser session only when they tick "remember me"; every Atlas session lasts the configured lifetime.', rememberable);
|
|
464
|
+
}
|
|
465
|
+
if (extend)
|
|
466
|
+
b.add('mapped', area, 'extend_remember_period', extend.expr === 'true' ? 'Activity extends the session.' : 'Activity does not extend the session.', extend.at);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
if (rememberFor)
|
|
470
|
+
b.add('mapped', area, 'remember_for', `No effect in Devise: ${modelName} is not :rememberable.`, rememberFor.at);
|
|
471
|
+
if (extend)
|
|
472
|
+
b.add('mapped', area, 'extend_remember_period', `No effect in Devise: ${modelName} is not :rememberable.`, extend.at);
|
|
473
|
+
if (!timeoutable && model)
|
|
474
|
+
b.add('differs', sessionArea, 'session lifetime', 'Devise sessions end when the browser closes (no :rememberable or :timeoutable); Atlas sessions keep its default lifetime. Set auth_config.session to shorten them.', modelAt, 'auth_config.session');
|
|
475
|
+
}
|
|
476
|
+
const expireAll = take('expire_all_remember_me_on_sign_out');
|
|
477
|
+
if (expireAll)
|
|
478
|
+
b.add('differs', area, 'expire_all_remember_me_on_sign_out', "Atlas sign-out ends the current session; sign the user out everywhere with the revoke-all-sessions API or the user's session list.", expireAll.at);
|
|
479
|
+
if (modAt('trackable'))
|
|
480
|
+
b.add('mapped', 'models', ':trackable', 'Atlas records every sign-in itself (time, IP, device). The old sign-in counters stay in your database if you need them.', modAt('trackable'));
|
|
481
|
+
// Two-factor (devise-two-factor), invitations, magic links, passkeys.
|
|
482
|
+
const totp = modAt('two_factor_authenticatable');
|
|
483
|
+
if (totp) {
|
|
484
|
+
b.set('mfa.factors.totp', true);
|
|
485
|
+
b.set('mfaPolicy', 'optional');
|
|
486
|
+
b.add('mapped', 'models', ':two_factor_authenticatable', 'Authenticator-app (TOTP) MFA on, optional per user. otp_secret carries over in the export: enrolled users keep their authenticator.', totp, 'auth_config.mfa.factors.totp');
|
|
487
|
+
}
|
|
488
|
+
const backup = modAt('two_factor_backupable');
|
|
489
|
+
if (backup) {
|
|
490
|
+
const count = Number(model?.options.get('otp_number_of_backup_codes')?.expr ?? '10');
|
|
491
|
+
b.set('mfa.backupCodes.count', Number.isFinite(count) ? (0, report_1.clamp)(count, 1, 20) : 10);
|
|
492
|
+
b.add('mapped', 'models', ':two_factor_backupable', `${Number.isFinite(count) ? count : 10} backup codes per user. Unused codes carry over (their bcrypt digests).`, backup, 'auth_config.mfa.backupCodes.count');
|
|
493
|
+
}
|
|
494
|
+
if (modAt('invitable'))
|
|
495
|
+
b.add('differs', 'models', ':invitable', 'Atlas invitations: an emailed sign-up link. Pending devise_invitable invitations do not carry over; send them again from Atlas.', modAt('invitable'), 'invitations');
|
|
496
|
+
const magic = modAt('magic_link_authenticatable');
|
|
497
|
+
if (magic) {
|
|
498
|
+
b.set('strategies.emailLink', true);
|
|
499
|
+
b.add('differs', 'models', ':magic_link_authenticatable', 'Email sign-in links on. Atlas sends them through your email provider; customise the "magic link" template instead of the devise-passwordless mailer.', magic, 'auth_config.strategies.emailLink');
|
|
500
|
+
}
|
|
501
|
+
const passkeys = modAt('passkey_authenticatable') ?? gemAt(project, 'webauthn');
|
|
502
|
+
if (passkeys) {
|
|
503
|
+
b.set('strategies.passkey', true);
|
|
504
|
+
b.add('differs', modAt('passkey_authenticatable') ? 'models' : 'Gemfile', modAt('passkey_authenticatable') ? ':passkey_authenticatable' : 'webauthn', 'Passkey sign-in on. Passkeys registered in the Rails app are not in the export: users add them again in Atlas (a passkey belongs to its domain).', passkeys, 'auth_config.strategies.passkey');
|
|
505
|
+
}
|
|
506
|
+
const KNOWN = new Set(['database_authenticatable', 'registerable', 'recoverable', 'rememberable', 'validatable', 'confirmable', 'lockable', 'timeoutable', 'trackable', 'omniauthable', 'two_factor_authenticatable', 'two_factor_backupable', 'encryptable', 'invitable', 'magic_link_authenticatable', 'passkey_authenticatable', 'pwned_password']);
|
|
507
|
+
for (const [name, at] of mods)
|
|
508
|
+
if (!KNOWN.has(name))
|
|
509
|
+
b.add('manual', 'models', `:${name}`, `:${name} comes from a Devise extension this step does not know; check what it enforces and rebuild it with Atlas settings or Actions.`, at);
|
|
510
|
+
for (const other of models.slice(1)) {
|
|
511
|
+
b.add('manual', 'models', other.name, `A second Devise model (${other.name}). An Atlas instance has one user pool: export it separately (--set users-table=${underscore(other.name)}s) into its own instance, or import its users into this one and tell them apart with a role or metadata.`, { file: other.file, line: other.line });
|
|
512
|
+
}
|
|
513
|
+
// OmniAuth providers.
|
|
514
|
+
const listed = new Set(rubySymbols(model?.options.get('omniauth_providers')?.expr ?? ''));
|
|
515
|
+
for (const o of omniauth) {
|
|
516
|
+
listed.delete(o.strategy);
|
|
517
|
+
const setting = `omniauth :${o.strategy}`;
|
|
518
|
+
if (o.strategy === 'saml') {
|
|
519
|
+
b.add('manual', 'omniauth', setting, 'SAML: create an Atlas enterprise SSO connection for the IdP (its metadata URL or certificate and SSO URL). Atlas routes users to it by email domain.', o.at, 'sso_connections');
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (o.strategy === 'openid_connect') {
|
|
523
|
+
b.add('manual', 'omniauth', setting, 'Generic OpenID Connect: add it as a custom OIDC provider (custom_oauth_providers, with the issuer) or, for a workforce IdP, an enterprise SSO connection.', o.at, 'custom_oauth_providers');
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const atlasKey = report_1.FRAMEWORK_PROVIDER_MAP[o.strategy];
|
|
527
|
+
if (!atlasKey) {
|
|
528
|
+
b.add('manual', 'omniauth', setting, `"${o.strategy}" has no same-named Atlas provider: add it as a custom OAuth / OIDC provider (custom_oauth_providers) with its endpoints.`, o.at, 'custom_oauth_providers');
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const { positional, keywords } = rubyArgs(o.args);
|
|
532
|
+
const upper = upperSnake(atlasKey);
|
|
533
|
+
const clientId = b.configString(positional[0] ? rubyValue(positional[0], o.at) : undefined, `${atlasKey} client id`, `${upper}_CLIENT_ID`, { area: 'omniauth', setting: `${setting} client id` });
|
|
534
|
+
const clientSecret = b.configString(positional[1] ? rubyValue(positional[1], o.at) : undefined, `${atlasKey} client secret`, `${upper}_CLIENT_SECRET`, { secret: true, area: 'omniauth', setting: `${setting} client secret` });
|
|
535
|
+
const scope = keywords.get('scope');
|
|
536
|
+
const scopeValue = scope ? rubyValue(scope, o.at) : undefined;
|
|
537
|
+
const scopes = scopeValue?.kind === 'literal' && typeof scopeValue.value === 'string' ? scopeValue.value.split(/[\s,]+/).filter(Boolean) : undefined;
|
|
538
|
+
b.oauthProvider({ sourceId: o.strategy, atlasKey, clientId, clientSecret, scopes, area: 'omniauth', setting, at: o.at });
|
|
539
|
+
}
|
|
540
|
+
for (const strategy of listed)
|
|
541
|
+
b.add('manual', 'omniauth', `omniauth_providers :${strategy}`, `${strategy} is listed on the model but not configured in ${initializer} (an OmniAuth::Builder middleware?): add it in Atlas with the same OAuth app.`, model?.options.get('omniauth_providers')?.at);
|
|
542
|
+
// Secrets and mail.
|
|
543
|
+
const secretKey = take('secret_key');
|
|
544
|
+
if (secretKey) {
|
|
545
|
+
const literal = rubyValue(secretKey.expr, secretKey.at).kind === 'literal';
|
|
546
|
+
b.add(literal ? 'manual' : 'mapped', area, 'secret_key', `Devise signs its confirmation, reset and unlock tokens with it; Atlas issues its own, so links sent before the switch stop working.${literal ? ' It is hard-coded in source: move it out of the code (Atlas never needs it).' : ''}`, secretKey.at);
|
|
547
|
+
}
|
|
548
|
+
const sender = take('mailer_sender');
|
|
549
|
+
if (sender) {
|
|
550
|
+
const v = rubyValue(sender.expr, sender.at);
|
|
551
|
+
b.add('manual', area, 'mailer_sender', `Set the sender address${v.kind === 'literal' ? ` (${v.value})` : ''} in Atlas's email settings: Atlas sends the account emails itself.`, sender.at, 'email settings');
|
|
552
|
+
}
|
|
553
|
+
const paranoid = take('paranoid');
|
|
554
|
+
if (paranoid)
|
|
555
|
+
b.add('mapped', area, 'paranoid', 'Atlas answers reset and confirmation requests the same way whether or not the account exists.', paranoid.at);
|
|
556
|
+
// Everything else the initializer sets.
|
|
557
|
+
for (const [name, s] of settings) {
|
|
558
|
+
if (used.has(name))
|
|
559
|
+
continue;
|
|
560
|
+
if (DEVISE_PLUMBING[name])
|
|
561
|
+
b.add('mapped', area, name, `Nothing to carry over: ${DEVISE_PLUMBING[name]}`, s.at);
|
|
562
|
+
else
|
|
563
|
+
b.add('manual', area, name, `Not translated (${snippet(name, s.expr)}): check whether your app relies on it.`, s.at);
|
|
564
|
+
}
|
|
565
|
+
sessionStore(project, b);
|
|
566
|
+
return b.result();
|
|
567
|
+
}
|
|
568
|
+
/** The line of `gem "<name>"` in the Gemfile, if any. */
|
|
569
|
+
function gemAt(project, name) {
|
|
570
|
+
const path = (0, node_path_1.join)(project, 'Gemfile');
|
|
571
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
572
|
+
return undefined;
|
|
573
|
+
const lines = (0, node_fs_1.readFileSync)(path, 'utf8').split(/\r?\n/);
|
|
574
|
+
const i = lines.findIndex((l) => new RegExp(`^\\s*gem\\s+['"]${name.replace(/[-]/g, '\\-')}['"]`).test(stripComment(l)));
|
|
575
|
+
return i < 0 ? undefined : { file: 'Gemfile', line: i + 1 };
|
|
576
|
+
}
|
|
577
|
+
/** The Rails session store → bridge preset (cookie store: devise; activerecord-session_store: devise_active_record). */
|
|
578
|
+
function sessionStore(project, b) {
|
|
579
|
+
let found;
|
|
580
|
+
let appModule;
|
|
581
|
+
for (const file of ['config/initializers/session_store.rb', 'config/application.rb', 'config/environments/production.rb']) {
|
|
582
|
+
const path = (0, node_path_1.join)(project, file);
|
|
583
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
584
|
+
continue;
|
|
585
|
+
for (const st of rubyStatements((0, node_fs_1.readFileSync)(path, 'utf8'))) {
|
|
586
|
+
const mod = /^module\s+(\w+)/.exec(st.code);
|
|
587
|
+
if (mod && file === 'config/application.rb')
|
|
588
|
+
appModule ??= mod[1];
|
|
589
|
+
const idx = st.code.search(/\bsession_store\b/);
|
|
590
|
+
const args = idx < 0 ? undefined : callArgs(st.code.slice(idx), 'session_store');
|
|
591
|
+
const parts = args === undefined ? [] : splitTop(args);
|
|
592
|
+
if (parts.length && /^:\w+$/.test(parts[0]) && !found) {
|
|
593
|
+
const key = rubyArgs(parts.slice(1)).keywords.get('key');
|
|
594
|
+
const kv = key ? rubyValue(key, { file, line: st.line }) : undefined;
|
|
595
|
+
found = { store: parts[0].slice(1), key: kv?.kind === 'literal' ? String(kv.value) : undefined, at: { file, line: st.line } };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const store = found?.store ?? 'cookie_store';
|
|
600
|
+
const cookieName = found?.key ?? (appModule ? `_${underscore(appModule)}_session` : undefined);
|
|
601
|
+
const setting = found ? `session_store :${store}` : 'session_store (default cookie_store)';
|
|
602
|
+
const vars = cookieName ? { cookie_name: cookieName } : {};
|
|
603
|
+
if (store === 'cookie_store') {
|
|
604
|
+
b.bridge = { preset: 'devise', vars };
|
|
605
|
+
b.add('mapped', 'bridge', setting, `Cookie-store sessions are opened on your server: pass deviseExtractor({ cookieName${cookieName ? ` ("${cookieName}")` : ''}, secretKeyBase, bridgeSecret }) to the bridge middleware (preset devise). secret_key_base never leaves your server. JSON (or hybrid) cookie serializer only.`, found?.at, 'bridge preset devise');
|
|
606
|
+
const serializer = cookieSerializer(project);
|
|
607
|
+
if (serializer) {
|
|
608
|
+
const ok = serializer.value === 'json' || serializer.value === 'hybrid';
|
|
609
|
+
b.add(ok ? 'mapped' : 'manual', 'bridge', 'cookies_serializer', ok ? `:${serializer.value}: the bridge extractor can read the session.` : `:${serializer.value}: the bridge extractor reads JSON cookies only. Switch to :hybrid a few weeks before the cutover (it rewrites cookies as JSON), or users sign in once more.`, serializer.at);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else if (store === 'active_record_store') {
|
|
613
|
+
b.bridge = { preset: 'devise_active_record', vars };
|
|
614
|
+
b.add('mapped', 'bridge', setting, 'activerecord-session_store sessions: the export imports the sessions table and the bridge matches the session cookie (preset devise_active_record).', found?.at, 'bridge preset devise_active_record');
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
b.add('manual', 'bridge', setting, `:${store} sessions live in a cache the export cannot read: users sign in once more after the switch, or read the session yourself in a custom extractToken.`, found?.at);
|
|
618
|
+
}
|
|
619
|
+
if (!cookieName && b.bridge)
|
|
620
|
+
b.add('manual', 'bridge', 'cookie_name', 'Could not tell the session cookie name: set cookie_name in the bridge settings (_<app>_session by default).', found?.at);
|
|
621
|
+
}
|
|
622
|
+
function cookieSerializer(project) {
|
|
623
|
+
for (const file of ['config/initializers/cookies_serializer.rb', 'config/application.rb']) {
|
|
624
|
+
const path = (0, node_path_1.join)(project, file);
|
|
625
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
626
|
+
continue;
|
|
627
|
+
for (const st of rubyStatements((0, node_fs_1.readFileSync)(path, 'utf8'))) {
|
|
628
|
+
const m = /cookies_serializer\s*=\s*:(\w+)/.exec(st.code);
|
|
629
|
+
if (m)
|
|
630
|
+
return { value: m[1], at: { file, line: st.line } };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
function tokenize(src) {
|
|
636
|
+
const out = [];
|
|
637
|
+
let i = 0;
|
|
638
|
+
let line = 1;
|
|
639
|
+
let lineStart = 0;
|
|
640
|
+
let depth = 0;
|
|
641
|
+
const push = (k, v, start) => out.push({ k, v, line, col: start - lineStart, start, end: i });
|
|
642
|
+
while (i < src.length) {
|
|
643
|
+
const c = src[i];
|
|
644
|
+
if (c === '\n') {
|
|
645
|
+
if (depth === 0)
|
|
646
|
+
push('nl', '', i);
|
|
647
|
+
i++;
|
|
648
|
+
line++;
|
|
649
|
+
lineStart = i;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (c === ' ' || c === '\t' || c === '\r') {
|
|
653
|
+
i++;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (c === '\\' && src[i + 1] === '\n') {
|
|
657
|
+
i += 2;
|
|
658
|
+
line++;
|
|
659
|
+
lineStart = i;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (c === '#') {
|
|
663
|
+
while (i < src.length && src[i] !== '\n')
|
|
664
|
+
i++;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const str = /^([rRbBuUfF]{0,2})('''|"""|'|")/.exec(src.slice(i, i + 5));
|
|
668
|
+
if (str && (str[1] || c === '"' || c === "'")) {
|
|
669
|
+
const start = i;
|
|
670
|
+
const startLine = line;
|
|
671
|
+
const startCol = i - lineStart;
|
|
672
|
+
const q = str[2];
|
|
673
|
+
i += str[0].length;
|
|
674
|
+
let value = '';
|
|
675
|
+
while (i < src.length && !src.startsWith(q, i)) {
|
|
676
|
+
if (src[i] === '\\' && !/[rR]/.test(str[1])) {
|
|
677
|
+
value += src[i + 1] ?? '';
|
|
678
|
+
i += 2;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (src[i] === '\n') {
|
|
682
|
+
line++;
|
|
683
|
+
lineStart = i + 1;
|
|
684
|
+
}
|
|
685
|
+
value += src[i];
|
|
686
|
+
i++;
|
|
687
|
+
}
|
|
688
|
+
i += q.length;
|
|
689
|
+
out.push({ k: /[fF]/.test(str[1]) ? 'fstr' : 'str', v: value, line: startLine, col: startCol, start, end: i });
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const num = /^\d[\d_]*(\.\d+)?([eE][+-]?\d+)?/.exec(src.slice(i, i + 40));
|
|
693
|
+
if (num) {
|
|
694
|
+
const start = i;
|
|
695
|
+
i += num[0].length;
|
|
696
|
+
push('num', num[0].replace(/_/g, ''), start);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const name = /^[A-Za-z_]\w*/.exec(src.slice(i, i + 200));
|
|
700
|
+
if (name) {
|
|
701
|
+
const start = i;
|
|
702
|
+
i += name[0].length;
|
|
703
|
+
push('name', name[0], start);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const two = src.slice(i, i + 2);
|
|
707
|
+
const start = i;
|
|
708
|
+
if (['+=', '==', '!=', '<=', '>=', '**', '//', '->', ':='].includes(two)) {
|
|
709
|
+
i += 2;
|
|
710
|
+
push('op', two, start);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if ('([{'.includes(c))
|
|
714
|
+
depth++;
|
|
715
|
+
else if (')]}'.includes(c))
|
|
716
|
+
depth = Math.max(0, depth - 1);
|
|
717
|
+
i++;
|
|
718
|
+
push('op', c, start);
|
|
719
|
+
}
|
|
720
|
+
return out;
|
|
721
|
+
}
|
|
722
|
+
/** Calls that read the environment: os.environ.get / os.getenv / django-environ env(), env.str() … / python-decouple config(). */
|
|
723
|
+
const ENV_CALLS = /^(os\.environ\.get|os\.getenv|environ\.get|getenv|env|env\.(str|int|bool|float|list|tuple|dict|json|url|db|db_url|cache|cache_url|email|email_url|path)|config)$/;
|
|
724
|
+
class PyParser {
|
|
725
|
+
toks;
|
|
726
|
+
src;
|
|
727
|
+
i = 0;
|
|
728
|
+
constructor(toks, src) {
|
|
729
|
+
this.toks = toks;
|
|
730
|
+
this.src = src;
|
|
731
|
+
}
|
|
732
|
+
peek() {
|
|
733
|
+
return this.toks[this.i];
|
|
734
|
+
}
|
|
735
|
+
isOp(v) {
|
|
736
|
+
const t = this.peek();
|
|
737
|
+
return t?.k === 'op' && t.v === v;
|
|
738
|
+
}
|
|
739
|
+
text(from) {
|
|
740
|
+
const a = this.toks[from];
|
|
741
|
+
const z = this.toks[Math.max(from, this.i - 1)];
|
|
742
|
+
return a && z ? this.src.slice(a.start, z.end).replace(/\s+/g, ' ') : '';
|
|
743
|
+
}
|
|
744
|
+
/** Skip to the bracket closing the one just opened. */
|
|
745
|
+
skipToClose() {
|
|
746
|
+
let depth = 1;
|
|
747
|
+
while (this.peek()) {
|
|
748
|
+
const t = this.toks[this.i++];
|
|
749
|
+
if (t.k !== 'op')
|
|
750
|
+
continue;
|
|
751
|
+
if ('([{'.includes(t.v))
|
|
752
|
+
depth++;
|
|
753
|
+
else if (')]}'.includes(t.v) && --depth === 0)
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
expr() {
|
|
758
|
+
const from = this.i;
|
|
759
|
+
let left = this.unary();
|
|
760
|
+
while (this.peek()?.k === 'op' && ['+', '-', '*', '/', '//', '|', '%'].includes(this.peek().v)) {
|
|
761
|
+
const op = this.toks[this.i++].v;
|
|
762
|
+
const right = this.unary();
|
|
763
|
+
if (left.t === 'num' && right.t === 'num') {
|
|
764
|
+
const v = op === '+' ? left.v + right.v : op === '-' ? left.v - right.v : op === '*' ? left.v * right.v : op === '/' ? left.v / right.v : op === '//' ? Math.floor(left.v / right.v) : NaN;
|
|
765
|
+
left = Number.isNaN(v) ? { t: 'expr', text: this.text(from), line: left.line } : { t: 'num', v, line: left.line };
|
|
766
|
+
}
|
|
767
|
+
else if (op === '+' && left.t === 'list' && right.t === 'list')
|
|
768
|
+
left = { t: 'list', items: [...left.items, ...right.items], line: left.line };
|
|
769
|
+
else if (op === '+' && left.t === 'str' && right.t === 'str')
|
|
770
|
+
left = { t: 'str', v: left.v + right.v, line: left.line };
|
|
771
|
+
else
|
|
772
|
+
left = { t: 'expr', text: this.text(from), line: left.line };
|
|
773
|
+
}
|
|
774
|
+
const t = this.peek();
|
|
775
|
+
if (t?.k === 'name' && ['if', 'for', 'and', 'or', 'in', 'not', 'is'].includes(t.v)) {
|
|
776
|
+
// A conditional or comprehension: keep its text.
|
|
777
|
+
while (this.peek() && !(this.peek().k === 'op' && [',', ')', ']', '}', ':'].includes(this.peek().v))) {
|
|
778
|
+
const n = this.toks[this.i++];
|
|
779
|
+
if (n.k === 'op' && '([{'.includes(n.v))
|
|
780
|
+
this.skipToClose();
|
|
781
|
+
}
|
|
782
|
+
return { t: 'expr', text: this.text(from), line: left.line };
|
|
783
|
+
}
|
|
784
|
+
return left;
|
|
785
|
+
}
|
|
786
|
+
unary() {
|
|
787
|
+
if (this.isOp('-')) {
|
|
788
|
+
this.i++;
|
|
789
|
+
const v = this.unary();
|
|
790
|
+
return v.t === 'num' ? { ...v, v: -v.v } : v;
|
|
791
|
+
}
|
|
792
|
+
if (this.peek()?.k === 'name' && this.peek().v === 'not') {
|
|
793
|
+
this.i++;
|
|
794
|
+
const v = this.unary();
|
|
795
|
+
return v.t === 'bool' ? { ...v, v: !v.v } : { t: 'expr', text: 'not …', line: v.line };
|
|
796
|
+
}
|
|
797
|
+
return this.atom();
|
|
798
|
+
}
|
|
799
|
+
/** Items up to `close`; null when the bracket holds a comprehension. */
|
|
800
|
+
items(close) {
|
|
801
|
+
const items = [];
|
|
802
|
+
let dict = null;
|
|
803
|
+
let comma = false;
|
|
804
|
+
while (this.peek() && !this.isOp(close)) {
|
|
805
|
+
if (this.isOp('**') || this.isOp('*')) {
|
|
806
|
+
this.i++;
|
|
807
|
+
this.expr();
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
const k = this.expr();
|
|
811
|
+
if (this.isOp(':')) {
|
|
812
|
+
this.i++;
|
|
813
|
+
(dict ??= []).push([k, this.expr()]);
|
|
814
|
+
}
|
|
815
|
+
else
|
|
816
|
+
items.push(k);
|
|
817
|
+
}
|
|
818
|
+
if (this.isOp(',')) {
|
|
819
|
+
this.i++;
|
|
820
|
+
comma = true;
|
|
821
|
+
}
|
|
822
|
+
else if (!this.isOp(close)) {
|
|
823
|
+
this.skipToClose();
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
this.i++;
|
|
828
|
+
return { items, dict, comma };
|
|
829
|
+
}
|
|
830
|
+
atom() {
|
|
831
|
+
const t = this.toks[this.i++];
|
|
832
|
+
if (!t || t.k === 'nl')
|
|
833
|
+
return { t: 'expr', text: '', line: t?.line ?? 0 };
|
|
834
|
+
const from = this.i - 1;
|
|
835
|
+
if (t.k === 'str') {
|
|
836
|
+
let v = t.v;
|
|
837
|
+
while (this.peek()?.k === 'str')
|
|
838
|
+
v += this.toks[this.i++].v;
|
|
839
|
+
return { t: 'str', v, line: t.line };
|
|
840
|
+
}
|
|
841
|
+
if (t.k === 'fstr')
|
|
842
|
+
return { t: 'expr', text: this.text(from), line: t.line };
|
|
843
|
+
if (t.k === 'num')
|
|
844
|
+
return { t: 'num', v: Number(t.v), line: t.line };
|
|
845
|
+
if (t.k === 'op') {
|
|
846
|
+
const close = t.v === '[' ? ']' : t.v === '(' ? ')' : t.v === '{' ? '}' : null;
|
|
847
|
+
if (!close)
|
|
848
|
+
return { t: 'expr', text: t.v, line: t.line };
|
|
849
|
+
const r = this.items(close);
|
|
850
|
+
if (!r)
|
|
851
|
+
return { t: 'expr', text: this.text(from), line: t.line };
|
|
852
|
+
if (t.v === '{' && r.dict)
|
|
853
|
+
return { t: 'dict', entries: r.dict, line: t.line };
|
|
854
|
+
if (t.v === '(' && r.items.length === 1 && !r.comma)
|
|
855
|
+
return r.items[0];
|
|
856
|
+
return { t: 'list', items: r.items, line: t.line };
|
|
857
|
+
}
|
|
858
|
+
// A name: True / False / None, or a dotted chain with calls and subscripts.
|
|
859
|
+
if (t.v === 'True' || t.v === 'False')
|
|
860
|
+
return { t: 'bool', v: t.v === 'True', line: t.line };
|
|
861
|
+
if (t.v === 'None')
|
|
862
|
+
return { t: 'none', line: t.line };
|
|
863
|
+
let path = t.v;
|
|
864
|
+
let result;
|
|
865
|
+
for (;;) {
|
|
866
|
+
if (this.isOp('.') && this.toks[this.i + 1]?.k === 'name') {
|
|
867
|
+
path += `.${this.toks[this.i + 1].v}`;
|
|
868
|
+
this.i += 2;
|
|
869
|
+
}
|
|
870
|
+
else if (this.isOp('(')) {
|
|
871
|
+
this.i++;
|
|
872
|
+
const args = [];
|
|
873
|
+
while (this.peek() && !this.isOp(')')) {
|
|
874
|
+
if (this.peek()?.k === 'name' && this.toks[this.i + 1]?.k === 'op' && this.toks[this.i + 1].v === '=')
|
|
875
|
+
this.i += 2;
|
|
876
|
+
else if (this.isOp('*') || this.isOp('**'))
|
|
877
|
+
this.i++;
|
|
878
|
+
args.push(this.expr());
|
|
879
|
+
if (this.isOp(','))
|
|
880
|
+
this.i++;
|
|
881
|
+
else if (!this.isOp(')')) {
|
|
882
|
+
this.skipToClose();
|
|
883
|
+
this.i--;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
this.i++;
|
|
887
|
+
const first = args[0];
|
|
888
|
+
result = !result && ENV_CALLS.test(path) && first?.t === 'str' ? { t: 'env', name: first.v, line: t.line } : { t: 'expr', text: this.text(from), line: t.line };
|
|
889
|
+
path = '';
|
|
890
|
+
}
|
|
891
|
+
else if (this.isOp('[')) {
|
|
892
|
+
this.i++;
|
|
893
|
+
const key = this.expr();
|
|
894
|
+
if (!this.isOp(']'))
|
|
895
|
+
this.skipToClose();
|
|
896
|
+
else
|
|
897
|
+
this.i++;
|
|
898
|
+
result = !result && /^(os\.)?environ$/.test(path) && key.t === 'str' ? { t: 'env', name: key.v, line: t.line } : { t: 'expr', text: this.text(from), line: t.line };
|
|
899
|
+
path = '';
|
|
900
|
+
}
|
|
901
|
+
else
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
return result && path === '' ? result : { t: 'expr', text: this.text(from), line: t.line };
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/** Module-level `NAME = …` / `NAME += …` assignments of every settings file, base files first. */
|
|
908
|
+
function djangoSettings(project) {
|
|
909
|
+
const files = walk(project, (r) => /(^|\/)settings\.py$/.test(r) || /(^|\/)settings\/[^/]+\.py$/.test(r)).map((p) => rel(project, p));
|
|
910
|
+
const rank = (f) => {
|
|
911
|
+
const base = f.split('/').pop();
|
|
912
|
+
return base === 'settings.py' ? 0 : base === 'base.py' || base === 'common.py' || base === 'defaults.py' ? 1 : base === '__init__.py' ? 2 : 3;
|
|
913
|
+
};
|
|
914
|
+
files.sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : 1));
|
|
915
|
+
const settings = new Map();
|
|
916
|
+
const overrides = [];
|
|
917
|
+
for (const file of files) {
|
|
918
|
+
const src = (0, node_fs_1.readFileSync)((0, node_path_1.join)(project, file), 'utf8');
|
|
919
|
+
const toks = tokenize(src);
|
|
920
|
+
let i = 0;
|
|
921
|
+
while (i < toks.length) {
|
|
922
|
+
const t = toks[i];
|
|
923
|
+
const op = toks[i + 1];
|
|
924
|
+
if (t.k === 'name' && t.col === 0 && /^[A-Z][A-Z0-9_]*$/.test(t.v) && op?.k === 'op' && (op.v === '=' || op.v === '+=')) {
|
|
925
|
+
let end = i + 2;
|
|
926
|
+
while (end < toks.length && toks[end].k !== 'nl')
|
|
927
|
+
end++;
|
|
928
|
+
const stmt = toks.slice(i + 2, end);
|
|
929
|
+
const value = stmt.length ? new PyParser(stmt, src).expr() : { t: 'expr', text: '', line: t.line };
|
|
930
|
+
const s = { name: t.v, value, file, line: t.line, text: stmt.length ? src.slice(stmt[0].start, stmt[stmt.length - 1].end) : '' };
|
|
931
|
+
const prev = settings.get(t.v);
|
|
932
|
+
if (op.v === '+=' && prev?.value.t === 'list' && value.t === 'list')
|
|
933
|
+
prev.value = { ...prev.value, items: [...prev.value.items, ...value.items] };
|
|
934
|
+
else if (!prev)
|
|
935
|
+
settings.set(t.v, s);
|
|
936
|
+
else if (prev.text.replace(/\s+/g, '') !== s.text.replace(/\s+/g, ''))
|
|
937
|
+
overrides.push(s);
|
|
938
|
+
i = end;
|
|
939
|
+
}
|
|
940
|
+
i++;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return { files, settings, overrides };
|
|
944
|
+
}
|
|
945
|
+
const pyStr = (v) => (v?.t === 'str' ? v.v : undefined);
|
|
946
|
+
const pyStrings = (v) => (v?.t === 'list' ? v.items.flatMap((x) => (x.t === 'str' ? [{ v: x.v, line: x.line }] : [])) : []);
|
|
947
|
+
function pyGet(v, key) {
|
|
948
|
+
if (v?.t !== 'dict')
|
|
949
|
+
return undefined;
|
|
950
|
+
return v.entries.find(([k]) => k.t === 'str' && k.v === key)?.[1];
|
|
951
|
+
}
|
|
952
|
+
/** Django hashers → how their hashes carry over. */
|
|
953
|
+
const DJANGO_HASHERS = {
|
|
954
|
+
PBKDF2PasswordHasher: { alg: 'pbkdf2_sha256', status: 'mapped' },
|
|
955
|
+
PBKDF2SHA1PasswordHasher: { alg: 'pbkdf2_sha1', status: 'mapped' },
|
|
956
|
+
Argon2PasswordHasher: { alg: 'argon2', status: 'mapped' },
|
|
957
|
+
BCryptSHA256PasswordHasher: { alg: 'bcrypt_sha256', status: 'mapped' },
|
|
958
|
+
BCryptPasswordHasher: { alg: 'bcrypt', status: 'mapped' },
|
|
959
|
+
ScryptPasswordHasher: { alg: 'scrypt', status: 'mapped' },
|
|
960
|
+
MD5PasswordHasher: { alg: 'md5', status: 'differs' },
|
|
961
|
+
SHA1PasswordHasher: { alg: 'sha1', status: 'differs' },
|
|
962
|
+
UnsaltedMD5PasswordHasher: { alg: 'unsalted_md5', status: 'differs' },
|
|
963
|
+
UnsaltedSHA1PasswordHasher: { alg: 'unsalted_sha1', status: 'differs' },
|
|
964
|
+
CryptPasswordHasher: { alg: 'crypt', status: 'manual' },
|
|
965
|
+
};
|
|
966
|
+
/** python-social-auth backends → [settings prefix, provider name in UserSocialAuth, Atlas key]. */
|
|
967
|
+
const SOCIAL_CORE = {
|
|
968
|
+
GoogleOAuth2: ['GOOGLE_OAUTH2', 'google-oauth2', 'google'],
|
|
969
|
+
GithubOAuth2: ['GITHUB', 'github', 'github'],
|
|
970
|
+
FacebookOAuth2: ['FACEBOOK', 'facebook', 'facebook'],
|
|
971
|
+
AppleIdAuth: ['APPLE_ID', 'apple-id', 'apple'],
|
|
972
|
+
TwitterOAuth2: ['TWITTER_OAUTH2', 'twitter-oauth2', 'x'],
|
|
973
|
+
MicrosoftOAuth2: ['MICROSOFT_GRAPH', 'microsoft-graph', 'microsoft'],
|
|
974
|
+
AzureADOAuth2: ['AZUREAD_OAUTH2', 'azuread-oauth2', 'microsoft'],
|
|
975
|
+
AzureADV2OAuth2: ['AZUREAD_V2_OAUTH2', 'azuread-v2-oauth2', 'microsoft'],
|
|
976
|
+
AzureADTenantOAuth2: ['AZUREAD_TENANT_OAUTH2', 'azuread-tenant-oauth2', 'microsoft'],
|
|
977
|
+
LinkedinOAuth2: ['LINKEDIN_OAUTH2', 'linkedin-oauth2', 'linkedin'],
|
|
978
|
+
LinkedinOpenIdConnect: ['LINKEDIN_OPENIDCONNECT', 'linkedin-openidconnect', 'linkedin'],
|
|
979
|
+
GitLabOAuth2: ['GITLAB', 'gitlab', 'gitlab'],
|
|
980
|
+
DiscordOAuth2: ['DISCORD', 'discord', 'discord'],
|
|
981
|
+
SlackOAuth2: ['SLACK', 'slack', 'slack'],
|
|
982
|
+
OktaOAuth2: ['OKTA_OAUTH2', 'okta-oauth2', 'okta'],
|
|
983
|
+
Auth0OAuth2: ['AUTH0', 'auth0', 'auth0'],
|
|
984
|
+
};
|
|
985
|
+
/** Prefixes of auth-related settings: one left unread gets a manual entry. */
|
|
986
|
+
const DJANGO_AUTH_PREFIXES = /^(ACCOUNT_|SOCIALACCOUNT_|MFA_|HEADLESS_|SESSION_|SOCIAL_AUTH_|PASSWORD_|AUTH_|AUTHENTICATION_|LOGIN_|LOGOUT_|OTP_|TWO_FACTOR_|USERSESSIONS_)/;
|
|
987
|
+
const DJANGO_PLUMBING = {
|
|
988
|
+
SESSION_COOKIE_SECURE: 'Atlas sets its own cookie flags (Secure, HttpOnly, SameSite).',
|
|
989
|
+
SESSION_COOKIE_HTTPONLY: 'Atlas sets its own cookie flags (Secure, HttpOnly, SameSite).',
|
|
990
|
+
SESSION_COOKIE_SAMESITE: 'Atlas sets its own cookie flags (Secure, HttpOnly, SameSite).',
|
|
991
|
+
SESSION_COOKIE_PATH: 'Atlas sets its own cookie path.',
|
|
992
|
+
SESSION_SERIALIZER: 'Session plumbing; the bridge reads the session the way Django wrote it (JSON).',
|
|
993
|
+
PASSWORD_RESET_TIMEOUT: 'Reset links use the Atlas email-link lifetime (verification.magicLink.ttlMinutes).',
|
|
994
|
+
ACCOUNT_UNIQUE_EMAIL: 'Atlas keeps one account per email address.',
|
|
995
|
+
ACCOUNT_EMAIL_REQUIRED: 'Atlas accounts have an email address.',
|
|
996
|
+
ACCOUNT_SESSION_REMEMBER: 'Atlas sessions last the configured lifetime; there is no "remember me" box.',
|
|
997
|
+
ACCOUNT_LOGOUT_ON_GET: 'Atlas signs out through its own endpoint.',
|
|
998
|
+
ACCOUNT_LOGOUT_REDIRECT_URL: 'Set where users land after sign-out in your app or the Atlas hosted pages settings.',
|
|
999
|
+
ACCOUNT_DEFAULT_HTTP_PROTOCOL: 'Atlas links are always HTTPS.',
|
|
1000
|
+
SOCIALACCOUNT_AUTO_SIGNUP: 'Atlas creates the account on the first social sign-in (allow_sign_up on each provider).',
|
|
1001
|
+
SOCIALACCOUNT_QUERY_EMAIL: 'Atlas asks each provider for the email address.',
|
|
1002
|
+
SOCIALACCOUNT_STORE_TOKENS: 'Pass --set oauth-tokens=true to the export to carry stored provider tokens.',
|
|
1003
|
+
HEADLESS_ONLY: "Atlas serves both hosted pages and an API; your frontend can keep its own screens on Atlas's frontend API.",
|
|
1004
|
+
};
|
|
1005
|
+
function translateDjango(project) {
|
|
1006
|
+
const { files, settings, overrides } = djangoSettings(project);
|
|
1007
|
+
const main = files[0];
|
|
1008
|
+
const b = new report_1.TranslationBuilder(main ? `Django settings at ${main}` : 'Django settings (not found)', main ? { file: main, line: 1 } : { file: '(none)', line: 0 });
|
|
1009
|
+
if (!main || !settings.size) {
|
|
1010
|
+
b.add('manual', 'setup', 'settings', `No Django settings module (settings.py or settings/*.py) was found under ${project}. Point the command at the project folder (where manage.py is).`);
|
|
1011
|
+
return b.result();
|
|
1012
|
+
}
|
|
1013
|
+
const used = new Set();
|
|
1014
|
+
const take = (name) => {
|
|
1015
|
+
used.add(name);
|
|
1016
|
+
return settings.get(name);
|
|
1017
|
+
};
|
|
1018
|
+
const at = (s, v) => ({ file: s.file, line: v?.line ?? s.line });
|
|
1019
|
+
const cv = (s, v) => {
|
|
1020
|
+
if (!v || v.t === 'none')
|
|
1021
|
+
return undefined;
|
|
1022
|
+
if (v.t === 'str' || v.t === 'num' || v.t === 'bool')
|
|
1023
|
+
return { kind: 'literal', value: v.v, at: at(s, v) };
|
|
1024
|
+
if (v.t === 'env')
|
|
1025
|
+
return { kind: 'env', name: v.name, at: at(s, v) };
|
|
1026
|
+
return { kind: 'opaque', text: v.t === 'expr' ? (v.text.length > 60 ? `${v.text.slice(0, 57)}…` : v.text) : v.t, at: at(s, v) };
|
|
1027
|
+
};
|
|
1028
|
+
const num = (s) => (s?.value.t === 'num' ? s.value.v : undefined);
|
|
1029
|
+
const bool = (s) => (s?.value.t === 'bool' ? s.value.v : undefined);
|
|
1030
|
+
const appsSetting = take('INSTALLED_APPS');
|
|
1031
|
+
const apps = pyStrings(appsSetting?.value);
|
|
1032
|
+
const app = (name) => apps.find((a) => a.v === name);
|
|
1033
|
+
const appAt = (name) => (appsSetting && app(name) ? at(appsSetting, { t: 'none', line: app(name).line }) : undefined);
|
|
1034
|
+
const allauth = !!app('allauth.account');
|
|
1035
|
+
const headless = !!app('allauth.headless');
|
|
1036
|
+
// Sign-in methods.
|
|
1037
|
+
b.set('strategies.password', true);
|
|
1038
|
+
let loginMethods;
|
|
1039
|
+
const methods = take('ACCOUNT_LOGIN_METHODS');
|
|
1040
|
+
const legacyMethod = take('ACCOUNT_AUTHENTICATION_METHOD');
|
|
1041
|
+
if (methods)
|
|
1042
|
+
loginMethods = pyStrings(methods.value).map((m) => m.v);
|
|
1043
|
+
else if (legacyMethod && pyStr(legacyMethod.value))
|
|
1044
|
+
loginMethods = pyStr(legacyMethod.value).split('_');
|
|
1045
|
+
if (allauth) {
|
|
1046
|
+
const s = methods ?? legacyMethod;
|
|
1047
|
+
const list = loginMethods ?? ['username'];
|
|
1048
|
+
if (list.includes('username'))
|
|
1049
|
+
b.set('strategies.username', true);
|
|
1050
|
+
if (list.includes('phone'))
|
|
1051
|
+
b.set('strategies.phoneCode', true);
|
|
1052
|
+
b.add('mapped', 'allauth', s ? s.name : 'ACCOUNT_LOGIN_METHODS (default username)', `Sign in with ${list.join(' or ')} and a password.`, s ? at(s) : appAt('allauth.account'), 'auth_config.strategies');
|
|
1053
|
+
if (legacyMethod && methods)
|
|
1054
|
+
b.add('mapped', 'allauth', 'ACCOUNT_AUTHENTICATION_METHOD', 'Superseded by ACCOUNT_LOGIN_METHODS.', at(legacyMethod));
|
|
1055
|
+
}
|
|
1056
|
+
else
|
|
1057
|
+
for (const s of [methods, legacyMethod])
|
|
1058
|
+
if (s)
|
|
1059
|
+
b.add('mapped', 'allauth', s.name, 'No effect: django-allauth is not in INSTALLED_APPS.', at(s));
|
|
1060
|
+
const backends = take('AUTHENTICATION_BACKENDS');
|
|
1061
|
+
const backendList = backends ? pyStrings(backends.value) : [{ v: 'django.contrib.auth.backends.ModelBackend', line: 0 }];
|
|
1062
|
+
const social = [];
|
|
1063
|
+
for (const be of backendList) {
|
|
1064
|
+
const where = backends ? at(backends, { t: 'none', line: be.line }) : undefined;
|
|
1065
|
+
const setting = backends ? `AUTHENTICATION_BACKENDS ${be.v.split('.').pop()}` : 'AUTHENTICATION_BACKENDS (default ModelBackend)';
|
|
1066
|
+
if (be.v === 'django.contrib.auth.backends.ModelBackend') {
|
|
1067
|
+
if (!allauth)
|
|
1068
|
+
b.set('strategies.username', true);
|
|
1069
|
+
b.add('mapped', 'auth', setting, allauth ? 'Username and password (the Django admin): allauth decides how users sign in.' : 'Sign in with username (USERNAME_FIELD) and password: username sign-in on.', where, 'auth_config.strategies.password');
|
|
1070
|
+
}
|
|
1071
|
+
else if (be.v.startsWith('allauth.'))
|
|
1072
|
+
b.add('mapped', 'auth', setting, 'django-allauth sign-in: see the ACCOUNT_* entries.', where);
|
|
1073
|
+
else if (be.v.startsWith('social_core.backends.'))
|
|
1074
|
+
social.push({ cls: be.v.split('.').pop(), line: be.line });
|
|
1075
|
+
else
|
|
1076
|
+
b.add('manual', 'auth', setting, `A custom or directory backend (${be.v}): connect the directory as an Atlas enterprise SSO connection (SAML, OIDC or LDAP), or check passwords against it with lazy migration.`, where);
|
|
1077
|
+
}
|
|
1078
|
+
const signup = take('ACCOUNT_SIGNUP_FIELDS');
|
|
1079
|
+
if (signup) {
|
|
1080
|
+
const fields = pyStrings(signup.value).map((f) => f.v.replace(/\*$/, '').replace(/^password1$/, 'password'));
|
|
1081
|
+
if (fields.includes('username'))
|
|
1082
|
+
b.set('strategies.username', true);
|
|
1083
|
+
b.add('mapped', 'allauth', 'ACCOUNT_SIGNUP_FIELDS', `Sign-up asks for ${fields.filter((f) => f !== 'password2').join(', ')}${fields.includes('password2') ? ' (Atlas has no password confirmation box)' : ''}.`, at(signup), 'auth_config.strategies');
|
|
1084
|
+
}
|
|
1085
|
+
const usernameRequired = take('ACCOUNT_USERNAME_REQUIRED');
|
|
1086
|
+
if (usernameRequired) {
|
|
1087
|
+
if (bool(usernameRequired) === true)
|
|
1088
|
+
b.set('strategies.username', true);
|
|
1089
|
+
b.add('mapped', 'allauth', 'ACCOUNT_USERNAME_REQUIRED', bool(usernameRequired) ? 'Users pick a username.' : 'No username.', at(usernameRequired), 'auth_config.strategies.username');
|
|
1090
|
+
}
|
|
1091
|
+
// Email verification.
|
|
1092
|
+
const verification = take('ACCOUNT_EMAIL_VERIFICATION');
|
|
1093
|
+
if (allauth || verification) {
|
|
1094
|
+
const mode = pyStr(verification?.value) ?? 'optional';
|
|
1095
|
+
b.set('requireEmailVerification', mode === 'mandatory');
|
|
1096
|
+
if (mode !== 'none')
|
|
1097
|
+
b.set('accountUx.emailVerification', 'link');
|
|
1098
|
+
b.add('mapped', 'allauth', verification ? 'ACCOUNT_EMAIL_VERIFICATION' : 'ACCOUNT_EMAIL_VERIFICATION (default optional)', mode === 'mandatory' ? 'Email verification required before the first session. Verified addresses (account_emailaddress.verified) carry over.' : mode === 'none' ? 'No email verification.' : 'Email verification is sent but not required to sign in.', verification ? at(verification) : appAt('allauth.account'), 'auth_config.requireEmailVerification');
|
|
1099
|
+
}
|
|
1100
|
+
const byCode = take('ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED');
|
|
1101
|
+
if (byCode) {
|
|
1102
|
+
if (bool(byCode))
|
|
1103
|
+
b.set('accountUx.emailVerification', 'code');
|
|
1104
|
+
b.add('mapped', 'allauth', 'ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED', bool(byCode) ? 'Email verification by code.' : 'Email verification by link.', at(byCode), 'auth_config.accountUx.emailVerification');
|
|
1105
|
+
}
|
|
1106
|
+
const loginByCode = take('ACCOUNT_LOGIN_BY_CODE_ENABLED');
|
|
1107
|
+
if (loginByCode) {
|
|
1108
|
+
b.set('strategies.emailCode', bool(loginByCode) === true);
|
|
1109
|
+
b.add('mapped', 'allauth', 'ACCOUNT_LOGIN_BY_CODE_ENABLED', bool(loginByCode) ? 'Sign in with a one-time email code.' : 'No sign-in by email code.', at(loginByCode), 'auth_config.strategies.emailCode');
|
|
1110
|
+
}
|
|
1111
|
+
const loginOnReset = take('ACCOUNT_LOGIN_ON_PASSWORD_RESET');
|
|
1112
|
+
if (loginOnReset) {
|
|
1113
|
+
b.set('signInAfterReset', bool(loginOnReset) === true);
|
|
1114
|
+
b.add('mapped', 'allauth', 'ACCOUNT_LOGIN_ON_PASSWORD_RESET', bool(loginOnReset) ? 'A completed reset signs the user in.' : 'A completed reset ends at the sign-in screen.', at(loginOnReset), 'auth_config.signInAfterReset');
|
|
1115
|
+
}
|
|
1116
|
+
// Rate limits.
|
|
1117
|
+
for (const name of ['ACCOUNT_RATE_LIMITS', 'ACCOUNT_LOGIN_ATTEMPTS_LIMIT', 'ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT']) {
|
|
1118
|
+
const s = take(name);
|
|
1119
|
+
if (s)
|
|
1120
|
+
b.add('differs', 'allauth', name, "Atlas rate-limits sign-in, sign-up, codes and resets itself and locks sign-in progressively after repeated failures (attack_protection.brute_force); the limits are not set per action.", at(s), 'auth_config.attackProtection.bruteForce');
|
|
1121
|
+
}
|
|
1122
|
+
for (const name of ['ACCOUNT_ADAPTER', 'SOCIALACCOUNT_ADAPTER', 'HEADLESS_ADAPTER', 'MFA_ADAPTER']) {
|
|
1123
|
+
const s = take(name);
|
|
1124
|
+
if (!s)
|
|
1125
|
+
continue;
|
|
1126
|
+
const mfa = name === 'MFA_ADAPTER';
|
|
1127
|
+
b.add('manual', 'allauth', name, mfa ? `A custom MFA adapter (${pyStr(s.value) ?? s.text}): run the export script inside your app so the adapter decrypts TOTP secrets and recovery codes.` : `A custom adapter (${pyStr(s.value) ?? s.text}): move its hooks (signup checks, redirects, user population) to Atlas Actions.`, at(s));
|
|
1128
|
+
}
|
|
1129
|
+
// Passwords: hashers and validators.
|
|
1130
|
+
const hashers = take('PASSWORD_HASHERS');
|
|
1131
|
+
let customHasher = false;
|
|
1132
|
+
if (hashers) {
|
|
1133
|
+
pyStrings(hashers.value).forEach((h, idx) => {
|
|
1134
|
+
const cls = h.v.split('.').pop();
|
|
1135
|
+
const known = h.v.startsWith('django.contrib.auth.hashers.') ? DJANGO_HASHERS[cls] : undefined;
|
|
1136
|
+
const where = at(hashers, { t: 'none', line: h.line });
|
|
1137
|
+
const setting = `PASSWORD_HASHERS ${cls}`;
|
|
1138
|
+
const first = idx === 0 ? ' (new hashes)' : '';
|
|
1139
|
+
if (!known) {
|
|
1140
|
+
customHasher = true;
|
|
1141
|
+
b.add('manual', 'passwords', setting, `${h.v} is a custom hasher: its hashes do not carry over. ${headless ? 'Turn on lazy migration (preset allauth_headless): Atlas checks each password against your app once, then stores its own hash.' : 'Those users reset their password, or add django-allauth headless so Atlas can check passwords against your app on first sign-in (lazy migration preset allauth_headless).'}`, where, headless ? 'lazy migration allauth_headless' : undefined);
|
|
1142
|
+
}
|
|
1143
|
+
else if (known.status === 'manual')
|
|
1144
|
+
b.add('manual', 'passwords', setting, `${known.alg} hashes (Unix crypt) do not carry over: those users reset their password${headless ? ' or migrate lazily (preset allauth_headless)' : ''}.`, where);
|
|
1145
|
+
else
|
|
1146
|
+
b.add(known.status, 'passwords', setting, `${known.alg} hashes${first} carry over in the export; Atlas verifies them and rehashes on first sign-in${known.status === 'differs' ? ' (a legacy digest: flagged in the dry run)' : ''}.`, where);
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
else
|
|
1150
|
+
b.add('mapped', 'passwords', 'PASSWORD_HASHERS (default)', "Django's default pbkdf2_sha256 hashes (and argon2 / bcrypt_sha256 / scrypt ones) carry over; Atlas verifies them and rehashes on first sign-in.", { file: main, line: 1 });
|
|
1151
|
+
const validators = take('AUTH_PASSWORD_VALIDATORS');
|
|
1152
|
+
if (validators?.value.t === 'list') {
|
|
1153
|
+
for (const item of validators.value.items) {
|
|
1154
|
+
const name = pyStr(pyGet(item, 'NAME')) ?? '';
|
|
1155
|
+
const cls = name.split('.').pop();
|
|
1156
|
+
const where = at(validators, item);
|
|
1157
|
+
if (cls === 'MinimumLengthValidator') {
|
|
1158
|
+
const n = pyGet(pyGet(item, 'OPTIONS'), 'min_length');
|
|
1159
|
+
b.minPasswordLength(n?.t === 'num' ? n.v : 8, 'passwords', 'MinimumLengthValidator', where, 'Django');
|
|
1160
|
+
}
|
|
1161
|
+
else if (cls === 'CommonPasswordValidator') {
|
|
1162
|
+
b.set('passwordPolicy.checkBreaches', true);
|
|
1163
|
+
b.add('differs', 'passwords', cls, "New passwords are checked against breached-password data, a larger list than Django's common-passwords file.", where, 'auth_config.passwordPolicy.checkBreaches');
|
|
1164
|
+
}
|
|
1165
|
+
else if (cls === 'UserAttributeSimilarityValidator' || cls === 'NumericPasswordValidator') {
|
|
1166
|
+
b.add('differs', 'passwords', cls, `Atlas has no ${cls === 'NumericPasswordValidator' ? 'all-digits' : 'similarity-to-name-or-email'} rule; the minimum length and breached-password checks apply.`, where);
|
|
1167
|
+
}
|
|
1168
|
+
else
|
|
1169
|
+
b.add('manual', 'passwords', cls || 'validator', `A custom validator (${name || 'unreadable'}): Atlas cannot run it. Enforce what it checks with a pre-sign-up Action, or drop it.`, where);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
else if (validators)
|
|
1173
|
+
b.add('manual', 'passwords', 'AUTH_PASSWORD_VALIDATORS', 'Cannot read the validators statically; set auth_config.passwordPolicy yourself.', at(validators));
|
|
1174
|
+
const legacyMin = take('ACCOUNT_PASSWORD_MIN_LENGTH');
|
|
1175
|
+
if (legacyMin && num(legacyMin) !== undefined && b.get('passwordPolicy.minLength') === undefined)
|
|
1176
|
+
b.minPasswordLength(num(legacyMin), 'allauth', 'ACCOUNT_PASSWORD_MIN_LENGTH', at(legacyMin), 'allauth');
|
|
1177
|
+
else if (legacyMin)
|
|
1178
|
+
b.add('mapped', 'allauth', 'ACCOUNT_PASSWORD_MIN_LENGTH', 'Superseded by MinimumLengthValidator.', at(legacyMin));
|
|
1179
|
+
const userModel = take('AUTH_USER_MODEL');
|
|
1180
|
+
if (userModel)
|
|
1181
|
+
b.add('mapped', 'auth', 'AUTH_USER_MODEL', `The export reads ${pyStr(userModel.value) ?? 'this model'} (the export script finds it; with a dumpdata file pass --set user-model=${pyStr(userModel.value) ?? 'app_label.model'}).`, at(userModel));
|
|
1182
|
+
// Sessions → the bridge.
|
|
1183
|
+
const engineS = take('SESSION_ENGINE');
|
|
1184
|
+
const engine = pyStr(engineS?.value) ?? 'django.contrib.sessions.backends.db';
|
|
1185
|
+
const cookieS = take('SESSION_COOKIE_NAME');
|
|
1186
|
+
const cookie = pyStr(cookieS?.value);
|
|
1187
|
+
const vars = cookie && cookie !== 'sessionid' ? { cookie_name: cookie } : {};
|
|
1188
|
+
const engineSetting = engineS ? 'SESSION_ENGINE' : 'SESSION_ENGINE (default database sessions)';
|
|
1189
|
+
const engineAt = engineS ? at(engineS) : undefined;
|
|
1190
|
+
if (/\.backends\.(db|cached_db)$/.test(engine)) {
|
|
1191
|
+
b.bridge = { preset: 'django', vars };
|
|
1192
|
+
b.add('mapped', 'bridge', engineSetting, 'Database sessions: the export imports the live django_session rows and the bridge matches the session cookie (preset django).', engineAt, 'bridge preset django');
|
|
1193
|
+
}
|
|
1194
|
+
else if (/\.backends\.signed_cookies$/.test(engine)) {
|
|
1195
|
+
b.bridge = { preset: 'django_signed_cookies', vars };
|
|
1196
|
+
b.add('mapped', 'bridge', engineSetting, 'Signed-cookie sessions are opened on your server: pass djangoSignedCookieExtractor({ secretKey, bridgeSecret }) to the bridge middleware (preset django_signed_cookies). SECRET_KEY never leaves your server.', engineAt, 'bridge preset django_signed_cookies');
|
|
1197
|
+
}
|
|
1198
|
+
else if (/\.backends\.(cache|file)$/.test(engine)) {
|
|
1199
|
+
b.add('manual', 'bridge', engineSetting, `${engine.split('.').pop()} sessions cannot be exported: users sign in once more after the switch. Switching to cached_db a session lifetime before the cutover lets the bridge carry them.`, engineAt);
|
|
1200
|
+
}
|
|
1201
|
+
else
|
|
1202
|
+
b.add('manual', 'bridge', engineSetting, `A custom session engine (${engine}): users sign in once more, or read the session yourself in a custom extractToken.`, engineAt);
|
|
1203
|
+
if (cookieS)
|
|
1204
|
+
b.add('mapped', 'bridge', 'SESSION_COOKIE_NAME', `The bridge reads "${cookie ?? cookieS.text}".`, at(cookieS), b.bridge ? `bridge preset ${b.bridge.preset}` : undefined);
|
|
1205
|
+
const ageS = take('SESSION_COOKIE_AGE');
|
|
1206
|
+
const everyS = take('SESSION_SAVE_EVERY_REQUEST');
|
|
1207
|
+
const rolling = bool(everyS) === true;
|
|
1208
|
+
const age = ageS ? num(ageS) : 14 * report_1.DAY;
|
|
1209
|
+
if (age === undefined)
|
|
1210
|
+
b.add('manual', 'sessions', 'SESSION_COOKIE_AGE', `Cannot read ${ageS.text} statically; set auth_config.session yourself.`, at(ageS));
|
|
1211
|
+
else
|
|
1212
|
+
b.session(age, rolling, 'sessions', ageS ? 'SESSION_COOKIE_AGE' : 'SESSION_COOKIE_AGE (default 2 weeks)', ageS ? at(ageS) : undefined, 'Django');
|
|
1213
|
+
if (everyS)
|
|
1214
|
+
b.add('mapped', 'sessions', 'SESSION_SAVE_EVERY_REQUEST', rolling ? 'Activity extends the session.' : 'Activity does not extend the session.', at(everyS), 'auth_config.session.idleTimeout');
|
|
1215
|
+
const browserClose = take('SESSION_EXPIRE_AT_BROWSER_CLOSE');
|
|
1216
|
+
if (browserClose && bool(browserClose))
|
|
1217
|
+
b.add('differs', 'sessions', 'SESSION_EXPIRE_AT_BROWSER_CLOSE', 'Atlas sessions outlive the browser for the configured lifetime; shorten auth_config.session if that matters.', at(browserClose), 'auth_config.session');
|
|
1218
|
+
else if (browserClose)
|
|
1219
|
+
b.add('mapped', 'sessions', 'SESSION_EXPIRE_AT_BROWSER_CLOSE', 'Sessions outlive the browser, as in Atlas.', at(browserClose));
|
|
1220
|
+
const domain = take('SESSION_COOKIE_DOMAIN');
|
|
1221
|
+
if (domain && pyStr(domain.value)) {
|
|
1222
|
+
b.set('session.crossSubdomainCookies', true);
|
|
1223
|
+
b.add('mapped', 'sessions', 'SESSION_COOKIE_DOMAIN', `Sessions are shared across subdomains of ${pyStr(domain.value).replace(/^\./, '')}.`, at(domain), 'auth_config.session.crossSubdomainCookies');
|
|
1224
|
+
}
|
|
1225
|
+
else if (domain)
|
|
1226
|
+
b.add('mapped', 'sessions', 'SESSION_COOKIE_DOMAIN', 'Host-only session cookie.', at(domain));
|
|
1227
|
+
const secret = take('SECRET_KEY');
|
|
1228
|
+
if (secret) {
|
|
1229
|
+
const literal = secret.value.t === 'str';
|
|
1230
|
+
b.add(literal ? 'manual' : 'mapped', 'auth', 'SECRET_KEY', `Atlas never needs it: only the signed-cookie bridge extractor and the export script use it, on your server.${literal ? ' It is hard-coded in settings: move it to the environment.' : ''}`, at(secret));
|
|
1231
|
+
}
|
|
1232
|
+
// django-allauth social accounts.
|
|
1233
|
+
const providersS = take('SOCIALACCOUNT_PROVIDERS');
|
|
1234
|
+
for (const a of apps) {
|
|
1235
|
+
const m = /^allauth\.socialaccount\.providers\.(\w+)$/.exec(a.v);
|
|
1236
|
+
if (!m)
|
|
1237
|
+
continue;
|
|
1238
|
+
const id = m[1];
|
|
1239
|
+
const setting = `allauth.socialaccount.providers.${id}`;
|
|
1240
|
+
const where = appAt(a.v);
|
|
1241
|
+
const cfg = pyGet(providersS?.value, id);
|
|
1242
|
+
if (cfg && providersS) {
|
|
1243
|
+
for (const [k] of cfg.t === 'dict' ? cfg.entries : []) {
|
|
1244
|
+
const key = k.t === 'str' ? k.v : '';
|
|
1245
|
+
if (!['APP', 'APPS', 'Apps', 'SCOPE'].includes(key))
|
|
1246
|
+
b.add('manual', 'socialaccount', `SOCIALACCOUNT_PROVIDERS.${id}.${key}`, `Provider option ${key}: set its Atlas counterpart on the provider (extra authorization parameters, fields) if your app depends on it.`, at(providersS, k));
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (id === 'saml') {
|
|
1250
|
+
b.add('manual', 'socialaccount', setting, 'SAML: create an Atlas enterprise SSO connection for each IdP (the SocialApp rows or APPS entries hold their metadata). Atlas routes users to it by email domain.', where, 'sso_connections');
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (id === 'openid_connect') {
|
|
1254
|
+
b.add('manual', 'socialaccount', setting, 'Generic OpenID Connect: add each server as a custom OIDC provider (custom_oauth_providers, with the issuer) or, for a workforce IdP, an enterprise SSO connection.', where, 'custom_oauth_providers');
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const atlasKey = report_1.FRAMEWORK_PROVIDER_MAP[id];
|
|
1258
|
+
if (!atlasKey) {
|
|
1259
|
+
b.add('manual', 'socialaccount', setting, `"${id}" has no same-named Atlas provider: add it as a custom OAuth / OIDC provider (custom_oauth_providers) with its endpoints.`, where, 'custom_oauth_providers');
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const appCfg = pyGet(cfg, 'APP') ?? (() => {
|
|
1263
|
+
const list = pyGet(cfg, 'APPS');
|
|
1264
|
+
return list?.t === 'list' ? list.items[0] : undefined;
|
|
1265
|
+
})();
|
|
1266
|
+
const upper = upperSnake(atlasKey);
|
|
1267
|
+
let clientId;
|
|
1268
|
+
let clientSecret;
|
|
1269
|
+
if (appCfg && providersS) {
|
|
1270
|
+
clientId = b.configString(cv(providersS, pyGet(appCfg, 'client_id')), `${atlasKey} client id`, `${upper}_CLIENT_ID`, { area: 'socialaccount', setting: `SOCIALACCOUNT_PROVIDERS.${id} client_id` });
|
|
1271
|
+
clientSecret = b.configString(cv(providersS, pyGet(appCfg, 'secret')), `${atlasKey} client secret`, `${upper}_CLIENT_SECRET`, { secret: true, area: 'socialaccount', setting: `SOCIALACCOUNT_PROVIDERS.${id} secret` });
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
b.env.set(`${upper}_CLIENT_ID`, `${atlasKey} client id`);
|
|
1275
|
+
b.env.set(`${upper}_CLIENT_SECRET`, `${atlasKey} client secret`);
|
|
1276
|
+
clientId = `\${${upper}_CLIENT_ID}`;
|
|
1277
|
+
clientSecret = `\${${upper}_CLIENT_SECRET}`;
|
|
1278
|
+
b.add('manual', 'socialaccount', `SocialApp ${id}`, `No APP in SOCIALACCOUNT_PROVIDERS: the client id and secret live in a SocialApp row in the database, where this step cannot read them. Set ${upper}_CLIENT_ID and ${upper}_CLIENT_SECRET before applying.`, where);
|
|
1279
|
+
}
|
|
1280
|
+
const scopes = pyStrings(pyGet(cfg, 'SCOPE')).map((s) => s.v);
|
|
1281
|
+
b.oauthProvider({ sourceId: id, atlasKey, clientId, clientSecret, scopes, area: 'socialaccount', setting, at: where });
|
|
1282
|
+
}
|
|
1283
|
+
for (const name of ['SOCIALACCOUNT_EMAIL_AUTHENTICATION', 'SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT']) {
|
|
1284
|
+
const s = take(name);
|
|
1285
|
+
if (!s)
|
|
1286
|
+
continue;
|
|
1287
|
+
if (bool(s))
|
|
1288
|
+
b.set('accountLinking', 'auto');
|
|
1289
|
+
b.add('mapped', 'socialaccount', name, bool(s) ? 'A social sign-in with a verified email that matches an existing account links to it.' : 'Social sign-ins do not link by email.', at(s), 'auth_config.accountLinking');
|
|
1290
|
+
}
|
|
1291
|
+
// python-social-auth (social_django).
|
|
1292
|
+
for (const { cls, line } of social) {
|
|
1293
|
+
const where = backends ? at(backends, { t: 'none', line }) : undefined;
|
|
1294
|
+
const setting = `AUTHENTICATION_BACKENDS ${cls}`;
|
|
1295
|
+
if (cls === 'SAMLAuth') {
|
|
1296
|
+
b.add('manual', 'social_django', setting, 'SAML: create an Atlas enterprise SSO connection for each IdP in SOCIAL_AUTH_SAML_ENABLED_IDPS.', where, 'sso_connections');
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
const known = SOCIAL_CORE[cls];
|
|
1300
|
+
if (!known) {
|
|
1301
|
+
b.add('manual', 'social_django', setting, `${cls} has no known Atlas provider: add it as a custom OAuth / OIDC provider (custom_oauth_providers).`, where, 'custom_oauth_providers');
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
const [prefix, sourceId, atlasKey] = known;
|
|
1305
|
+
const upper = upperSnake(atlasKey);
|
|
1306
|
+
const keyS = take(`SOCIAL_AUTH_${prefix}_KEY`);
|
|
1307
|
+
const secretS = take(`SOCIAL_AUTH_${prefix}_SECRET`);
|
|
1308
|
+
const clientId = b.configString(keyS ? cv(keyS, keyS.value) : undefined, `${atlasKey} client id`, `${upper}_CLIENT_ID`, { area: 'social_django', setting: `SOCIAL_AUTH_${prefix}_KEY` });
|
|
1309
|
+
const clientSecret = b.configString(secretS ? cv(secretS, secretS.value) : undefined, `${atlasKey} client secret`, `${upper}_CLIENT_SECRET`, { secret: true, area: 'social_django', setting: `SOCIAL_AUTH_${prefix}_SECRET` });
|
|
1310
|
+
const scopeS = take(`SOCIAL_AUTH_${prefix}_SCOPE`);
|
|
1311
|
+
b.oauthProvider({ sourceId, atlasKey, clientId, clientSecret, scopes: pyStrings(scopeS?.value).map((s) => s.v), area: 'social_django', setting, at: where });
|
|
1312
|
+
}
|
|
1313
|
+
if (app('social_django')) {
|
|
1314
|
+
b.add('differs', 'social_django', 'social_django', 'python-social-auth links (UserSocialAuth) are not in the export: users relink on their first social sign-in, by verified email (account linking auto).', appAt('social_django'), 'auth_config.accountLinking');
|
|
1315
|
+
if (b.get('accountLinking') === undefined)
|
|
1316
|
+
b.set('accountLinking', 'auto');
|
|
1317
|
+
}
|
|
1318
|
+
// MFA.
|
|
1319
|
+
if (app('allauth.mfa')) {
|
|
1320
|
+
const typesS = take('MFA_SUPPORTED_TYPES');
|
|
1321
|
+
const types = typesS ? pyStrings(typesS.value).map((t) => t.v) : ['recovery_codes', 'totp'];
|
|
1322
|
+
const where = typesS ? at(typesS) : appAt('allauth.mfa');
|
|
1323
|
+
const setting = typesS ? 'MFA_SUPPORTED_TYPES' : 'MFA_SUPPORTED_TYPES (default)';
|
|
1324
|
+
b.set('mfaPolicy', 'optional');
|
|
1325
|
+
if (types.includes('totp')) {
|
|
1326
|
+
b.set('mfa.factors.totp', true);
|
|
1327
|
+
b.add('mapped', 'mfa', `${setting} totp`, 'Authenticator-app (TOTP) MFA on, optional per user. Enrolled authenticators carry over (the export script decrypts their secrets).', where, 'auth_config.mfa.factors.totp');
|
|
1328
|
+
}
|
|
1329
|
+
if (types.includes('recovery_codes')) {
|
|
1330
|
+
const countS = take('MFA_RECOVERY_CODE_COUNT');
|
|
1331
|
+
const count = num(countS) ?? 10;
|
|
1332
|
+
b.set('mfa.backupCodes.count', (0, report_1.clamp)(count, 1, 20));
|
|
1333
|
+
b.add('mapped', 'mfa', `${setting} recovery_codes`, `${count} recovery codes per user. Unused codes carry over with the export script.`, where, 'auth_config.mfa.backupCodes.count');
|
|
1334
|
+
if (countS)
|
|
1335
|
+
b.add(count === (0, report_1.clamp)(count, 1, 20) ? 'mapped' : 'differs', 'mfa', 'MFA_RECOVERY_CODE_COUNT', `${(0, report_1.clamp)(count, 1, 20)} codes.`, at(countS), 'auth_config.mfa.backupCodes.count');
|
|
1336
|
+
}
|
|
1337
|
+
if (types.includes('webauthn')) {
|
|
1338
|
+
b.set('mfa.factors.passkey', true);
|
|
1339
|
+
b.add('mapped', 'mfa', `${setting} webauthn`, 'Security keys and passkeys as a second factor. Registered keys carry over when you pass the RP id to the export (--set rp-id=<your domain>).', where, 'auth_config.mfa.factors.passkey');
|
|
1340
|
+
}
|
|
1341
|
+
const passkeyLogin = take('MFA_PASSKEY_LOGIN_ENABLED');
|
|
1342
|
+
if (passkeyLogin) {
|
|
1343
|
+
b.set('strategies.passkey', bool(passkeyLogin) === true);
|
|
1344
|
+
b.add('mapped', 'mfa', 'MFA_PASSKEY_LOGIN_ENABLED', bool(passkeyLogin) ? 'Passkey sign-in on.' : 'No passkey sign-in.', at(passkeyLogin), 'auth_config.strategies.passkey');
|
|
1345
|
+
}
|
|
1346
|
+
for (const [name, key] of [['MFA_TOTP_PERIOD', 'mfa.totp.period'], ['MFA_TOTP_DIGITS', 'mfa.totp.digits']]) {
|
|
1347
|
+
const s = take(name);
|
|
1348
|
+
if (s && num(s) !== undefined) {
|
|
1349
|
+
b.set(key, num(s));
|
|
1350
|
+
b.add('mapped', 'mfa', name, `${num(s)}.`, at(s), `auth_config.${key}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
for (const otp of ['django_otp', 'two_factor']) {
|
|
1355
|
+
if (app(otp))
|
|
1356
|
+
b.add('manual', 'mfa', otp, `${otp === 'two_factor' ? 'django-two-factor-auth' : 'django-otp'} devices are not in the export: users enrol their authenticator again in Atlas (turn on mfa.factors.totp).`, appAt(otp), 'auth_config.mfa.factors.totp');
|
|
1357
|
+
}
|
|
1358
|
+
if (app('allauth.usersessions'))
|
|
1359
|
+
b.add('mapped', 'allauth', 'allauth.usersessions', "Atlas lists a user's sessions and lets them sign out others itself.", appAt('allauth.usersessions'));
|
|
1360
|
+
// Headless → lazy migration.
|
|
1361
|
+
if (headless) {
|
|
1362
|
+
const loginField = loginMethods && !loginMethods.includes('email') && loginMethods.includes('username') ? 'username' : undefined;
|
|
1363
|
+
b.lazy = { preset: 'allauth_headless', config: loginField ? { login_field: loginField } : {} };
|
|
1364
|
+
b.add('mapped', 'allauth', 'allauth.headless', `Lazy migration can check passwords against your app's headless login endpoint (preset allauth_headless, your site URL as base_url)${customHasher ? ': needed for the custom hasher' : ', for any hash that does not carry over'}.`, appAt('allauth.headless'), 'lazy migration allauth_headless');
|
|
1365
|
+
}
|
|
1366
|
+
const frontendUrls = take('HEADLESS_FRONTEND_URLS');
|
|
1367
|
+
if (frontendUrls)
|
|
1368
|
+
b.add('manual', 'allauth', 'HEADLESS_FRONTEND_URLS', "Links in Atlas's emails open Atlas's hosted pages. To keep your own screens, point the email templates at your URLs and complete the flow with the Atlas frontend API.", at(frontendUrls));
|
|
1369
|
+
for (const name of ['LOGIN_URL', 'LOGIN_REDIRECT_URL', 'LOGOUT_REDIRECT_URL']) {
|
|
1370
|
+
const s = take(name);
|
|
1371
|
+
if (s)
|
|
1372
|
+
b.add('manual', 'auth', name, `${pyStr(s.value) ?? s.text}: send users to Atlas's sign-in and pass this as the redirect (an allowed redirect URL in Atlas).`, at(s), 'redirect_urls');
|
|
1373
|
+
}
|
|
1374
|
+
// Everything else that looks auth-related.
|
|
1375
|
+
for (const [name, s] of settings) {
|
|
1376
|
+
if (used.has(name) || !DJANGO_AUTH_PREFIXES.test(name))
|
|
1377
|
+
continue;
|
|
1378
|
+
if (DJANGO_PLUMBING[name])
|
|
1379
|
+
b.add('mapped', 'settings', name, `Nothing to carry over: ${DJANGO_PLUMBING[name]}`, at(s));
|
|
1380
|
+
else
|
|
1381
|
+
b.add('manual', 'settings', name, `Not translated (${snippet(name, s.text)}): check whether your app relies on it.`, at(s));
|
|
1382
|
+
}
|
|
1383
|
+
for (const o of overrides) {
|
|
1384
|
+
const base = settings.get(o.name);
|
|
1385
|
+
if (!used.has(o.name) && !DJANGO_AUTH_PREFIXES.test(o.name) && o.name !== 'SECRET_KEY')
|
|
1386
|
+
continue;
|
|
1387
|
+
b.add('manual', 'settings', o.name, `Also set in ${o.file}:${o.line} (${snippet(o.name, o.text)}); the translation used ${base.file}:${base.line}. Check which one production runs with.`, { file: o.file, line: o.line });
|
|
1388
|
+
}
|
|
1389
|
+
return b.result();
|
|
1390
|
+
}
|
|
1391
|
+
//# sourceMappingURL=translate-rails-django.js.map
|