@shieldlabs-ai/js 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 +50 -0
- package/LICENSE +21 -0
- package/README.md +420 -0
- package/dist/index.cjs +287 -0
- package/dist/index.d.cts +99 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +283 -0
- package/dist/shieldlabs.iife.js +2 -0
- package/package.json +78 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@shieldlabs-ai/js` 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
|
+
- `load(options)`: imports the hosted agent from `https://cdn.shieldlabs.ai/snippet.js` at runtime
|
|
14
|
+
and resolves a `ShieldLabsAgent`. Memoized per agent URL and Public Key: concurrent calls share one
|
|
15
|
+
import. A failed import is retried on the next call with a new URL, because browsers remember a
|
|
16
|
+
failed module load per URL. The import is bounded by `timeout`: a call that runs out of time
|
|
17
|
+
rejects with `timeout`, and the import keeps running for later calls.
|
|
18
|
+
- `ShieldLabsAgent.identify()`: a fresh identification with a new request ID on every call, for
|
|
19
|
+
protected actions.
|
|
20
|
+
- `ShieldLabsAgent.check()`: the agent's limited background check (one per visit every five
|
|
21
|
+
minutes); resolves `null` when the agent skips it.
|
|
22
|
+
- `ShieldLabsAgent.identifyOnInteraction(target)`: starts an identification on the first `focusin`,
|
|
23
|
+
`pointerdown` or `keydown` on a form; `take()` returns it for the submission while it is fresh
|
|
24
|
+
(a new one starts when the early one failed or finished more than four minutes ago) and re-arms;
|
|
25
|
+
while the form is in use, interactions start a new identification at most every four minutes, and
|
|
26
|
+
after a failure at most every five seconds; `dispose()` removes the listeners.
|
|
27
|
+
- `ShieldLabsError` with the codes `invalid_options`, `unsupported_environment`, `load_failed`,
|
|
28
|
+
`not_initialized` and `timeout`, and `VERSION`.
|
|
29
|
+
- Option checks: the agent's Public Key rule with a one-time warning for keys that are not 32
|
|
30
|
+
lowercase hex characters, and server-side secrets (`sec_…`, `whsec_…`) rejected as `publicKey`;
|
|
31
|
+
User HID rules (non-empty, reserved values rejected) with one-time warnings for email-like values,
|
|
32
|
+
for `/`, `?`, `#` and `%`, and for the values `.` and `..` (User HIDs that are hard or impossible
|
|
33
|
+
to look up in the History API).
|
|
34
|
+
- Timeouts for loading the agent and for each agent call (default 10 seconds) that never leave
|
|
35
|
+
timers behind; late agent answers are ignored.
|
|
36
|
+
- `unsupported_environment` during server-side rendering, in workers and on pages that are not a
|
|
37
|
+
secure context (`localhost` and `127.0.0.1` are allowed over `http`).
|
|
38
|
+
- `environment` (`production` or `development`) and `scriptUrl` options.
|
|
39
|
+
- ESM, CommonJS and TypeScript declarations, plus a minified IIFE build that defines
|
|
40
|
+
`window.ShieldLabsJS`. No runtime dependencies.
|
|
41
|
+
- Examples: `examples/vanilla` (script tag, classic form post) and `examples/vite` (TypeScript
|
|
42
|
+
signup form).
|
|
43
|
+
|
|
44
|
+
### Removed
|
|
45
|
+
|
|
46
|
+
- The `0.1.0` placeholder API (`getResult()`, `DEFAULT_SCRIPT_URL`). The browser receives a request
|
|
47
|
+
ID; results are read on your server.
|
|
48
|
+
|
|
49
|
+
[Unreleased]: https://github.com/ShieldLabs-ai/shieldlabs-js/compare/v1.0.0...HEAD
|
|
50
|
+
[1.0.0]: https://github.com/ShieldLabs-ai/shieldlabs-js/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,420 @@
|
|
|
1
|
+
# @shieldlabs-ai/js
|
|
2
|
+
|
|
3
|
+
Load the ShieldLabs agent in the browser and get a request ID for every identification, with
|
|
4
|
+
promises, TypeScript types and safe defaults.
|
|
5
|
+
|
|
6
|
+
[](https://github.com/ShieldLabs-ai/shieldlabs-js/actions/workflows/ci.yml)
|
|
7
|
+
[](https://www.npmjs.com/package/@shieldlabs-ai/js)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
`@shieldlabs-ai/js` is a small loader (about 2.5 kB min+gzip, no dependencies). At runtime it imports the
|
|
11
|
+
hosted ShieldLabs agent from `https://cdn.shieldlabs.ai` and wraps its callbacks in promises.
|
|
12
|
+
Identification runs in the agent; scoring runs on ShieldLabs servers.
|
|
13
|
+
|
|
14
|
+
New to ShieldLabs? [Start free](https://app.shieldlabs.ai), then copy the Public Key of your domain
|
|
15
|
+
from Integration > API keys in the analytics dashboard (the Install tab also shows a ready snippet
|
|
16
|
+
that contains it).
|
|
17
|
+
|
|
18
|
+
## How it fits
|
|
19
|
+
|
|
20
|
+
1. **Browser.** `@shieldlabs-ai/js` loads the agent and runs an identification. The page receives a
|
|
21
|
+
`requestId`.
|
|
22
|
+
2. **Your backend.** It receives the `requestId` with the protected action (signup, login,
|
|
23
|
+
checkout) and reads the verdict for it from the History API with a ShieldLabs server SDK, or
|
|
24
|
+
receives it in a signed `identification.scored` webhook.
|
|
25
|
+
3. **Decision.** Your backend acts on the Risk Score (bands: trusted 0-29, suspicious 30-59,
|
|
26
|
+
dangerous 60-100), the detection flags and identifiers such as the device ID.
|
|
27
|
+
|
|
28
|
+
The browser only ever gets the request ID. The Risk Score, risk signals, detection flags, visitor ID
|
|
29
|
+
and device ID are read on your server. The webhook is delivered once per identification today
|
|
30
|
+
(1 second timeout, no retries), so use the History API when your backend must have the verdict, and
|
|
31
|
+
make the webhook handler idempotent on `data.request_id`: future retries will resend identical bytes.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @shieldlabs-ai/js
|
|
37
|
+
# or
|
|
38
|
+
yarn add @shieldlabs-ai/js
|
|
39
|
+
# or
|
|
40
|
+
pnpm add @shieldlabs-ai/js
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Without a bundler, use the IIFE build, which defines the global `ShieldLabsJS`:
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<script src="https://cdn.jsdelivr.net/npm/@shieldlabs-ai/js@1.0.0/dist/shieldlabs.iife.js"></script>
|
|
47
|
+
<script>
|
|
48
|
+
ShieldLabsJS.load({ publicKey: '0123456789abcdef0123456789abcdef' })
|
|
49
|
+
.then((agent) => {
|
|
50
|
+
// agent.identify(), agent.check(), agent.identifyOnInteraction(form)
|
|
51
|
+
})
|
|
52
|
+
.catch((error) => console.warn('ShieldLabs:', error.code, error.message));
|
|
53
|
+
</script>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The same file ships in the package as `dist/shieldlabs.iife.js` if you prefer to serve it from your
|
|
57
|
+
own origin. Pin the version you use. This file is only the loader: the agent itself always comes
|
|
58
|
+
from `https://cdn.shieldlabs.ai`.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { load, type LoadOptions } from '@shieldlabs-ai/js';
|
|
64
|
+
|
|
65
|
+
const options: LoadOptions = { publicKey: import.meta.env.VITE_SHIELDLABS_PUBLIC_KEY };
|
|
66
|
+
|
|
67
|
+
// Start loading the agent now, but do not await it here: the form must keep working when the
|
|
68
|
+
// agent cannot load (a content blocker, a network error).
|
|
69
|
+
load(options).catch(() => {}); // handled in the submit handler
|
|
70
|
+
|
|
71
|
+
const form = document.querySelector<HTMLFormElement>('#signup')!;
|
|
72
|
+
form.addEventListener('submit', async (event) => {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
let requestId: string | null = null;
|
|
75
|
+
try {
|
|
76
|
+
// load() again: it returns the loaded agent at once, waits for a load that is still running
|
|
77
|
+
// and tries again after a failed one.
|
|
78
|
+
const agent = await load(options);
|
|
79
|
+
({ requestId } = await agent.identify());
|
|
80
|
+
} catch {
|
|
81
|
+
// No identification: send the signup anyway. Your server treats it as unverified.
|
|
82
|
+
}
|
|
83
|
+
await fetch('/api/signup', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ email: form.email.value, requestId }),
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Avoid a top-level `await load(...)`. When the agent is blocked, the rest of the module never runs,
|
|
92
|
+
so the submit handler is never attached. Some build targets also reject top-level `await`. Call
|
|
93
|
+
`load()` again wherever you need the agent rather than keeping its first promise: that promise stays
|
|
94
|
+
rejected after a passing network error or a slow load, and a new call recovers from both.
|
|
95
|
+
|
|
96
|
+
On your server, read the verdict for `requestId` with a ShieldLabs server SDK, for example
|
|
97
|
+
`identifications.get(requestId)` in [`@shieldlabs-ai/node`](https://github.com/ShieldLabs-ai/shieldlabs-node),
|
|
98
|
+
which waits until the identification has been scored. The History row appears about 1 to 3 seconds
|
|
99
|
+
after the browser call and can be refined for up to about 10 seconds while follow-up checks finish,
|
|
100
|
+
so start the identification when the user begins the action, for example with
|
|
101
|
+
`identifyOnInteraction()` (see [Protect a form](#protect-a-form)).
|
|
102
|
+
|
|
103
|
+
> **Keep the page alive after `identify()` resolves.** The agent posts the identification right
|
|
104
|
+
> after it hands over the request ID. Keep the page open until your own request has been sent, and
|
|
105
|
+
> do not navigate away the moment `identify()` resolves (for example with `location.href = ...` in
|
|
106
|
+
> its `then` callback): the agent's post may not have gone out yet. For classic full-page form
|
|
107
|
+
> posts, start the identification early with `identifyOnInteraction()` (see the guide).
|
|
108
|
+
|
|
109
|
+
> **Test on a registered domain.** ShieldLabs records identifications only for the domains
|
|
110
|
+
> registered in your account. On `localhost` the page still receives a `requestId`, but the
|
|
111
|
+
> identification is rejected with `401` and your backend never finds it. Test on a development
|
|
112
|
+
> domain with its own keys, as described in [Environments](https://docs.shieldlabs.ai/setup/environments).
|
|
113
|
+
|
|
114
|
+
## Guide
|
|
115
|
+
|
|
116
|
+
### Protect a form
|
|
117
|
+
|
|
118
|
+
`identifyOnInteraction(form)` starts `identify()` on the first `focusin`, `pointerdown` or `keydown`
|
|
119
|
+
inside the form. By the time the user submits, the identification is usually done. `take()` returns
|
|
120
|
+
it for this submission and re-arms the handle, so the next submission gets its own request ID.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { load, ShieldLabsError, type InteractionIdentifier, type LoadOptions } from '@shieldlabs-ai/js';
|
|
124
|
+
|
|
125
|
+
const options: LoadOptions = { publicKey: import.meta.env.VITE_SHIELDLABS_PUBLIC_KEY };
|
|
126
|
+
const form = document.querySelector<HTMLFormElement>('#signup')!;
|
|
127
|
+
|
|
128
|
+
// One handle for the form, created once the agent has loaded. load() reuses the loaded agent on
|
|
129
|
+
// every call and tries again after a failed load, so a submit after a failure still recovers.
|
|
130
|
+
let handle: InteractionIdentifier | undefined;
|
|
131
|
+
async function identifier(): Promise<InteractionIdentifier> {
|
|
132
|
+
const agent = await load(options);
|
|
133
|
+
return (handle ??= agent.identifyOnInteraction(form));
|
|
134
|
+
}
|
|
135
|
+
identifier().catch(() => {}); // start loading now; errors are handled in the submit handler
|
|
136
|
+
|
|
137
|
+
form.addEventListener('submit', async (event) => {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
let requestId: string | null = null;
|
|
140
|
+
try {
|
|
141
|
+
({ requestId } = await (await identifier()).take());
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// No identification: send the form anyway. Your server treats it as unverified.
|
|
144
|
+
if (error instanceof ShieldLabsError) console.warn(error.code, error.message);
|
|
145
|
+
}
|
|
146
|
+
await fetch('/api/signup', {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify({ email: form.email.value, requestId }),
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For a classic full-page post, put the request ID in a hidden field and submit the form yourself:
|
|
155
|
+
|
|
156
|
+
```html
|
|
157
|
+
<form id="signup" action="/signup" method="post">
|
|
158
|
+
<input name="email" type="email" required />
|
|
159
|
+
<input type="hidden" name="requestId" />
|
|
160
|
+
<button>Sign up</button>
|
|
161
|
+
</form>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
form.addEventListener('submit', async (event) => {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
try {
|
|
168
|
+
const { requestId } = await (await identifier()).take();
|
|
169
|
+
(form.elements.namedItem('requestId') as HTMLInputElement).value = requestId;
|
|
170
|
+
} catch {
|
|
171
|
+
// No identification: submit without a requestId. Your server treats it as unverified.
|
|
172
|
+
}
|
|
173
|
+
form.submit();
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Because the identification starts on the first interaction, it has normally finished posting by the
|
|
178
|
+
time the user submits. If users can submit without interacting first (for example autofill and a
|
|
179
|
+
single click on the button), prefer sending the form with `fetch()`, which keeps the page alive.
|
|
180
|
+
|
|
181
|
+
`take()` hands out the early identification only while it is fresh. When it failed, or finished more
|
|
182
|
+
than 4 minutes ago, `take()` starts a new one, so the request ID your server receives stays inside a
|
|
183
|
+
5-minute freshness window (replacing a stale one costs one more identification). Interactions keep
|
|
184
|
+
the early identification fresh as well, even when nothing is submitted: while users keep interacting
|
|
185
|
+
with the form, a new identification starts at most every 4 minutes, and after a failure the next
|
|
186
|
+
interaction starts a new attempt, at most one every 5 seconds. Each of them is billed.
|
|
187
|
+
|
|
188
|
+
When the form goes away (for example when a single-page app leaves the route), call
|
|
189
|
+
`handle?.dispose()` to remove the listeners and set `handle = undefined`. On the server, accept each
|
|
190
|
+
request ID once and only within your freshness window (the examples use 5 minutes): one
|
|
191
|
+
identification authorizes one protected action.
|
|
192
|
+
|
|
193
|
+
### Signed-in users: pass a User HID
|
|
194
|
+
|
|
195
|
+
Pass a User HID so ShieldLabs ties the identification to the account. Compute it **on your server**
|
|
196
|
+
from your account ID with a secret key, for example with the `userHid()` helper of the ShieldLabs
|
|
197
|
+
server SDKs (HMAC-SHA256, 64 hex characters), and render it into the page:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// server (Node.js)
|
|
201
|
+
import { userHid } from '@shieldlabs-ai/node';
|
|
202
|
+
const hid = userHid(String(account.id), userHidSecret); // a secret you keep on the server
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// browser (load and options as in the quick start): call it when the signed-in user submits
|
|
207
|
+
async function identifySignedIn(userHid: string): Promise<string | null> {
|
|
208
|
+
try {
|
|
209
|
+
const agent = await load(options);
|
|
210
|
+
return (await agent.identify({ userId: userHid })).requestId;
|
|
211
|
+
} catch {
|
|
212
|
+
return null; // no identification: send the action anyway, your server treats it as unverified
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Never pass a raw email address, phone number or database ID. The SDK rejects the reserved values
|
|
218
|
+
`"anonymous"`, `"fail"`, `"-1"` and `"unknown"` with `invalid_options`, and logs a one-time warning
|
|
219
|
+
for values that look like an email address, contain `/`, `?`, `#` or `%`, or are `.` or `..`. The
|
|
220
|
+
History API looks a User HID up as a URL path segment: it cannot search a value that contains `/`,
|
|
221
|
+
the ShieldLabs server SDKs reject `.` and `..` there, and the other characters are easy to escape
|
|
222
|
+
wrongly. Use a hex hash. Omit `userId` for visitors who are not signed in.
|
|
223
|
+
|
|
224
|
+
### Background checks with `check()`
|
|
225
|
+
|
|
226
|
+
`check()` runs the agent's limited check: at most one identification per visit every five minutes
|
|
227
|
+
for the same user, shared across tabs. It resolves `null` when the agent skipped the check. Use it
|
|
228
|
+
for passive monitoring of a visit, for example once after sign-in:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// hid: the User HID your server rendered into the page
|
|
232
|
+
async function monitorVisit(hid: string): Promise<void> {
|
|
233
|
+
try {
|
|
234
|
+
const agent = await load(options);
|
|
235
|
+
const result = await agent.check({ userId: hid });
|
|
236
|
+
if (result) {
|
|
237
|
+
// Optional: send result.requestId to your backend to follow the visit.
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Passive monitoring only: nothing to do when the agent is not available.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Use `identify()` for protected actions: it always runs and always returns a new request ID.
|
|
246
|
+
|
|
247
|
+
### Frameworks
|
|
248
|
+
|
|
249
|
+
The framework packages load the agent once per app and add loading and error state:
|
|
250
|
+
|
|
251
|
+
- React: [`@shieldlabs-ai/react`](https://github.com/ShieldLabs-ai/shieldlabs-react)
|
|
252
|
+
- Vue and Nuxt: [`@shieldlabs-ai/vue`](https://github.com/ShieldLabs-ai/shieldlabs-vue)
|
|
253
|
+
- Angular: [`@shieldlabs-ai/angular`](https://github.com/ShieldLabs-ai/shieldlabs-angular)
|
|
254
|
+
- Svelte and SvelteKit: [`@shieldlabs-ai/svelte`](https://github.com/ShieldLabs-ai/shieldlabs-svelte)
|
|
255
|
+
- Next.js: [`@shieldlabs-ai/next`](https://github.com/ShieldLabs-ai/shieldlabs-next)
|
|
256
|
+
|
|
257
|
+
### Server-side rendering
|
|
258
|
+
|
|
259
|
+
Importing `@shieldlabs-ai/js` has no side effects and touches no browser globals, so it is safe in
|
|
260
|
+
server bundles. `load()` rejects with `unsupported_environment` on the server and in workers: call
|
|
261
|
+
it in the browser, for example in an effect or a mount hook. Calls are memoized per agent URL and
|
|
262
|
+
Public Key, so mounting a component twice (or React StrictMode) loads the agent once.
|
|
263
|
+
|
|
264
|
+
### Development environment and `scriptUrl`
|
|
265
|
+
|
|
266
|
+
For your own development and staging sites, keep the default environment and register a separate
|
|
267
|
+
domain with its own keys (see [Environments](https://docs.shieldlabs.ai/setup/environments)).
|
|
268
|
+
`environment: 'development'` loads the agent of the ShieldLabs development environment
|
|
269
|
+
(`https://dev.cdn.shieldlabs.ai/snippet.js`). `scriptUrl` overrides the agent module URL for tests:
|
|
270
|
+
it must be an absolute `https` URL (plain `http` is accepted only for `localhost` and `127.0.0.1`),
|
|
271
|
+
and the SDK adds the `publicKey` query parameter.
|
|
272
|
+
|
|
273
|
+
## Call budget
|
|
274
|
+
|
|
275
|
+
Every identification that runs is billed and takes part of a small per-IP budget, so call the agent
|
|
276
|
+
only when it matters:
|
|
277
|
+
|
|
278
|
+
- Call `identify()` once per protected action. Never call the agent on every render or on every
|
|
279
|
+
client-side route change.
|
|
280
|
+
- The ingest accepts about 15 requests per minute per visitor IP, and one identification uses 4 to 5
|
|
281
|
+
of them. An IP over the limit is blocked for 10 minutes. During the block `identify()` still
|
|
282
|
+
resolves with a request ID, but the ingest rejects the identification, so your backend never finds
|
|
283
|
+
it (`identifications.get()` returns `null`): treat it as unverified. The block itself can appear
|
|
284
|
+
once as a separate identification with the Risk Score marker 999 and its own request ID, never
|
|
285
|
+
under the request IDs your page received.
|
|
286
|
+
- Never clear the agent's storage (its first-party cookie ID in `localStorage` and a cookie). Several
|
|
287
|
+
fresh cookie IDs from one device in a short time raise a browser automation risk signal.
|
|
288
|
+
- The agent also runs its own limited background checks: it patches `history.pushState` and
|
|
289
|
+
`history.replaceState` and checks again on clicks, key presses, form submits and navigation once
|
|
290
|
+
its five-minute window has passed. The page never receives those request IDs, so your backend can
|
|
291
|
+
see extra identifications for the same user and session.
|
|
292
|
+
|
|
293
|
+
## Content Security Policy
|
|
294
|
+
|
|
295
|
+
If your site sends a `Content-Security-Policy` header, allow the agent's origins:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
script-src 'self' https://cdn.shieldlabs.ai;
|
|
299
|
+
connect-src 'self' https://rest.shieldlabs.ai wss://rest.shieldlabs.ai https://webrtc.shieldlabs.ai stun:ice.shieldlabs.ai:3478;
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
| Directive | Origin | Used for |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| `script-src` | `https://cdn.shieldlabs.ai` | The agent module (`snippet.js`) and its supporting modules |
|
|
305
|
+
| `connect-src` | `https://rest.shieldlabs.ai` | Posting the identification |
|
|
306
|
+
| `connect-src` | `wss://rest.shieldlabs.ai` | The WebSocket used by the network check |
|
|
307
|
+
| `connect-src` | `https://webrtc.shieldlabs.ai` | The network check session |
|
|
308
|
+
| `connect-src` | `stun:ice.shieldlabs.ai:3478` | The network check (STUN) |
|
|
309
|
+
|
|
310
|
+
The agent is loaded with a dynamic `import()`, so the policy does not need `'unsafe-eval'`. If
|
|
311
|
+
`https://webrtc.shieldlabs.ai` is blocked, identifications still arrive but can carry the
|
|
312
|
+
`stun_not_checked` risk signal. If you load the IIFE build from a public npm CDN, add that origin to
|
|
313
|
+
`script-src` as well. Details: [Content Security Policy](https://docs.shieldlabs.ai/setup/csp).
|
|
314
|
+
|
|
315
|
+
## Consent
|
|
316
|
+
|
|
317
|
+
The agent does not read your consent banner or consent manager. Where your policy or applicable law
|
|
318
|
+
requires consent, call `load()` and any identification only after consent is given (for example
|
|
319
|
+
from your consent manager's accept callback). The agent keeps a first-party cookie ID (`cookieID`,
|
|
320
|
+
in `localStorage` and a first-party cookie); list it in your cookie notice with its fraud prevention
|
|
321
|
+
purpose. Never pass directly identifying data as the User HID.
|
|
322
|
+
|
|
323
|
+
## Reference
|
|
324
|
+
|
|
325
|
+
| Export | Description |
|
|
326
|
+
|---|---|
|
|
327
|
+
| `load(options: LoadOptions): Promise<ShieldLabsAgent>` | Imports the agent from the CDN and resolves an agent. Memoized per agent URL and Public Key; concurrent calls share one import; a failed import is retried on the next call (with a new URL, because browsers remember a failed module load); rejects with `timeout` when the agent does not load within `timeout`, while the import keeps running for later calls |
|
|
328
|
+
| `ShieldLabsError` | The only error type the SDK throws or rejects with. Has `code` and optional `cause` |
|
|
329
|
+
| `VERSION` | The package version, for example `"1.0.0"` |
|
|
330
|
+
| Types | `LoadOptions`, `IdentifyOptions`, `IdentifyResult`, `ShieldLabsAgent`, `InteractionIdentifier`, `ShieldLabsErrorCode` |
|
|
331
|
+
|
|
332
|
+
`LoadOptions`
|
|
333
|
+
|
|
334
|
+
| Option | Type | Default | Description |
|
|
335
|
+
|---|---|---|---|
|
|
336
|
+
| `publicKey` | `string` | required | Public Key of your domain. Must match `^[A-Za-z0-9_-]{1,128}$`; a one-time warning is logged when it is not 32 lowercase hex characters. Server-side secrets (`sec_…`, `whsec_…`) are rejected |
|
|
337
|
+
| `environment` | `'production' \| 'development'` | `'production'` | Which ShieldLabs CDN to load the agent from |
|
|
338
|
+
| `scriptUrl` | `string` | | Advanced: agent module URL override (`https`, or `http` on `localhost` and `127.0.0.1`) |
|
|
339
|
+
| `timeout` | `number` | `10000` | Milliseconds to wait for the agent to load, and the default for each agent call |
|
|
340
|
+
|
|
341
|
+
`ShieldLabsAgent`
|
|
342
|
+
|
|
343
|
+
| Method | Returns | Description |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| `identify(options?)` | `Promise<IdentifyResult>` | Fresh identification now (the agent's force call). Always a new request ID. Use it for protected actions |
|
|
346
|
+
| `check(options?)` | `Promise<IdentifyResult \| null>` | Background check, limited by the agent to one per visit every five minutes. `null` when the agent skipped it |
|
|
347
|
+
| `identifyOnInteraction(target, options?)` | `InteractionIdentifier` | Starts `identify()` on the first `focusin`, `pointerdown` or `keydown` on `target` |
|
|
348
|
+
|
|
349
|
+
`IdentifyOptions`
|
|
350
|
+
|
|
351
|
+
| Option | Type | Description |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `userId` | `string` | User HID computed on your server. Omit for anonymous checks. Must be a non-empty string other than `"anonymous"`, `"fail"`, `"-1"` and `"unknown"` |
|
|
354
|
+
| `timeout` | `number` | Milliseconds to wait for this call. Overrides `LoadOptions.timeout` |
|
|
355
|
+
|
|
356
|
+
`IdentifyResult`
|
|
357
|
+
|
|
358
|
+
| Field | Type | Description |
|
|
359
|
+
|---|---|---|
|
|
360
|
+
| `requestId` | `string` | Send it to your backend with the protected action |
|
|
361
|
+
| `userId` | `string \| null` | The User HID used, `null` for anonymous checks |
|
|
362
|
+
|
|
363
|
+
`InteractionIdentifier`
|
|
364
|
+
|
|
365
|
+
| Method | Description |
|
|
366
|
+
|---|---|
|
|
367
|
+
| `take(): Promise<IdentifyResult>` | The identification for this submission: the early one while it is fresh, otherwise a new `identify()` (when none is running, the early one failed, or it finished more than 4 minutes ago). Then re-arms for the next submission. While the form is in use, interactions also start a new identification at most every 4 minutes |
|
|
368
|
+
| `dispose(): void` | Removes the event listeners |
|
|
369
|
+
|
|
370
|
+
## Errors and retries
|
|
371
|
+
|
|
372
|
+
Every error is a `ShieldLabsError`. Branch on `error.code`:
|
|
373
|
+
|
|
374
|
+
| `code` | When | What to do |
|
|
375
|
+
|---|---|---|
|
|
376
|
+
| `invalid_options` | An option failed validation (`publicKey`, `environment`, `scriptUrl`, `timeout`, `userId`, the interaction target), or `publicKey` holds a server-side secret (`sec_…`, `whsec_…`). Nothing was loaded or called | Fix the call; retrying does not help. Rotate a secret that reached browser code |
|
|
377
|
+
| `unsupported_environment` | No browser page (server-side rendering, a worker), or the page is not a secure context | Call `load()` in the browser; serve the page over HTTPS (`localhost` and `127.0.0.1` also work over `http`) |
|
|
378
|
+
| `load_failed` | The agent module could not be imported: network error, content blocker, Content Security Policy, wrong `scriptUrl`. `cause` holds the original error | Continue without an identification. You can call `load()` again later: failed loads are not cached, and the next call imports the agent again |
|
|
379
|
+
| `not_initialized` | `identify()` only: the agent did not start an identification, for example because another one is running in this or another tab. `check()` resolves `null` instead | Retry once later, or continue without an identification |
|
|
380
|
+
| `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 a later `load()` call uses it once it arrives; a late answer to an agent call is ignored |
|
|
381
|
+
|
|
382
|
+
Whenever there is no identification, send the protected action anyway without a `requestId`: your
|
|
383
|
+
backend treats a missing identification as unverified (for example step-up or review), never as
|
|
384
|
+
clean.
|
|
385
|
+
|
|
386
|
+
The SDK never repeats an agent call on its own, because every `identify()` is a billable
|
|
387
|
+
identification. `load()` is safe to call again at any time: it returns the loaded agent at once,
|
|
388
|
+
waits for a load that is still running, and imports the agent again (with a new URL) after a failed
|
|
389
|
+
load.
|
|
390
|
+
|
|
391
|
+
## Compatibility
|
|
392
|
+
|
|
393
|
+
- Browsers that support ES modules, dynamic `import()` and WebCrypto: current versions of Chrome,
|
|
394
|
+
Edge, Firefox, Safari, Opera and Samsung Internet, on desktop and mobile.
|
|
395
|
+
- The page must be a secure context: HTTPS, or `http://localhost` and `http://127.0.0.1` during
|
|
396
|
+
development.
|
|
397
|
+
- Output: ES2019 syntax as ESM, CommonJS and a minified IIFE (`window.ShieldLabsJS`), with bundled
|
|
398
|
+
TypeScript declarations. No runtime dependencies.
|
|
399
|
+
- Bundlers: the runtime import carries the `webpackIgnore` and `@vite-ignore` hints, so webpack
|
|
400
|
+
(including Next.js) and Vite leave the CDN URL alone.
|
|
401
|
+
- Server runtimes (Node.js 18+, Bun, Deno, edge): safe to import; `load()` rejects there.
|
|
402
|
+
|
|
403
|
+
## Development
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
npm ci
|
|
407
|
+
npm run typecheck
|
|
408
|
+
npm run lint
|
|
409
|
+
npm test -- --coverage # builds first, then runs the tests
|
|
410
|
+
npm run test:browser # runs the built package in Chromium (npx playwright-core install chromium)
|
|
411
|
+
npm run build
|
|
412
|
+
npm run size # dist/index.js must stay at or below 3072 bytes gzip
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). Documentation: <https://docs.shieldlabs.ai>. Analytics
|
|
416
|
+
dashboard: <https://app.shieldlabs.ai>. Support: <contact@shieldlabs.ai>.
|
|
417
|
+
|
|
418
|
+
## License
|
|
419
|
+
|
|
420
|
+
[MIT](./LICENSE), Copyright (c) 2026 ShieldLabs Inc.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var s = class extends Error {
|
|
5
|
+
constructor(n, o, i) {
|
|
6
|
+
super(o);
|
|
7
|
+
this.name = "ShieldLabsError";
|
|
8
|
+
this.code = n;
|
|
9
|
+
if (i !== void 0) this.cause = i;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(s, "name", { value: "ShieldLabsError" });
|
|
13
|
+
|
|
14
|
+
// src/interaction.ts
|
|
15
|
+
var C = ["focusin", "pointerdown", "keydown"];
|
|
16
|
+
var b = { capture: true, passive: true };
|
|
17
|
+
var I = 4 * 60 * 1e3;
|
|
18
|
+
var U = 5e3;
|
|
19
|
+
function A(e, n) {
|
|
20
|
+
let o;
|
|
21
|
+
let i = false;
|
|
22
|
+
let t = 0;
|
|
23
|
+
const r = () => {
|
|
24
|
+
const d = e();
|
|
25
|
+
o = d;
|
|
26
|
+
i = false;
|
|
27
|
+
t = Infinity;
|
|
28
|
+
const a = (h) => () => {
|
|
29
|
+
if (o !== d) return;
|
|
30
|
+
i = !h;
|
|
31
|
+
t = Date.now();
|
|
32
|
+
};
|
|
33
|
+
d.then(a(true), a(false));
|
|
34
|
+
return d;
|
|
35
|
+
};
|
|
36
|
+
const u = () => Date.now() - t;
|
|
37
|
+
const p = () => {
|
|
38
|
+
if (!o || u() >= (i ? U : I)) void r();
|
|
39
|
+
};
|
|
40
|
+
const f = (d) => {
|
|
41
|
+
for (const a of C) {
|
|
42
|
+
if (d) n.addEventListener(a, p, b);
|
|
43
|
+
else n.removeEventListener(a, p, b);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
f(true);
|
|
47
|
+
return {
|
|
48
|
+
take() {
|
|
49
|
+
const d = o && !i && u() < I ? o : r();
|
|
50
|
+
o = void 0;
|
|
51
|
+
return d;
|
|
52
|
+
},
|
|
53
|
+
dispose() {
|
|
54
|
+
f(false);
|
|
55
|
+
o = void 0;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/validate.ts
|
|
61
|
+
var K = /^[A-Za-z0-9_-]{1,128}$/;
|
|
62
|
+
var M = /^[0-9a-f]{32}$/;
|
|
63
|
+
var D = /^(wh)?sec_/;
|
|
64
|
+
var N = ["anonymous", "fail", "-1", "unknown"];
|
|
65
|
+
var z = /[/?#%]|^\.\.?$/;
|
|
66
|
+
var H = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
67
|
+
var j = 2147483647;
|
|
68
|
+
var E = {};
|
|
69
|
+
function y(e, n) {
|
|
70
|
+
if (E[e]) return;
|
|
71
|
+
E[e] = true;
|
|
72
|
+
console.warn("[ShieldLabs] " + n);
|
|
73
|
+
}
|
|
74
|
+
function c(e) {
|
|
75
|
+
return new s("invalid_options", e);
|
|
76
|
+
}
|
|
77
|
+
function m(e) {
|
|
78
|
+
return typeof e === "object" && e !== null;
|
|
79
|
+
}
|
|
80
|
+
function k(e) {
|
|
81
|
+
if (typeof e !== "string" || !K.test(e)) {
|
|
82
|
+
throw c("publicKey must match ^[A-Za-z0-9_-]{1,128}$ (the Public Key of your domain).");
|
|
83
|
+
}
|
|
84
|
+
if (D.test(e)) {
|
|
85
|
+
throw c(
|
|
86
|
+
"publicKey is a server-side secret (sec_ or whsec_). Remove it from browser code, rotate it in the analytics dashboard and pass the Public Key of your domain."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!M.test(e)) {
|
|
90
|
+
y("publicKey", "publicKey is not 32 lowercase hex characters. Is it the Public Key of your domain?");
|
|
91
|
+
}
|
|
92
|
+
return e;
|
|
93
|
+
}
|
|
94
|
+
function g(e) {
|
|
95
|
+
if (e === void 0) return void 0;
|
|
96
|
+
if (typeof e !== "number" || !(e > 0 && e <= j)) {
|
|
97
|
+
throw c("timeout must be a number of milliseconds above 0.");
|
|
98
|
+
}
|
|
99
|
+
return e;
|
|
100
|
+
}
|
|
101
|
+
function V(e) {
|
|
102
|
+
if (e === void 0 || e === null) return void 0;
|
|
103
|
+
if (typeof e !== "string" || e.trim() === "") {
|
|
104
|
+
throw c("userId must be a non-empty string. Omit it for anonymous checks.");
|
|
105
|
+
}
|
|
106
|
+
if (N.includes(e)) {
|
|
107
|
+
throw c('userId "' + e + '" is reserved. Omit it for anonymous checks.');
|
|
108
|
+
}
|
|
109
|
+
if (z.test(e)) {
|
|
110
|
+
y("userIdCharacters", "userId is . or .. or contains / ? # or %, which the History API cannot always search. Use a hex hash.");
|
|
111
|
+
}
|
|
112
|
+
if (H.test(e)) {
|
|
113
|
+
y("userIdEmail", "userId looks like an email address. Pass a User HID computed on your server instead.");
|
|
114
|
+
}
|
|
115
|
+
return e;
|
|
116
|
+
}
|
|
117
|
+
function w(e) {
|
|
118
|
+
if (e === void 0 || e === null) return { userId: void 0, timeout: void 0 };
|
|
119
|
+
if (!m(e)) throw c("options must be an object.");
|
|
120
|
+
return { userId: V(e.userId), timeout: g(e.timeout) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/agent.ts
|
|
124
|
+
var $ = ["checkAnonymous", "checkAuthenticatedUser", "forceCheckAnonymous", "forceCheckAuthenticatedUser"];
|
|
125
|
+
function L(e) {
|
|
126
|
+
return m(e) && $.every((n) => typeof e[n] === "function");
|
|
127
|
+
}
|
|
128
|
+
function O(e, n, o, i) {
|
|
129
|
+
return new Promise((t, r) => {
|
|
130
|
+
const { userId: u, timeout: p = i } = w(o);
|
|
131
|
+
let f = false;
|
|
132
|
+
const d = setTimeout(() => {
|
|
133
|
+
f = true;
|
|
134
|
+
r(new s("timeout", "The agent did not answer within " + String(p) + " ms."));
|
|
135
|
+
}, p);
|
|
136
|
+
const a = (l, P) => {
|
|
137
|
+
if (f) return;
|
|
138
|
+
f = true;
|
|
139
|
+
clearTimeout(d);
|
|
140
|
+
if (typeof l === "string" && l !== "") {
|
|
141
|
+
t({ requestId: l, userId: u != null ? u : null });
|
|
142
|
+
} else {
|
|
143
|
+
r(new s("not_initialized", "The agent did not start an identification.", P));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const h = {
|
|
147
|
+
onInitialized: (l) => {
|
|
148
|
+
a(m(l) && l.status === "initialized" ? l.requestID : void 0);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
if (u === void 0) {
|
|
153
|
+
if (n) e.forceCheckAnonymous(h);
|
|
154
|
+
else e.checkAnonymous(h);
|
|
155
|
+
} else if (n) {
|
|
156
|
+
e.forceCheckAuthenticatedUser(u, h);
|
|
157
|
+
} else {
|
|
158
|
+
e.checkAuthenticatedUser(u, h);
|
|
159
|
+
}
|
|
160
|
+
} catch (l) {
|
|
161
|
+
a(void 0, l);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function S(e, n) {
|
|
166
|
+
const o = {
|
|
167
|
+
identify: (i) => O(e, true, i, n),
|
|
168
|
+
check: (i) => O(e, false, i, n).catch((t) => {
|
|
169
|
+
if (t instanceof s && t.code === "not_initialized") return null;
|
|
170
|
+
throw t;
|
|
171
|
+
}),
|
|
172
|
+
identifyOnInteraction(i, t) {
|
|
173
|
+
const r = i;
|
|
174
|
+
if (!m(r) || typeof r.addEventListener !== "function" || typeof r.removeEventListener !== "function") {
|
|
175
|
+
throw c("identifyOnInteraction() needs an EventTarget such as a form.");
|
|
176
|
+
}
|
|
177
|
+
w(t);
|
|
178
|
+
return A(() => o.identify(t), i);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
return o;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/environment.ts
|
|
185
|
+
var G = "https://cdn.shieldlabs.ai/snippet.js";
|
|
186
|
+
var X = "https://dev.cdn.shieldlabs.ai/snippet.js";
|
|
187
|
+
function x(e) {
|
|
188
|
+
return e === "localhost" || e === "127.0.0.1";
|
|
189
|
+
}
|
|
190
|
+
function R() {
|
|
191
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
192
|
+
throw new s("unsupported_environment", "load() needs a browser page, not server-side rendering or a worker.");
|
|
193
|
+
}
|
|
194
|
+
const e = window.location;
|
|
195
|
+
const n = typeof window.isSecureContext === "boolean" ? window.isSecureContext : e.protocol === "https:";
|
|
196
|
+
if (!n && !x(e.hostname)) {
|
|
197
|
+
throw new s("unsupported_environment", "The page is not a secure context: the agent needs HTTPS (or localhost).");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function v(e, n, o) {
|
|
201
|
+
if (n !== void 0 && n !== "production" && n !== "development") {
|
|
202
|
+
throw c('environment must be "production" or "development".');
|
|
203
|
+
}
|
|
204
|
+
let i = n === "development" ? X : G;
|
|
205
|
+
if (o !== void 0) {
|
|
206
|
+
let t;
|
|
207
|
+
if (typeof o === "string") {
|
|
208
|
+
try {
|
|
209
|
+
t = new URL(o);
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!t || !(t.protocol === "https:" || t.protocol === "http:" && x(t.hostname))) {
|
|
214
|
+
throw c("scriptUrl must be an absolute https URL (http only for localhost and 127.0.0.1).");
|
|
215
|
+
}
|
|
216
|
+
t.hash = "";
|
|
217
|
+
if (t.searchParams.has("publicKey")) t.searchParams.delete("publicKey");
|
|
218
|
+
if (!t.search) t.search = "";
|
|
219
|
+
i = t.href;
|
|
220
|
+
}
|
|
221
|
+
return i + (i.includes("?") ? "&" : "?") + "publicKey=" + encodeURIComponent(e);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/import-agent.ts
|
|
225
|
+
function _(e) {
|
|
226
|
+
return import(
|
|
227
|
+
/* webpackIgnore: true */
|
|
228
|
+
/* @vite-ignore */
|
|
229
|
+
e
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/load.ts
|
|
234
|
+
var Y = 1e4;
|
|
235
|
+
var T = /* @__PURE__ */ new Map();
|
|
236
|
+
function B(e) {
|
|
237
|
+
let n = T.get(e);
|
|
238
|
+
if (!n) T.set(e, n = { failures: 0 });
|
|
239
|
+
if (n.module) return n.module;
|
|
240
|
+
const o = n;
|
|
241
|
+
const i = o.failures ? e + "&retry=" + String(o.failures) : e;
|
|
242
|
+
const t = new Promise((r) => {
|
|
243
|
+
r(_(i));
|
|
244
|
+
}).then(
|
|
245
|
+
(r) => {
|
|
246
|
+
if (L(r)) return r;
|
|
247
|
+
throw new s("load_failed", "The module at " + i + " is not the ShieldLabs agent.");
|
|
248
|
+
},
|
|
249
|
+
(r) => {
|
|
250
|
+
throw new s("load_failed", "Could not load the ShieldLabs agent from " + i + ".", r);
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
o.module = t;
|
|
254
|
+
t.then(void 0, () => {
|
|
255
|
+
o.failures += 1;
|
|
256
|
+
o.module = void 0;
|
|
257
|
+
});
|
|
258
|
+
return t;
|
|
259
|
+
}
|
|
260
|
+
function F(e) {
|
|
261
|
+
return new Promise((n, o) => {
|
|
262
|
+
var d;
|
|
263
|
+
if (!m(e)) throw c("load() needs an options object.");
|
|
264
|
+
const i = k(e.publicKey);
|
|
265
|
+
const t = (d = g(e.timeout)) != null ? d : Y;
|
|
266
|
+
const r = v(i, e.environment, e.scriptUrl);
|
|
267
|
+
R();
|
|
268
|
+
const u = setTimeout(() => {
|
|
269
|
+
o(new s("timeout", "The ShieldLabs agent did not load within " + String(t) + " ms."));
|
|
270
|
+
}, t);
|
|
271
|
+
const p = B(r);
|
|
272
|
+
const f = () => {
|
|
273
|
+
clearTimeout(u);
|
|
274
|
+
};
|
|
275
|
+
void p.then(f, f);
|
|
276
|
+
void p.then((a) => {
|
|
277
|
+
n(S(a, t));
|
|
278
|
+
}, o);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/version.ts
|
|
283
|
+
var Z = "1.0.0";
|
|
284
|
+
|
|
285
|
+
exports.ShieldLabsError = s;
|
|
286
|
+
exports.VERSION = Z;
|
|
287
|
+
exports.load = F;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Handle returned by `identifyOnInteraction()`. */
|
|
2
|
+
interface InteractionIdentifier {
|
|
3
|
+
/**
|
|
4
|
+
* The identification for this submission. Returns the one started by the first interaction while
|
|
5
|
+
* it is fresh, or starts `identify()` now: when none is running, when the early one failed, or
|
|
6
|
+
* when it finished more than four minutes ago (servers accept a request ID for five minutes).
|
|
7
|
+
* Every call re-arms the handle, so the next interaction starts a new identification for the
|
|
8
|
+
* next submission.
|
|
9
|
+
*
|
|
10
|
+
* Interactions keep the early identification fresh even without a submission: while users keep
|
|
11
|
+
* interacting with the target, a new identification starts at most every four minutes (and at
|
|
12
|
+
* most every five seconds after a failed one). Each identification is billed.
|
|
13
|
+
*/
|
|
14
|
+
take(): Promise<IdentifyResult>;
|
|
15
|
+
/** Removes the event listeners. Call it when the form goes away. */
|
|
16
|
+
dispose(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface IdentifyOptions {
|
|
20
|
+
/**
|
|
21
|
+
* User HID: a hashed or pseudonymous account ID computed on your server. Omit it for anonymous
|
|
22
|
+
* checks. Must be a non-empty string other than the reserved values "anonymous", "fail", "-1" and
|
|
23
|
+
* "unknown".
|
|
24
|
+
*/
|
|
25
|
+
userId?: string;
|
|
26
|
+
/** Milliseconds to wait for the agent. Overrides `LoadOptions.timeout`. */
|
|
27
|
+
timeout?: number;
|
|
28
|
+
}
|
|
29
|
+
interface IdentifyResult {
|
|
30
|
+
/** Send this to your backend with the protected action. */
|
|
31
|
+
requestId: string;
|
|
32
|
+
/** The User HID used, `null` for anonymous checks. */
|
|
33
|
+
userId: string | null;
|
|
34
|
+
}
|
|
35
|
+
interface ShieldLabsAgent {
|
|
36
|
+
/**
|
|
37
|
+
* Runs a fresh identification now (the agent's force call). Always creates a new request ID.
|
|
38
|
+
* Use it for protected actions such as signup, login or checkout.
|
|
39
|
+
*/
|
|
40
|
+
identify(options?: IdentifyOptions): Promise<IdentifyResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Background check, limited by the agent to one per visit every five minutes. Resolves `null`
|
|
43
|
+
* when the agent skipped it.
|
|
44
|
+
*/
|
|
45
|
+
check(options?: IdentifyOptions): Promise<IdentifyResult | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Starts `identify()` on the first interaction with `target` (`focusin`, `pointerdown` or
|
|
48
|
+
* `keydown`) and returns a handle whose `take()` yields the result for this submission, then
|
|
49
|
+
* re-arms. While users keep interacting, a new identification starts at most every four minutes.
|
|
50
|
+
*/
|
|
51
|
+
identifyOnInteraction(target: EventTarget, options?: IdentifyOptions): InteractionIdentifier;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface LoadOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Public Key of your domain (Integration > API keys in the analytics dashboard). Must match
|
|
57
|
+
* `^[A-Za-z0-9_-]{1,128}$`; a one-time warning is logged when it is not 32 lowercase hex characters.
|
|
58
|
+
* Server-side secrets (`sec_…`, `whsec_…`) are rejected.
|
|
59
|
+
*/
|
|
60
|
+
publicKey: string;
|
|
61
|
+
/** `'production'` (default) loads `https://cdn.shieldlabs.ai/snippet.js`, `'development'` the development agent. */
|
|
62
|
+
environment?: 'production' | 'development';
|
|
63
|
+
/** Advanced: agent module URL override. https only (http is allowed for localhost and 127.0.0.1). */
|
|
64
|
+
scriptUrl?: string;
|
|
65
|
+
/** Milliseconds to wait for the agent to load, and the default for each agent call. Default 10000. */
|
|
66
|
+
timeout?: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Loads the ShieldLabs agent from the CDN and returns an agent you can identify with.
|
|
70
|
+
* Memoized per agent URL and Public Key: concurrent and repeated calls share one import. When the
|
|
71
|
+
* import takes longer than `timeout`, the call rejects with `timeout` and the import continues: a
|
|
72
|
+
* later call uses it once it has loaded.
|
|
73
|
+
*/
|
|
74
|
+
declare function load(options: LoadOptions): Promise<ShieldLabsAgent>;
|
|
75
|
+
|
|
76
|
+
/** The reason behind a {@link ShieldLabsError}. */
|
|
77
|
+
type ShieldLabsErrorCode = 'invalid_options' | 'unsupported_environment' | 'load_failed' | 'not_initialized' | 'timeout';
|
|
78
|
+
/**
|
|
79
|
+
* The only error type the SDK throws or rejects with. Branch on `code`; the message is for people.
|
|
80
|
+
*
|
|
81
|
+
* - `invalid_options`: an option failed validation. Nothing was loaded or called.
|
|
82
|
+
* - `unsupported_environment`: no browser page (server-side rendering, a worker) or the page is
|
|
83
|
+
* not a secure context.
|
|
84
|
+
* - `load_failed`: the agent module could not be imported from the CDN.
|
|
85
|
+
* - `not_initialized`: the agent did not start an identification.
|
|
86
|
+
* - `timeout`: the agent did not answer in time.
|
|
87
|
+
*/
|
|
88
|
+
declare class ShieldLabsError extends Error {
|
|
89
|
+
/** Machine-readable reason. */
|
|
90
|
+
readonly code: ShieldLabsErrorCode;
|
|
91
|
+
/** The underlying error, when there is one (for example the failed import). */
|
|
92
|
+
readonly cause?: unknown;
|
|
93
|
+
constructor(code: ShieldLabsErrorCode, message: string, cause?: unknown);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Version of `@shieldlabs-ai/js`. A test keeps it equal to `package.json`. */
|
|
97
|
+
declare const VERSION = "1.0.0";
|
|
98
|
+
|
|
99
|
+
export { type IdentifyOptions, type IdentifyResult, type InteractionIdentifier, type LoadOptions, type ShieldLabsAgent, ShieldLabsError, type ShieldLabsErrorCode, VERSION, load };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Handle returned by `identifyOnInteraction()`. */
|
|
2
|
+
interface InteractionIdentifier {
|
|
3
|
+
/**
|
|
4
|
+
* The identification for this submission. Returns the one started by the first interaction while
|
|
5
|
+
* it is fresh, or starts `identify()` now: when none is running, when the early one failed, or
|
|
6
|
+
* when it finished more than four minutes ago (servers accept a request ID for five minutes).
|
|
7
|
+
* Every call re-arms the handle, so the next interaction starts a new identification for the
|
|
8
|
+
* next submission.
|
|
9
|
+
*
|
|
10
|
+
* Interactions keep the early identification fresh even without a submission: while users keep
|
|
11
|
+
* interacting with the target, a new identification starts at most every four minutes (and at
|
|
12
|
+
* most every five seconds after a failed one). Each identification is billed.
|
|
13
|
+
*/
|
|
14
|
+
take(): Promise<IdentifyResult>;
|
|
15
|
+
/** Removes the event listeners. Call it when the form goes away. */
|
|
16
|
+
dispose(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface IdentifyOptions {
|
|
20
|
+
/**
|
|
21
|
+
* User HID: a hashed or pseudonymous account ID computed on your server. Omit it for anonymous
|
|
22
|
+
* checks. Must be a non-empty string other than the reserved values "anonymous", "fail", "-1" and
|
|
23
|
+
* "unknown".
|
|
24
|
+
*/
|
|
25
|
+
userId?: string;
|
|
26
|
+
/** Milliseconds to wait for the agent. Overrides `LoadOptions.timeout`. */
|
|
27
|
+
timeout?: number;
|
|
28
|
+
}
|
|
29
|
+
interface IdentifyResult {
|
|
30
|
+
/** Send this to your backend with the protected action. */
|
|
31
|
+
requestId: string;
|
|
32
|
+
/** The User HID used, `null` for anonymous checks. */
|
|
33
|
+
userId: string | null;
|
|
34
|
+
}
|
|
35
|
+
interface ShieldLabsAgent {
|
|
36
|
+
/**
|
|
37
|
+
* Runs a fresh identification now (the agent's force call). Always creates a new request ID.
|
|
38
|
+
* Use it for protected actions such as signup, login or checkout.
|
|
39
|
+
*/
|
|
40
|
+
identify(options?: IdentifyOptions): Promise<IdentifyResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Background check, limited by the agent to one per visit every five minutes. Resolves `null`
|
|
43
|
+
* when the agent skipped it.
|
|
44
|
+
*/
|
|
45
|
+
check(options?: IdentifyOptions): Promise<IdentifyResult | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Starts `identify()` on the first interaction with `target` (`focusin`, `pointerdown` or
|
|
48
|
+
* `keydown`) and returns a handle whose `take()` yields the result for this submission, then
|
|
49
|
+
* re-arms. While users keep interacting, a new identification starts at most every four minutes.
|
|
50
|
+
*/
|
|
51
|
+
identifyOnInteraction(target: EventTarget, options?: IdentifyOptions): InteractionIdentifier;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface LoadOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Public Key of your domain (Integration > API keys in the analytics dashboard). Must match
|
|
57
|
+
* `^[A-Za-z0-9_-]{1,128}$`; a one-time warning is logged when it is not 32 lowercase hex characters.
|
|
58
|
+
* Server-side secrets (`sec_…`, `whsec_…`) are rejected.
|
|
59
|
+
*/
|
|
60
|
+
publicKey: string;
|
|
61
|
+
/** `'production'` (default) loads `https://cdn.shieldlabs.ai/snippet.js`, `'development'` the development agent. */
|
|
62
|
+
environment?: 'production' | 'development';
|
|
63
|
+
/** Advanced: agent module URL override. https only (http is allowed for localhost and 127.0.0.1). */
|
|
64
|
+
scriptUrl?: string;
|
|
65
|
+
/** Milliseconds to wait for the agent to load, and the default for each agent call. Default 10000. */
|
|
66
|
+
timeout?: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Loads the ShieldLabs agent from the CDN and returns an agent you can identify with.
|
|
70
|
+
* Memoized per agent URL and Public Key: concurrent and repeated calls share one import. When the
|
|
71
|
+
* import takes longer than `timeout`, the call rejects with `timeout` and the import continues: a
|
|
72
|
+
* later call uses it once it has loaded.
|
|
73
|
+
*/
|
|
74
|
+
declare function load(options: LoadOptions): Promise<ShieldLabsAgent>;
|
|
75
|
+
|
|
76
|
+
/** The reason behind a {@link ShieldLabsError}. */
|
|
77
|
+
type ShieldLabsErrorCode = 'invalid_options' | 'unsupported_environment' | 'load_failed' | 'not_initialized' | 'timeout';
|
|
78
|
+
/**
|
|
79
|
+
* The only error type the SDK throws or rejects with. Branch on `code`; the message is for people.
|
|
80
|
+
*
|
|
81
|
+
* - `invalid_options`: an option failed validation. Nothing was loaded or called.
|
|
82
|
+
* - `unsupported_environment`: no browser page (server-side rendering, a worker) or the page is
|
|
83
|
+
* not a secure context.
|
|
84
|
+
* - `load_failed`: the agent module could not be imported from the CDN.
|
|
85
|
+
* - `not_initialized`: the agent did not start an identification.
|
|
86
|
+
* - `timeout`: the agent did not answer in time.
|
|
87
|
+
*/
|
|
88
|
+
declare class ShieldLabsError extends Error {
|
|
89
|
+
/** Machine-readable reason. */
|
|
90
|
+
readonly code: ShieldLabsErrorCode;
|
|
91
|
+
/** The underlying error, when there is one (for example the failed import). */
|
|
92
|
+
readonly cause?: unknown;
|
|
93
|
+
constructor(code: ShieldLabsErrorCode, message: string, cause?: unknown);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Version of `@shieldlabs-ai/js`. A test keeps it equal to `package.json`. */
|
|
97
|
+
declare const VERSION = "1.0.0";
|
|
98
|
+
|
|
99
|
+
export { type IdentifyOptions, type IdentifyResult, type InteractionIdentifier, type LoadOptions, type ShieldLabsAgent, ShieldLabsError, type ShieldLabsErrorCode, VERSION, load };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var s = class extends Error {
|
|
3
|
+
constructor(n, o, i) {
|
|
4
|
+
super(o);
|
|
5
|
+
this.name = "ShieldLabsError";
|
|
6
|
+
this.code = n;
|
|
7
|
+
if (i !== void 0) this.cause = i;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(s, "name", { value: "ShieldLabsError" });
|
|
11
|
+
|
|
12
|
+
// src/interaction.ts
|
|
13
|
+
var C = ["focusin", "pointerdown", "keydown"];
|
|
14
|
+
var b = { capture: true, passive: true };
|
|
15
|
+
var I = 4 * 60 * 1e3;
|
|
16
|
+
var U = 5e3;
|
|
17
|
+
function A(e, n) {
|
|
18
|
+
let o;
|
|
19
|
+
let i = false;
|
|
20
|
+
let t = 0;
|
|
21
|
+
const r = () => {
|
|
22
|
+
const d = e();
|
|
23
|
+
o = d;
|
|
24
|
+
i = false;
|
|
25
|
+
t = Infinity;
|
|
26
|
+
const a = (h) => () => {
|
|
27
|
+
if (o !== d) return;
|
|
28
|
+
i = !h;
|
|
29
|
+
t = Date.now();
|
|
30
|
+
};
|
|
31
|
+
d.then(a(true), a(false));
|
|
32
|
+
return d;
|
|
33
|
+
};
|
|
34
|
+
const u = () => Date.now() - t;
|
|
35
|
+
const p = () => {
|
|
36
|
+
if (!o || u() >= (i ? U : I)) void r();
|
|
37
|
+
};
|
|
38
|
+
const f = (d) => {
|
|
39
|
+
for (const a of C) {
|
|
40
|
+
if (d) n.addEventListener(a, p, b);
|
|
41
|
+
else n.removeEventListener(a, p, b);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
f(true);
|
|
45
|
+
return {
|
|
46
|
+
take() {
|
|
47
|
+
const d = o && !i && u() < I ? o : r();
|
|
48
|
+
o = void 0;
|
|
49
|
+
return d;
|
|
50
|
+
},
|
|
51
|
+
dispose() {
|
|
52
|
+
f(false);
|
|
53
|
+
o = void 0;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/validate.ts
|
|
59
|
+
var K = /^[A-Za-z0-9_-]{1,128}$/;
|
|
60
|
+
var M = /^[0-9a-f]{32}$/;
|
|
61
|
+
var D = /^(wh)?sec_/;
|
|
62
|
+
var N = ["anonymous", "fail", "-1", "unknown"];
|
|
63
|
+
var z = /[/?#%]|^\.\.?$/;
|
|
64
|
+
var H = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
65
|
+
var j = 2147483647;
|
|
66
|
+
var E = {};
|
|
67
|
+
function y(e, n) {
|
|
68
|
+
if (E[e]) return;
|
|
69
|
+
E[e] = true;
|
|
70
|
+
console.warn("[ShieldLabs] " + n);
|
|
71
|
+
}
|
|
72
|
+
function c(e) {
|
|
73
|
+
return new s("invalid_options", e);
|
|
74
|
+
}
|
|
75
|
+
function m(e) {
|
|
76
|
+
return typeof e === "object" && e !== null;
|
|
77
|
+
}
|
|
78
|
+
function k(e) {
|
|
79
|
+
if (typeof e !== "string" || !K.test(e)) {
|
|
80
|
+
throw c("publicKey must match ^[A-Za-z0-9_-]{1,128}$ (the Public Key of your domain).");
|
|
81
|
+
}
|
|
82
|
+
if (D.test(e)) {
|
|
83
|
+
throw c(
|
|
84
|
+
"publicKey is a server-side secret (sec_ or whsec_). Remove it from browser code, rotate it in the analytics dashboard and pass the Public Key of your domain."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (!M.test(e)) {
|
|
88
|
+
y("publicKey", "publicKey is not 32 lowercase hex characters. Is it the Public Key of your domain?");
|
|
89
|
+
}
|
|
90
|
+
return e;
|
|
91
|
+
}
|
|
92
|
+
function g(e) {
|
|
93
|
+
if (e === void 0) return void 0;
|
|
94
|
+
if (typeof e !== "number" || !(e > 0 && e <= j)) {
|
|
95
|
+
throw c("timeout must be a number of milliseconds above 0.");
|
|
96
|
+
}
|
|
97
|
+
return e;
|
|
98
|
+
}
|
|
99
|
+
function V(e) {
|
|
100
|
+
if (e === void 0 || e === null) return void 0;
|
|
101
|
+
if (typeof e !== "string" || e.trim() === "") {
|
|
102
|
+
throw c("userId must be a non-empty string. Omit it for anonymous checks.");
|
|
103
|
+
}
|
|
104
|
+
if (N.includes(e)) {
|
|
105
|
+
throw c('userId "' + e + '" is reserved. Omit it for anonymous checks.');
|
|
106
|
+
}
|
|
107
|
+
if (z.test(e)) {
|
|
108
|
+
y("userIdCharacters", "userId is . or .. or contains / ? # or %, which the History API cannot always search. Use a hex hash.");
|
|
109
|
+
}
|
|
110
|
+
if (H.test(e)) {
|
|
111
|
+
y("userIdEmail", "userId looks like an email address. Pass a User HID computed on your server instead.");
|
|
112
|
+
}
|
|
113
|
+
return e;
|
|
114
|
+
}
|
|
115
|
+
function w(e) {
|
|
116
|
+
if (e === void 0 || e === null) return { userId: void 0, timeout: void 0 };
|
|
117
|
+
if (!m(e)) throw c("options must be an object.");
|
|
118
|
+
return { userId: V(e.userId), timeout: g(e.timeout) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/agent.ts
|
|
122
|
+
var $ = ["checkAnonymous", "checkAuthenticatedUser", "forceCheckAnonymous", "forceCheckAuthenticatedUser"];
|
|
123
|
+
function L(e) {
|
|
124
|
+
return m(e) && $.every((n) => typeof e[n] === "function");
|
|
125
|
+
}
|
|
126
|
+
function O(e, n, o, i) {
|
|
127
|
+
return new Promise((t, r) => {
|
|
128
|
+
const { userId: u, timeout: p = i } = w(o);
|
|
129
|
+
let f = false;
|
|
130
|
+
const d = setTimeout(() => {
|
|
131
|
+
f = true;
|
|
132
|
+
r(new s("timeout", "The agent did not answer within " + String(p) + " ms."));
|
|
133
|
+
}, p);
|
|
134
|
+
const a = (l, P) => {
|
|
135
|
+
if (f) return;
|
|
136
|
+
f = true;
|
|
137
|
+
clearTimeout(d);
|
|
138
|
+
if (typeof l === "string" && l !== "") {
|
|
139
|
+
t({ requestId: l, userId: u != null ? u : null });
|
|
140
|
+
} else {
|
|
141
|
+
r(new s("not_initialized", "The agent did not start an identification.", P));
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const h = {
|
|
145
|
+
onInitialized: (l) => {
|
|
146
|
+
a(m(l) && l.status === "initialized" ? l.requestID : void 0);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
try {
|
|
150
|
+
if (u === void 0) {
|
|
151
|
+
if (n) e.forceCheckAnonymous(h);
|
|
152
|
+
else e.checkAnonymous(h);
|
|
153
|
+
} else if (n) {
|
|
154
|
+
e.forceCheckAuthenticatedUser(u, h);
|
|
155
|
+
} else {
|
|
156
|
+
e.checkAuthenticatedUser(u, h);
|
|
157
|
+
}
|
|
158
|
+
} catch (l) {
|
|
159
|
+
a(void 0, l);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function S(e, n) {
|
|
164
|
+
const o = {
|
|
165
|
+
identify: (i) => O(e, true, i, n),
|
|
166
|
+
check: (i) => O(e, false, i, n).catch((t) => {
|
|
167
|
+
if (t instanceof s && t.code === "not_initialized") return null;
|
|
168
|
+
throw t;
|
|
169
|
+
}),
|
|
170
|
+
identifyOnInteraction(i, t) {
|
|
171
|
+
const r = i;
|
|
172
|
+
if (!m(r) || typeof r.addEventListener !== "function" || typeof r.removeEventListener !== "function") {
|
|
173
|
+
throw c("identifyOnInteraction() needs an EventTarget such as a form.");
|
|
174
|
+
}
|
|
175
|
+
w(t);
|
|
176
|
+
return A(() => o.identify(t), i);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
return o;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/environment.ts
|
|
183
|
+
var G = "https://cdn.shieldlabs.ai/snippet.js";
|
|
184
|
+
var X = "https://dev.cdn.shieldlabs.ai/snippet.js";
|
|
185
|
+
function x(e) {
|
|
186
|
+
return e === "localhost" || e === "127.0.0.1";
|
|
187
|
+
}
|
|
188
|
+
function R() {
|
|
189
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
190
|
+
throw new s("unsupported_environment", "load() needs a browser page, not server-side rendering or a worker.");
|
|
191
|
+
}
|
|
192
|
+
const e = window.location;
|
|
193
|
+
const n = typeof window.isSecureContext === "boolean" ? window.isSecureContext : e.protocol === "https:";
|
|
194
|
+
if (!n && !x(e.hostname)) {
|
|
195
|
+
throw new s("unsupported_environment", "The page is not a secure context: the agent needs HTTPS (or localhost).");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function v(e, n, o) {
|
|
199
|
+
if (n !== void 0 && n !== "production" && n !== "development") {
|
|
200
|
+
throw c('environment must be "production" or "development".');
|
|
201
|
+
}
|
|
202
|
+
let i = n === "development" ? X : G;
|
|
203
|
+
if (o !== void 0) {
|
|
204
|
+
let t;
|
|
205
|
+
if (typeof o === "string") {
|
|
206
|
+
try {
|
|
207
|
+
t = new URL(o);
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!t || !(t.protocol === "https:" || t.protocol === "http:" && x(t.hostname))) {
|
|
212
|
+
throw c("scriptUrl must be an absolute https URL (http only for localhost and 127.0.0.1).");
|
|
213
|
+
}
|
|
214
|
+
t.hash = "";
|
|
215
|
+
if (t.searchParams.has("publicKey")) t.searchParams.delete("publicKey");
|
|
216
|
+
if (!t.search) t.search = "";
|
|
217
|
+
i = t.href;
|
|
218
|
+
}
|
|
219
|
+
return i + (i.includes("?") ? "&" : "?") + "publicKey=" + encodeURIComponent(e);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/import-agent.ts
|
|
223
|
+
function _(e) {
|
|
224
|
+
return import(
|
|
225
|
+
/* webpackIgnore: true */
|
|
226
|
+
/* @vite-ignore */
|
|
227
|
+
e
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/load.ts
|
|
232
|
+
var Y = 1e4;
|
|
233
|
+
var T = /* @__PURE__ */ new Map();
|
|
234
|
+
function B(e) {
|
|
235
|
+
let n = T.get(e);
|
|
236
|
+
if (!n) T.set(e, n = { failures: 0 });
|
|
237
|
+
if (n.module) return n.module;
|
|
238
|
+
const o = n;
|
|
239
|
+
const i = o.failures ? e + "&retry=" + String(o.failures) : e;
|
|
240
|
+
const t = new Promise((r) => {
|
|
241
|
+
r(_(i));
|
|
242
|
+
}).then(
|
|
243
|
+
(r) => {
|
|
244
|
+
if (L(r)) return r;
|
|
245
|
+
throw new s("load_failed", "The module at " + i + " is not the ShieldLabs agent.");
|
|
246
|
+
},
|
|
247
|
+
(r) => {
|
|
248
|
+
throw new s("load_failed", "Could not load the ShieldLabs agent from " + i + ".", r);
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
o.module = t;
|
|
252
|
+
t.then(void 0, () => {
|
|
253
|
+
o.failures += 1;
|
|
254
|
+
o.module = void 0;
|
|
255
|
+
});
|
|
256
|
+
return t;
|
|
257
|
+
}
|
|
258
|
+
function F(e) {
|
|
259
|
+
return new Promise((n, o) => {
|
|
260
|
+
var d;
|
|
261
|
+
if (!m(e)) throw c("load() needs an options object.");
|
|
262
|
+
const i = k(e.publicKey);
|
|
263
|
+
const t = (d = g(e.timeout)) != null ? d : Y;
|
|
264
|
+
const r = v(i, e.environment, e.scriptUrl);
|
|
265
|
+
R();
|
|
266
|
+
const u = setTimeout(() => {
|
|
267
|
+
o(new s("timeout", "The ShieldLabs agent did not load within " + String(t) + " ms."));
|
|
268
|
+
}, t);
|
|
269
|
+
const p = B(r);
|
|
270
|
+
const f = () => {
|
|
271
|
+
clearTimeout(u);
|
|
272
|
+
};
|
|
273
|
+
void p.then(f, f);
|
|
274
|
+
void p.then((a) => {
|
|
275
|
+
n(S(a, t));
|
|
276
|
+
}, o);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/version.ts
|
|
281
|
+
var Z = "1.0.0";
|
|
282
|
+
|
|
283
|
+
export { s as ShieldLabsError, Z as VERSION, F as load };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var ShieldLabsJS=(function(exports){'use strict';var s=class extends Error{constructor(n,o,i){super(o),this.name="ShieldLabsError",this.code=n,i!==void 0&&(this.cause=i);}};Object.defineProperty(s,"name",{value:"ShieldLabsError"});var C=["focusin","pointerdown","keydown"],b={capture:true,passive:true},I=240*1e3,U=5e3;function A(e,n){let o,i=false,t=0,r=()=>{let d=e();o=d,i=false,t=1/0;let a=h=>()=>{o===d&&(i=!h,t=Date.now());};return d.then(a(true),a(false)),d},u=()=>Date.now()-t,p=()=>{(!o||u()>=(i?U:I))&&r();},f=d=>{for(let a of C)d?n.addEventListener(a,p,b):n.removeEventListener(a,p,b);};return f(true),{take(){let d=o&&!i&&u()<I?o:r();return o=void 0,d},dispose(){f(false),o=void 0;}}}var K=/^[A-Za-z0-9_-]{1,128}$/,M=/^[0-9a-f]{32}$/,D=/^(wh)?sec_/,N=["anonymous","fail","-1","unknown"],z=/[/?#%]|^\.\.?$/,H=/^[^\s@]+@[^\s@]+\.[^\s@]+$/,j=2147483647,E={};function y(e,n){E[e]||(E[e]=true,console.warn("[ShieldLabs] "+n));}function c(e){return new s("invalid_options",e)}function m(e){return typeof e=="object"&&e!==null}function k(e){if(typeof e!="string"||!K.test(e))throw c("publicKey must match ^[A-Za-z0-9_-]{1,128}$ (the Public Key of your domain).");if(D.test(e))throw c("publicKey is a server-side secret (sec_ or whsec_). Remove it from browser code, rotate it in the analytics dashboard and pass the Public Key of your domain.");return M.test(e)||y("publicKey","publicKey is not 32 lowercase hex characters. Is it the Public Key of your domain?"),e}function g(e){if(e!==void 0){if(typeof e!="number"||!(e>0&&e<=j))throw c("timeout must be a number of milliseconds above 0.");return e}}function V(e){if(e!=null){if(typeof e!="string"||e.trim()==="")throw c("userId must be a non-empty string. Omit it for anonymous checks.");if(N.includes(e))throw c('userId "'+e+'" is reserved. Omit it for anonymous checks.');return z.test(e)&&y("userIdCharacters","userId is . or .. or contains / ? # or %, which the History API cannot always search. Use a hex hash."),H.test(e)&&y("userIdEmail","userId looks like an email address. Pass a User HID computed on your server instead."),e}}function w(e){if(e==null)return {userId:void 0,timeout:void 0};if(!m(e))throw c("options must be an object.");return {userId:V(e.userId),timeout:g(e.timeout)}}var $=["checkAnonymous","checkAuthenticatedUser","forceCheckAnonymous","forceCheckAuthenticatedUser"];function L(e){return m(e)&&$.every(n=>typeof e[n]=="function")}function O(e,n,o,i){return new Promise((t,r)=>{let{userId:u,timeout:p=i}=w(o),f=false,d=setTimeout(()=>{f=true,r(new s("timeout","The agent did not answer within "+String(p)+" ms."));},p),a=(l,P)=>{f||(f=true,clearTimeout(d),typeof l=="string"&&l!==""?t({requestId:l,userId:u!=null?u:null}):r(new s("not_initialized","The agent did not start an identification.",P)));},h={onInitialized:l=>{a(m(l)&&l.status==="initialized"?l.requestID:void 0);}};try{u===void 0?n?e.forceCheckAnonymous(h):e.checkAnonymous(h):n?e.forceCheckAuthenticatedUser(u,h):e.checkAuthenticatedUser(u,h);}catch(l){a(void 0,l);}})}function S(e,n){let o={identify:i=>O(e,true,i,n),check:i=>O(e,false,i,n).catch(t=>{if(t instanceof s&&t.code==="not_initialized")return null;throw t}),identifyOnInteraction(i,t){let r=i;if(!m(r)||typeof r.addEventListener!="function"||typeof r.removeEventListener!="function")throw c("identifyOnInteraction() needs an EventTarget such as a form.");return w(t),A(()=>o.identify(t),i)}};return o}var G="https://cdn.shieldlabs.ai/snippet.js",X="https://dev.cdn.shieldlabs.ai/snippet.js";function x(e){return e==="localhost"||e==="127.0.0.1"}function R(){if(typeof window=="undefined"||typeof document=="undefined")throw new s("unsupported_environment","load() needs a browser page, not server-side rendering or a worker.");let e=window.location;if(!(typeof window.isSecureContext=="boolean"?window.isSecureContext:e.protocol==="https:")&&!x(e.hostname))throw new s("unsupported_environment","The page is not a secure context: the agent needs HTTPS (or localhost).")}function v(e,n,o){if(n!==void 0&&n!=="production"&&n!=="development")throw c('environment must be "production" or "development".');let i=n==="development"?X:G;if(o!==void 0){let t;if(typeof o=="string")try{t=new URL(o);}catch{}if(!t||!(t.protocol==="https:"||t.protocol==="http:"&&x(t.hostname)))throw c("scriptUrl must be an absolute https URL (http only for localhost and 127.0.0.1).");t.hash="",t.searchParams.has("publicKey")&&t.searchParams.delete("publicKey"),t.search||(t.search=""),i=t.href;}return i+(i.includes("?")?"&":"?")+"publicKey="+encodeURIComponent(e)}function _(e){return import(e)}var Y=1e4,T=new Map;function B(e){let n=T.get(e);if(n||T.set(e,n={failures:0}),n.module)return n.module;let o=n,i=o.failures?e+"&retry="+String(o.failures):e,t=new Promise(r=>{r(_(i));}).then(r=>{if(L(r))return r;throw new s("load_failed","The module at "+i+" is not the ShieldLabs agent.")},r=>{throw new s("load_failed","Could not load the ShieldLabs agent from "+i+".",r)});return o.module=t,t.then(void 0,()=>{o.failures+=1,o.module=void 0;}),t}function F(e){return new Promise((n,o)=>{var d;if(!m(e))throw c("load() needs an options object.");let i=k(e.publicKey),t=(d=g(e.timeout))!=null?d:Y,r=v(i,e.environment,e.scriptUrl);R();let u=setTimeout(()=>{o(new s("timeout","The ShieldLabs agent did not load within "+String(t)+" ms."));},t),p=B(r),f=()=>{clearTimeout(u);};p.then(f,f),p.then(a=>{n(S(a,t));},o);})}var Z="1.0.0";
|
|
2
|
+
exports.ShieldLabsError=s;exports.VERSION=Z;exports.load=F;return exports;})({});
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shieldlabs-ai/js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Browser loader for ShieldLabs device intelligence: loads the hosted agent from the ShieldLabs CDN and returns a request ID for every identification.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"shieldlabs",
|
|
7
|
+
"device-intelligence",
|
|
8
|
+
"fraud-detection",
|
|
9
|
+
"risk-score",
|
|
10
|
+
"identification",
|
|
11
|
+
"fingerprinting",
|
|
12
|
+
"visitor-identification"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://docs.shieldlabs.ai",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/ShieldLabs-ai/shieldlabs-js/issues",
|
|
17
|
+
"email": "contact@shieldlabs.ai"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/ShieldLabs-ai/shieldlabs-js.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "ShieldLabs Inc. <contact@shieldlabs.ai>",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/index.cjs",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"unpkg": "./dist/shieldlabs.iife.js",
|
|
31
|
+
"jsdelivr": "./dist/shieldlabs.iife.js",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/index.d.cts",
|
|
40
|
+
"default": "./dist/index.cjs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"CHANGELOG.md"
|
|
48
|
+
],
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "node scripts/clean.mjs && tsup",
|
|
57
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json",
|
|
58
|
+
"lint": "eslint .",
|
|
59
|
+
"pretest": "npm run build",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:browser": "npm run build && vitest run --config vitest.browser.config.ts",
|
|
62
|
+
"size": "node scripts/size.mjs",
|
|
63
|
+
"prepublishOnly": "npm run build"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@eslint/js": "~9.39.5",
|
|
67
|
+
"@types/node": "^22.0.0",
|
|
68
|
+
"@vitest/coverage-v8": "~3.2.7",
|
|
69
|
+
"eslint": "~9.39.5",
|
|
70
|
+
"happy-dom": "^20.14.5",
|
|
71
|
+
"playwright-core": "1.63.0",
|
|
72
|
+
"tsup": "^8.5.1",
|
|
73
|
+
"typescript": "~5.9.3",
|
|
74
|
+
"typescript-eslint": "^8.71.0",
|
|
75
|
+
"vite": "~6.4.3",
|
|
76
|
+
"vitest": "~3.2.7"
|
|
77
|
+
}
|
|
78
|
+
}
|