Documentation / Developers

Swift SDK (iOS / macOS)

Integrate MimiKeySDK in iOS and macOS apps — callback setup, authorization, picking, reading, creating, updating and revoking, plus the waiting UI and error handling.

Overview

MimiKeySDK is the Apple-platform implementation of third-party authorization v3. It switches to MimiKey, waits while the user acts on an approval card, exchanges and refreshes tokens, signs every call, and opens sealed results on the device. Your code calls a handful of async methods.

  • Platforms: iOS 17+ and macOS 14+, Swift 5.9 toolchain.
  • Dependencies: CryptoKit, Security and SwiftUI only; no third-party packages.
  • Client type: public (no client_secret; the token exchange uses PKCE).

Note To download the SDK source and a ready-to-run demo app, see SDK download & demo app.

Before you begin

  1. In MimiKey for Mac, open Developer access (开发者接入) in the sidebar and register a public client whose redirect URL uses your custom scheme, for example com.example.ai://mimikey/callback. Note the client_id (pmc_…). See Getting a client_id.
  2. Install MimiKey on your test device and sign in.

Installation

In Xcode, choose File → Add Package Dependencies…, click Add Local…, select the unzipped MimiKeySDK folder, and add the MimiKeySDK product to your app target.

With Package.swift:

// swift-tools-version:5.9
dependencies: [
    .package(path: "../MimiKeySDK"),
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "MimiKeySDK", package: "MimiKeySDK"),
    ]),
]

Configure Info.plist

You need two entries: register your callback scheme, and declare that you query the mimikey scheme (the SDK uses it to detect whether MimiKey is installed).

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.example.ai</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>com.example.ai</string>
    </array>
  </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
  <string>mimikey</string>
</array>
  • The callback URL must exactly match one of the registered redirect_uris.
  • On iOS, without LSApplicationQueriesSchemes the SDK always concludes MimiKey is missing and throws notInstalled.
  • macOS also needs CFBundleURLTypes. LSApplicationQueriesSchemes is harmless there; the SDK uses NSWorkspace to detect the app.
  • Sandboxed Mac apps need the com.apple.security.network.client entitlement.

Create the client

Keep one MimiKeyClient instance for your whole app:

import MimiKeySDK

let mimikey = MimiKeyClient(configuration: MimiKeyConfiguration(
    clientID: "pmc_xxxxxxxxxxxxxxxx",
    redirectURL: URL(string: "com.example.ai://mimikey/callback")!
))

Parameters of MimiKeyConfiguration(clientID:redirectURL:origin:downloadURL:):

ParameterDefaultDescription
clientID—Your registered client_id
redirectURL—A registered redirect URL
originhttps://password.tminos.comThe MimiKey server
downloadURLhttps://password.tminos.comWhere the notInstalled error points the user

MimiKeyClient.init also accepts optional store (custom secret storage; the system keychain by default), urlSession, appSwitcher, pollInterval (2 seconds by default) and authorizeTimeout (600 seconds by default). You normally only set these in tests.

Hand callbacks to the SDK

Pass every URL your app receives to handleCallback(_:). It matches the waiting flow by state; for anything that is not a MimiKey callback, or whose state does not match, it returns false and ignores the URL.

SwiftUI (iOS and macOS)

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    mimikey.handleCallback(url)
                }
        }
    }
}

UIKit

// SceneDelegate
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    for context in URLContexts {
        mimikey.handleCallback(context.url)
    }
}

AppKit

func applicationWillFinishLaunching(_ notification: Notification) {
    NSAppleEventManager.shared().setEventHandler(
        self,
        andSelector: #selector(handleGetURL(_:withReplyEvent:)),
        forEventClass: AEEventClass(kInternetEventClass),
        andEventID: AEEventID(kAEGetURL)
    )
}

@objc func handleGetURL(_ event: NSAppleEventDescriptor, withReplyEvent reply: NSAppleEventDescriptor) {
    guard let string = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
          let url = URL(string: string) else { return }
    mimikey.handleCallback(url)
}

Note Callback URLs carry only code, request_id, state and status — never ciphertext or plaintext. Results are always fetched by the SDK with a signed request and opened with the seal private key on the device.

Authorize

let grant = try await mimikey.authorize()
print(grant.grantID, grant.approvalKeyFingerprint, grant.vaults)

