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
5 changes: 5 additions & 0 deletions .changeset/native-apple-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": patch
---

Recognize native-only Sign in with Apple in `clerk deploy` and `clerk deploy status`. Apple counts as configured once its production Bundle ID registration and Native API are ready; until then deploy offers Apple web credentials or pauses with a link to the production Native Applications page. Doctor recognizes Platform API keys and verifies account access through the Clerk API when OAuth userinfo rejects a valid session. Concurrent OAuth token refreshes in one process now share a single refresh.
27 changes: 16 additions & 11 deletions packages/cli-core/src/commands/deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ In agent mode, `clerk deploy status` emits JSON on stdout with:
- `domainStatus`: per-component DNS, SSL, and email DNS status when a domain exists.
- `pendingDnsRecords`: CNAME records still tied to pending DNS-backed checks, each with `host`, `value`, and Clerk's `required` flag (some targets are optional).
- `oauth`: configured, pending, and unsupported provider slugs.
- `nativeAppleReadinessIssue`: present only when Apple is native-only (a `bundle_id` and no hosted credentials) and not yet ready in production. Development's native-only connection counts when production Apple has neither a Bundle ID nor hosted credentials, as creating production leaves it. Holds the `bundleId`, a `reason` (`bundle-id-missing`, `authentication-disabled`, `registration-missing`, `registration-ambiguous`, `registration-bundle-case-mismatch`, `native-api-disabled`, or `verification-unavailable`), and the production Native Applications `dashboardUrl`. `nextAction` includes the matching guidance.
- `urls`: the production instance's Dashboard page (`instance`) and its Domains page (`domains`), or `null` before a production instance exists. The same URLs appear in `nextAction` prose; this field is the one to read programmatically.
- `nextAction`: the next step an agent should present to the user. While domain setup remains (`domain_provisioning` and `domain_pending`) it includes the Clerk Dashboard domains URL, and agents should ask whether to open that URL for the user; `not_started`, `interrupted`, and `oauth_pending` carry no URL, and `complete` carries the instance root instead. While DNS or email DNS records are unverified it says to add the records in `pendingDnsRecords` at the domain's DNS provider rather than to keep polling; if that list is empty (the API returned no CNAME targets) it says so and points at the Dashboard Domains page instead; when only SSL is pending it says to wait. At `complete` it says the production keys still have to reach the host (`clerk env pull --instance prod`, alongside the other Clerk variables in the env file) and to sign up on the domain to confirm — "complete" is Clerk's side only — and links the instance root (users, settings, billing) instead of the domains page, since nothing is left to monitor there. At `complete` and `oauth_pending` it also names any providers in `oauth.unsupported`, since `oauth.complete` covers only what the CLI manages and those providers' sign-in fails in production until configured in the Dashboard. `oauth_pending` carries no Domains URL (the domain is verified). Human mode prints its own sentence, rendered from the same classification of the report (`deployNextStep` in `status.ts`) rather than by rewriting the agent's: no unsupported-provider clause (the warning row above already says it), no "ask the user" (the reader is the user), no `--wait` (human mode always waits; the wizard is what resumes setup), and, when records are pending, the records themselves printed first so the sentence only says what happens once they are added. Before a production instance exists, human mode omits the OAuth row rather than printing "pending: none" for something that was never checked.

Expand Down Expand Up @@ -168,17 +169,19 @@ sequenceDiagram

All endpoints are on the **Platform API** (`/v1/platform/...`) and are live HTTP calls. The deploy command calls the helpers in `lib/plapi.ts` directly.

| Step | Method | Endpoint | Helper |
| --------------------------- | ------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Auth | n/a | Local config | Token stored from `clerk auth login` or `CLERK_PLATFORM_API_KEY`. |
| Read instance config | `GET` | `/v1/platform/applications/{appID}/instances/{instanceID}/config` | `fetchInstanceConfig` from `lib/plapi.ts`. Discovers enabled `connection_oauth_*` providers. |
| Read instance config schema | `GET` | `/v1/platform/applications/{appID}/instances/{instanceID}/config/schema` | `fetchInstanceConfigSchema`. Reads schemas for supported OAuth config keys so deploy can derive credential prompts. |
| Patch instance config | `PATCH` | `/v1/platform/applications/{appID}/instances/{instanceID}/config` | `patchInstanceConfig`. Writes production OAuth credentials. |
| Read application | `GET` | `/v1/platform/applications/{appID}` | `fetchApplication`. Resolves development and production instance IDs. |
| List production domains | `GET` | `/v1/platform/applications/{appID}/domains` | `listApplicationDomains`. Recovers production domain name and CNAME targets on each run. |
| Create production instance | `POST` | `/v1/platform/applications/{appID}/instances` | `createProductionInstance`. Returns prod instance, primary domain, keys, and DNS records nested under `active_domain.cname_targets[]`. |
| Trigger domain DNS check | `POST` | `/v1/platform/applications/{appID}/domains/{domainIDOrName}/dns_check` | `triggerApplicationDomainDNSCheck`. Called by the wizard and by `clerk deploy status`; agent mode waits briefly, then reads one status snapshot. |
| Poll domain status | `GET` | `/v1/platform/applications/{appID}/domains/{domainIDOrName}/status` | `getApplicationDomainStatus`. Drives the wizard spinner and the human-mode `clerk deploy status` wait loop over the full domain status response. |
| Step | Method | Endpoint | Helper |
| --------------------------- | ------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Auth | n/a | Local config | Token stored from `clerk auth login` or `CLERK_PLATFORM_API_KEY`. |
| Read instance config | `GET` | `/v1/platform/applications/{appID}/instances/{instanceID}/config` | `fetchInstanceConfig` from `lib/plapi.ts`. Discovers enabled `connection_oauth_*` providers. |
| Read instance config schema | `GET` | `/v1/platform/applications/{appID}/instances/{instanceID}/config/schema` | `fetchInstanceConfigSchema`. Reads schemas for supported OAuth config keys so deploy can derive credential prompts. |
| Patch instance config | `PATCH` | `/v1/platform/applications/{appID}/instances/{instanceID}/config` | `patchInstanceConfig`. Writes production OAuth credentials. |
| Read application | `GET` | `/v1/platform/applications/{appID}` | `fetchApplication`. Resolves development and production instance IDs. |
| List production domains | `GET` | `/v1/platform/applications/{appID}/domains` | `listApplicationDomains`. Recovers production domain name and CNAME targets on each run. |
| Read native settings | `GET` | `/v1/platform/applications/{appID}/instances/{instanceID}/native_settings` | `getNativeSettings`. Checks Native API for native-only Apple readiness. |
| List iOS registrations | `GET` | `/v1/platform/applications/{appID}/instances/{instanceID}/native_applications/ios` | `listIOSApplications`. Checks the production registration for a native-only Apple Bundle ID. |
| Create production instance | `POST` | `/v1/platform/applications/{appID}/instances` | `createProductionInstance`. Returns prod instance, primary domain, keys, and DNS records nested under `active_domain.cname_targets[]`. |
| Trigger domain DNS check | `POST` | `/v1/platform/applications/{appID}/domains/{domainIDOrName}/dns_check` | `triggerApplicationDomainDNSCheck`. Called by the wizard and by `clerk deploy status`; agent mode waits briefly, then reads one status snapshot. |
| Poll domain status | `GET` | `/v1/platform/applications/{appID}/domains/{domainIDOrName}/status` | `getApplicationDomainStatus`. Drives the wizard spinner and the human-mode `clerk deploy status` wait loop over the full domain status response. |

