Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/scripts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.env
.env.local
247 changes: 202 additions & 45 deletions app/scripts/bootstrap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "./utils/discovery.mjs"
import { writeStringsFile } from "./utils/strings-writer.mjs"
import { confirmWithUser } from "./utils/helpers.mjs"
import { getManualActions } from "./utils/manual-actions.mjs"
import {
applyConnectionProfileChanges,
applyUserAttributeProfileChanges,
Expand All @@ -24,12 +25,18 @@ import {
applyPromptSettingsChanges,
applyTenantSettingsChanges,
} from "./utils/tenant-config.mjs"
import { applyGuardianFactorChanges } from "./utils/guardian-factors.mjs"
import {
checkAuth0CLI,
checkNodeVersion,
hasMachineCredentials,
printScopeUsageDetails,
validateAndroidProject,
validateAuth0Session,
validateMyAccountScopes,
validateTenant,
} from "./utils/validation.mjs"
import { ChangeAction } from "./utils/change-plan.mjs"

// ============================================================================
// Main Bootstrap Flow
Expand All @@ -42,61 +49,112 @@ async function main() {
const args = process.argv.slice(2)

if (args.includes("--help") || args.includes("-h")) {
console.log("Usage: npm run auth0:bootstrap <tenant-domain>")
console.log("Usage: npm run auth0:bootstrap <tenant-domain> [--yes]")
console.log("\nArguments:")
console.log(
" tenant-domain Required. The Auth0 tenant domain to configure."
)
console.log(" Must match your Auth0 CLI active tenant.")
console.log("\nOptions:")
console.log(
" --yes, -y Skip the confirmation prompt and apply changes."
)
console.log(
" Auto-enabled when M2M credentials are set and stdin is"
)
console.log(" not a TTY (headless/CI). Env: AUTH0_BOOTSTRAP_YES=1")
console.log("\nExample:")
console.log(" npm run auth0:bootstrap my-tenant.us.auth0.com")
console.log("\nPrerequisites:")
console.log(" 1. Install Auth0 CLI: https://github.com/auth0/auth0-cli")
console.log(" 2. Login to Auth0 CLI: auth0 login")
console.log(" 3. Select your tenant: auth0 tenants use <tenant-domain>")
console.log(
"\nNote: The script checks your Auth0 CLI session and, if needed, logs you"
)
console.log(
" in and switches to the requested tenant automatically."
)
console.log("\nNon-interactive (standalone / CI) login:")
console.log(
" Set these environment variables for a Management-API M2M app and the"
)
console.log(
" script authenticates via client credentials — no browser required:"
)
console.log(" AUTH0_CLIENT_ID Client ID of the M2M application")
console.log(" AUTH0_CLIENT_SECRET Client secret of the M2M application")
console.log(
" AUTH0_DOMAIN Tenant domain (optional; defaults to the arg)"
)
console.log(
"\nNote: Tenant name is required as a safety measure to prevent accidentally"
)
console.log(" configuring the wrong tenant.")
// Expand the full per-scope rationale for users who want to know why each
// Management API permission is requested at login.
printScopeUsageDetails()
process.exit(0)
}

const tenantName = args[0]
// Flags: --yes/-y (or AUTH0_BOOTSTRAP_YES) skip the confirm prompt. The first
// non-flag argument is the tenant domain.
const flags = args.filter((a) => a.startsWith("-"))
const tenantName = args.find((a) => !a.startsWith("-"))
const yesFlag =
flags.includes("--yes") ||
flags.includes("-y") ||
process.env.AUTH0_BOOTSTRAP_YES === "1"

// Consistent step numbering across the run. TOTAL is the count of top-level
// steps below; bump it if you add/remove one.
const TOTAL_STEPS = 5
let stepNo = 0
const step = (emoji, title) =>
console.log(`\n${emoji} Step ${++stepNo}/${TOTAL_STEPS}: ${title}`)

// Step 1: Validation
console.log("📋 Step 1: Pre-flight Checks")
step("📋", "Pre-flight Checks")
checkNodeVersion()
await checkAuth0CLI()
await validateAuth0Session(tenantName)
const domain = await validateTenant(tenantName)
const androidConfig = validateAndroidProject()
const scheme = "demo" // Default scheme for Android sample app
// Auth0.Android's WebAuthProvider uses the configured scheme (mirrored by the
// auth0Scheme manifest placeholder) for the custom-scheme callback. The sample
// app's MainActivity calls .withScheme("demo"), so the registered redirect and
// the strings.xml value must both be "demo" for the OAuth redirect to reach it.
const scheme = "demo"
androidConfig.scheme = scheme
console.log("")

// Step 2: Discovery
console.log("🔍 Step 2: Resource Discovery")
step("🔍", "Resource Discovery")
const resources = await discoverExistingResources(domain)
console.log("")
validateMyAccountScopes(resources, domain)

// Step 3: Build Change Plan
console.log("📝 Step 3: Analyzing Changes")
step("📝", "Analyzing Changes")
const plan = await buildChangePlan(resources, domain, androidConfig)
console.log("")

// Step 4: Display Plan
displayChangePlan(plan)

// Check if there are any changes to apply
const hasChanges =
plan.clients.dashboard.action !== "skip" ||
plan.clientGrants.myAccount.action !== "skip" ||
plan.connection.action !== "skip" ||
plan.connectionProfile.action !== "skip" ||
plan.userAttributeProfile.action !== "skip" ||
plan.resourceServer.action !== "skip" ||
plan.roles.admin.action !== "skip" ||
plan.tenantConfig.settings.action !== "skip" ||
plan.tenantConfig.prompts.action !== "skip"
// Flatten the plan into a single list so change detection and the end-of-run
// summary work off the same source of truth.
const planItems = [
plan.tenantConfig.settings,
plan.tenantConfig.prompts,
plan.connectionProfile,
plan.userAttributeProfile,
plan.resourceServer,
plan.clients.dashboard,
plan.clientGrants.myAccount,
plan.connection,
plan.roles.admin,
plan.guardianFactors,
]
const countByAction = (action) =>
planItems.filter((i) => i.action === action).length
const hasChanges = planItems.some((i) => i.action !== ChangeAction.SKIP)

if (!hasChanges) {
console.log(
Expand All @@ -115,29 +173,46 @@ async function main() {
console.log("\n✅ strings.xml updated!\n")
}

// Even when nothing changed on the tenant, still show how to build and run
// the sample app — this is the path taken on every re-run of a
// fully-configured tenant, so the build/run guidance must not be skipped.
printNextSteps()
process.exit(0)
}

// Step 5: User Confirmation
const confirmed = await confirmWithUser(
"Do you want to proceed with these changes? "
)
if (!confirmed) {
console.log("\n❌ Bootstrap cancelled by user.\n")
process.exit(0)
// User Confirmation. Skip the prompt when --yes is set, or automatically in a
// non-interactive M2M run (no TTY) so the "standalone/CI" path doesn't hang.
const autoConfirm =
yesFlag || (hasMachineCredentials(tenantName) && !process.stdin.isTTY)

if (autoConfirm) {
console.log(
`\n▶️ Proceeding with ${countByAction(ChangeAction.CREATE)} create, ` +
`${countByAction(ChangeAction.UPDATE)} update ` +
`(auto-confirmed${yesFlag ? " via --yes" : " — non-interactive M2M run"}).`
)
} else {
const confirmed = await confirmWithUser(
"Do you want to proceed with these changes? "
)
if (!confirmed) {
console.log("\n❌ Bootstrap cancelled by user.\n")
process.exit(0)
}
}
console.log("")

// Step 6: Apply Changes
console.log("⚙️ Step 4: Applying Changes\n")
// Step 4: Apply Changes
step("⚙️ ", "Applying Changes")
console.log("")

// 6a. Tenant Configuration
// 4a. Tenant Configuration
console.log("Configuring Tenant...")
await applyTenantSettingsChanges(plan.tenantConfig.settings)
await applyPromptSettingsChanges(plan.tenantConfig.prompts)
console.log("")

// 6b. Profiles
// 4b. Profiles
console.log("Configuring Profiles...")
const connectionProfile = await applyConnectionProfileChanges(
plan.connectionProfile
Expand All @@ -147,12 +222,12 @@ async function main() {
)
console.log("")

// 6c. Resource Server (My Account API)
// 4c. Resource Server (My Account API)
console.log("Configuring My Account API...")
await applyMyAccountResourceServerChanges(plan.resourceServer, domain)
console.log("")

// 6d. Native Client
// 4d. Native Client
console.log("Configuring Native Client...")
const dashboardClient = await applyDashboardClientChanges(
plan.clients.dashboard,
Expand All @@ -163,7 +238,7 @@ async function main() {
)
console.log("")

// 6e. Client Grants
// 4e. Client Grants
console.log("Configuring Client Grants...")
await applyMyAccountClientGrantChanges(
plan.clientGrants.myAccount,
Expand All @@ -172,21 +247,29 @@ async function main() {
)
console.log("")

// 6f. Database Connection
// 4f. Database Connection
console.log("Configuring Database Connection...")
const connection = await applyDatabaseConnectionChanges(
plan.connection,
dashboardClient.client_id
)
console.log("")

// 6g. Roles
// 4g. Roles
console.log("Configuring Roles...")
await applyAdminRoleChanges(plan.roles.admin)
console.log("")

// Step 7: Generate strings.xml
console.log("📝 Step 5: Generating strings.xml\n")
// 4h. MFA Factors (WebAuthn / Passkey)
console.log("Configuring MFA Factors...")
await applyGuardianFactorChanges(plan.guardianFactors)
console.log("")

// Step 5: Generate strings.xml (domain + client id + callback scheme). On
// Android this single file wires both the SDK config and the RedirectActivity
// (via the auth0Domain / auth0Scheme manifest placeholders).
step("📝", "Generating strings.xml")
console.log("")
await writeStringsFile(
domain,
dashboardClient.client_id,
Expand All @@ -196,13 +279,87 @@ async function main() {

// Done!
console.log("\n✅ Bootstrap complete!\n")

// Summary of what the plan intended, plus anything that needs manual follow-up.
const manualCount = getManualActions().length
console.log(
`Summary: ${countByAction(ChangeAction.CREATE)} created, ` +
`${countByAction(ChangeAction.UPDATE)} updated, ` +
`${countByAction(ChangeAction.SKIP)} already up to date` +
(manualCount > 0 ? `, ${manualCount} need manual attention` : "") +
".\n"
)

reportManualActions()

printNextSteps()
}

/**
* Print the build-and-run guidance shown at the end of the bootstrap. Kept as a
* shared helper so it is printed on BOTH exit paths — after a full apply run and
* after the "already configured" short-circuit — rather than only when changes
* were applied.
*/
function printNextSteps() {
console.log("Next steps:")
console.log(" 1. Review the updated strings.xml file")
console.log(" 2. Build the sample app:")
console.log(" ./gradlew :app:assembleDebug")
console.log(" 3. Run the sample app:")
console.log(" ./gradlew :app:installDebug")
console.log(" 4. Login and explore the MFA components\n")
console.log(
" 1. Review the updated app/src/main/res/values/strings.xml (domain,"
)
console.log(
" client id, and callback scheme have already been configured for you)."
)
console.log(
" 2. From the project root, build the debug APK of the sample app:"
)
console.log(" ./gradlew :app:assembleDebug")
console.log(
" 3. With an emulator running or a device connected (adb devices), install"
)
console.log(" and launch the sample app:")
console.log(" ./gradlew :app:installDebug")
console.log(
" adb shell am start -n com.auth0.android.sample/.MainActivity"
)
console.log(" 4. Log in and explore the Passkey + MFA components.\n")
console.log(
" ⚠️ If the Gradle build fails (e.g. it cannot download dependencies or"
)
console.log(
" times out resolving artifacts), turn off your VPN if connected and"
)
console.log(" try the build again.\n")
}

/**
* Print a consolidated list of operations that were skipped because the
* authenticated identity lacked the required Management API scope. Each entry
* names the scope to grant (or the dashboard step to perform) so the tenant
* can be finished without re-running everything blindly.
*/
function reportManualActions() {
const actions = getManualActions()
if (actions.length === 0) return

console.log(
"⚠️ Some steps need manual attention (the login identity lacked the scope):\n"
)
actions.forEach((a, i) => {
console.log(` ${i + 1}. ${a.resource}`)
if (a.scope) console.log(` Missing scope: ${a.scope}`)
if (a.reason) console.log(` Why it matters: ${a.reason}`)
if (a.manualStep) console.log(` Fix: ${a.manualStep}`)
console.log("")
})
console.log(
" Tip: grant the scope(s) above to your M2M app (Dashboard → Applications →"
)
console.log(
" APIs → Auth0 Management API → Machine to Machine Applications), then re-run"
)
console.log(
" the bootstrap. It is idempotent — completed resources will be skipped.\n"
)
}

// Run the main function
Expand Down
Loading
Loading