/** * Configuration types for FIDO2-JS SDK * * Defines externalized configuration for authenticators, API endpoints, * and SDK behavior settings. * * @module types/config */ /** * Configuration for a specific authenticator device * * @interface AuthenticatorConfig */ export interface AuthenticatorConfig { /** * Authenticator Attestation GUID (AAGUID) in hex format * Format: "XX:XX:XX:XX:..." (16 bytes, colon-separated) * * @example "00:76:63:1b:d4:a0:42:7f:57:73:0e:c7:1c:9e:02:79" */ aaguid: string; /** * Additional AAGUIDs that also identify this authenticator, checked when {@link aaguid} * does not match. * * An AAGUID identifies an authenticator *model*, and the UltraPass model's changed once * already: a July 2026 firmware revision replaced * `00:76:63:1b:d4:a0:42:7f:57:73:0e:c7:1c:9e:02:79` with a fresh RFC 4122 v4 value. During a * rollout both builds are in the field, so pinning exactly one value rejects half the fleet * with an opaque "AAGUID mismatch" — accept the set instead, and drop retired entries once * the old build is gone. * * Used only when no {@link platformAaguids} entry covers the running platform. * * @example ["00:76:63:1b:d4:a0:42:7f:57:73:0e:c7:1c:9e:02:79"] */ additionalAaguids?: string[]; /** * AAGUIDs accepted per platform, overriding {@link aaguid} / {@link additionalAaguids} * entirely for the platforms listed. * * The AAGUID rotation landed in the native builds at different times: the 1.0.13 Windows MSI * reports the new value, while the iOS and Android apps still report the legacy one (as of * 2026-08-04 the rotation had not reached the mobile builds). Accepting both everywhere would * work, but it loosens the check on platforms where only one value is correct — and the point * of verifying an AAGUID is to pin the authenticator *model*, so a credential from an * unexpected model should still be rejected. * * A platform key present but empty (`[]`) means "no AAGUID is acceptable here", which * effectively disables that platform; omit the key instead to fall back to the defaults. * * Keys correspond to {@link Platform} values, lowercased. `desktop` covers Windows, macOS and * Linux browsers alike, since they share the direct-WebAuthn path. * * @example * ```typescript * platformAaguids: { * ios: ['00:76:63:1b:d4:a0:42:7f:57:73:0e:c7:1c:9e:02:79'], * android: ['00:76:63:1b:d4:a0:42:7f:57:73:0e:c7:1c:9e:02:79'], * desktop: ['80:48:2e:30:06:9e:4d:f9:b5:3e:2f:ce:89:59:6b:b1'] * } * ``` */ platformAaguids?: { ios?: string[]; android?: string[]; desktop?: string[]; unknown?: string[]; }; /** * Human-readable name of the authenticator * * @example "UltraPass" */ name: string; } /** * Main SDK configuration interface * * @interface SDKConfig */ export interface SDKConfig { /** * Map of authenticator configurations by identifier * * @example * ```typescript * { * ultrapass: { * aaguid: "00:76:63:1b:d4:a0:42:7f:57:73:0e:c7:1c:9e:02:79", * name: "UltraPass" * } * } * ``` */ authenticators: Record; /** * API endpoint URLs for backend services */ apiEndpoints: { /** * Verification endpoint URL for JWE token verification * * Must point at a `/v4/fido2/verify` URL — see VERIFY_ENDPOINTS in config/defaults. */ verify: string; }; /** * Target environment. Selects the default verify endpoint when * `apiEndpoints.verify` is not set explicitly. * * Only 'production' has a built-in endpoint (fido2-server.privateid.com). 'development' * and 'staging' are deployment-specific and ship no default — with either of those you * must set `apiEndpoints.verify` or `FIDO2_VERIFY_URL`, or construction throws. * * An explicit `apiEndpoints.verify` always wins over this. * * @default 'production' */ environment?: 'development' | 'staging' | 'production'; /** * URL scheme of the UltraPass build to open on iOS, without `://`. * * Production and staging UltraPass are separate, side-by-side installable apps with * different schemes, so this MUST match the build on the device or the deeplink opens * nothing (or the wrong app): * * - `'ultrapass'` — production build * - `'ultrapass-staging'` — staging build * * Defaults from `environment`: production → `ultrapass`, development/staging → * `ultrapass-staging`. Set explicitly to override. * * The scheme also has to agree with the environment your API key belongs to, since each * build talks to its own backend. Mixing them fails at verify time with an invalid-key * error while enrollment appears to succeed. * * Android is unaffected — it registers a single `privateid://` scheme for all builds. */ ultrapassScheme?: string; /** * LargeBlob support level requested at registration. * * The biometric-proof JWE is delivered through the WebAuthn `largeBlob` extension, so a * credential must declare largeBlob support at creation for the blob to be readable * during authentication. The native launchers register with `required`. * * - 'required' — matches native. Registration fails on authenticators without largeBlob. * - 'preferred' — permits authenticators lacking largeBlob, but those credentials will * never yield a JWE, so face verification cannot be checked for them. * * @default 'required' */ largeBlobSupport?: 'required' | 'preferred'; /** * Which extension name declares blob support at registration. * * - `'largeBlob'` (default) — the WebAuthn Level 3 extension, matching the current launcher * (`registrationRequest.largeBlob = .supportRequired`) paired with `.read` on the assertion. * - `'largeBlobKey'` — a different, CTAP2-level extension for fetching a per-credential blob * *key*. Browsers do not map one onto the other. Retained only as an escape hatch. * * Older web references (`webauthn_client/webauthn_v2.js`, the repo's `poc-deeplink`) use * `largeBlobKey`, but both are out of use and should not be treated as the contract. * * @default 'largeBlob' */ largeBlobExtension?: 'largeBlob' | 'largeBlobKey'; /** * Whether the ceremonies request the PRF (hmac-secret) extension. * * PRF carries the encoded custom biometric options (liveness, age, geo) as a 32-byte salt, * which is how the deeplink flow passes them to UltraPass. On desktop, however, the * PingFederate adapter — the reference that works on Windows — requests PRF **nowhere**: * neither `prf: {}` at registration nor `prf.eval` on the assertion * (`ultrapass-pf-adapter` `templates/ultrapass.{enrollment,webauthn}.template.html`, branch * `feat/usernameless-auth`, which sends only the largeBlob extension). Asking a Windows * authenticator for an extension it does not implement is a candidate cause of an assertion * finding no usable credential. * * - `'auto'` — request PRF only on platforms using the deeplink flow (iOS/Android), and omit * it on desktop, matching the PF adapter. * - `'always'` — request PRF on every platform. Previous behaviour. * - `'never'` — never request PRF. Custom biometric options are then not conveyed through * the WebAuthn ceremony at all. * * @default 'auto' */ prfExtension?: 'auto' | 'always' | 'never'; /** * Whether `allowCredentials` entries carry a `transports` hint. * * A transports list that omits the authenticator's actual transport can make an otherwise * valid credential unmatchable, which surfaces as `NotAllowedError` — indistinguishable from * a user cancellation. The PF adapter sends bare `{ type, id }` entries with no transports; * the reference `webauthn_client/webauthn_v2.js` sends `['internal','hybrid','usb']`. * * - `'auto'` — send the hint only on deeplink platforms, omit it on desktop. * - `'none'` — never send it. * - an explicit array — always send exactly these. * * @default 'auto' */ allowCredentialsTransports?: 'auto' | 'none' | AuthenticatorTransport[]; /** * Whether to enforce `timeout` client-side with an AbortController. * * `publicKey.timeout` is only a hint the platform may ignore. On Windows the native WebAuthn * dialog can stay open past it while the authenticator keeps retrying the face match, so the * ceremony never settles and the caller's timeout means nothing. The PF adapter arms an * `AbortController` and calls `abort()` on expiry, which sends the CTAP cancel, dismisses the * OS dialog, and rejects with `AbortError`. * * - `'auto'` — enforce on desktop only, leaving the deeplink flows' timing untouched. * - `true` / `false` — enforce always / never. * * @default 'auto' */ enforceTimeoutWithAbort?: 'auto' | boolean; /** * Whether to collect the biometric JWE over the authenticator's loopback control plane when * the assertion returns no `largeBlob`. * * On Windows 11 24H2/25H2 the authenticator can register as a *plugin authenticator* — a COM * object Windows' WebAuthn stack calls directly instead of an emulated CTAP2 USB-HID device. * A ceremony there is exactly one CTAP command, and the platform forwards neither `largeBlob` * nor `prf` across it (measured on 25H2 26200.8875, with `credProtect` crossing in the same * request as the positive control). The JWE is still minted; it has no route home. * * `/payload` on the loopback control plane is that route: the page presents the assertion's * base64 `clientDataJSON`, which both names the assertion and authorizes the handover, and the * service returns the payload. One-shot, expiring 120s after minting. * * Only consulted when `largeBlob` is genuinely absent, so transports that do deliver a blob * are unaffected. * * - `'auto'` — try it on desktop, where the plugin transport exists. * - `true` / `false` — always / never. * * @default 'auto' */ loopbackPayloadFallback?: 'auto' | boolean; /** * Origin of the authenticator's loopback control plane, without a path. * * Externalized because the port is the authenticator service's choice, not a protocol * constant. * * @default 'ws://127.0.0.1:47213' */ loopbackURL?: string; /** * How long to wait for the loopback control plane, in milliseconds. * * A refused port fails fast; a firewalled one can hang. The cap stops the fallback outlasting * the ceremony it exists to rescue. * * @default 3000 */ loopbackTimeout?: number; /** * Whether registration requests the CTAP2.1 `credProtect` extension keys. * * We send `credentialProtectionPolicy` / `enforceCredentialProtectionPolicy` as top-level * extension keys; the PF adapter sends neither. Browsers generally ignore unknown extensions, * so this is the least likely of the divergences to matter — it is configurable so it can be * ruled out rather than argued about. * * - `'auto'` — request them only on deeplink platforms, omit on desktop. * - `true` / `false` — always / never. * * @default 'auto' */ credProtectExtension?: 'auto' | boolean; /** * Which iOS deeplink host `authenticate({ useMobileFlow: true })` targets. * * - `'start'` — `ultrapass://start` with `verifyOnly=true`. The current unified entry point * (the app's `start` deeplink branch). UltraPass resolves enroll-vs-verify itself and reports * the outcome back as `mode` on the return URL. Reads `options`, so custom biometric options * are honoured, and takes `credentialIds` as a comma-separated list. * - `'legacy-verify'` — `ultrapass://verify` without `mode`, sending singular `credentialId`. * For UltraPass builds that predate the `start` host. * * Note there is a third branch in the app, `ultrapass://verify?mode=verify`, that this SDK * deliberately does **not** use for authentication: it does not read `options`, so all custom * biometric options would be silently dropped. It understands only `verifyMethod`. * * @default 'start' */ iosAuthPath?: 'start' | 'legacy-verify'; /** * Whether the Phase 2 assertion after a deeplink callback sends `allowCredentials`. * * - `true` — send the stored credential ID, matching the reference implementation * (`webauthn_client/webauthn_v2.js`, which passes `allowCredentials` with * `transports: ['internal','hybrid','usb']` and `userVerification: 'required'`). * - `false` — omit it and rely on resident-key discovery. * * This matters because registration requests `residentKey: 'preferred'`, not `'required'`, so * the credential is **not guaranteed to be discoverable**. When it isn't, an assertion with no * allowCredentials has nothing to find and the browser rejects with `NotAllowedError`, which * surfaces confusingly as "User cancelled the authentication". * * Set to `false` if you see a second face prompt during Phase 2 — passing an allowList can make * the credential provider re-verify rather than reuse the scan from Phase 1. The trade-off is * genuinely between those two failure modes, which is why it is exposed rather than hardcoded. * * @default true */ phase2AllowCredentials?: boolean; /** * API key for backend authentication * * @optional */ apiKey?: string; /** * Policy ID for authenticator configuration * Used for mobile deeplink flows and authenticator behavior * * @default 'mobile' */ policyId?: string; /** * Timeout for WebAuthn operations in milliseconds * * @default 60000 */ timeout?: number; /** * Enable debug logging * * @default false */ debug?: boolean; /** * App store URLs for fallback when mobile app is not installed * * @example * ```typescript * { * iOS: 'https://apps.apple.com/app/ultrapass/id...', * Android: 'https://play.google.com/store/apps/details?id=com.privateid.ultrapass' * } * ``` */ appStoreUrls?: { iOS?: string; Android?: string; }; /** * Enable app installation check and fallback * If true, will detect if mobile app is not installed and offer to redirect to app store * * @default false */ enableAppInstallCheck?: boolean; /** * Android package name of the UltraPass build to look for. * * Used by `checkUltraPassInstalled()` as the `navigator.getInstalledRelatedApps()` target, and * as the Play Store package. Must match the package your `assetlinks.json` associates with this * origin, or the check silently reports "not installed". * * @default 'com.privateid.ultrapass' */ androidPackageName?: string; /** * Numeric App Store ID for the iOS Smart App Banner — digits only, no `id` prefix. * * Setting this lets `installSmartAppBanner()` add the `apple-itunes-app` meta tag, which is the * only install affordance on iOS whose state is accurate: Safari checks what is installed and * labels the banner OPEN or GET itself. Script cannot read that state, and no other iOS * mechanism can determine it. * * Defaults to the production Ultrapass listing (`6758148839`). Set to `''` on staging pages — * staging UltraPass ships outside the App Store, so the production banner points at the wrong * build. */ appleItunesAppId?: string; /** * Delay in milliseconds before triggering Phase 2 WebAuthn after mobile callback * * CRITICAL for iOS Safari: After deeplink redirect, Safari needs time to render * before WebAuthn can be triggered. Without this delay, WebAuthn may fail silently. * * This delay allows: * - Safari to complete page render * - DOM to stabilize * - Credential provider to initialize * - User to see visual feedback * * Based on iOS dev reference implementation which uses a 3000ms countdown; this SDK * defaults to 5000ms because first-time callbacks need longer while Safari initializes. * * @default 5000 (5 seconds) * @platform iOS * * @example * ```typescript * // Use default 3s delay (recommended) * const sdk = new FIDO2SDK({ apiKey: '...' }); * * // Custom delay for testing * const sdk = new FIDO2SDK({ * apiKey: '...', * mobileCallbackDelay: 2000 // 2 seconds * }); * * // Disable delay (not recommended for iOS) * const sdk = new FIDO2SDK({ * apiKey: '...', * mobileCallbackDelay: 0 * }); * ``` */ mobileCallbackDelay?: number; /** * How long to wait after opening the Android app's `privateid://init` deeplink before starting * the WebAuthn ceremony. * * Android does not use the two-phase callback pattern: the SDK fires the init deeplink into a * hidden iframe, waits, then calls `navigator.credentials` directly in the same page. The wait * is the only thing standing between the deeplink and the ceremony, so if the UltraPass app is * cold starting it may not have registered as a credential provider yet — Android's Credential * Manager then fails the request with `NotReadableError`. * * Raise this if `NotReadableError` recurs on first use after the app has been killed. It costs * nothing but latency on every Android ceremony, so do not raise it further than needed. * * @default 1000 (1 second) * @platform Android * * @example * ```typescript * // Give a cold-starting app longer to register as a provider * const sdk = new FIDO2SDK({ * apiKey: '...', * androidInitDelay: 2500 * }); * ``` */ androidInitDelay?: number; } /** * Partial SDK configuration for user-provided overrides * * Allows users to provide partial configuration that will be merged * with default configuration. * * @type PartialSDKConfig */ export type PartialSDKConfig = Partial & { /** * API key is required when creating SDK instance */ apiKey: string; }; //# sourceMappingURL=config.d.ts.map