Frameworks
Wisent Desktop Auth
    
│ .task -> store.start()
▼
WisentAuthStore state machine
│
├─ Supabase Auth REST (OTP, OAuth/PKCE, refresh, logout)
├─ Supabase REST/RPC (organizations, invites, members)
├─ macOS Keychain (stored session + selected organization ID)
└─ WisentFailureClassifier (safe UI message + operator log)
│
▼
WisentIdentity(user, email, organization, access token)
│
▼
host product service authorization
The store is @MainActor. Network requests run through an actor-backed identity
client. The gate calls start() once, renders each authentication state, and
places the ready WisentIdentity in the SwiftUI environment.
Quick start
Prerequisites
- macOS 14 or newer;
- Swift 5.10 or newer;
- a host app with a stable bundle identifier;
- a Supabase identity project whose auth, organization tables/RPCs, RLS, email, and chosen OAuth providers match this package's contract.
Add the package:
.package(
url: "https://github.com/wisent-ai/wisent-desktop-auth.git",
from: "0.1.0"
)
Add the product to the application target:
.product(name: "WisentAuth", package: "wisent-desktop-auth")
Packaged hosts must build the shared Keychain helper from the resolved package
checkout, place it at
Contents/Helpers/WisentIdentityKeychainHelper, and sign it with the same stable
Wisent identity and the fixed identifier
ai.wisent.identity.keychain-helper:
AUTH_CHECKOUT=.build/checkouts/wisent-desktop-auth
"$AUTH_CHECKOUT/scripts/build-keychain-helper.sh" \
"$APP_BUNDLE/Contents/Helpers/WisentIdentityKeychainHelper"
codesign --force --identifier ai.wisent.identity.keychain-helper \
--sign "$CODESIGN_IDENTITY" \
"$APP_BUNDLE/Contents/Helpers/WisentIdentityKeychainHelper"
The helper sends the session only through inherited pipes and owns the one
shared login-Keychain item. A source-only swift run has no packaged helper and
therefore keeps bundle-scoped storage.
Wrap the application content:
import SwiftUI
import WisentAuth
@main
struct ExampleApp: App {
@StateObject private var auth = WisentAuthStore(productName: "Example")
var body: some Scene {
WindowGroup {
WisentAuthGate(store: auth) {
ProductRootView()
}
}
}
}
Read the selected identity in a descendant view:
struct ProductRootView: View {
@Environment(\.wisentIdentity) private var identity
var body: some View {
Text(identity?.organization.name ?? "No organization")
}
}
Expected result: the gate restores a valid stored session or displays sign-in;
after identity and organization resolution it renders ProductRootView.
For OAuth, register the callback scheme in the host app's CFBundleURLTypes and
with the identity provider. The default redirect is
<bundle-identifier>://auth-callback.
Primary interfaces
WisentAuthStore
Create one store per host application identity surface:
let auth = WisentAuthStore(productName: "Weles")
Public observable state includes status, session, organization list/selection, identity, busy flags, pending invitations, management lists, and classified failures. Public operations cover start, OTP/OAuth sign-in, email change, organization selection/invitation review, organization management, failure clearing, and sign-out.
Do not copy the access token into app preferences, logs, crash metadata, or UI. Use it only for authorized requests to the host product service.
WisentAuthGate
WisentAuthGate(store: auth) {
ProductRootView()
}
The gate owns the common restoring, signed-out, code-entry, invitation, organization-picker, ready-content, account-toolbar, and organization-management presentation. Host content remains application-owned.
Environment identity
@Environment(\.wisentIdentity) private var identity: WisentIdentity?
A ready identity exposes:
- user ID and email;
- selected organization ID, slug, name, and role;
- current access token.
Absence means the protected content is outside a ready identity state; do not manufacture an anonymous/product fallback.
Configuration
The public convenience initializer uses:
| Variable | Meaning | Default behavior |
|---|---|---|
SUPABASE_URL |
identity API base | Wisent production project URL |
SUPABASE_ANON_KEY |
public Supabase client key | Wisent production anon key |
WISENT_AUTH_CALLBACK_SCHEME |
app URL callback scheme | host bundle identifier |
WISENT_AUTH_REDIRECT_URL |
OAuth redirect URL | <scheme>://auth-callback |
WISENT_AUTH_OAUTH_ENABLED |
OAuth presentation | enabled unless exactly 0 |
The Supabase anon key identifies the public client; it is not a service-role secret. Never put a service-role key into this client package or host app.
Failure semantics
The library classifies failures by stable code, service, impact, retryability, and user-safe title/message. It distinguishes authentication rejection from rate limiting, network, timeout, upstream identity, storage, invalid response, configuration, cancellation, and unknown failures.
Raw URLs, upstream bodies, and Keychain status details are reserved for operator
logging and do not enter the public WisentFailure UI payload. A host should use
the classified failure rather than parsing text in errorMessage.
Security and privacy
- Access and refresh tokens are saved as one generic login-Keychain item owned
by the packaged
WisentIdentityKeychainHelper. Every client signs an identical helper identifier with the same Wisent signing identity, so Keychain evaluates one designated requirement without a restricted access-group entitlement or provisioning profile. - Keychain storage protects persistence; it does not prevent a compromised host process from reading the public in-memory identity/access token.
- The default session begins refresh five minutes before expiry. Servers must still validate every request and handle revocation/expiry.
- OAuth uses
ASWebAuthenticationSession, a five-minute client timeout, and PKCE S256. Callback scheme/provider registration must exactly match the host. - Email addresses, user IDs, organization metadata, invitations, membership, roles, access tokens, and refresh tokens are sensitive identity data.
- Do not log tokens, upstream response bodies, invitation tokens, or full request headers. Restrict operator logs and retention.
- Supabase RLS and RPC authorization must fail closed. UI role checks and selected organization IDs are attacker-controlled client input.
- Share only the user's Wisent identity session. Provider, workload, browser, payment, and customer-resource credentials remain outside the shared store and under their owning product or Skarbiec boundary.
- Production hosts must package and sign the fixed-identifier helper. Source-only and incomplete bundles intentionally fall back to isolated, bundle-scoped storage.
Operational model
- Configuration: bundle identifier, public Supabase URL/anon key, callback scheme/redirect, and OAuth enablement.
- State: observable in-memory state plus Keychain session and selected organization ID.
- Credentials: user access/refresh tokens in Keychain; no service-role or downstream workload credentials.
- Observability: classified user-safe failures and subsystem diagnostics via the package's operator logger.
- Recovery: retry when classified retryable; reauthenticate on rejected/expired identity; repair configuration/provider callback/RLS at the owning system.
- Cost: identity-provider requests, email delivery, OAuth/provider service, database RPCs, and operational support; no billing logic is implemented here.
Project status and support
- Maturity: public development Swift package used by Wisent macOS products.
- Compatibility: macOS 14+, Swift tools 5.10, SwiftUI, AuthenticationServices, and Security.
- Distribution: Swift Package Manager source dependency; no stable binary framework distribution is promised.
- Issues:
wisent-ai/wisent-desktop-auth. - Security: use private GitHub Security Advisories; never attach tokens, invitation material, upstream identity bodies, customer organization/member data, callback secrets, or Keychain exports to a public issue.
- License: Apache License 2.0; see
LICENSE.