## OAuth Provider Config Format

Expand Down Expand Up @@ -209,6 +212,8 @@ The CLI keeps small local overrides for provider setup details that schema does
| Google | Optional Google Cloud Console JSON import, OAuth consent screen warning, and a tip that the consent-screen name is what users see, so choose the name you want them to see |
| Apple | `.p8` file import, production-required `team_id` and `key_id`, native-only field omissions |

Apple configured with only a `bundle_id` (no `client_id`, `client_secret`, `team_id`, or `key_id`, matching the backend's rule) is native-only. It counts as configured once Apple is enabled for authentication, the Bundle ID has exactly one production iOS registration, and Native API is enabled. Until then the wizard offers to add Apple web credentials; declining prints the fix with a link to the production Native Applications page and pauses the deploy, as skipping any provider does.

For Google, the wizard can load `client_id` and `client_secret` from the top-level `web` object in a Google Cloud Console OAuth client JSON file, or from `installed` for desktop-style client downloads. The file contents are used in memory and are not written to CLI config.

Providers not currently supported by automated deploy setup are cloned to production without automated credential setup. Configure those providers from the Clerk Dashboard before going live.
Expand Down
29 changes: 29 additions & 0 deletions packages/cli-core/src/commands/deploy/copy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { bold, cyan, dim, green, yellow } from "../../lib/color.ts";
import type { CnameTarget } from "../../lib/plapi.ts";
import type { NativeAppleReadinessIssue } from "./providers.ts";
import { buildDashboardUrl } from "../../lib/environment.ts";
import { wrap } from "../../lib/wrap.ts";

Expand Down Expand Up @@ -608,6 +609,34 @@ export function domainsDashboardUrl(appId: string, productionInstanceId: string)
return buildDashboardUrl(appId, productionInstanceId, "domains");
}

export function nativeApplicationsDashboardUrl(
appId: string,
productionInstanceId: string,
): string {
return buildDashboardUrl(appId, productionInstanceId, "native-applications");
}

/** What blocks native-only Sign in with Apple in production, and how to fix it. */
export function nativeAppleGuidance(issue: NativeAppleReadinessIssue): string {
const { bundleId, dashboardUrl } = issue;
switch (issue.reason) {
case "bundle-id-missing":
return `Production Sign in with Apple has no Bundle ID, so native sign-in for ${bundleId} isn't set up there yet. In the Clerk Dashboard, set the production Apple connection's Bundle ID to ${bundleId}, then register the app and enable Native API at ${dashboardUrl}`;
case "authentication-disabled":
return `Sign in with Apple for ${bundleId} is not enabled for authentication in production. Enable it in the Apple connection settings in the Clerk Dashboard.`;
case "registration-missing":
return `Sign in with Apple needs a production iOS registration for ${bundleId}. Register it at ${dashboardUrl}`;
case "registration-ambiguous":
return `${bundleId} is registered under more than one App ID Prefix in production. Remove the extra registrations at ${dashboardUrl}`;
case "registration-bundle-case-mismatch":
return `The Apple connection's Bundle ID ${bundleId} differs in letter case from its production iOS registration. Make them match exactly at ${dashboardUrl}`;
case "native-api-disabled":
return `Native API is disabled on the production instance. Enable it at ${dashboardUrl}`;
case "verification-unavailable":
return `Clerk could not verify the production iOS registration for ${bundleId}. Retry shortly, and don't create another registration based on this result.`;
}
}

export function pausedMessage(stepDescription: string): string {
return `Deploy paused at: ${stepDescription}

Expand Down
Loading
Loading