authorize():

  1. Generates a seal key pair (X25519), an approval key pair (Ed25519), a PKCE verifier and a state, all unique to this authorization;
  2. Opens mimikey://authorize, where MimiKey shows the consent card;
  3. Waits for the callback (up to authorizeTimeout, 600 seconds by default) and exchanges the code and PKCE verifier for tokens;
  4. Saves keys and tokens to the keychain and returns a MimiKeyGrant.
MimiKeyGrant propertyDescription
grantIDGrant ID
accountIDThe authorizing user's account ID
vaultsVaults the user selected ([MimiKeyVault] with id, name, kind, mode)
approvalKeyFingerprintApproval-key fingerprint (16 hex characters), identical to the one on the consent card

Authorization completes only through the callback. If the user switches back to your app without acting in MimiKey, authorize() keeps waiting until it times out, you call cancelPending(), or its task is cancelled.

Read local state without a network call:

mimikey.isAuthorized   // whether this device holds a usable grant
mimikey.grant          // the MimiKeyGrant saved at authorization, or nil
mimikey.reset()        // forget local credentials without telling the server (e.g. when your user signs out)

Calling authorize() again creates a new grant and replaces the one stored on the device.

Pick an item

let item = try await mimikey.pickItem(
    hint: MimiKeyHint(url: "https://bank.example.com", kind: .login),
    reason: "Sign in to Example Bank to reconcile statements"
)
item.id                 // item ID
item.revision           // revision number
item.record.username
item.record.password    // lives only in your process memory

MimiKey shows the pick card. Candidates come from the granted vaults and are ordered by hint (same registrable domain first, then title match, then same kind); the user can search. When the user taps Use this item, the result comes back sealed.

ParameterDescription
hintMimiKeyHint(url:title:kind:), all optional; only affects ordering
reasonPurpose shown on the card (up to 500 characters, visible to the server)
vaultIDOptional; nil picks across every vault in the grant

Read a known item

let same = try await mimikey.getItem(id: item.id, vaultID: grant.vaults[0].id)
  • ask policy: MimiKey shows the read approval card and the user taps Allow once.
  • trust policy: the request is approved on creation. Opening MimiKey executes it directly; the card only flashes "Provided per your trust setting".

Create an item

let created = try await mimikey.createItem(
    kind: .login,
    prefill: MimiKeyPrefill(
        title: "Example Bank",
        url: "https://bank.example.com/login",
        usernameHint: "me@example.com"
    ),
    reason: "Sign in to Example Bank to reconcile statements"
)
if let item = created.item {
    // Saved into a granted vault: you receive the full item
} else {
    // created.savedOutOfScope == true: saved to a vault outside the grant, no plaintext for this task
}

MimiKey shows the safety card: the full edit form for that item kind. The user types the password in MimiKey; your app never handles it.

ParameterDescription
kindMimiKeyItemKind: .login, .creditCard, .bankCard, .apiKey, .server, .secureNote
prefillMimiKeyPrefill(title:url:usernameHint:extraHint:), prefilled into the form and editable by the user; never put credentials here
reasonPurpose shown on the card
vaultIDOptional; defaults to the grant's first vault
MimiKeyCreated propertyDescription
requestIDRequest ID
itemThe full item; nil if saved outside the grant
summaryMimiKeyItemSummary (id, title, url, kind), never password or notes
savedOutOfScopeWhether the item was saved outside the grant

If the same grant and vault already have a pending create request for the same URL host, the server returns that request instead of creating a new one, and the SDK waits on it.

Update an item

let updated = try await mimikey.updateItem(
    id: item.id,
    vaultID: grant.vaults[0].id,
    patch: [.password: "new password"],
    reason: "Password was changed on the website; sync it to MimiKey"
)
updated.itemID      // item ID
updated.revision    // new revision after the write

The SDK first creates a draft request to obtain the request_id, then seals the patch {"patch":{…}} to the account identity public key and uploads it; the server sees only ciphertext. On the update card the user sees the old and new value of each field and taps Save changes, after which the device writes a new revision.

Patch keys are MimiKeyField values: the standard .title, .username, .password, .url and .notes, or .extra("card_number") for a kind-specific field. Your app does not receive the updated plaintext, only the new revision number.

Revoke

try await mimikey.revoke()

Calls POST /api/oauth/revoke (signed with the approval key) and then wipes every credential from the keychain. If the grant is already gone on the server, revoke() still wipes local state and returns normally.

Waiting UI

After MimiKey opens, the user may switch back to your app without doing anything. The SDK waits for the callback, polls the request every 2 seconds, and checks immediately when your app returns to the foreground; the server's expires_at sets the deadline. Your waiting screen must offer two ways out — Retry and Cancel:

