diff --git a/README.md b/README.md index 7e3071ce..9077bc86 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,8 @@ To identify visitors, you need a Fingerprint Pro account (you can [sign up for f Configure the SDK by wrapping your application in `FingerprintProvider`. +> ⚠️ **Important**: Applications should create only one client across the entire app, either through `FingerprintProvider` or through the API client (`start()`). If you create another client, it will overwrite the underlying native client of the first one. + ```javascript // src/index.js import React from 'react'; @@ -249,6 +251,8 @@ export default function App() { Create a client with `start()` and call `get()`: +> ⚠️ **Important**: Applications should create only one client across the entire app, either through `FingerprintProvider` or through the API client (`start()`). If you create another client, it will overwrite the underlying native client of the first one. + ```javascript import React, { useEffect } from 'react'; import { start } from '@fingerprint/react-native'; @@ -284,7 +288,7 @@ The response is a flat, snake_case object that matches the Fingerprint Server AP ```typescript interface FingerprintResponse { - visitor_id: string + visitor_id?: string // `undefined` if [zero-trust-mode](https://docs.fingerprint.com/docs/zero-trust-mode) is enabled event_id: string suspect_score?: number // present only when Smart Signals are enabled sealed_result: string | null // base64 sealed result, or null when unavailable diff --git a/sdk/README.md b/sdk/README.md index 7e3071ce..9077bc86 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -187,6 +187,8 @@ To identify visitors, you need a Fingerprint Pro account (you can [sign up for f Configure the SDK by wrapping your application in `FingerprintProvider`. +> ⚠️ **Important**: Applications should create only one client across the entire app, either through `FingerprintProvider` or through the API client (`start()`). If you create another client, it will overwrite the underlying native client of the first one. + ```javascript // src/index.js import React from 'react'; @@ -249,6 +251,8 @@ export default function App() { Create a client with `start()` and call `get()`: +> ⚠️ **Important**: Applications should create only one client across the entire app, either through `FingerprintProvider` or through the API client (`start()`). If you create another client, it will overwrite the underlying native client of the first one. + ```javascript import React, { useEffect } from 'react'; import { start } from '@fingerprint/react-native'; @@ -284,7 +288,7 @@ The response is a flat, snake_case object that matches the Fingerprint Server AP ```typescript interface FingerprintResponse { - visitor_id: string + visitor_id?: string // `undefined` if [zero-trust-mode](https://docs.fingerprint.com/docs/zero-trust-mode) is enabled event_id: string suspect_score?: number // present only when Smart Signals are enabled sealed_result: string | null // base64 sealed result, or null when unavailable diff --git a/sdk/__tests__/tags.test.ts b/sdk/__tests__/tags.test.ts new file mode 100644 index 00000000..1f890468 --- /dev/null +++ b/sdk/__tests__/tags.test.ts @@ -0,0 +1,19 @@ +import { validateTags } from '../src/tags' + +describe('validateTags', () => { + it.each([Infinity, -Infinity, NaN])('rejects invalid numeric values', (value) => { + expect(() => { + validateTags(value) + }).toThrow(new TypeError('tags must be a finite number')) + }) + + it.each([Infinity, -Infinity, NaN])('rejects nested invalid numeric values', (value) => { + expect(() => { + validateTags({ + value: { + numeric: value, + }, + }) + }).toThrow(new TypeError("tags['value']['numeric'] must be a finite number")) + }) +}) diff --git a/sdk/android/fingerprint.gradle b/sdk/android/fingerprint.gradle index 48a1fe7e..59ec6719 100644 --- a/sdk/android/fingerprint.gradle +++ b/sdk/android/fingerprint.gradle @@ -1,4 +1,4 @@ -ext.fingerprint_native_sdk_version = "[4.0.0, 5.0.0)" +ext.fingerprint_native_sdk_version = "[4.1.0, 5.0.0)" task printFingerprintNativeSDKVersion { doLast { diff --git a/sdk/ios/FPError+React.swift b/sdk/ios/FPError+React.swift index 30d87d89..a41bc314 100644 --- a/sdk/ios/FPError+React.swift +++ b/sdk/ios/FPError+React.swift @@ -72,10 +72,10 @@ extension APIError { // The SDK's `APIError.Code` has no explicit raw values, so `rawValue` is the camelCase case name // (e.g. `tooManyRequests`). Map it to the canonical snake_case codes shared with Android and // `@fingerprint/agent` so `FingerprintError.code` is identical across platforms. Falls back to - // the generic `failed` code when the server error carries no recognized code. + // the generic `unknown_error` code when the server error carries no recognized code. var reactCode: String { guard let code = self.errorDetails?.code else { - return "failed" + return "unknown_error" } switch code { case .requestCannotBeParsed: return "request_cannot_be_parsed" diff --git a/sdk/src/FingerprintClient.ts b/sdk/src/FingerprintClient.ts index 4f79960c..5255b763 100644 --- a/sdk/src/FingerprintClient.ts +++ b/sdk/src/FingerprintClient.ts @@ -3,6 +3,7 @@ import RNFingerprint, { type NativeVisitorData } from './specs/NativeRNFingerpri import type { FingerprintClient, FingerprintResponse, GetOptions, StartOptions, TagsValue } from './types' import { unwrapError } from './unwrapError' import { isDefined, isTruthy } from './utils' +import { validateTags } from './tags' const packageVersion = '__VERSION__' @@ -37,7 +38,7 @@ function toNativeTag(tag: TagsValue | undefined): CodegenTypes.UnsafeObject | nu function normalizeResponse(data: NativeVisitorData): FingerprintResponse { return { - visitor_id: data.visitorId, + visitor_id: isTruthy(data.visitorId) ? data.visitorId : undefined, event_id: data.eventId, suspect_score: data.suspectScore >= 0 ? data.suspectScore : undefined, sealed_result: isTruthy(data.sealedResult) ? data.sealedResult : null, @@ -65,12 +66,10 @@ class NativeFingerprintClient implements FingerprintClient { } public async get(options?: GetOptions): Promise { + validateTags(options?.tags) + const nativeTags = toNativeTag(options?.tags) try { - const data = await RNFingerprint.getVisitorData( - toNativeTag(options?.tags), - options?.linkedId ?? null, - options?.timeout ?? null - ) + const data = await RNFingerprint.getVisitorData(nativeTags, options?.linkedId ?? null, options?.timeout ?? null) return normalizeResponse(data) } catch (error) { throw unwrapError(error) @@ -81,6 +80,10 @@ class NativeFingerprintClient implements FingerprintClient { /** * Creates a Fingerprint client with the given options. * + * Applications should create only one client: either through the API client (`start()`) + * or through a single `FingerprintProvider`. Creating another client will overwrite the underlying native client of the + * previous one. + * * @group API Client approach * * @example diff --git a/sdk/src/FingerprintClient.web.ts b/sdk/src/FingerprintClient.web.ts index 09ba42a8..017262d7 100644 --- a/sdk/src/FingerprintClient.web.ts +++ b/sdk/src/FingerprintClient.web.ts @@ -36,7 +36,7 @@ class WebFingerprintClient implements FingerprintClient { return { // `visitor_id` is only omitted by the backend in zeroTrust mode; default to '' to keep the // cross-platform shape stable. - visitor_id: result.visitor_id ?? '', + visitor_id: result.visitor_id ?? undefined, event_id: result.event_id, suspect_score: result.suspect_score, sealed_result: result.sealed_result ? result.sealed_result.base64() : null, diff --git a/sdk/src/FingerprintProvider.tsx b/sdk/src/FingerprintProvider.tsx index 24b34f35..3a5f5481 100644 --- a/sdk/src/FingerprintProvider.tsx +++ b/sdk/src/FingerprintProvider.tsx @@ -8,6 +8,10 @@ import { usePromiseStore } from './usePromiseStore' /** * Provides the {@link FingerprintContext} to its child components. * + * Applications should create only one client: either through a single `FingerprintProvider` + * at the root of the component tree or through the API client (`start()`). Creating another + * client will overwrite the underlying native client of the previous one. + * * @group Hooks approach * * @example diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 3de67011..51cdb9b1 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -6,4 +6,6 @@ export * from './useFingerprint' export { FingerprintError, isFingerprintError, FingerprintContextNotAvailableError, ErrorCode } from './errors' +export { validateTags } from './tags' + export * from './types' diff --git a/sdk/src/tags.ts b/sdk/src/tags.ts new file mode 100644 index 00000000..049d599f --- /dev/null +++ b/sdk/src/tags.ts @@ -0,0 +1,52 @@ +/** + * Rejects NaN / Infinity in `tags`. + * + * `TagsPrimitive` is `number`, so `{ score: Number('oops') }` type-checks. + * JSON has no literal for those values. Each platform rewrites them + * differently (`null` on web, `0` on iOS via `NSNumber.intValue`, usually + * `null` on Android) and identification still succeeds. Throw instead of + * storing the wrong tag. + * + * `Date`, `Map`, and class instances are already a type error on `TagsValue`. + * This walk does not reject them. The RN bridge and JS agent do not agree on + * those values (a `Date` is an ISO string on web and `{}` on native), but we + * choose to ignore that. + * + * Size is a server `payload_too_large`. + * https://docs.fingerprint.com/docs/tagging-information + */ + +export function validateTags(tags?: unknown): void { + if (tags === null || tags === undefined) { + return + } + walk(tags, 'tags') +} + +function walk(value: unknown, path: string): void { + if (value === null || value === undefined) { + return + } + + if (typeof value === 'number') { + if (!Number.isFinite(value)) { + throw new TypeError(`${path} must be a finite number`) + } + return + } + + if (typeof value !== 'object') { + return + } + + if (Array.isArray(value)) { + for (const [index, entry] of value.entries()) { + walk(entry, `${path}[${String(index)}]`) + } + return + } + + for (const [key, entry] of Object.entries(value)) { + walk(entry, `${path}['${key}']`) + } +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 74169a55..64f25002 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -208,9 +208,9 @@ export interface GetOptions { */ export interface FingerprintResponse { /** - * The visitor identifier. + * The visitor identifier. `undefined` if [zero-trust-mode](https://docs.fingerprint.com/docs/zero-trust-mode) is enabled. */ - visitor_id: string + visitor_id?: string /** * A unique id associated with the successful identification request. */