@shieldlabs-ai/react 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +514 -0
- package/dist/index.cjs +380 -0
- package/dist/index.d.cts +118 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +364 -0
- package/package.json +86 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@shieldlabs-ai/react` are documented in this file. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.0.0] - 2026-09-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `ShieldLabsProvider` with the props `publicKey`, `environment`, `scriptUrl`, `timeout`,
|
|
14
|
+
`autoLoad` and `checkOnLoad`: loads the agent once with `load()` of `@shieldlabs-ai/js`, in an effect
|
|
15
|
+
after the first render, and reports `status` (`loading`, `ready` or `error`) and `error`. Invalid
|
|
16
|
+
props and pages that are not a secure context also log a console warning. Changing an option
|
|
17
|
+
loads the agent for the new options.
|
|
18
|
+
- `autoLoad={false}` defers loading, for example until consent: nothing loads until `load()` is
|
|
19
|
+
called or `autoLoad` becomes `true`. Until then `useIdentify().identify()` resolves `null` at
|
|
20
|
+
once with a `not_initialized` error, and `runOnMount` ends the same way without running again
|
|
21
|
+
after `load()`. `useShieldLabs().identify()` rejects with `not_initialized`, `check()` resolves
|
|
22
|
+
`null` and `getAgent()` waits, with no timeout of its own.
|
|
23
|
+
- `useShieldLabs()`: `status`, `error`, `identify()`, `check()`, `load()` and `getAgent()` of the
|
|
24
|
+
closest provider. `getAgent()` resolves the loaded agent of `@shieldlabs-ai/js`, for early
|
|
25
|
+
identification with `agent.identifyOnInteraction(form)`. Calls made while the agent loads wait
|
|
26
|
+
for it, and the call timeout covers the whole call: that wait, then the agent's answer in the
|
|
27
|
+
time that is left. A failed or timed-out load is tried again by the next call.
|
|
28
|
+
- `useIdentify({ userId, runOnMount })`: `identify()`, `result`, `isLoading`, `error` and
|
|
29
|
+
`reset()`. `identify()` resolves the result, or `null` with the reason in `error`, and never
|
|
30
|
+
rejects. A call with the same User HID and `timeout` as a running call of the same hook returns
|
|
31
|
+
that call, so a double submit costs one identification; a call without `timeout` counts as one
|
|
32
|
+
with the provider `timeout`. Options of `identify()` with a `userId` key override the hook
|
|
33
|
+
option, also with `undefined` or `null`, which both identify anonymously; only options without
|
|
34
|
+
the key use the User HID of the hook. The state follows the call made last.
|
|
35
|
+
- `checkOnLoad` runs `check()` once per provider mount when the agent is ready, unless an
|
|
36
|
+
`identify()` or `check()` for the same User HID is running at that moment, and `runOnMount` runs
|
|
37
|
+
`identify()` once per component mount. Both run once in StrictMode.
|
|
38
|
+
- Server rendering that touches neither `window` nor `document`, no state updates after unmount,
|
|
39
|
+
state that stays current inside a hidden `<Activity>` (React 19.2 and later), and hooks outside
|
|
40
|
+
the provider throw an error that names the hook.
|
|
41
|
+
- Re-exports of `ShieldLabsError` and the types `IdentifyOptions`, `IdentifyResult`,
|
|
42
|
+
`InteractionIdentifier`, `LoadOptions`, `ShieldLabsAgent` and `ShieldLabsErrorCode` from
|
|
43
|
+
`@shieldlabs-ai/js`.
|
|
44
|
+
- ESM, CommonJS and TypeScript declarations with a `"use client"` directive. Peer dependencies:
|
|
45
|
+
`react` 18 or 19 and `@shieldlabs-ai/js` 1.x.
|
|
46
|
+
- `examples/vite`: a signup form that starts an identification on the first interaction with
|
|
47
|
+
`identifyOnInteraction()` and sends the `requestId` with the submit.
|
|
48
|
+
|
|
49
|
+
### Removed
|
|
50
|
+
|
|
51
|
+
- The placeholder `useShieldLabs(options)` hook of the pre-release scaffold, which returned `data`.
|
|
52
|
+
The browser receives a request ID; results are read on your server.
|
|
53
|
+
|
|
54
|
+
[Unreleased]: https://github.com/ShieldLabs-ai/shieldlabs-react/compare/v1.0.0...HEAD
|
|
55
|
+
[1.0.0]: https://github.com/ShieldLabs-ai/shieldlabs-react/releases/tag/v1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShieldLabs Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# @shieldlabs-ai/react
|
|
2
|
+
|
|
3
|
+
React bindings for ShieldLabs device intelligence: a provider that loads the ShieldLabs agent once,
|
|
4
|
+
and hooks that return a request ID for every identification, with loading and error state.
|
|
5
|
+
|
|
6
|
+
[](https://github.com/ShieldLabs-ai/shieldlabs-react/actions/workflows/ci.yml)
|
|
7
|
+
[](https://www.npmjs.com/package/@shieldlabs-ai/react)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
`@shieldlabs-ai/react` is a thin layer over [`@shieldlabs-ai/js`](https://github.com/ShieldLabs-ai/shieldlabs-js),
|
|
11
|
+
the browser loader that imports the hosted agent from `https://cdn.shieldlabs.ai` at runtime. It
|
|
12
|
+
supports React 18 and 19, renders on the server without touching browser globals, and loads the
|
|
13
|
+
agent once per app, also in StrictMode.
|
|
14
|
+
|
|
15
|
+
New to ShieldLabs? [Start free](https://app.shieldlabs.ai), then copy the Public Key of your domain
|
|
16
|
+
from Integration > API keys in the analytics dashboard (the Install tab also shows a ready snippet
|
|
17
|
+
that contains it).
|
|
18
|
+
|
|
19
|
+
## How it fits
|
|
20
|
+
|
|
21
|
+
1. **Browser.** `ShieldLabsProvider` loads the agent, and `useIdentify()` runs an identification for
|
|
22
|
+
a protected action. The page receives a `requestId`.
|
|
23
|
+
2. **Your backend.** It receives the `requestId` with the protected action (signup, login,
|
|
24
|
+
checkout) and reads the verdict for it from the History API with a ShieldLabs server SDK, or
|
|
25
|
+
receives it in a signed `identification.scored` webhook.
|
|
26
|
+
3. **Decision.** Your backend acts on the Risk Score (bands: trusted 0-29, suspicious 30-59,
|
|
27
|
+
dangerous 60-100), the detection flags and identifiers such as the device ID.
|
|
28
|
+
|
|
29
|
+
The browser only ever gets the request ID. The Risk Score, risk signals, detection flags, visitor ID
|
|
30
|
+
and device ID are read on your server, with one of the server SDKs:
|
|
31
|
+
[Node.js](https://github.com/ShieldLabs-ai/shieldlabs-node),
|
|
32
|
+
[Python](https://github.com/ShieldLabs-ai/shieldlabs-python),
|
|
33
|
+
[Go](https://github.com/ShieldLabs-ai/shieldlabs-go),
|
|
34
|
+
[PHP](https://github.com/ShieldLabs-ai/shieldlabs-php),
|
|
35
|
+
[Java](https://github.com/ShieldLabs-ai/shieldlabs-java) or
|
|
36
|
+
[.NET](https://github.com/ShieldLabs-ai/shieldlabs-dotnet).
|
|
37
|
+
|
|
38
|
+
The `identification.scored` webhook is delivered once per identification today (1-second timeout,
|
|
39
|
+
no retries). Use the History API when you need a guaranteed read, and make webhook handlers
|
|
40
|
+
idempotent on `data.request_id`, because future retries will resend identical bytes.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @shieldlabs-ai/react @shieldlabs-ai/js
|
|
46
|
+
# or
|
|
47
|
+
yarn add @shieldlabs-ai/react @shieldlabs-ai/js
|
|
48
|
+
# or
|
|
49
|
+
pnpm add @shieldlabs-ai/react @shieldlabs-ai/js
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`@shieldlabs-ai/js` (1.x) and `react` (18 or 19) are peer dependencies.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
The snippets use Vite with TypeScript; other bundlers expose environment variables their own way.
|
|
57
|
+
Put the Public Key of your domain in `.env` (the value below is a placeholder):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# .env
|
|
61
|
+
VITE_SHIELDLABS_PUBLIC_KEY=0123456789abcdef0123456789abcdef
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Render `ShieldLabsProvider` once, near the root of your app, around the components that identify:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// main.tsx
|
|
68
|
+
import { StrictMode } from 'react';
|
|
69
|
+
import { createRoot } from 'react-dom/client';
|
|
70
|
+
import { ShieldLabsProvider } from '@shieldlabs-ai/react';
|
|
71
|
+
import { SignupForm } from './SignupForm';
|
|
72
|
+
|
|
73
|
+
createRoot(document.getElementById('root')!).render(
|
|
74
|
+
<StrictMode>
|
|
75
|
+
<ShieldLabsProvider publicKey={import.meta.env.VITE_SHIELDLABS_PUBLIC_KEY}>
|
|
76
|
+
<SignupForm />
|
|
77
|
+
</ShieldLabsProvider>
|
|
78
|
+
</StrictMode>,
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Run an identification when the user submits a protected action, and send the `requestId` with it:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// SignupForm.tsx
|
|
86
|
+
import type { SyntheticEvent } from 'react';
|
|
87
|
+
import { useIdentify } from '@shieldlabs-ai/react';
|
|
88
|
+
|
|
89
|
+
export function SignupForm() {
|
|
90
|
+
const { identify, isLoading } = useIdentify();
|
|
91
|
+
|
|
92
|
+
async function onSubmit(event: SyntheticEvent<HTMLFormElement>) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
const email = new FormData(event.currentTarget).get('email');
|
|
95
|
+
// null when there is no identification (the reason is in `error`). The signup goes out anyway,
|
|
96
|
+
// and your server treats it as unverified.
|
|
97
|
+
const result = await identify();
|
|
98
|
+
await fetch('/api/signup', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ email, requestId: result?.requestId ?? null }),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<form onSubmit={onSubmit}>
|
|
107
|
+
<input name="email" type="email" required />
|
|
108
|
+
<button disabled={isLoading}>Sign up</button>
|
|
109
|
+
</form>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`identify()` never rejects: it resolves the result, or `null` with the reason in `error`. A second
|
|
115
|
+
submit while the identification runs gets the same one, so a double click costs one identification.
|
|
116
|
+
|
|
117
|
+
On your server, read the verdict for `requestId` with a server SDK, for example
|
|
118
|
+
`identifications.get(requestId)` in [`@shieldlabs-ai/node`](https://github.com/ShieldLabs-ai/shieldlabs-node),
|
|
119
|
+
which waits until the identification has been scored. The History row appears about 1-3 seconds
|
|
120
|
+
after `identify()` resolves and can be refined for up to about 10 seconds as follow-up checks
|
|
121
|
+
finish, so starting the identification when the user begins the action (see
|
|
122
|
+
[Protect a form](#protect-a-form)) gets your server the verdict sooner. Accept each request ID once
|
|
123
|
+
and only within your freshness window (the examples use 5 minutes): one identification authorizes
|
|
124
|
+
one protected action.
|
|
125
|
+
|
|
126
|
+
> **Keep the page alive after `identify()` resolves.** The agent posts the identification right
|
|
127
|
+
> after it hands over the request ID. Sending your request with `fetch()`, as above, keeps the page
|
|
128
|
+
> open. If you navigate right after the submit (a full-page form post or a redirect), start the
|
|
129
|
+
> identification early instead (see [Protect a form](#protect-a-form)).
|
|
130
|
+
|
|
131
|
+
> **Test on a registered domain.** ShieldLabs records identifications only for the domains
|
|
132
|
+
> registered in your account. On `localhost` the page still receives a `requestId`, but the
|
|
133
|
+
> identification is rejected with `401` and your backend never finds it. Test on a development
|
|
134
|
+
> domain with its own keys, as described in [Environments](https://docs.shieldlabs.ai/setup/environments).
|
|
135
|
+
|
|
136
|
+
## Guide
|
|
137
|
+
|
|
138
|
+
### Protect a form
|
|
139
|
+
|
|
140
|
+
`identify()` on submit, as in the quick start, is enough for most single-page apps. To have the
|
|
141
|
+
identification finished by the time the user submits, start it on the first interaction with the
|
|
142
|
+
form. `getAgent()` from `useShieldLabs()` resolves the loaded agent of `@shieldlabs-ai/js`, and its
|
|
143
|
+
`identifyOnInteraction(form)` starts `identify()` on the first `focusin`, `pointerdown` or `keydown`
|
|
144
|
+
inside the form. The handle's `take()` returns that identification for this submission and re-arms,
|
|
145
|
+
so the next submission gets its own request ID:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { useEffect, useRef, type SyntheticEvent } from 'react';
|
|
149
|
+
import { useIdentify, useShieldLabs, type InteractionIdentifier } from '@shieldlabs-ai/react';
|
|
150
|
+
|
|
151
|
+
export function SignupForm() {
|
|
152
|
+
const { getAgent } = useShieldLabs();
|
|
153
|
+
const { identify } = useIdentify();
|
|
154
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
155
|
+
const early = useRef<InteractionIdentifier | null>(null);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const form = formRef.current;
|
|
159
|
+
if (!form) return;
|
|
160
|
+
let active = true;
|
|
161
|
+
getAgent().then(
|
|
162
|
+
(agent) => {
|
|
163
|
+
if (active) early.current = agent.identifyOnInteraction(form);
|
|
164
|
+
},
|
|
165
|
+
() => {}, // the agent could not load: the submit handler tries again
|
|
166
|
+
);
|
|
167
|
+
return () => {
|
|
168
|
+
active = false;
|
|
169
|
+
early.current?.dispose();
|
|
170
|
+
early.current = null;
|
|
171
|
+
};
|
|
172
|
+
}, [getAgent]);
|
|
173
|
+
|
|
174
|
+
async function onSubmit(event: SyntheticEvent<HTMLFormElement>) {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
const email = new FormData(event.currentTarget).get('email');
|
|
177
|
+
// The early identification while it is fresh, otherwise a new one. Without an early handle,
|
|
178
|
+
// identify() loads the agent again; it resolves null when there is no identification.
|
|
179
|
+
const result = early.current ? await early.current.take().catch(() => null) : await identify();
|
|
180
|
+
await fetch('/api/signup', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
183
|
+
body: JSON.stringify({ email, requestId: result?.requestId ?? null }),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<form ref={formRef} onSubmit={onSubmit}>
|
|
189
|
+
<input name="email" type="email" required />
|
|
190
|
+
<button>Sign up</button>
|
|
191
|
+
</form>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
For a classic full-page post, put the request ID in a hidden field
|
|
197
|
+
(`<input type="hidden" name="requestId" />` in a `<form method="post" action="/signup">`) and submit
|
|
198
|
+
the form yourself:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
async function onSubmit(event: SyntheticEvent<HTMLFormElement>) {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
const form = event.currentTarget;
|
|
204
|
+
const result = await early.current?.take().catch(() => null);
|
|
205
|
+
(form.elements.namedItem('requestId') as HTMLInputElement).value = result?.requestId ?? '';
|
|
206
|
+
form.submit();
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Because the identification starts on the first interaction, it has normally finished posting by the
|
|
211
|
+
time the user submits. If users can submit without interacting first (for example autofill and a
|
|
212
|
+
single click on the button), prefer sending the form with `fetch()`, which keeps the page alive.
|
|
213
|
+
|
|
214
|
+
`take()` hands out the early identification only while it is fresh. When it failed, or finished more
|
|
215
|
+
than 4 minutes ago, `take()` starts a new one, so the request ID your server receives stays inside a
|
|
216
|
+
5-minute freshness window. While users keep interacting with the form, a new identification starts
|
|
217
|
+
at most every 4 minutes (after a failure, at most one attempt every 5 seconds), and each of them is
|
|
218
|
+
billed. The effect's cleanup removes the listeners when the form unmounts. With
|
|
219
|
+
`autoLoad={false}` (see [Consent](#consent)), `getAgent()` waits for `load()`, so the form is armed
|
|
220
|
+
once `load()` has been called and the agent has loaded.
|
|
221
|
+
|
|
222
|
+
[`examples/vite`](./examples/vite) is a complete signup form built this way.
|
|
223
|
+
|
|
224
|
+
### Signed-in users: pass a User HID
|
|
225
|
+
|
|
226
|
+
Pass a User HID so ShieldLabs ties the identification to the account. Compute it **on your server**
|
|
227
|
+
from your account ID with a secret key, for example with the `userHid(userId, secret)` helper of
|
|
228
|
+
the server SDKs (HMAC-SHA256, 64 hex characters), and hand it to the page, for example in your
|
|
229
|
+
session data:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
const { identify } = useIdentify({ userId: session.userHid });
|
|
233
|
+
// Later, for the protected action. identify({ userId }) overrides the User HID for one call.
|
|
234
|
+
const result = await identify(); // result?.userId is the User HID that was sent
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Options of `identify()` with a `userId` key override the User HID of the hook for that call, also
|
|
238
|
+
when the value is `undefined` or `null`: `identify({ userId: undefined })` identifies anonymously.
|
|
239
|
+
Only options without the key use the User HID of the hook.
|
|
240
|
+
|
|
241
|
+
Never pass a raw email address, phone number or database ID. Omit `userId` for visitors who are not
|
|
242
|
+
signed in. The rules for the value (reserved values, characters that are hard to search) are in the
|
|
243
|
+
[`@shieldlabs-ai/js` guide](https://github.com/ShieldLabs-ai/shieldlabs-js#signed-in-users-pass-a-user-hid).
|
|
244
|
+
|
|
245
|
+
### Identify when a component mounts
|
|
246
|
+
|
|
247
|
+
`useIdentify({ runOnMount: true })` runs `identify()` once when the component mounts, as soon as the
|
|
248
|
+
agent is ready. `isLoading` is `true` from the first render. Re-renders, prop changes and StrictMode
|
|
249
|
+
do not run it again; a new mount of the component does. With `autoLoad={false}`, a mount before
|
|
250
|
+
`load()` ends at once with a `not_initialized` error and does not run again after `load()` (see
|
|
251
|
+
[Consent](#consent)). Every run is a billable identification, so use it for a component that is
|
|
252
|
+
itself the protected step, never in a layout, a list item or a component that mounts on every route.
|
|
253
|
+
|
|
254
|
+
`result` is one identification, and one identification authorizes one protected action: send its
|
|
255
|
+
`requestId` with a single request, within your freshness window (5 minutes in the examples). Your
|
|
256
|
+
server rejects a request ID it has seen before or one that is too old. For every later action (a
|
|
257
|
+
retry after a declined card, a second submit, a user who comes back after a break), call
|
|
258
|
+
`identify()` again. For form submits, `identify()` in the submit handler, as in the quick start, is
|
|
259
|
+
the simpler choice.
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
function RecoveryStep({ userHid }: { userHid: string }) {
|
|
263
|
+
const { result, isLoading } = useIdentify({ userId: userHid, runOnMount: true });
|
|
264
|
+
if (isLoading) return <p>Loading</p>;
|
|
265
|
+
// The request ID goes with the one request that loads the recovery options. Without a result (the
|
|
266
|
+
// identification failed), that request is sent without a requestId.
|
|
267
|
+
return <RecoveryOptions requestId={result?.requestId ?? null} />;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Background checks with `checkOnLoad`
|
|
272
|
+
|
|
273
|
+
`checkOnLoad` runs `check()` once per provider mount when the agent is ready, for passive monitoring
|
|
274
|
+
of the visit. `true` checks anonymously, `{ userId }` passes a User HID:
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
<ShieldLabsProvider publicKey={publicKey} checkOnLoad={session ? { userId: session.userHid } : true}>
|
|
278
|
+
<App />
|
|
279
|
+
</ShieldLabsProvider>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The agent limits `check()` to one identification per visit every five minutes, shared across tabs.
|
|
283
|
+
The check uses the value of `checkOnLoad` at the moment the agent becomes ready, and changes after
|
|
284
|
+
that do not run it again. It is skipped when an `identify()` or `check()` for the same User HID is
|
|
285
|
+
still running at that moment (for example `runOnMount`, or a submit made while the agent loaded):
|
|
286
|
+
that call already identifies the visit, and the agent runs one identification at a time for a User
|
|
287
|
+
HID. A skipped or failed check is ignored (invalid options log a console warning). Your backend sees
|
|
288
|
+
these identifications like any other; to get the request ID in the page, call `check()` from
|
|
289
|
+
`useShieldLabs()` instead, which resolves `null` when the agent skipped it.
|
|
290
|
+
|
|
291
|
+
### Loading and error state
|
|
292
|
+
|
|
293
|
+
`useShieldLabs()` returns the agent status, the provider's `identify()` and `check()`, `load()`
|
|
294
|
+
and `getAgent()`:
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
function AgentStatus() {
|
|
298
|
+
const { status, error } = useShieldLabs();
|
|
299
|
+
if (status === 'error') return <small>Identification is unavailable ({error?.code}).</small>;
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
You do not need to wait for `'ready'`: `identify()` and `check()` called while the agent loads wait
|
|
305
|
+
for it. The call's `timeout` (default: the provider `timeout`, 10 seconds) covers the whole call:
|
|
306
|
+
that wait and then the agent's answer, which gets only the time that is left. Never block a
|
|
307
|
+
protected action on the status. When the agent cannot load (a content blocker, a network error),
|
|
308
|
+
send the action without a `requestId`; your backend treats it as unverified. A failed or timed-out
|
|
309
|
+
load is tried again by the next `identify()`, `check()`, `getAgent()` or `load()`, and the status
|
|
310
|
+
follows. When the provider props are invalid (for example a missing `publicKey` because an
|
|
311
|
+
environment variable is not set) or the page is not a secure context, the provider also logs a
|
|
312
|
+
console warning that starts with `[ShieldLabs]`.
|
|
313
|
+
|
|
314
|
+
`identify()` and `check()` of `useShieldLabs()` are the calls of `@shieldlabs-ai/js`: `identify()`
|
|
315
|
+
rejects with a `ShieldLabsError` when there is no identification. `useIdentify()` wraps it with
|
|
316
|
+
`result`, `isLoading` and `error`, never rejects and shares a running call.
|
|
317
|
+
|
|
318
|
+
### Server-side rendering and StrictMode
|
|
319
|
+
|
|
320
|
+
- The provider and the hooks render on the server. Nothing touches `window` or `document` during
|
|
321
|
+
render; the agent loads in an effect after hydration (with `autoLoad={false}`, once `load()` is
|
|
322
|
+
called). The server HTML shows `status: 'loading'` (and `isLoading: true` for `runOnMount`).
|
|
323
|
+
- In StrictMode the provider loads the agent once, and `runOnMount` and `checkOnLoad` run once per
|
|
324
|
+
mount. `load()` of `@shieldlabs-ai/js` is memoized per agent URL and Public Key, so mounting the
|
|
325
|
+
provider again reuses the agent that is already loaded.
|
|
326
|
+
- The hooks never identify on re-renders or on route changes. Keep the provider above your router so
|
|
327
|
+
that navigation does not remount it.
|
|
328
|
+
- Inside `<Activity mode="hidden">` (React 19.2 and later) the provider and the hooks keep their
|
|
329
|
+
state. A load or an identification that finishes while the content is hidden shows up when it is
|
|
330
|
+
visible again, and showing it again does not run `runOnMount` or `checkOnLoad` again.
|
|
331
|
+
- The built files start with the `"use client"` directive, so bundlers for React Server Components
|
|
332
|
+
treat the package as client code.
|
|
333
|
+
|
|
334
|
+
### Next.js
|
|
335
|
+
|
|
336
|
+
Use [`@shieldlabs-ai/next`](https://github.com/ShieldLabs-ai/shieldlabs-next). It provides this
|
|
337
|
+
provider and these hooks as a client module for the App Router (the Pages Router is documented
|
|
338
|
+
there) and adds server helpers in `@shieldlabs-ai/next/server` for reading identifications and
|
|
339
|
+
verifying webhooks in route handlers.
|
|
340
|
+
|
|
341
|
+
### Call budget and Content Security Policy
|
|
342
|
+
|
|
343
|
+
The rules of `@shieldlabs-ai/js` apply unchanged:
|
|
344
|
+
|
|
345
|
+
- [Call budget](https://github.com/ShieldLabs-ai/shieldlabs-js#call-budget): one identification per
|
|
346
|
+
protected action, and a small per-IP budget on the ingest. Never clear the agent's storage.
|
|
347
|
+
- [Content Security Policy](https://github.com/ShieldLabs-ai/shieldlabs-js#content-security-policy):
|
|
348
|
+
the `script-src` and `connect-src` origins the agent needs.
|
|
349
|
+
|
|
350
|
+
### Consent
|
|
351
|
+
|
|
352
|
+
The agent does not read your consent banner (see
|
|
353
|
+
[Consent](https://github.com/ShieldLabs-ai/shieldlabs-js#consent) in the `@shieldlabs-ai/js` guide).
|
|
354
|
+
Where your policy requires consent before the agent loads, render the provider with
|
|
355
|
+
`autoLoad={false}`: nothing loads until `load()` from `useShieldLabs()` is called, or until
|
|
356
|
+
`autoLoad` becomes `true`.
|
|
357
|
+
|
|
358
|
+
```tsx
|
|
359
|
+
import { ShieldLabsProvider, useShieldLabs } from '@shieldlabs-ai/react';
|
|
360
|
+
import { SignupForm } from './SignupForm';
|
|
361
|
+
|
|
362
|
+
export function App() {
|
|
363
|
+
return (
|
|
364
|
+
<ShieldLabsProvider publicKey={import.meta.env.VITE_SHIELDLABS_PUBLIC_KEY} autoLoad={false}>
|
|
365
|
+
<ConsentBanner />
|
|
366
|
+
<SignupForm />
|
|
367
|
+
</ShieldLabsProvider>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function ConsentBanner() {
|
|
372
|
+
const { load } = useShieldLabs();
|
|
373
|
+
// Record the choice as your consent tool requires, then load the agent.
|
|
374
|
+
return <button onClick={load}>Accept</button>;
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
When your consent state lives in React already, pass it instead:
|
|
379
|
+
`<ShieldLabsProvider publicKey={publicKey} autoLoad={consentGiven}>`. Once loading has started,
|
|
380
|
+
calls wait for the agent as usual.
|
|
381
|
+
|
|
382
|
+
Until then, the rest of the app works unchanged and nothing waits for consent:
|
|
383
|
+
|
|
384
|
+
- `identify()` from `useIdentify()` resolves `null` at once, with a `not_initialized` error, so
|
|
385
|
+
forms go out without a `requestId` and your server treats them as unverified. `runOnMount` ends
|
|
386
|
+
the same way and does not run again by itself after `load()`.
|
|
387
|
+
- `useShieldLabs().identify()` rejects with `not_initialized`, and `check()` resolves `null`.
|
|
388
|
+
- `getAgent()` waits, with no timeout of its own, so a form set up for early identification (see
|
|
389
|
+
[Protect a form](#protect-a-form)) is armed once `load()` has been called and the agent has
|
|
390
|
+
loaded. When that load fails, `getAgent()` rejects with its error. `checkOnLoad` runs once the
|
|
391
|
+
agent is ready.
|
|
392
|
+
- `status` stays `'loading'`.
|
|
393
|
+
|
|
394
|
+
Setting `autoLoad` back to `false` does not unload an agent that has loaded.
|
|
395
|
+
|
|
396
|
+
## Reference
|
|
397
|
+
|
|
398
|
+
| Export | Description |
|
|
399
|
+
|---|---|
|
|
400
|
+
| `ShieldLabsProvider` | Loads the agent once and provides it to the hooks below it |
|
|
401
|
+
| `useShieldLabs()` | Agent status, load error, `identify()`, `check()`, `load()` and `getAgent()` of the closest provider |
|
|
402
|
+
| `useIdentify(options?)` | Identification with `result`, `isLoading`, `error` and `reset()`. Its `identify()` resolves `null` instead of rejecting |
|
|
403
|
+
| `ShieldLabsError` | The error class of `@shieldlabs-ai/js` (re-exported). Has `code` and optional `cause` |
|
|
404
|
+
| Types | `ShieldLabsProviderProps`, `ShieldLabsStatus`, `UseShieldLabsResult`, `UseIdentifyOptions`, `UseIdentifyResult`, and from `@shieldlabs-ai/js`: `IdentifyOptions`, `IdentifyResult`, `InteractionIdentifier`, `LoadOptions`, `ShieldLabsAgent`, `ShieldLabsErrorCode` |
|
|
405
|
+
|
|
406
|
+
`<ShieldLabsProvider>` props
|
|
407
|
+
|
|
408
|
+
| Prop | Type | Default | Description |
|
|
409
|
+
|---|---|---|---|
|
|
410
|
+
| `publicKey` | `string` | required | Public Key of your domain |
|
|
411
|
+
| `environment` | `'production' \| 'development'` | `'production'` | Which ShieldLabs CDN to load the agent from |
|
|
412
|
+
| `scriptUrl` | `string` | | Advanced: agent module URL override (`https`, or `http` on `localhost` and `127.0.0.1`) |
|
|
413
|
+
| `timeout` | `number` | `10000` | Milliseconds to wait for the agent to load, and the default timeout of each `identify()` and `check()` call |
|
|
414
|
+
| `autoLoad` | `boolean` | `true` | Loads the agent after the first render. With `false`, nothing loads until `load()` is called or `autoLoad` becomes `true` (see [Consent](#consent)) |
|
|
415
|
+
| `checkOnLoad` | `boolean \| { userId?: string }` | `false` | Runs `check()` once per provider mount when the agent is ready, unless a call for the same User HID is running |
|
|
416
|
+
| `children` | `ReactNode` | | Your app |
|
|
417
|
+
|
|
418
|
+
Changing `publicKey`, `environment`, `scriptUrl` or `timeout` loads the agent for the new options
|
|
419
|
+
(once loading is allowed), and the status goes back to `'loading'`.
|
|
420
|
+
|
|
421
|
+
`useShieldLabs()` returns
|
|
422
|
+
|
|
423
|
+
| Field | Type | Description |
|
|
424
|
+
|---|---|---|
|
|
425
|
+
| `status` | `'loading' \| 'ready' \| 'error'` | `'loading'` until the agent has loaded, then `'ready'`, or `'error'` when loading failed |
|
|
426
|
+
| `error` | `ShieldLabsError \| null` | Why loading failed while `status` is `'error'` |
|
|
427
|
+
| `identify(options?)` | `Promise<IdentifyResult>` | Fresh identification, a new request ID on every call. Waits for the agent while it loads. Rejects with a `ShieldLabsError` |
|
|
428
|
+
| `check(options?)` | `Promise<IdentifyResult \| null>` | Background check, limited by the agent to one per visit every five minutes. `null` when skipped |
|
|
429
|
+
| `load()` | `void` | Starts loading the agent: needed only with `autoLoad={false}`. Also loads again after a failed load. Safe to call more than once; call it from an event handler or an effect |
|
|
430
|
+
| `getAgent()` | `Promise<ShieldLabsAgent>` | The loaded agent of `@shieldlabs-ai/js`, for example for `identifyOnInteraction(form)`. Waits while the agent loads (with `autoLoad={false}`, until `load()`), with no timeout of its own. Rejects with the load error |
|
|
431
|
+
|
|
432
|
+
`useIdentify(options?)`
|
|
433
|
+
|
|
434
|
+
| Option | Type | Default | Description |
|
|
435
|
+
|---|---|---|---|
|
|
436
|
+
| `userId` | `string` | | User HID for every identification of this hook. Options of `identify()` with a `userId` key override it, also with `undefined` or `null` (an anonymous identification) |
|
|
437
|
+
| `runOnMount` | `boolean` | `false` | Runs `identify()` once when the component mounts, as soon as the agent is ready |
|
|
438
|
+
|
|
439
|
+
| Field | Type | Description |
|
|
440
|
+
|---|---|---|
|
|
441
|
+
| `identify(options?)` | `Promise<IdentifyResult \| null>` | Starts a new identification and resolves its result, or `null` when there is none (the reason is in `error`). Never rejects. While a call of this hook with the same User HID and `timeout` runs, returns that call instead of starting another (a call without `timeout` counts as one with the provider `timeout`) |
|
|
442
|
+
| `result` | `IdentifyResult \| null` | Result of the latest identification. `null` while a new one runs, when it failed and after `reset()` |
|
|
443
|
+
| `isLoading` | `boolean` | `true` while the latest identification runs |
|
|
444
|
+
| `error` | `ShieldLabsError \| null` | Why the latest identification failed |
|
|
445
|
+
| `reset()` | `void` | Clears `result` and `error`. A running identification no longer updates the state, and the next `identify()` starts a new one |
|
|
446
|
+
|
|
447
|
+
The state follows the call made last. A call with another User HID or `timeout` than a running one
|
|
448
|
+
starts its own identification. The User HID of a call is the `userId` of its options when they have
|
|
449
|
+
that key (`undefined` and `null` both mean anonymous), else the `userId` of the hook: in a hook with
|
|
450
|
+
a User HID, `identify()` and `identify({ userId: undefined })` are two identifications. A call
|
|
451
|
+
without `timeout` counts as one with the provider `timeout` (10 seconds by default), so `identify()`
|
|
452
|
+
and `identify({ timeout: 10000 })` share one identification. Two `useIdentify()` hooks never share
|
|
453
|
+
a call. The functions keep their identity across renders (`identify` changes when `userId`
|
|
454
|
+
changes), so they are safe in effect dependencies.
|
|
455
|
+
|
|
456
|
+
`IdentifyOptions` (from `@shieldlabs-ai/js`)
|
|
457
|
+
|
|
458
|
+
| Option | Type | Description |
|
|
459
|
+
|---|---|---|
|
|
460
|
+
| `userId` | `string` | User HID computed on your server. Omit for anonymous checks |
|
|
461
|
+
| `timeout` | `number` | Milliseconds the whole call may take: a wait for the agent to load, then the agent's answer in the time that is left. Overrides the provider `timeout`, which also limits the load itself |
|
|
462
|
+
|
|
463
|
+
`IdentifyResult` (from `@shieldlabs-ai/js`)
|
|
464
|
+
|
|
465
|
+
| Field | Type | Description |
|
|
466
|
+
|---|---|---|
|
|
467
|
+
| `requestId` | `string` | Send it to your backend with the protected action |
|
|
468
|
+
| `userId` | `string \| null` | The User HID used, `null` for anonymous checks |
|
|
469
|
+
|
|
470
|
+
## Errors and retries
|
|
471
|
+
|
|
472
|
+
Every error is a `ShieldLabsError`. `useIdentify().identify()` stores it in `error` and resolves
|
|
473
|
+
`null`; the calls of `useShieldLabs()` reject with it. Branch on `error.code`:
|
|
474
|
+
|
|
475
|
+
| `code` | When | What happens and what to do |
|
|
476
|
+
|---|---|---|
|
|
477
|
+
| `invalid_options` | A provider prop or a call option failed validation, or `publicKey` holds a server-side secret | A bad prop sets `status` to `'error'` and logs a console warning; a bad call option fails that call. Fix the value; retrying does not help |
|
|
478
|
+
| `unsupported_environment` | The page is not a secure context | `status` is `'error'`, with a console warning. Serve the page over HTTPS (`localhost` and `127.0.0.1` also work over `http`) |
|
|
479
|
+
| `load_failed` | The agent module could not be imported (network error, content blocker, Content Security Policy) | `status` is `'error'`. Continue without an identification; the next `identify()`, `check()`, `getAgent()` or `load()` loads again |
|
|
480
|
+
| `timeout` | The agent did not load, or an agent call did not answer, within the timeout (default 10 seconds) | Continue without an identification. A load that timed out keeps running, and the next call uses it once it arrives |
|
|
481
|
+
| `not_initialized` | `identify()` only: the agent did not start an identification, for example because another one is running in this or another tab, or the provider has `autoLoad={false}` and `load()` has not been called. `check()` resolves `null` instead | Retry once later, or continue without an identification |
|
|
482
|
+
|
|
483
|
+
Whenever there is no identification, send the protected action anyway without a `requestId`: your
|
|
484
|
+
backend treats a missing identification as unverified (for example step-up or review), never as
|
|
485
|
+
clean. Calling a hook outside `ShieldLabsProvider` throws an `Error` that names the hook.
|
|
486
|
+
|
|
487
|
+
## Compatibility
|
|
488
|
+
|
|
489
|
+
- React 18 and 19 (React DOM), with TypeScript types for both.
|
|
490
|
+
- Browsers: the same as `@shieldlabs-ai/js` (ES modules, dynamic `import()` and WebCrypto, in a secure
|
|
491
|
+
context).
|
|
492
|
+
- Server rendering with `react-dom/server` in Node.js 18 or later; the agent loads only in the
|
|
493
|
+
browser.
|
|
494
|
+
- Output: ES2019 syntax as ESM and CommonJS with TypeScript declarations, marked `"use client"`.
|
|
495
|
+
No dependencies besides the peer dependencies.
|
|
496
|
+
|
|
497
|
+
## Development
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
npm ci
|
|
501
|
+
# Until @shieldlabs-ai/js is on npm, install a local pack of it (see CONTRIBUTING.md):
|
|
502
|
+
npm install --no-save ../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz
|
|
503
|
+
npm run typecheck
|
|
504
|
+
npm run lint
|
|
505
|
+
npm test -- --coverage # builds first, then runs the tests
|
|
506
|
+
npm run build
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). Documentation: <https://docs.shieldlabs.ai>. Analytics
|
|
510
|
+
dashboard: <https://app.shieldlabs.ai>. Support: <contact@shieldlabs.ai>.
|
|
511
|
+
|
|
512
|
+
## License
|
|
513
|
+
|
|
514
|
+
[MIT](./LICENSE), Copyright (c) 2026 ShieldLabs Inc.
|