# Changelog

Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Entries below rc.3 were reconstructed from git history, since this file did not exist while
rc.1 and rc.2 were published — they are therefore less detailed than what follows, not less real.

Published under the `rc` dist-tag: `npm i @privateid/fido2-web-sdk@rc`.

## [Unreleased]

## [1.0.0-rc.5] — 2026-08-13

### Added

- **`src/core/readiness.ts` — UltraPass setup guidance and install detection.** Users routinely
  start a ceremony without UltraPass installed, or without having selected it in their passkey
  settings, and the first thing they see is a failed ceremony.

  Whether UltraPass is *selected as the credential provider* turns out to be undetectable — not
  just from the web, but from any native app that is not UltraPass itself. iOS exposes no API, and
  Android's `isEnabledCredentialProviderService()` rejects callers outside the provider's own
  package. Guidance is therefore unconditional on mobile rather than gated on a check, and the
  provider cases are classified after the fact from the ceremony error.

  - `getSetupGuidance(platform, options)` — per-platform setup copy, ported from the native apps
    so the web instructions match what users are told in-app. Every string overridable.
  - `classifyReadinessFailure(error, platform)` — maps an AAGUID mismatch to `wrong-provider` and
    `NotAllowedError` / `No create options available` to `provider-unavailable`, leaving unrelated
    errors alone.
  - `checkUltraPassInstalled(options)` — `navigator.getInstalledRelatedApps()` where it exists
    (Chromium/Android only), an honest `unknown` everywhere else. Never rejects.
  - `getSmartAppBannerContent()` / `installSmartAppBanner()` — the iOS Smart App Banner, which is
    the only accurate install affordance on iOS: Safari resolves the app ID against what is
    installed and labels the banner OPEN or GET itself, without telling the page.
  - `ULTRAPASS_APP_STORE_ID`, `ULTRAPASS_STORE_URLS`, `DEFAULT_ANDROID_PACKAGE`,
    `DEFAULT_READINESS_COPY`.

- `SDKConfig.androidPackageName` and `SDKConfig.appleItunesAppId`.

- Both demos show a collapsed "Check your setup" strip on mobile that expands on click, and
  auto-expands with the specific reason on a provider-shaped failure.

### Fixed

- **The demo advertised the wrong iOS app.** `examples/html-basic/index.html` pointed at App Store
  ID `6504606597`; the Ultrapass listing is `6758148839` (seller "PRIVATE IDENTITY LLC").

### Added (previously unreleased)

- `apps/demo` — a Next.js app serving the html-basic and session-handoff demos plus the relay API
  on one origin, for deployment to ECS. One origin is required, not preferred: the RP ID is the
  host and AASA associates the app per-origin, so splitting the API elsewhere breaks credentials
  and iOS association rather than just the API.
- `examples/session-handoff` — start a ceremony on a desktop, hand it to a phone by QR, and get
  the verified result back. WebAuthn's own cross-device QR cannot do this for UltraPass: it
  delivers a raw CTAP request, so the app never runs its Phase 1 face capture and the assertion
  returns an empty largeBlob.
- The phone streams its console to the relay, so both halves of a flow that spans two apps and
  usually two browsers appear as one ordered timeline on the desktop.

## [1.0.0-rc.4] — 2026-08-11

### Fixed

- **`base64ToBuffer` rejected Base64URL.** It passed its input straight to `atob`, which rejects
  `-` and `_` and requires padding — so it handled only one of the two encodings WebAuthn
  produces. The same credential appears in both spellings in a single result object
  (`credentialId` is standard, `rawCredential.id` is Base64URL), and decoding the second threw
  `InvalidCharacterError` from an `allowCredentials` entry. Both alphabets are now accepted, and
  genuinely malformed input names the offending value instead of surfacing `atob`'s bare message.
- **Rotated UltraPass AAGUID rejected on Android.** `80482e30-069e-4df9-b53e-2fce89596bb1` was
  listed only for desktop, so an Android registration failed the AAGUID check *after* the face
  capture had already completed. Now accepted on Android, and on iOS pre-emptively so its
  rotation cannot repeat the breakage.
- **`NotReadableError` had no explanation.** Android's Credential Manager raises it when no
  provider was ready, and unlike its neighbours in the hint map the remedy is to retry rather
  than relax an option. It now says so.
- **The unconfirmed-largeBlob warning overstated its case.** It claimed the credential "cannot
  carry the biometric-proof JWE", but both mobile providers return empty extension results for
  credentials whose blob reads back fine. Reworded to "unconfirmed", pointing at the only
  decisive test — whether a read at authentication returns bytes.
- **The empty-largeBlob error blamed Windows on every platform.** Its loopback/HID advice is
  nonsense on a Mac using the cross-device QR. Causes are now listed and ordered by platform.

### Added

- `androidInitDelay` (default 1000 ms) — the wait between opening `privateid://init` and starting
  the WebAuthn ceremony, previously hardcoded. Raise it if `NotReadableError` recurs against a
  cold-starting app.

## [1.0.0-rc.3] — 2026-08-11

### Fixed

- **Rotated UltraPass AAGUID rejected on Android** — the first half of the fix completed in rc.4.
- Log-injection findings from CodeQL.

### Changed

- `examples/html-basic`: the SDK is no longer left `null` when configuration is rejected — a
  non-production environment without a verify endpoint used to surface as
  `null is not an object (evaluating 'sdk.register')`.
- `examples/html-basic`: API key, environment, verify endpoint and policy ID now survive the iOS
  redirect. They previously reset on every round trip through the UltraPass app, silently
  pointing the callback at a different backend.

## [1.0.0-rc.2] — 2026-08-07

First release under the current package name, `@privateid/fido2-web-sdk`. `ultrapass-web-sdk` was
unavailable — it belongs to a different, active product.

## 1.0.0-rc.1

Initial publish. No release commit is identifiable in this repository's history, so it carries no
tag — the published artifact on npm is the only record.

[Unreleased]: https://github.com/openinfer/fido2-js/compare/v1.0.0-rc.4...HEAD
[1.0.0-rc.4]: https://github.com/openinfer/fido2-js/compare/v1.0.0-rc.3...v1.0.0-rc.4
[1.0.0-rc.3]: https://github.com/openinfer/fido2-js/compare/v1.0.0-rc.2...v1.0.0-rc.3
[1.0.0-rc.2]: https://github.com/openinfer/fido2-js/releases/tag/v1.0.0-rc.2
