Skip to content

Releases: keylight-dev/keylight-swift

0.8.3

Choose a tag to compare

@Halloweedev Halloweedev released this 17 Jul 04:40

Release 0.8.3. See CHANGELOG.md.

0.8.2

Choose a tag to compare

@Halloweedev Halloweedev released this 09 Jul 04:21

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 (IOPlatformUUID on macOS, identifierForVendor on
    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.sdkVersion is now "0.8.2".

0.8.1

Choose a tag to compare

@Halloweedev Halloweedev released this 10 Jun 10:17

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's CFBundleShortVersionString (when available)
    • sdk_version — the Keylight SDK version
    • platformmacOS, iOS, watchOS, tvOS, or visionOS
  • 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

Choose a tag to compare

@Halloweedev Halloweedev released this 06 Jun 16:25

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 on Keylight.manager(...) — optional, defaults to .encryptedFile() (the existing behavior). Pass .encryptedFile(keychainMirror: true) to also keep a Keychain recovery copy, or .keychain for the legacy Keychain-authoritative backend. It forwards straight to KeylightConfiguration.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 KeylightConfiguration only to set storage, you can drop that and pass storage: 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

Choose a tag to compare

@Halloweedev Halloweedev released this 05 Jun 06:26

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). LicenseManager sends 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.
  • KeylessReportState enum (.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: 0 and free tier enabled, a brand-new keyless install now resolves to .freeTier on the first launch. Previously it spent the first session in a degenerate .trial(daysLeft: 0) and only became .freeTier on 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 for reportKeylessState(.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 LicenseProvider conformers: 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

Choose a tag to compare

@Halloweedev Halloweedev released this 01 Jun 05:10

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

  • StorageBackend configuration enum with two cases:
    • .encryptedFile(keychainMirror: Bool = false)new default. The license/trial blob is sealed with a key derived from the device identity, your tenantId, and productId, and stored under Application Support. The Keychain is never read or written, so there is no first-launch popup. Pass keychainMirror: true to also keep a Keychain recovery copy.
    • .keychain — legacy behavior: the Keychain is authoritative, with the encrypted file as a crash-recovery fallback.
  • KeylightConfiguration.storage: StorageBackend initializer 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, or storage: .keychain to 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

Choose a tag to compare

@Halloweedev Halloweedev released this 01 Jun 04:50

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-session endpoint.

Unchanged

  • No wire-format changes. getCachedLicenseKey() stays on LicenseProvider.

0.4.1 — narrow Package.swift platforms

Choose a tag to compare

@Halloweedev Halloweedev released this 27 May 10:48

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.swift platforms narrowed to macOS(.v13) and iOS(.v16).
  • Build script default platform list reduced to ios ios-sim macos.

Not changed

  • Same KeylightSDK.xcframework as 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

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/