Skip to content
LR Defender
Book a demo

Frameworks

Every framework loads the SDK the same way: one script tag, then the browser global it defines. There is no framework package to install, and none is needed.

How it fits

The pattern is the same in every framework, and it follows the quick start:

  1. The script tag loads before your application's own script.
  2. 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.
  3. The page calls identify() and sends the request ID with the form.
  4. Your server fetches the verdict for that request ID and decides.

The shared helper

Two small files serve every framework. The first gives TypeScript the shape of the global the script defines; the second runs one check and returns the request ID, or null when the check could not run.

// Types for the one global the script defines. Nothing to install.interface LightningResearchResult {  requestId?: string;  deviceId: string;  status?: string;} interface Window {  LightningResearchSDK: {    LightningResearch: new (options: { sessionToken: string }) => {      identify(options?: { tag?: string; linkedId?: string }): Promise<LightningResearchResult>;    };  };}

The session route is yours. Build it as in step 2 of the quick start with your secret key, on any server stack.

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/_document.tsx.

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://lr-edge.lrdefender.workers.dev, 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.

Get a verdict in your own app.

After a demo call, we set up your account and email you a link to set your password. It starts with a 90-day free trial on the Pro plan's limits, with no card.