From b49459afc74016fb5540e86e38efea4ceb1241f1 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 27 May 2026 12:58:20 -0700 Subject: [PATCH 1/4] feat(backend): dual-accept hexclave-mobile-oauth-url:// alongside legacy scheme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mobile OAuth URL scheme is a Tier 0 wire identifier (see RENAME-TO-HEXCLAVE.md). It is baked into customer iOS/macOS apps via Info.plist, so the backend must accept the new canonical 'hexclave-mobile-oauth-url://' without ever dropping the legacy 'stack-auth-mobile-oauth-url://' — removing the latter would break OAuth for every already-shipped customer app built against the frozen StackAuth Swift SDK. - packages/stack-shared: isAcceptedNativeAppUrl() now returns true for either protocol. - apps/backend tests: assert the new scheme is accepted by isAcceptedNativeAppUrl and (parity with old) rejected by validateRedirectUrl in the no-trusted-domain case. - sdks/spec/client-app.spec.md: document both schemes; canonical for new SDKs is the Hexclave one. Out of scope: the frozen StackAuth Swift SDK keeps registering and emitting the legacy scheme; the new Hexclave Swift package (which will use the new scheme) is not yet created. --- apps/backend/src/lib/redirect-urls.test.tsx | 13 ++++++++++++- packages/stack-shared/src/utils/redirect-urls.tsx | 8 +++++++- sdks/spec/src/apps/client-app.spec.md | 8 +++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/lib/redirect-urls.test.tsx b/apps/backend/src/lib/redirect-urls.test.tsx index 41cd3799cc..6d8ff60839 100644 --- a/apps/backend/src/lib/redirect-urls.test.tsx +++ b/apps/backend/src/lib/redirect-urls.test.tsx @@ -612,6 +612,9 @@ describe('validateRedirectUrl', () => { expect(validateRedirectUrl('stack-auth-mobile-oauth-url://success', tenancy)).toBe(false); expect(validateRedirectUrl('stack-auth-mobile-oauth-url://error', tenancy)).toBe(false); expect(validateRedirectUrl('stack-auth-mobile-oauth-url://oauth-callback', tenancy)).toBe(false); + expect(validateRedirectUrl('hexclave-mobile-oauth-url://success', tenancy)).toBe(false); + expect(validateRedirectUrl('hexclave-mobile-oauth-url://error', tenancy)).toBe(false); + expect(validateRedirectUrl('hexclave-mobile-oauth-url://oauth-callback', tenancy)).toBe(false); }); it('should not accept other custom schemes without trusted domain config', () => { @@ -631,15 +634,23 @@ describe('validateRedirectUrl', () => { }); describe('isAcceptedNativeAppUrl', () => { - it('should accept the native app OAuth URL scheme', () => { + it('should accept the legacy native app OAuth URL scheme', () => { expect(isAcceptedNativeAppUrl('stack-auth-mobile-oauth-url://success')).toBe(true); expect(isAcceptedNativeAppUrl('stack-auth-mobile-oauth-url://error')).toBe(true); }); + it('should accept the canonical Hexclave native app OAuth URL scheme', () => { + expect(isAcceptedNativeAppUrl('hexclave-mobile-oauth-url://success')).toBe(true); + expect(isAcceptedNativeAppUrl('hexclave-mobile-oauth-url://error')).toBe(true); + expect(isAcceptedNativeAppUrl('hexclave-mobile-oauth-url://oauth-callback')).toBe(true); + }); + it('should reject other custom schemes', () => { expect(isAcceptedNativeAppUrl('myapp://callback')).toBe(false); expect(isAcceptedNativeAppUrl('stackauth-myapp://callback')).toBe(false); expect(isAcceptedNativeAppUrl('stack-auth://callback')).toBe(false); + expect(isAcceptedNativeAppUrl('hexclave://callback')).toBe(false); + expect(isAcceptedNativeAppUrl('hexclave-mobile-oauth-url-extra://callback')).toBe(false); expect(isAcceptedNativeAppUrl('https://example.com/callback')).toBe(false); expect(isAcceptedNativeAppUrl('http://localhost:3000/callback')).toBe(false); }); diff --git a/packages/stack-shared/src/utils/redirect-urls.tsx b/packages/stack-shared/src/utils/redirect-urls.tsx index 5ac92217cd..028e954d8a 100644 --- a/packages/stack-shared/src/utils/redirect-urls.tsx +++ b/packages/stack-shared/src/utils/redirect-urls.tsx @@ -160,7 +160,13 @@ export function isAcceptedNativeAppUrl(urlOrString: string): boolean { const url = createUrlIfValid(urlOrString); if (!url) return false; - return url.protocol === 'stack-auth-mobile-oauth-url:'; + // Dual-accept the legacy `stack-auth-mobile-oauth-url://` scheme alongside the + // canonical `hexclave-mobile-oauth-url://`. The old scheme is baked into every + // already-shipped customer iOS/macOS app via `Info.plist`; removing it would + // break OAuth for those apps until they pushed an App Store update. Keep both + // accepted indefinitely (see RENAME-TO-HEXCLAVE.md, Tier 0). + return url.protocol === 'stack-auth-mobile-oauth-url:' + || url.protocol === 'hexclave-mobile-oauth-url:'; } export function validateRedirectUrl( diff --git a/sdks/spec/src/apps/client-app.spec.md b/sdks/spec/src/apps/client-app.spec.md index f2785be7da..ee389659b1 100644 --- a/sdks/spec/src/apps/client-app.spec.md +++ b/sdks/spec/src/apps/client-app.spec.md @@ -66,7 +66,8 @@ Note: Additional provider scopes are configured via oauthScopesOnSignIn construc Implementation: 1. Construct full redirect URLs using a fixed callback scheme: - - Native apps: "stack-auth-mobile-oauth-url://success" and "stack-auth-mobile-oauth-url://error" + - Native apps (canonical): "hexclave-mobile-oauth-url://success" and "hexclave-mobile-oauth-url://error" + - Native apps (legacy, still accepted indefinitely): "stack-auth-mobile-oauth-url://success" and "stack-auth-mobile-oauth-url://error" - Browser: Use the configured OAuth callback handler URL as redirect_uri and window.location to construct absolute URLs - Browser: If options.returnTo is provided, pass it as afterCallbackRedirectUrl, not as redirect_uri @@ -82,7 +83,7 @@ Implementation: 4. Open the authorization URL: - Browser: perform redirect according to redirectMethod - - iOS/macOS: ASWebAuthenticationSession with callbackURLScheme: "stack-auth-mobile-oauth-url" + - iOS/macOS: ASWebAuthenticationSession with callbackURLScheme: "hexclave-mobile-oauth-url" (new SDKs) or "stack-auth-mobile-oauth-url" (legacy SDKs; backend accepts both indefinitely) - Android: Custom Tabs with callback URL registered as deep link - Desktop: Open system browser with registered URL scheme for callback @@ -166,7 +167,8 @@ Returns: { url: string, state: string, codeVerifier: string, redirectUrl: string redirectUrl: The redirect URL (same as input, needed for token exchange - must match exactly) Note on URL schemes: -- The "stack-auth-mobile-oauth-url://" scheme is automatically accepted by the backend without any configuration. +- The "hexclave-mobile-oauth-url://" scheme is the canonical scheme for new SDKs and is automatically accepted by the backend without any configuration. +- The legacy "stack-auth-mobile-oauth-url://" scheme is also accepted indefinitely for backwards compatibility with already-shipped customer apps built against the frozen StackAuth Swift SDK. Implementation: 1. Generate or use provided state and codeVerifier From ccdc1a626569a4b0360ee340e7e8ee44b1f53701 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 27 May 2026 13:09:33 -0700 Subject: [PATCH 2/4] feat(swift): emit hexclave-mobile-oauth-url:// from StackAuth SDK; shorten backend comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on the previous commit. Backend now accepts both schemes, so the StackAuth Swift SDK can safely emit the canonical hexclave-mobile-oauth-url scheme without breaking already-shipped customer App Store binaries (those still emit the legacy scheme and the backend still accepts it). ASWebAuthenticationSession's callbackURLScheme: parameter intercepts the callback ephemerally, so no customer Info.plist change is required. - StackClientApp.swift: callbackScheme constant + fatalError example strings. - OAuthTests.swift: test fixture URLs (no assertions depend on the scheme). - StackAuthiOSApp.swift / StackAuthMacOSApp.swift: default values in the example UIs. - swift/README.md: docs point at the new scheme; compat note mentions the legacy one. - spec/client-app.spec.md: tightened wording — old scheme is for already-shipped binaries, not 'the frozen StackAuth Swift SDK' (which now also emits the new scheme). - redirect-urls.tsx: shortened the dual-accept comment to one line. --- packages/stack-shared/src/utils/redirect-urls.tsx | 6 +----- .../StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift | 4 ++-- .../StackAuthiOS/StackAuthiOS/StackAuthiOSApp.swift | 4 ++-- sdks/implementations/swift/README.md | 8 ++++---- .../swift/Sources/StackAuth/StackClientApp.swift | 6 +++--- .../swift/Tests/StackAuthTests/OAuthTests.swift | 4 ++-- sdks/spec/src/apps/client-app.spec.md | 6 +++--- 7 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/stack-shared/src/utils/redirect-urls.tsx b/packages/stack-shared/src/utils/redirect-urls.tsx index 028e954d8a..29baf15572 100644 --- a/packages/stack-shared/src/utils/redirect-urls.tsx +++ b/packages/stack-shared/src/utils/redirect-urls.tsx @@ -160,11 +160,7 @@ export function isAcceptedNativeAppUrl(urlOrString: string): boolean { const url = createUrlIfValid(urlOrString); if (!url) return false; - // Dual-accept the legacy `stack-auth-mobile-oauth-url://` scheme alongside the - // canonical `hexclave-mobile-oauth-url://`. The old scheme is baked into every - // already-shipped customer iOS/macOS app via `Info.plist`; removing it would - // break OAuth for those apps until they pushed an App Store update. Keep both - // accepted indefinitely (see RENAME-TO-HEXCLAVE.md, Tier 0). + // Legacy scheme accepted indefinitely; baked into already-shipped Swift SDK binaries. return url.protocol === 'stack-auth-mobile-oauth-url:' || url.protocol === 'hexclave-mobile-oauth-url:'; } diff --git a/sdks/implementations/swift/Examples/StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift b/sdks/implementations/swift/Examples/StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift index e7df831d16..c12c18063e 100644 --- a/sdks/implementations/swift/Examples/StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift +++ b/sdks/implementations/swift/Examples/StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift @@ -1238,8 +1238,8 @@ class MacOSPresentationContextProvider: NSObject, ASWebAuthenticationPresentatio struct OAuthView: View { @Bindable var viewModel: SDKTestViewModel @State private var provider = "google" - @State private var redirectUrl = "stack-auth-mobile-oauth-url://success" - @State private var errorRedirectUrl = "stack-auth-mobile-oauth-url://error" + @State private var redirectUrl = "hexclave-mobile-oauth-url://success" + @State private var errorRedirectUrl = "hexclave-mobile-oauth-url://error" @State private var isSigningIn = false private let presentationProvider = MacOSPresentationContextProvider() diff --git a/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS/StackAuthiOSApp.swift b/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS/StackAuthiOSApp.swift index 91ef545277..7d0d93ea7a 100644 --- a/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS/StackAuthiOSApp.swift +++ b/sdks/implementations/swift/Examples/StackAuthiOS/StackAuthiOS/StackAuthiOSApp.swift @@ -1252,8 +1252,8 @@ struct ContactChannelsView: View { struct OAuthView: View { @Bindable var viewModel: SDKTestViewModel @State private var provider = "google" - @State private var redirectUrl = "stack-auth-mobile-oauth-url://success" - @State private var errorRedirectUrl = "stack-auth-mobile-oauth-url://error" + @State private var redirectUrl = "hexclave-mobile-oauth-url://success" + @State private var errorRedirectUrl = "hexclave-mobile-oauth-url://error" @State private var isSigningIn = false private let presentationProvider = iOSPresentationContextProvider() diff --git a/sdks/implementations/swift/README.md b/sdks/implementations/swift/README.md index 591d1655f9..f46a483b11 100644 --- a/sdks/implementations/swift/README.md +++ b/sdks/implementations/swift/README.md @@ -88,20 +88,20 @@ Two approaches for OAuth authentication: ```swift // Opens auth session, handles callback automatically -// Uses fixed callback scheme: stack-auth-mobile-oauth-url:// +// Uses fixed callback scheme: hexclave-mobile-oauth-url:// try await stack.signInWithOAuth(provider: "google") ``` **2. Manual URL handling** - For custom implementations: -> **Note:** The `stack-auth-mobile-oauth-url://` scheme is automatically accepted. +> **Note:** The `hexclave-mobile-oauth-url://` scheme is automatically accepted (the legacy `stack-auth-mobile-oauth-url://` scheme also remains accepted for backwards compatibility). ```swift // Get the OAuth URL (must provide absolute URLs) let oauth = try await stack.getOAuthUrl( provider: "google", - redirectUrl: "stack-auth-mobile-oauth-url://success", - errorRedirectUrl: "stack-auth-mobile-oauth-url://error" + redirectUrl: "hexclave-mobile-oauth-url://success", + errorRedirectUrl: "hexclave-mobile-oauth-url://error" ) // Open oauth.url in your own browser/webview diff --git a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift index a4dedfde08..c570be745e 100644 --- a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift +++ b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift @@ -131,10 +131,10 @@ public actor StackClientApp { ) async throws -> OAuthUrlResult { // Validate that URLs are absolute URLs (panic if not - these are programmer errors) guard redirectUrl.contains("://") else { - fatalError("redirectUrl must be an absolute URL (e.g., 'stack-auth-mobile-oauth-url://success')") + fatalError("redirectUrl must be an absolute URL (e.g., 'hexclave-mobile-oauth-url://success')") } guard errorRedirectUrl.contains("://") else { - fatalError("errorRedirectUrl must be an absolute URL (e.g., 'stack-auth-mobile-oauth-url://error')") + fatalError("errorRedirectUrl must be an absolute URL (e.g., 'hexclave-mobile-oauth-url://error')") } let actualState = state ?? generateRandomString(length: 32) @@ -186,7 +186,7 @@ public actor StackClientApp { return } - let callbackScheme = "stack-auth-mobile-oauth-url" + let callbackScheme = "hexclave-mobile-oauth-url" let oauth = try await getOAuthUrl( provider: provider, redirectUrl: callbackScheme + "://success", diff --git a/sdks/implementations/swift/Tests/StackAuthTests/OAuthTests.swift b/sdks/implementations/swift/Tests/StackAuthTests/OAuthTests.swift index 97fbeb249f..caa197f9bf 100644 --- a/sdks/implementations/swift/Tests/StackAuthTests/OAuthTests.swift +++ b/sdks/implementations/swift/Tests/StackAuthTests/OAuthTests.swift @@ -6,8 +6,8 @@ import Foundation struct OAuthTests { // Default test URLs (must be absolute URLs) - let testRedirectUrl = "stack-auth-mobile-oauth-url://success" - let testErrorRedirectUrl = "stack-auth-mobile-oauth-url://error" + let testRedirectUrl = "hexclave-mobile-oauth-url://success" + let testErrorRedirectUrl = "hexclave-mobile-oauth-url://error" // MARK: - OAuth URL Generation Tests diff --git a/sdks/spec/src/apps/client-app.spec.md b/sdks/spec/src/apps/client-app.spec.md index ee389659b1..13eb073ed7 100644 --- a/sdks/spec/src/apps/client-app.spec.md +++ b/sdks/spec/src/apps/client-app.spec.md @@ -83,7 +83,7 @@ Implementation: 4. Open the authorization URL: - Browser: perform redirect according to redirectMethod - - iOS/macOS: ASWebAuthenticationSession with callbackURLScheme: "hexclave-mobile-oauth-url" (new SDKs) or "stack-auth-mobile-oauth-url" (legacy SDKs; backend accepts both indefinitely) + - iOS/macOS: ASWebAuthenticationSession with callbackURLScheme: "hexclave-mobile-oauth-url" (the legacy "stack-auth-mobile-oauth-url" scheme is still accepted by the backend for app binaries built against older SDK versions) - Android: Custom Tabs with callback URL registered as deep link - Desktop: Open system browser with registered URL scheme for callback @@ -167,8 +167,8 @@ Returns: { url: string, state: string, codeVerifier: string, redirectUrl: string redirectUrl: The redirect URL (same as input, needed for token exchange - must match exactly) Note on URL schemes: -- The "hexclave-mobile-oauth-url://" scheme is the canonical scheme for new SDKs and is automatically accepted by the backend without any configuration. -- The legacy "stack-auth-mobile-oauth-url://" scheme is also accepted indefinitely for backwards compatibility with already-shipped customer apps built against the frozen StackAuth Swift SDK. +- The "hexclave-mobile-oauth-url://" scheme is the canonical scheme and is automatically accepted by the backend without any configuration. +- The legacy "stack-auth-mobile-oauth-url://" scheme is also accepted indefinitely for backwards compatibility with already-shipped customer app binaries that linked against older SDK versions. Implementation: 1. Generate or use provided state and codeVerifier From e95b023fc294abcd78cce4ecc7a736dbd3eda0d3 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 27 May 2026 13:13:38 -0700 Subject: [PATCH 3/4] docs(spec): drop legacy scheme references from client-app spec The spec is the source of truth for generating SDK implementations; it should describe the canonical wire behavior only. Backend dual-accept of the legacy 'stack-auth-mobile-oauth-url://' scheme is a backend implementation detail (documented in the code comment at packages/stack-shared/src/utils/redirect-urls.tsx) and git history preserves the previous spec wording. New SDK authors reading this spec now see one canonical scheme. --- sdks/spec/src/apps/client-app.spec.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sdks/spec/src/apps/client-app.spec.md b/sdks/spec/src/apps/client-app.spec.md index 13eb073ed7..ea76075d65 100644 --- a/sdks/spec/src/apps/client-app.spec.md +++ b/sdks/spec/src/apps/client-app.spec.md @@ -66,8 +66,7 @@ Note: Additional provider scopes are configured via oauthScopesOnSignIn construc Implementation: 1. Construct full redirect URLs using a fixed callback scheme: - - Native apps (canonical): "hexclave-mobile-oauth-url://success" and "hexclave-mobile-oauth-url://error" - - Native apps (legacy, still accepted indefinitely): "stack-auth-mobile-oauth-url://success" and "stack-auth-mobile-oauth-url://error" + - Native apps: "hexclave-mobile-oauth-url://success" and "hexclave-mobile-oauth-url://error" - Browser: Use the configured OAuth callback handler URL as redirect_uri and window.location to construct absolute URLs - Browser: If options.returnTo is provided, pass it as afterCallbackRedirectUrl, not as redirect_uri @@ -83,7 +82,7 @@ Implementation: 4. Open the authorization URL: - Browser: perform redirect according to redirectMethod - - iOS/macOS: ASWebAuthenticationSession with callbackURLScheme: "hexclave-mobile-oauth-url" (the legacy "stack-auth-mobile-oauth-url" scheme is still accepted by the backend for app binaries built against older SDK versions) + - iOS/macOS: ASWebAuthenticationSession with callbackURLScheme: "hexclave-mobile-oauth-url" - Android: Custom Tabs with callback URL registered as deep link - Desktop: Open system browser with registered URL scheme for callback @@ -167,8 +166,7 @@ Returns: { url: string, state: string, codeVerifier: string, redirectUrl: string redirectUrl: The redirect URL (same as input, needed for token exchange - must match exactly) Note on URL schemes: -- The "hexclave-mobile-oauth-url://" scheme is the canonical scheme and is automatically accepted by the backend without any configuration. -- The legacy "stack-auth-mobile-oauth-url://" scheme is also accepted indefinitely for backwards compatibility with already-shipped customer app binaries that linked against older SDK versions. +- The "hexclave-mobile-oauth-url://" scheme is automatically accepted by the backend without any configuration. Implementation: 1. Generate or use provided state and codeVerifier From f932a6aba980c4bac4630d15f157afbcc4683905 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 27 May 2026 14:58:20 -0700 Subject: [PATCH 4/4] fix(dev-tool): use Hexclave logo in trigger instead of legacy Stack Auth glyph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dev-tool floating trigger button was still rendering the old stacked-layers Stack Auth mark even after the rest of the dev tool (aria-label, title, storage keys, docs URL) was rebranded to Hexclave. Replace STACK_LOGO_SVG with HEXCLAVE_LOGO_SVG — a monochrome currentColor rendering of the hexagon-with-three-radial-bars benzene mark from apps/dashboard/public/hexclave-icon.svg. Gradient and glow are stripped because the trigger glyph is 22x22 inside a colored chip with color: white, so a flat monochrome reads better at that size. --- packages/template/src/dev-tool/dev-tool-core.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/template/src/dev-tool/dev-tool-core.ts b/packages/template/src/dev-tool/dev-tool-core.ts index c267deb36b..ba135e8c8e 100644 --- a/packages/template/src/dev-tool/dev-tool-core.ts +++ b/packages/template/src/dev-tool/dev-tool-core.ts @@ -75,7 +75,10 @@ const DEFAULT_STATE: DevToolState = { panelHeight: 520, }; -const STACK_LOGO_SVG = ''; +// Hexclave mark — hexagon outline with three radial bars, monochrome via currentColor +// so it inherits the trigger logo's color. Sourced from apps/dashboard/public/hexclave-icon.svg +// (gradient + glow stripped; this is a tiny trigger glyph, not the full brand mark). +const HEXCLAVE_LOGO_SVG = ''; // --------------------------------------------------------------------------- // State management @@ -454,7 +457,7 @@ function createTrigger(onClick: () => void): { element: HTMLElement; cleanup: () title: 'Hexclave Dev Tools', }); const logoSpan = h('span', { className: 'sdt-trigger-logo' }); - setHtml(logoSpan, STACK_LOGO_SVG); + setHtml(logoSpan, HEXCLAVE_LOGO_SVG); btn.appendChild(logoSpan); let placement = loadPlacement() ?? { corner: 'bottom-right' as TriggerCorner };