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
103 changes: 101 additions & 2 deletions scripts/bootstrap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import {
applyDashboardClientChanges,
applyMyAccountClientGrantChanges,
DEFAULT_CLIENT_NAME,
setClientName,
} from "./utils/clients.mjs"
import { applyDatabaseConnectionChanges } from "./utils/connections.mjs"
import {
Expand All @@ -11,7 +13,7 @@ import {
} from "./utils/discovery.mjs"
import { writeAuth0Plist } from "./utils/plist-writer.mjs"
import { writeInfoPlistUrlScheme } from "./utils/info-plist-writer.mjs"
import { confirmWithUser } from "./utils/helpers.mjs"
import { confirmWithUser, promptWithUser } from "./utils/helpers.mjs"
import { getManualActions } from "./utils/manual-actions.mjs"
import {
applyMyAccountResourceServerChanges,
Expand Down Expand Up @@ -47,13 +49,29 @@ 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> [--yes]")
console.log(
"Usage: npm run auth0:bootstrap <tenant-domain> [--client-name=<name>] [--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(" --client-name=<name>")
console.log(
" Name of the native Auth0 application. Interactive runs"
)
console.log(
` are prompted for it; the default is "${DEFAULT_CLIENT_NAME}".`
)
console.log(" Env: AUTH0_CLIENT_NAME")
console.log(
" The name is the lookup key on re-runs: reusing a name"
)
console.log(
" updates that application, a new name creates a new one."
)
console.log(
" --yes, -y Skip the confirmation prompt and apply changes."
)
Expand Down Expand Up @@ -125,6 +143,13 @@ async function main() {
const scheme = iosConfig.bundleIdentifier
iosConfig.scheme = scheme

// Resolve the native application's name before discovery: it is the key used
// to find an existing app (exact name + app_type "native"), so the change plan
// depends on it. Held in module state via setClientName so every check/apply
// reads the same name.
setClientName(await resolveClientName(flags))
console.log("")

// Step 2: Discovery
step("🔍", "Resource Discovery")
const resources = await discoverExistingResources(domain)
Expand Down Expand Up @@ -317,6 +342,80 @@ function reportManualActions() {
)
}

/**
* Reject client names the Management API would reject, before spending a
* round-trip on them. Auth0 requires a non-empty name without `<` or `>`.
*
* @param {string} name - Candidate client name (already trimmed)
* @returns {string | null} An error message, or null when the name is valid
*/
function validateClientName(name) {
if (!name) return "Name cannot be empty."
if (/[<>]/.test(name)) return "Name cannot contain < or >."
return null
}

/**
* Decide what to name the native Auth0 application for this run.
*
* Precedence: `--client-name=<name>` → `AUTH0_CLIENT_NAME` → interactive prompt
* (defaulting to DEFAULT_CLIENT_NAME) → the default itself when stdin is not a
* TTY (headless/CI), so a non-interactive run never hangs on input.
*
* The interactive path loops until the user accepts a valid name, so a typo does
* not silently create a stray application in the tenant.
*
* @param {string[]} flags - CLI flags from argv
* @returns {Promise<string>} The client name to use
*/
async function resolveClientName(flags) {
const prefix = "--client-name="
const flagValue = flags
.find((f) => f.startsWith(prefix))
?.slice(prefix.length)
.trim()
const preset = flagValue || process.env.AUTH0_CLIENT_NAME?.trim()

if (preset) {
const error = validateClientName(preset)
if (error) {
throw new Error(
`Invalid client name "${preset}" (--client-name / AUTH0_CLIENT_NAME): ${error}`
)
}
console.log(`✅ Native application name: ${preset}`)
return preset
}

// No TTY means there is nobody to answer the prompt, so take the default
// rather than looping on empty input.
if (!process.stdin.isTTY) {
console.log(`✅ Native application name: ${DEFAULT_CLIENT_NAME} (default)`)
return DEFAULT_CLIENT_NAME
}

console.log(
"\n🏷️ Name of the native Auth0 application to create or update.\n" +
" Reusing a name updates that application; a new name creates a new one.\n" +
" Press Enter to accept the default."
)

for (;;) {
const answer = await promptWithUser(" Application name", DEFAULT_CLIENT_NAME)

const error = validateClientName(answer)
if (error) {
console.log(` ⚠️ ${error}`)
continue
}

if (await confirmWithUser(` Use "${answer}" as the application name?`)) {
return answer
}
console.log(" No problem — enter a different name.")
}
}

// Run the main function
main().catch((error) => {
console.error("\n❌ Bootstrap failed:", error.message)
Expand Down
66 changes: 49 additions & 17 deletions scripts/utils/clients.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,35 @@ import {
recordManualAction,
} from "./manual-actions.mjs"

// Constants
export const CLIENT_NAME = "iOS UI Components Demo"
// The default name for the native Auth0 application. The name actually used for
// a run is resolved once at startup (CLI flag / env / interactive prompt) and
// stored in module state via setClientName. The name is the lookup key — re-runs
// find the app by exact name + app_type "native" — so every check and apply
// below reads it through getClientName() rather than a hardcoded constant.
export const DEFAULT_CLIENT_NAME = "iOS UI Components Demo"

let _clientName = DEFAULT_CLIENT_NAME

/**
* Set the native application's name for this run. Called once from bootstrap
* before discovery, because the change plan (find-or-create the app by exact
* name match) depends on it.
*
* @param {string} name - The resolved application name
*/
export function setClientName(name) {
_clientName = name
}

/**
* The native application's name for this run (defaults to DEFAULT_CLIENT_NAME
* until setClientName is called).
*
* @returns {string}
*/
export function getClientName() {
return _clientName
}

/**
* Build the allowed callback / logout URLs for the native iOS client.
Expand Down Expand Up @@ -81,6 +108,7 @@ export async function checkDashboardClientChanges(
myAccountApiScopes
) {
const { bundleIdentifier } = iosConfig
const clientName = getClientName()

// Auth0.swift's WebAuthentication builds its redirect URL from the bundle
// identifier, not an arbitrary scheme. The two supported forms are the
Expand All @@ -89,14 +117,16 @@ export async function checkDashboardClientChanges(
// whether or not associated domains are configured.
const redirectUrls = buildRedirectUrls(domain, bundleIdentifier)

// Re-runs find the app by exact name + app_type "native": the same name
// updates it, a different name creates a second application (§3.7).
const existingClient = existingClients.find(
(c) => c.name === CLIENT_NAME && c.app_type === "native"
(c) => c.name === clientName && c.app_type === "native"
)

if (!existingClient) {
return createChangeItem(ChangeAction.CREATE, {
resource: "Native Client",
name: CLIENT_NAME,
name: clientName,
redirectUrls,
})
}
Expand Down Expand Up @@ -157,7 +187,7 @@ export async function checkDashboardClientChanges(

return createChangeItem(ChangeAction.UPDATE, {
resource: "Native Client",
name: CLIENT_NAME,
name: clientName,
existing: existingClient,
redirectUrls,
updates,
Expand All @@ -167,7 +197,7 @@ export async function checkDashboardClientChanges(

return createChangeItem(ChangeAction.SKIP, {
resource: "Native Client",
name: CLIENT_NAME,
name: clientName,
existing: existingClient,
})
}
Expand All @@ -181,6 +211,8 @@ export async function applyDashboardClientChanges(
domain,
myAccountApiScopes
) {
const clientName = getClientName()

if (changePlan.action === ChangeAction.SKIP) {
const spinner = ora({
text: `Native Client is up to date: ${changePlan.name}`,
Expand All @@ -191,12 +223,12 @@ export async function applyDashboardClientChanges(

if (changePlan.action === ChangeAction.CREATE) {
const spinner = ora({
text: `Creating Native Client: ${CLIENT_NAME}`,
text: `Creating Native Client: ${clientName}`,
}).start()

try {
const clientData = {
name: CLIENT_NAME,
name: clientName,
description:
"Native client for Auth0 iOS UI Components sample app",
app_type: "native",
Expand Down Expand Up @@ -235,7 +267,7 @@ export async function applyDashboardClientChanges(
const { stdout } = await $`auth0 ${createClientArgs}`
const client = JSON.parse(stdout)

spinner.succeed(`Created Native Client: ${CLIENT_NAME}`)
spinner.succeed(`Created Native Client: ${clientName}`)
return client
} catch (e) {
// The native client is the anchor for everything downstream (client
Expand All @@ -246,7 +278,7 @@ export async function applyDashboardClientChanges(
const scope = extractMissingScope(e) || "create:clients"
spinner.fail(`Cannot create Native Client — M2M app lacks scope: ${scope}`)
recordManualAction({
resource: `Native Client: ${CLIENT_NAME}`,
resource: `Native Client: ${clientName}`,
scope,
reason:
"The native client is required for the sample app to authenticate; the rest of the bootstrap depends on it.",
Expand All @@ -262,7 +294,7 @@ export async function applyDashboardClientChanges(

if (changePlan.action === ChangeAction.UPDATE) {
const spinner = ora({
text: `Updating Native Client: ${CLIENT_NAME}`,
text: `Updating Native Client: ${clientName}`,
}).start()

try {
Expand Down Expand Up @@ -326,7 +358,7 @@ export async function applyDashboardClientChanges(
const { stdout } = await $`auth0 ${getArgs}`
const client = JSON.parse(stdout)

spinner.succeed(`Updated Native Client: ${CLIENT_NAME}`)
spinner.succeed(`Updated Native Client: ${clientName}`)
return client
} catch (e) {
// A missing scope (e.g. update:clients on the M2M app) should not abort
Expand All @@ -338,7 +370,7 @@ export async function applyDashboardClientChanges(
`Skipped updating Native Client — M2M app lacks scope: ${scope}`
)
recordManualAction({
resource: `Native Client: ${CLIENT_NAME}`,
resource: `Native Client: ${clientName}`,
scope,
reason:
"The native client's callback/logout URLs (and My Account refresh-token policy) must be set for the app's login/logout redirects to resolve.",
Expand Down Expand Up @@ -406,6 +438,8 @@ export async function applyMyAccountClientGrantChanges(
domain,
clientId
) {
const clientName = getClientName()

if (changePlan.action === ChangeAction.SKIP) {
const spinner = ora({
text: `My Account API Client Grant is up to date`,
Expand All @@ -416,7 +450,7 @@ export async function applyMyAccountClientGrantChanges(

if (changePlan.action === ChangeAction.CREATE) {
const spinner = ora({
text: `Creating ${CLIENT_NAME} client grants for My Account API`,
text: `Creating ${clientName} client grants for My Account API`,
}).start()

try {
Expand All @@ -435,7 +469,7 @@ export async function applyMyAccountClientGrantChanges(
spinner.succeed(`Created My Account API Client Grant`)
} catch (e) {
spinner.fail(
`Failed to create the ${CLIENT_NAME} client grants for My Account API`
`Failed to create the ${clientName} client grants for My Account API`
)
throw e
}
Expand Down Expand Up @@ -464,5 +498,3 @@ export async function applyMyAccountClientGrantChanges(
}
}
}


Loading
Loading