Frameworks
How it fits
The pattern is the same in every framework, and it follows the quick start:
- The script tag loads before your application's own script.
- When a form is submitted, your page asks your server for a session, for example from a route of your own at
POST /api/.lr-session - The page calls
identify()and sends the request ID with the form. - Your server fetches the verdict for that request ID and decides.
React
Call the helper when the form is submitted, and put the script tag in the head of index.html, before your bundle.
import { useState, type FormEvent } from "react";import { checkVisitor } from "./check-visitor"; export function SignUpForm() { const [busy, setBusy] = useState(false); async function onSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setBusy(true); const form = new FormData(event.currentTarget); form.set("lrRequestId", (await checkVisitor()) ?? ""); await fetch("/api/sign-up", { method: "POST", body: form }); setBusy(false); } return ( <form onSubmit={onSubmit}> {/* your fields */} <button type="submit" disabled={busy}>Create account</button> </form> );}Vue
The component calls the helper from its submit handler, and the same tag goes in the head of index.html.
<script setup lang="ts">import { ref } from "vue";import { checkVisitor } from "./check-visitor"; const busy = ref(false); async function onSubmit(event: Event) { busy.value = true; const form = new FormData(event.target as HTMLFormElement); form.set("lrRequestId", (await checkVisitor()) ?? ""); await fetch("/api/sign-up", { method: "POST", body: form }); busy.value = false;}</script> <template> <form @submit.prevent="onSubmit"> <!-- your fields --> <button type="submit" :disabled="busy">Create account</button> </form></template>Angular
Inject the service into the component that owns the form. Add the tag to the head of src/index.html, and put lr-sdk.d.ts in src: the default tsconfig.app.json includes every declaration file there.
import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root" })export class DeviceCheckService { // Ask your server for a session, run the check, and return the request ID (or null). async requestId(): Promise<string | null> { try { const res = await fetch("/api/lr-session", { method: "POST" }); if (!res.ok) return null; const { sessionToken } = await res.json(); const lr = new window.LightningResearchSDK.LightningResearch({ sessionToken }); const result = await lr.identify(); return result.requestId ?? null; } catch { return null; } }}Next.js
With the App Router, load the script from the root layout with strategy="beforeInteractive", so it is ready before your components run. The session route is a route handler on the same app, and the secret key stays in the server's environment. With the Pages Router, put the same Script in pages/.
import Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <Script src="https://cdn.lrdefender.lightningresearch.ai/sdk/v1/lr.min.js" strategy="beforeInteractive" /> </body> </html> );}Call checkVisitor() from a client component, exactly as in the React example.
Content-Security-Policy
If your pages send a Content-Security-Policy header, it must let the script load from our script host and send its request to our API. Some probes run in a worker the SDK creates from a blob: address, the timing probes compile small WebAssembly modules, which needs 'wasm-unsafe-eval' (it allows WebAssembly only, not JavaScript eval), and the layout probes add small inline styles, which needs 'unsafe-inline' in style-src. Keep your own sources in each directive and add these:
script-src 'self' https://cdn.lrdefender.lightningresearch.ai 'wasm-unsafe-eval';connect-src 'self' https://api.lrdefender.lightningresearch.ai wss://lr-edge.lrdefender.workers.dev;worker-src 'self' blob:;style-src 'self' 'unsafe-inline';The SDK also opens one WebSocket to our edge service at wss://, which is why that address is in connect-src: the proxy timing check runs over it. Under a connect-src without it (a default-src without it counts), the browser refuses that connection and reports a violation. Identification still completes without it, and a timing check that could not run gives no timing verdict, never a clean one (see the proxy fields).
The layout probes insert <style> blocks and style attributes into the page, and into a hidden frame that inherits its policy, then measure how this browser lays them out. Under a style-src without 'unsafe-inline' (a default-src without it counts), the browser refuses those styles and reports a violation for each. Identification still completes, but those probes measure unstyled elements, so their results differ from the same browser on a page that allows them, and a frame one probe hides with an inline style can show as an empty box at the end of the page. If your style-src uses a nonce or a hash, browsers ignore 'unsafe-inline' in it, with the same result.
One engine probe evaluates short test snippets as JavaScript. Under a policy without 'unsafe-eval' the browser refuses them and reports a Content-Security-Policy violation in the console. Identification still completes, and that probe records the refusal in place of its result. We do not ask you to add 'unsafe-eval'.
Requests to your own origin
The SDK makes no request to your page's own origin unless you name one. Apart from loading the script and the WebSocket to our edge service named above, every request it sends goes to the endpoint you configured, and the worker it starts is created from a blob: URL in the page, so there is nothing for you to serve and no failed load in the console.
Three optional inputs are fetched only from an address you pass when you create the client:
fontListUrl: a JSON array of font names to probe on top of the built-in list. Unset, only the built-in list is probed.taskConfigUrl: a task-capture configuration, as JSON. Unset, there is no task capture.workerScriptUrl: the SDK's standalone engine script, on your own origin, started as a worker. Unset, no worker is started from a file.
A font list or task configuration that has not answered within 3 seconds is skipped. These settings apply to the whole page: every client on it shares them, and a client created later without one keeps the earlier value. The policy above allows addresses on your own origin; a font list or task configuration on another origin needs that origin in connect-src.
With task capture on, the script fetches one more file on first use: its task capture code, a second file that sits beside the script on our script host, which the policy above already allows. It never comes from your origin unless you set tasksScriptBase to the absolute https address a proxy of yours serves the SDK's files from; that host then needs to be in script-src.
Mobile
Mobile web works today through the browser SDK: the same script tag runs in mobile browsers. Native iOS and Android SDKs are not available yet.
If you need a native SDK, tell us which platform.