Skip to content

0.4.0 — defensive-readiness hardening

Choose a tag to compare

@Halloweedev Halloweedev released this 27 May 10:04

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 adds kid:, freeTierEnabled:, and maxOfflineDays: 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 to true if 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; pass nil to disable for air-gapped deployments.
  • KeylightConfiguration.init now precondition-asserts that tenantId and productId match [a-z0-9_-]+. Malformed identifiers crash loudly at init.

Added

  • LicenseManager.refreshIfNeeded() recovers from .limited and .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 .limited instead of .invalid. Real users rebooting on an airplane keep running; next refresh reconciles.
  • LeaseVerifier.verify(...) uses Int(floor(...)) matching the Worker's Math.floor(Date.now() / 1000).
  • LeaseVerifier.verify(...) rejects leases with empty signatures up front.
  • KeylightProvider.getCachedLease() no longer pre-filters on lease.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() clears timestamp.lastSeen to prevent false-positive clock-manipulation verdicts.
  • ActivateResponse.activated and ValidateResponse.valid are non-optional Bool — 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.

  • LicenseLifecycleEvent enum with three cases: .expired, .restored, .renewed. Posted via NotificationCenter.default on the keylightLifecycleEvent notification name.
  • .expired fires when an active license (.licensed / .trial) transitions to a denying state. Suppressed on the very first applyState of a session (cold-start paths) and on user-initiated deactivate().
  • .restored fires when a denying state transitions back to .licensed or .trial. Also suppressed on first-launch.
  • .renewed fires when an already-licensed user's expiry advances. userInfo carries previousLicenseExpiresAt and newLicenseExpiresAt (the latter omitted on lifetime upgrades).
  • Stale entitlements cleared on deny. currentEntitlements is 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.

Docs: https://docs.keylight.dev/swift-sdk/install/