Releases: keylight-dev/keylight-swift
Release list
0.8.3
0.8.2
The anonymous keyless/free-tier beacon now reports a one-way machine hash, so
your Keylight dashboard counts one device per physical machine instead of one
per install — reinstalling your app on the same Mac (or iPhone/iPad) updates the
same free-tier row instead of creating a duplicate. Automatic; the raw device
identifier never leaves the device, and there are no API changes.
Added
- The keyless beacon now sends a
machine_hash: a SHA-256 of a platform-stable
device identifier (IOPlatformUUIDon macOS,identifierForVendoron
iOS/tvOS/watchOS/visionOS), namespaced to your tenant and product. Only the
hash is transmitted — never the raw identifier. When no stable identifier is
available the field is omitted and the SDK keeps using its per-install id.
The derivation is byte-for-byte identical to the Rust and JS SDKs.
Changed
KeylightProvider.sdkVersionis now"0.8.2".
0.8.1
The SDK now attaches lightweight, anonymous version telemetry to its network
calls, so your Keylight dashboard can break activations and validations down by
app version, SDK version, and platform. Automatic — no API changes and nothing
to call.
Added
- The SDK now sends three optional fields on
activate,validate, and the
anonymous keyless beacon:app_version— your app'sCFBundleShortVersionString(when available)sdk_version— the Keylight SDK versionplatform—macOS,iOS,watchOS,tvOS, orvisionOS
KeylightProvider.sdkVersion— the current SDK version string constant.
Migration
- Nothing to do. The fields are added automatically; no source changes, and no
wire-format or lease changes.
Unchanged
- No behavior, lease, or public-API changes beyond the added request fields.
0.8.0
The one-call factory now lets you pick the storage backend directly, so the 0.6.0 storage options no longer require hand-building a KeylightConfiguration.
Added
storage:parameter onKeylight.manager(...)— optional, defaults to.encryptedFile()(the existing behavior). Pass.encryptedFile(keychainMirror: true)to also keep a Keychain recovery copy, or.keychainfor the legacy Keychain-authoritative backend. It forwards straight toKeylightConfiguration.storage.
Migration
- Most apps: nothing to do. The parameter is optional and defaults to the current behavior — omit it and nothing changes.
- If you previously hand-built a
KeylightConfigurationonly to setstorage, you can drop that and passstorage:to the factory instead.
Unchanged
- No wire-format, lease, or behavior changes. The default storage backend is still the device-bound encrypted file (no Keychain popup), and existing on-disk state still migrates automatically.
0.7.0
The SDK now treats trial, free tier, and expired as distinct states, and reports an anonymous keyless lifecycle signal so Keylight can show a conversion funnel. Most apps update with no code changes; the two behavior changes below are fixes for free-tier-enabled products.
Added
reportKeylessState(_:)— an anonymous, debounced, fire-and-forget heartbeat that reports the device's keyless state (.trial/.freeTier/.expired).LicenseManagersends it automatically on state transitions to power a trials started → converted / in free tier / expired funnel, with conversions attributed to the prior state. You don't call it yourself.KeylessReportStateenum (.trial,.freeTier,.expired).LicenseProvider.reportKeylessState(_:)— a protocol requirement with a no-op default, so custom providers compile unchanged.
Changed
- Free-tier devices are counted on launch #1. On a product with
trialDurationDays: 0and free tier enabled, a brand-new keyless install now resolves to.freeTieron the first launch. Previously it spent the first session in a degenerate.trial(daysLeft: 0)and only became.freeTieron launch #2. deactivate()on a free-tier product now resolves to.freeTier(previously.expired) — releasing a paid seat drops the user to the free tier they're entitled to, rather than the paywall.- The anonymous instance ID is now generated at trial start (so trial devices remain attributable if they later convert).
checkTrialStatus()stays trial-only and truthful.
Deprecated
reportFreeTier()— now a thin wrapper forreportKeylessState(.freeTier). It still works; no action needed (it's normally invoked by the SDK, not your code).
Migration
- Most apps: nothing to do. Both behavior changes are improvements and need no code change; reporting is automatic.
- Custom
LicenseProviderconformers:reportKeylessState(_:)ships with a default no-op implementation — you won't get a build break. Implement it only if you wrap/proxy provider calls and want to forward the signal.
Unchanged
- No wire-format or lease changes.
TrialStatus's public cases are unchanged.
0.6.0
License state now lives in a device-bound encrypted file by default, and the Keychain is left untouched — so the OS no longer shows a Keychain permission prompt on first launch. This is the new default; no code change is required to get it.
Added
StorageBackendconfiguration enum with two cases:.encryptedFile(keychainMirror: Bool = false)— new default. The license/trial blob is sealed with a key derived from the device identity, yourtenantId, andproductId, and stored under Application Support. The Keychain is never read or written, so there is no first-launch popup. PasskeychainMirror: trueto also keep a Keychain recovery copy..keychain— legacy behavior: the Keychain is authoritative, with the encrypted file as a crash-recovery fallback.
KeylightConfiguration.storage: StorageBackendinitializer parameter (defaults to.encryptedFile()).
Changed
- Default storage backend is now
.encryptedFile()instead of Keychain-authoritative. Existing on-disk license/trial state is migrated automatically and popup-free: on first load the SDK reads the legacy files, re-seals them into the encrypted file, and deletes the originals. Migrated installs keep their license — no re-activation.
Migration
- Most apps: nothing to do. The new default removes the first-launch Keychain prompt and migrates existing on-disk state automatically.
- If you relied on license state living in the Keychain specifically (e.g. you read it outside the SDK, or want Keychain sync/restore), set
storage: .encryptedFile(keychainMirror: true)to keep a Keychain copy, orstorage: .keychainto retain the previous default. - On platforms without Application Support (e.g. watchOS) or when a stable device ID is unavailable, the SDK transparently falls back to the Keychain — it never locks the user out.
Unchanged
- No wire-format or server changes. Lease signing/verification and the public
LicenseManager/ provider surface are the same.
0.5.0 — remove the hosted upgrade-URL helper
Breaking. LicenseManager.upgradeURL(to:) and the makeUpgradeURL(...) free function are removed. They built a link to Keylight's hosted upgrade page (/p/<tenant>/upgrade/<product>), which has been retired — customers now upgrade from the signed-in customer portal. That page already returns 404, so any in-app "Upgrade" button wired to upgradeURL was already failing at runtime; this release turns the dead call into a compile error so you catch it.
Removed
LicenseManager.upgradeURL(to:)makeUpgradeURL(origin:tenantId:productId:licenseKey:targetKeyTypeId:)
Migration
- Point your in-app "Upgrade" button at the customer portal: open
https://portal.keylight.dev. The customer signs in (magic link to the email on their license) and upgrades from their license detail — no key re-entry. - A native in-app upgrade flow (no browser hop) is planned and will build on the existing
/upgrade-sessionendpoint.
Unchanged
- No wire-format changes.
getCachedLicenseKey()stays onLicenseProvider.
0.4.1 — narrow Package.swift platforms
Manifest-only patch. Same 0.4.0 binary, tighter Package.swift.
The previous manifest declared tvOS, watchOS, and visionOS support that the binary never had — apps resolving against 0.4.0 from one of those targets would link successfully and then trap at runtime. 0.4.1 removes them so SwiftPM rejects unsupported platforms at resolve time with a clear error.
Changed
Package.swiftplatforms narrowed tomacOS(.v13)andiOS(.v16).- Build script default platform list reduced to
ios ios-sim macos.
Not changed
- Same
KeylightSDK.xcframeworkas 0.4.0 (binary URL unchanged in the manifest; checksum unchanged). - No public API change.
- No code change.
Coming back later
tvOS, watchOS, and visionOS support will return once the build matrix is restored.
0.4.0 — defensive-readiness hardening
14-finding internal audit of the Swift SDK's behavior under current Worker contracts, plus a new lifecycle-event notification surface for apps that want to react to license state transitions. No wire-format changes; the factory grew three optional parameters whose defaults preserve existing behavior. Apps that update without code changes get the bug fixes for free.
Breaking (opt-in)
Keylight.manager(...)factory addskid:,freeTierEnabled:, andmaxOfflineDays:parameters (all with defaults —"k1",false,15).kid:lets apps target a non-default signing key once tenant key rotation lands (0.5.0).freeTierEnabled:must be set totrueif your dashboard has the keyless free tier enabled — otherwise post-trial users resolve to.expired(hard paywall) instead of.freeTier.maxOfflineDays:exposes the previously-hidden 15-day offline cap; passnilto disable for air-gapped deployments.
KeylightConfiguration.initnow precondition-asserts thattenantIdandproductIdmatch[a-z0-9_-]+. Malformed identifiers crash loudly at init.
Added
LicenseManager.refreshIfNeeded()recovers from.limitedand.expired. Users whose subscription lapses then renews via payment-provider webhook recover automatically on next foreground refresh — no app relaunch.lease.status == "expired"maps to.expired(distinct from.invalid). SDK now surfaces "your license expired" rather than "your license is invalid."
Changed
checkOnLaunch()on network failure resolves to.limitedinstead of.invalid. Real users rebooting on an airplane keep running; next refresh reconciles.LeaseVerifier.verify(...)usesInt(floor(...))matching the Worker'sMath.floor(Date.now() / 1000).LeaseVerifier.verify(...)rejects leases with empty signatures up front.KeylightProvider.getCachedLease()no longer pre-filters onlease.isExpired; verifier's 300s skew tolerance is the single gate.KeylightProvider.hasStoredLicense()re-verifies signature before re-populating Keychain from file fallback. Tampered file leases are discarded.KeylightProvider.deactivateLicense()clearstimestamp.lastSeento prevent false-positive clock-manipulation verdicts.ActivateResponse.activatedandValidateResponse.validare non-optionalBool— contract drift surfaces as a decode error rather than silent failure.
Lifecycle event notifications
A new opt-in notification surface lets apps react to license state transitions — for example, dismissing a paywall when a renewal lands — without polling LicenseManager.licenseState.
LicenseLifecycleEventenum with three cases:.expired,.restored,.renewed. Posted viaNotificationCenter.defaulton thekeylightLifecycleEventnotification name..expiredfires when an active license (.licensed/.trial) transitions to a denying state. Suppressed on the very firstapplyStateof a session (cold-start paths) and on user-initiateddeactivate()..restoredfires when a denying state transitions back to.licensedor.trial. Also suppressed on first-launch..renewedfires when an already-licensed user's expiry advances.userInfocarriespreviousLicenseExpiresAtandnewLicenseExpiresAt(the latter omitted on lifetime upgrades).- Stale entitlements cleared on deny.
currentEntitlementsis forced to[]on.expired/.invalid/.freeTier.
Tests
- 13 new tests in
PatchV04Tests.swift(audit findings). - 14 new tests in
LifecycleEventNotificationTests.swift(lifecycle events). - 134/134 tests passing.
Migration
No code changes required for most apps — defaults preserve behavior. If your product has the keyless free tier enabled, you must pass freeTierEnabled: true to the factory.
Deferred to 0.5.0
- Multi-key trust sets (
trustedPublicKeys: [String: String]) for graceful Ed25519 signing-key rotation.
Platforms shipped: iOS device, iOS Simulator, macOS. tvOS / watchOS / visionOS coming when the build matrix is restored.