@MainActor
final class Model: ObservableObject {
    @Published var waiting: MimiKeyWaiting?
    init() {
        mimikey.onWaiting = { [weak self] w in self?.waiting = w }   // called on the main thread
    }
    func retry() { Task { try? await mimikey.retryOpen() } }          // reopen MimiKey
    func cancel() { mimikey.cancelPending() }                         // the waiting call throws .cancelled
}
MimiKeyWaiting propertyDescription
phase.authorize or .request(op:)
requestIDRequest ID (nil while authorizing)
waitingOnWhose turn it is: user or device
waitingReasonconsent, approval, pick, input, review, no_unlocked_device
expiresAtExpiry time
messageReference wording in Chinese

Suggested wording:

waitingOn / waitingReasonMessage
user / consentWaiting for you to confirm in MimiKey
user / approvalWaiting for you to approve this use in MimiKey
user / pickWaiting for you to pick an item in MimiKey
user / inputWaiting for you to fill in and save the safety card in MimiKey
user / reviewWaiting for you to confirm the change in MimiKey
device / no_unlocked_deviceApproved — waiting for any of your devices to unlock MimiKey

cancelPending() makes a best effort to set the server request to cancelled. Cancelling the Task that is awaiting the call has the same effect.

The optional MimiKeyButton renders a button that follows your app's styling, labelled 用 MimiKey 填入 ("Fill with MimiKey") by default; pass your own title to localize it:

MimiKeyButton("Fill with MimiKey") { Task { try await pick() } }

Error handling

Every method throws MimiKeyError:

ErrorMeaningSuggested handling
.notInstalled(downloadURL:)MimiKey is not on this deviceSend the user to downloadURL to install it
.notAuthorizedNo grant on this deviceCall authorize()
.cancelledYou cancelled the wait, or the request was cancelled on the serverNo message needed
.deniedThe user declined on the cardRespect the decision; do not retry immediately
.expiredThe code or request expired, or no result arrived in timeLet the user try again
.notSignedInMimiKey has no signed-in accountAsk the user to sign in to MimiKey first
.revokedThe grant was revoked, the token family was invalidated, or the integrator was disabled; the SDK has wiped local credentialsAsk the user to authorize() again
.conflictThe item changed elsewhere during the updateFetch the latest item and update again
.network(Error)A network request failedRetry later
.protocol(String)Any other protocol or server error; the string is the error codeLog the code; see Errors
do {
    let item = try await mimikey.pickItem(reason: "Sign in to Example Bank")
    use(item)
} catch MimiKeyError.notInstalled(let url) {
    showInstallPrompt(url)
} catch MimiKeyError.revoked, MimiKeyError.notAuthorized {
    _ = try await mimikey.authorize()
} catch MimiKeyError.denied, MimiKeyError.cancelled {
    // The user's decision — stay quiet
} catch {
    showError(error.localizedDescription)
}

Tokens are managed for you: when fewer than 30 seconds remain on the access token, the SDK refreshes it first; if a call returns token_expired, it refreshes and retries once. refresh_token_reused, grant_revoked, client_disabled and similar codes surface as .revoked and are never retried automatically.

Note If the item was changed elsewhere while the update card was open, MimiKey keeps the user's version and denies the request with reason conflict; the SDK throws .conflict. Fetch the item again with getItem, then resubmit the change.

Keychain storage

The SDK stores each client's credentials as generic-password items in the system keychain:

ItemValue
servicecom.tminos.mimikey.sdk.<clientID>
AccessibilitykSecAttrAccessibleAfterFirstUnlockThisDeviceOnly (never synced, never migrated to another device)
ContentsApproval-key seed, seal private key, access and refresh tokens with expiry, grant_id, account_id, account identity public key, granted vault list

To share credentials between your app and its extensions, pass MimiKeyKeychain(clientID:accessGroup:) as the store. The SDK writes no logs, caches no plaintext, and zeroes decryption buffers after use.

Testing

  • Unit tests: inject MimiKeyMemoryStore() as the store and your own MimiKeyAppSwitching implementation in place of real app switching to test your flows without MimiKey.
  • Simulator: the iOS Simulator usually has no MimiKey installed, so calls throw notInstalled. For end-to-end tests, use a physical device or a Mac with MimiKey installed.
  • Demo app: Examples/MimiKeyDemo walks through authorize, pick, create, update and revoke. See SDK download & demo app.

Last updated: 2026-10-23