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.