@shieldlabs-ai/vue 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 ADDED
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@shieldlabs-ai/vue` 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
+ - `createShieldLabs(options)`: a Vue plugin for `app.use()` that loads the ShieldLabs agent once per
14
+ app with `@shieldlabs-ai/js`, after the app is mounted in the browser. It takes the `@shieldlabs-ai/js`
15
+ load options (`publicKey`, `environment`, `scriptUrl`, `timeout`) plus `checkOnLoad` (one
16
+ `check()` when the agent becomes ready, anonymous or for a User HID, skipped when an `identify()`
17
+ or `check()` for the same User HID is already in flight) and `autoLoad` (default `true`; with
18
+ `false` nothing loads until `load()` is called, for example after consent, and until then
19
+ `identify()` and `check()` fail fast instead of waiting: `identify()` rejects with
20
+ `not_initialized`, `check()` resolves `null` and the `identify()` of `useIdentify()` resolves `null`
21
+ with a `not_initialized` error). A second plugin on the same app is ignored with a warning.
22
+ - `useShieldLabs()`: `status` (`loading`, `ready` or `error`) and `error` refs, `identify()` and
23
+ `check()` that wait for the agent, `load()` that starts loading it, and `getAgent()` that resolves
24
+ the `@shieldlabs-ai/js` agent once it has loaded (for example for `identifyOnInteraction()` on a
25
+ form; with `autoLoad: false` it waits for `load()`, with no timeout of its own). A failed load is
26
+ retried by the next `identify()`, `check()`, `getAgent()` or `load()`.
27
+ - One timeout per call: the `timeout` of `identify()` and `check()` (else the plugin's `timeout`,
28
+ 10 seconds by default) covers the wait for the agent to load and the agent's answer together. A
29
+ call whose timeout ends before the agent is ready fails with `timeout` and never reaches the
30
+ agent.
31
+ - `useIdentify({ userId, runOnMount })`: an identify helper with read-only `result`, `isLoading`
32
+ and `error` refs (holding the objects from `@shieldlabs-ai/js` as they are) and `reset()`.
33
+ `identify()` never rejects (it resolves `null` and puts the reason in `error`), reads `userId`
34
+ from a string, ref or getter when it runs, and returns a call of the same helper already in flight
35
+ with the same `userId` and `timeout` (a call without `timeout` counts as one with the plugin's
36
+ `timeout`) instead of starting another identification, so a double submit costs one
37
+ identification. `runOnMount` identifies once after mount; with `autoLoad: false` before `load()`
38
+ it resolves `null` with a `not_initialized` error, like `identify()`, and does not run again after
39
+ `load()`.
40
+ - Server-side rendering: nothing touches browser globals during setup or render and nothing loads
41
+ on the server. `status` is `loading` during server rendering and in the first render of every
42
+ component, so hydration matches the server HTML. The composables also render during server
43
+ rendering without the plugin (a client-only Nuxt plugin).
44
+ - `shieldLabsKey` (injection key), `VERSION`, the `ShieldLabsError` class and the types of
45
+ `@shieldlabs-ai/js` (including `InteractionIdentifier`), and the types `ShieldLabsOptions`,
46
+ `CheckOnLoadOption`, `ShieldLabsPlugin`, `ShieldLabsContext`, `ShieldLabsStatus`,
47
+ `UseShieldLabsReturn`, `UseIdentifyOptions` and `UseIdentifyReturn`.
48
+ - ESM and CommonJS builds with TypeScript declarations. `vue` (`^3.3.0`) and `@shieldlabs-ai/js`
49
+ (`^1.0.0`) are peer dependencies; no runtime dependencies.
50
+ - `examples/vite`: a Vue signup form. README guides for Vue and Nuxt 3.
51
+
52
+ ### Removed
53
+
54
+ - The `0.0.0` placeholder `useShieldLabs({ apiKey })`. The browser receives a request ID; results
55
+ are read on your server.
56
+
57
+ [Unreleased]: https://github.com/ShieldLabs-ai/shieldlabs-vue/compare/v1.0.0...HEAD
58
+ [1.0.0]: https://github.com/ShieldLabs-ai/shieldlabs-vue/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,589 @@
1
+ # @shieldlabs-ai/vue
2
+
3
+ Vue 3 and Nuxt bindings for ShieldLabs device intelligence: a plugin that loads the agent once and
4
+ composables that return a request ID with loading and error state.
5
+
6
+ [![CI](https://github.com/ShieldLabs-ai/shieldlabs-vue/actions/workflows/ci.yml/badge.svg)](https://github.com/ShieldLabs-ai/shieldlabs-vue/actions/workflows/ci.yml)
7
+ [![npm](https://img.shields.io/npm/v/@shieldlabs-ai/vue)](https://www.npmjs.com/package/@shieldlabs-ai/vue)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
9
+
10
+ `@shieldlabs-ai/vue` builds on [`@shieldlabs-ai/js`](https://github.com/ShieldLabs-ai/shieldlabs-js), the
11
+ browser loader that imports the hosted ShieldLabs agent from `https://cdn.shieldlabs.ai` at
12
+ runtime. It adds a Vue plugin, composables with refs, and safe server-side rendering for Vue SSR
13
+ and Nuxt 3.
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.** `@shieldlabs-ai/vue` loads the agent and runs an identification. Your component
22
+ 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.
31
+
32
+ Webhooks: today each identification is delivered once, with a 1 second timeout and no retries.
33
+ Make your webhook handler idempotent on `data.request_id`, because future retries will resend
34
+ identical bytes, and read the History API when you need a guaranteed result.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install @shieldlabs-ai/vue @shieldlabs-ai/js
40
+ # or
41
+ yarn add @shieldlabs-ai/vue @shieldlabs-ai/js
42
+ # or
43
+ pnpm add @shieldlabs-ai/vue @shieldlabs-ai/js
44
+ ```
45
+
46
+ `vue` (3.3 or later) and `@shieldlabs-ai/js` are peer dependencies: your app provides one copy of each.
47
+
48
+ ## Quick start
49
+
50
+ Install the plugin once:
51
+
52
+ ```ts
53
+ // src/main.ts
54
+ import { createShieldLabs } from '@shieldlabs-ai/vue';
55
+ import { createApp } from 'vue';
56
+ import App from './App.vue';
57
+
58
+ createApp(App)
59
+ .use(createShieldLabs({ publicKey: import.meta.env.VITE_SHIELDLABS_PUBLIC_KEY }))
60
+ .mount('#app');
61
+ ```
62
+
63
+ Identify when the user submits a protected form, and send the `requestId` with the request:
64
+
65
+ ```vue
66
+ <script setup lang="ts">
67
+ import { useIdentify } from '@shieldlabs-ai/vue';
68
+ import { ref } from 'vue';
69
+
70
+ const email = ref('');
71
+ const { identify, isLoading } = useIdentify();
72
+
73
+ async function onSubmit() {
74
+ const result = await identify(); // null when there is no identification
75
+ await fetch('/api/signup', {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({ email: email.value, requestId: result?.requestId ?? null }),
79
+ });
80
+ }
81
+ </script>
82
+
83
+ <template>
84
+ <form @submit.prevent="onSubmit">
85
+ <input v-model="email" type="email" required />
86
+ <button :disabled="isLoading">Sign up</button>
87
+ </form>
88
+ </template>
89
+ ```
90
+
91
+ On your server, read the verdict for `requestId` with a ShieldLabs server SDK, for example
92
+ `identifications.get(requestId)` in [`@shieldlabs-ai/node`](https://github.com/ShieldLabs-ai/shieldlabs-node),
93
+ which waits until the identification has been scored. The History row appears about 1-3 seconds
94
+ after `identify()` resolves and can be refined for up to about 10 seconds as follow-up checks
95
+ finish. To keep that wait off the submit, start the identification when the user begins the action
96
+ (see [Start on the first interaction with the form](#start-on-the-first-interaction-with-the-form)).
97
+ Server SDKs:
98
+ [Node.js](https://github.com/ShieldLabs-ai/shieldlabs-node),
99
+ [Python](https://github.com/ShieldLabs-ai/shieldlabs-python),
100
+ [Go](https://github.com/ShieldLabs-ai/shieldlabs-go),
101
+ [PHP](https://github.com/ShieldLabs-ai/shieldlabs-php),
102
+ [Java](https://github.com/ShieldLabs-ai/shieldlabs-java) and
103
+ [.NET](https://github.com/ShieldLabs-ai/shieldlabs-dotnet).
104
+
105
+ > **Keep the page alive after `identify()` resolves.** The agent posts the identification right
106
+ > after it hands over the request ID. Send your request with `fetch()` as above, and start a full
107
+ > page navigation (`location.href = ...`, a classic form post) only after it has been sent.
108
+ > Client-side navigation with Vue Router keeps the page alive. Details:
109
+ > [`@shieldlabs-ai/js` Quick start](https://github.com/ShieldLabs-ai/shieldlabs-js#quick-start).
110
+
111
+ A complete app is in [`examples/vite`](examples/vite).
112
+
113
+ ## Guide
114
+
115
+ ### Protect a form with `useIdentify()`
116
+
117
+ `useIdentify()` gives each component its own identify helper:
118
+
119
+ - `identify(options?)` runs a fresh identification and resolves `{ requestId, userId }`, or `null`
120
+ when there is no identification (the reason is in `error`). It never rejects, so your submit
121
+ handler always goes on to send the protected action: test the resolved value for `null` instead
122
+ of catching. Your backend treats a request without `requestId` as unverified (step-up or review),
123
+ never as clean.
124
+ - `result`, `isLoading` and `error` are refs for the latest call; `reset()` clears them.
125
+
126
+ One identification authorizes one protected action. Call `identify()` for each submission and keep
127
+ the submit button disabled while `isLoading` is `true`, as in the Quick start. A new call clears
128
+ `result`. While a call with the same `userId` and `timeout` is in flight, `identify()` returns that
129
+ call instead of starting another identification, so a double submit costs one identification: the
130
+ agent runs one identification at a time for a User HID and would refuse the second one with
131
+ `not_initialized`. A call without `timeout` counts as one with the plugin's `timeout` (10 seconds by
132
+ default), so with the default `identify()` and `identify({ timeout: 10000 })` share one
133
+ identification. Calls are shared within one `useIdentify()` helper, also after `reset()`, which only
134
+ clears the refs. On the server, accept each request ID once and only within your freshness window
135
+ (the server SDK examples use 5 minutes).
136
+
137
+ ### Signed-in users: pass a User HID
138
+
139
+ ```vue
140
+ <script setup lang="ts">
141
+ import { useIdentify } from '@shieldlabs-ai/vue';
142
+
143
+ const props = defineProps<{ userHid: string }>(); // computed on your server
144
+ const { identify } = useIdentify({ userId: () => props.userHid });
145
+ </script>
146
+ ```
147
+
148
+ `userId` accepts a string, a ref or a getter, and is read when `identify()` runs: changing it never
149
+ starts an identification. `null` and `undefined` mean anonymous. Compute the User HID on your
150
+ server from your account ID with a secret key, for example with the `userHid()` helper of the
151
+ server SDKs (HMAC-SHA256, 64 hex characters). Never pass a raw email address, phone number or
152
+ database ID. `@shieldlabs-ai/js` rejects the reserved values `"anonymous"`, `"fail"`, `"-1"` and
153
+ `"unknown"`, and warns once about values that look like an email address or contain `/`, `?`, `#`
154
+ or `%`, which are hard to search in the History API (see
155
+ [Signed-in users](https://github.com/ShieldLabs-ai/shieldlabs-js#signed-in-users-pass-a-user-hid)).
156
+
157
+ Options passed to `identify()` override the composable for that call: `identify({ userId: hid })`,
158
+ `identify({ userId: undefined })` for an anonymous identification, or `identify({ timeout: 5000 })`
159
+ for a shorter wait (milliseconds). The timeout bounds the whole call: the wait for the agent to load
160
+ and the agent's answer together. Without one, the plugin's `timeout` applies (10 seconds by
161
+ default). A call whose timeout ends before the agent is ready resolves `null` with a `timeout`
162
+ error and never reaches the agent.
163
+
164
+ ### Identify once when a component mounts
165
+
166
+ `useIdentify({ runOnMount: true })` calls `identify()` once, after the component is mounted and the
167
+ agent is ready. Every identification is billed: use it in a component that mounts once per
168
+ protected flow (for example a checkout step), not in a layout or in a component that remounts on
169
+ every route change. With `autoLoad: false`, call `load()` before the component mounts (see
170
+ [Wait for consent](#wait-for-consent-with-autoload-false)); otherwise that identification ends right
171
+ away with `not_initialized`, like `identify()`, and it does not run again by itself after `load()`.
172
+
173
+ ### Passive monitoring with `checkOnLoad`
174
+
175
+ ```ts
176
+ app.use(createShieldLabs({ publicKey, checkOnLoad: true })); // anonymous visitor
177
+ app.use(createShieldLabs({ publicKey, checkOnLoad: { userId: hid } })); // signed-in user
178
+ ```
179
+
180
+ `checkOnLoad` runs the agent's limited `check()` once, when the agent becomes ready. The agent runs
181
+ at most one such check per visit every five minutes, shared across tabs. The page does not need its
182
+ request ID: your backend sees the identification in the History API. For later background checks,
183
+ call `check()` from `useShieldLabs()`; it resolves `null` when the agent skipped the check.
184
+
185
+ The agent runs one identification at a time for a User HID, which shapes how the check fits in:
186
+
187
+ - When an `identify()` or `check()` for the same User HID is already in flight as the agent becomes
188
+ ready (for example `runOnMount`, a submit while the agent loads, or the call that loads the agent
189
+ again after a failed load), the plugin skips the check. That call identifies the visit, and a
190
+ check sent ahead of it would make the agent refuse it. Calls for another User HID do not stop
191
+ the check.
192
+ - While the check is in progress, an `identify()` for the same User HID can get `not_initialized`
193
+ (`useIdentify()` resolves `null`). Use `checkOnLoad` on pages without an immediate protected
194
+ action; on a signup, login or checkout page, the `identify()` on submit already covers the visit.
195
+
196
+ ### Agent state with `useShieldLabs()`
197
+
198
+ ```vue
199
+ <script setup lang="ts">
200
+ import { useShieldLabs } from '@shieldlabs-ai/vue';
201
+
202
+ const { status, error } = useShieldLabs();
203
+ </script>
204
+
205
+ <template>
206
+ <p v-if="status === 'error'">Identification is unavailable ({{ error?.code }}). You can still sign up.</p>
207
+ </template>
208
+ ```
209
+
210
+ - `status` is `'loading'`, `'ready'` or `'error'`. In a component it stays `'loading'` until the
211
+ component is mounted, as during server-side rendering, so hydration always matches the server
212
+ HTML.
213
+ - `error` is the `ShieldLabsError` of the last failed load, otherwise `null`. The next
214
+ `identify()`, `check()`, `getAgent()` or `load()` loads the agent again.
215
+ - `identify(options?)` and `check(options?)` are the agent methods of `@shieldlabs-ai/js` and wait for
216
+ the agent within their timeout. Unlike `useIdentify()`, this `identify()` rejects with a
217
+ `ShieldLabsError` when there is no identification. With `autoLoad: false` they do not wait before
218
+ `load()` is called: `identify()` rejects with `not_initialized` and `check()` resolves `null` right
219
+ away.
220
+ - `load()` starts loading the agent now, unless it is loaded or loading, and loads again after a
221
+ failed load. With `autoLoad: false` nothing loads before it (see
222
+ [Wait for consent](#wait-for-consent-with-autoload-false)).
223
+ - `getAgent()` resolves the `@shieldlabs-ai/js` agent once it has loaded, for example for
224
+ `identifyOnInteraction()` (see
225
+ [Start on the first interaction with the form](#start-on-the-first-interaction-with-the-form)).
226
+ It loads the agent when needed (with `autoLoad: false` it waits for `load()`, with no timeout of
227
+ its own) and rejects with the `ShieldLabsError` of a failed load.
228
+
229
+ Nothing needs to wait for `'ready'`: `identify()` waits for the agent itself (with `autoLoad: false`,
230
+ once `load()` has been called).
231
+
232
+ ### Nuxt 3
233
+
234
+ Add the Public Key to the public runtime config and set it with an environment variable:
235
+
236
+ ```ts
237
+ // nuxt.config.ts
238
+ export default defineNuxtConfig({
239
+ runtimeConfig: {
240
+ public: {
241
+ shieldlabsPublicKey: '', // NUXT_PUBLIC_SHIELDLABS_PUBLIC_KEY
242
+ },
243
+ },
244
+ });
245
+ ```
246
+
247
+ ```bash
248
+ # .env
249
+ NUXT_PUBLIC_SHIELDLABS_PUBLIC_KEY=0123456789abcdef0123456789abcdef
250
+ ```
251
+
252
+ Install the plugin in the browser only, with a `.client` plugin file:
253
+
254
+ ```ts
255
+ // plugins/shieldlabs.client.ts
256
+ import { createShieldLabs } from '@shieldlabs-ai/vue';
257
+
258
+ export default defineNuxtPlugin((nuxtApp) => {
259
+ const config = useRuntimeConfig();
260
+ nuxtApp.vueApp.use(createShieldLabs({ publicKey: config.public.shieldlabsPublicKey }));
261
+ });
262
+ ```
263
+
264
+ Use the composables in pages and components as in any Vue app. During server rendering they render
265
+ the initial state (`status` `'loading'`, no result) without the plugin and never load anything; in
266
+ the browser the plugin loads the agent once, after hydration.
267
+
268
+ ```vue
269
+ <!-- pages/signup.vue -->
270
+ <script setup lang="ts">
271
+ import { useIdentify } from '@shieldlabs-ai/vue';
272
+
273
+ const email = ref('');
274
+ const { identify, isLoading } = useIdentify();
275
+
276
+ async function onSubmit() {
277
+ const result = await identify();
278
+ await $fetch('/api/signup', { method: 'POST', body: { email: email.value, requestId: result?.requestId ?? null } });
279
+ }
280
+ </script>
281
+
282
+ <template>
283
+ <form @submit.prevent="onSubmit">
284
+ <input v-model="email" type="email" required />
285
+ <button :disabled="isLoading">Sign up</button>
286
+ </form>
287
+ </template>
288
+ ```
289
+
290
+ The matching server route reads the verdict with `@shieldlabs-ai/node`:
291
+
292
+ ```ts
293
+ // server/api/signup.post.ts
294
+ import { ShieldLabs, evaluateIdentification } from '@shieldlabs-ai/node';
295
+
296
+ const shieldlabs = new ShieldLabs({ apiKey: process.env.SHIELDLABS_API_KEY! });
297
+ const usedRequestIds = new Set<string>(); // use a shared store such as Redis in production
298
+
299
+ export default defineEventHandler(async (event) => {
300
+ const { requestId } = await readBody<{ email: string; requestId: string | null }>(event);
301
+ const identification = requestId ? await shieldlabs.identifications.get(requestId) : null;
302
+ const verdict = evaluateIdentification(identification, {
303
+ isReplay: (id) => usedRequestIds.has(id), // your store of request IDs already used
304
+ });
305
+ if (!verdict.ok) throw createError({ statusCode: 403, statusMessage: 'Signup refused' });
306
+ usedRequestIds.add(identification!.request_id);
307
+ // Create the account.
308
+ });
309
+ ```
310
+
311
+ ### Server-side rendering and hydration
312
+
313
+ - Importing the package and installing the plugin on the server is safe: nothing touches `window`
314
+ or `document` during setup or render.
315
+ - The plugin loads the agent in the browser after the app is mounted. The server renderer never
316
+ mounts an app, so the server never loads the agent, and `runOnMount` and `checkOnLoad` run only
317
+ in the browser.
318
+ - `status` is `'loading'` during server rendering and in the first render of every component, so
319
+ hydration matches the server HTML even when a lazy component hydrates after the agent is ready.
320
+ - Without the plugin on the server (a client-only plugin, as in the Nuxt guide) the composables
321
+ render the same initial state. Calling `identify()` there rejects with `unsupported_environment`
322
+ (`useIdentify()` resolves `null` and sets `error`).
323
+ - On the server `load()` does nothing and `getAgent()` rejects with `unsupported_environment`, with
324
+ or without the plugin.
325
+
326
+ ### Wait for consent with `autoLoad: false`
327
+
328
+ The agent does not read your consent banner. Where your policy or applicable law requires consent
329
+ before identification, create the plugin with `autoLoad: false`. Nothing loads until you call
330
+ `load()` from `useShieldLabs()`, for example when the visitor accepts:
331
+
332
+ ```ts
333
+ // src/main.ts
334
+ import { createShieldLabs, useShieldLabs } from '@shieldlabs-ai/vue';
335
+ import { createApp } from 'vue';
336
+ import App from './App.vue';
337
+
338
+ const app = createApp(App).use(
339
+ createShieldLabs({ publicKey: import.meta.env.VITE_SHIELDLABS_PUBLIC_KEY, autoLoad: false }),
340
+ );
341
+ // A returning visitor who already gave consent (hasConsent() reads your consent storage). Call
342
+ // load() before mount(), so that identifications started on mount wait for the agent.
343
+ if (hasConsent()) app.runWithContext(() => useShieldLabs().load());
344
+ app.mount('#app');
345
+ ```
346
+
347
+ ```vue
348
+ <!-- ConsentBanner.vue -->
349
+ <script setup lang="ts">
350
+ import { useShieldLabs } from '@shieldlabs-ai/vue';
351
+
352
+ const { load } = useShieldLabs();
353
+
354
+ function accept() {
355
+ saveConsent(); // your consent storage
356
+ load();
357
+ }
358
+ </script>
359
+ ```
360
+
361
+ Until `load()` is called, `status` stays `'loading'` and `getAgent()` waits for it, with no timeout
362
+ of its own. `identify()` and `check()` do not wait: `useIdentify()` resolves `null` right away with a
363
+ `not_initialized` error, so the protected action goes ahead unverified, and from `useShieldLabs()`,
364
+ `identify()` rejects with `not_initialized` and `check()` resolves `null`. A `runOnMount`
365
+ identification that starts then ends the same way, and `load()` does not start it again. Once
366
+ `load()` has been called, calls wait for the agent within their timeout as usual. `checkOnLoad` runs
367
+ once the agent is ready after `load()`. See
368
+ [Consent](https://github.com/ShieldLabs-ai/shieldlabs-js#consent) for the cookie ID the agent keeps.
369
+
370
+ ### Start on the first interaction with the form
371
+
372
+ An identification takes a moment, and the agent posts it right after it hands over the request ID.
373
+ For a classic full-page form post, start the identification on the first interaction with the form
374
+ with `identifyOnInteraction()` of the agent from `getAgent()`. By the time the user submits, it has
375
+ normally finished, so the page can navigate right away:
376
+
377
+ ```vue
378
+ <script setup lang="ts">
379
+ import { useShieldLabs, type InteractionIdentifier } from '@shieldlabs-ai/vue';
380
+ import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
381
+
382
+ const form = ref<HTMLFormElement | null>(null);
383
+ const requestId = ref('');
384
+ const { getAgent } = useShieldLabs();
385
+ let identifier: InteractionIdentifier | undefined;
386
+ let unmounted = false;
387
+
388
+ onMounted(() => {
389
+ getAgent()
390
+ .then((agent) => {
391
+ if (!unmounted && form.value) identifier = agent.identifyOnInteraction(form.value);
392
+ })
393
+ .catch(() => {
394
+ // No agent: the form is posted without a requestId.
395
+ });
396
+ });
397
+
398
+ onBeforeUnmount(() => {
399
+ unmounted = true;
400
+ identifier?.dispose();
401
+ });
402
+
403
+ async function onSubmit() {
404
+ // The identification for this submission (none while the agent has not loaded); take() re-arms
405
+ // for the next one.
406
+ const result = await identifier?.take().catch(() => null);
407
+ requestId.value = result?.requestId ?? '';
408
+ await nextTick(); // the hidden field now holds the request ID
409
+ form.value?.submit();
410
+ }
411
+ </script>
412
+
413
+ <template>
414
+ <form ref="form" action="/signup" method="post" @submit.prevent="onSubmit">
415
+ <input name="email" type="email" required />
416
+ <input type="hidden" name="requestId" :value="requestId" />
417
+ <button>Sign up</button>
418
+ </form>
419
+ </template>
420
+ ```
421
+
422
+ `getAgent()` resolves once the agent has loaded (with `autoLoad: false`, after `load()`), and the
423
+ plugin loads the agent only once. `take()` hands out the early identification only while it is
424
+ fresh and otherwise starts a new one, and `form.submit()` does not trigger the submit handler
425
+ again. If users can submit without interacting first (for example autofill and a single click),
426
+ prefer sending the form with `fetch()` as in the Quick start, which keeps the page alive. Details:
427
+ [Protect a form](https://github.com/ShieldLabs-ai/shieldlabs-js#protect-a-form).
428
+
429
+ ### Test your components
430
+
431
+ Provide a stand-in context under `shieldLabsKey`, so component tests need no agent. With the form
432
+ from the Quick start in `SignupForm.vue`:
433
+
434
+ ```ts
435
+ import { shieldLabsKey, type ShieldLabsContext, type ShieldLabsStatus } from '@shieldlabs-ai/vue';
436
+ import { flushPromises, mount } from '@vue/test-utils';
437
+ import { expect, it, vi } from 'vitest';
438
+ import { ref } from 'vue';
439
+ import SignupForm from './SignupForm.vue';
440
+
441
+ it('sends the request ID with the signup', async () => {
442
+ const identify = vi.fn(async () => ({ requestId: '6f1c2e8a-3b5d-4c7e-8f9a-0b1c2d3e4f5a', userId: null }));
443
+ const shieldlabs: ShieldLabsContext = {
444
+ status: ref<ShieldLabsStatus>('ready'),
445
+ error: ref(null),
446
+ identify,
447
+ check: vi.fn(async () => null),
448
+ load: vi.fn(),
449
+ getAgent: vi.fn(() => Promise.reject(new Error('No agent in component tests.'))),
450
+ };
451
+ const fetchMock = vi.fn(async () => new Response(null, { status: 201 }));
452
+ vi.stubGlobal('fetch', fetchMock);
453
+
454
+ const wrapper = mount(SignupForm, {
455
+ global: { provide: { [shieldLabsKey as symbol]: shieldlabs } },
456
+ });
457
+ await wrapper.find('form').trigger('submit');
458
+ await flushPromises();
459
+
460
+ expect(identify).toHaveBeenCalledTimes(1);
461
+ expect(fetchMock).toHaveBeenCalledWith('/api/signup', expect.objectContaining({ method: 'POST' }));
462
+ });
463
+ ```
464
+
465
+ ### Call budget, Content Security Policy and consent
466
+
467
+ Every identification that runs is billed and uses part of a small per-IP ingest budget. Call
468
+ `identify()` once per protected action, never on every render or route change: the plugin and the
469
+ composables identify only when you call them, plus `runOnMount` and `checkOnLoad` when you turn
470
+ them on. Before going live, read these sections of the `@shieldlabs-ai/js` README:
471
+
472
+ - [Call budget](https://github.com/ShieldLabs-ai/shieldlabs-js#call-budget): limits per visitor IP
473
+ and the agent's own background checks.
474
+ - [Content Security Policy](https://github.com/ShieldLabs-ai/shieldlabs-js#content-security-policy):
475
+ the `script-src` and `connect-src` origins the agent needs.
476
+ - [Consent](https://github.com/ShieldLabs-ai/shieldlabs-js#consent): the agent's first-party cookie
477
+ ID and when to load.
478
+
479
+ ## Reference
480
+
481
+ | Export | Description |
482
+ |---|---|
483
+ | `createShieldLabs(options)` | Returns the Vue plugin for `app.use()`. One per app; loads the agent once |
484
+ | `useShieldLabs()` | `{ status, error, identify, check, load, getAgent }` of the app |
485
+ | `useIdentify(options?)` | `{ identify, result, isLoading, error, reset }`, an identify helper per component |
486
+ | `shieldLabsKey` | `InjectionKey<ShieldLabsContext>` under which the plugin provides its context |
487
+ | `ShieldLabsError` | The error class of `@shieldlabs-ai/js` (re-exported). Has `code` and optional `cause` |
488
+ | `VERSION` | The package version, for example `"1.0.0"` |
489
+ | Types | `ShieldLabsOptions`, `CheckOnLoadOption`, `ShieldLabsPlugin`, `ShieldLabsContext`, `ShieldLabsStatus`, `UseShieldLabsReturn`, `UseIdentifyOptions`, `UseIdentifyReturn`, and from `@shieldlabs-ai/js`: `LoadOptions`, `IdentifyOptions`, `IdentifyResult`, `ShieldLabsAgent`, `InteractionIdentifier`, `ShieldLabsErrorCode` |
490
+
491
+ `createShieldLabs(options)`
492
+
493
+ | Option | Type | Default | Description |
494
+ |---|---|---|---|
495
+ | `publicKey` | `string` | required | Public Key of your domain. `@shieldlabs-ai/js` checks it when the agent loads |
496
+ | `environment` | `'production' \| 'development'` | `'production'` | Which ShieldLabs CDN to load the agent from |
497
+ | `scriptUrl` | `string` | | Advanced: agent module URL override (`https`, or `http` on `localhost` and `127.0.0.1`) |
498
+ | `timeout` | `number` | `10000` | Milliseconds to wait for the agent to load, and the default timeout of each call (the wait for the agent plus its answer) |
499
+ | `checkOnLoad` | `boolean \| { userId?: string \| null }` | `false` | Runs `check()` once when the agent becomes ready, unless an `identify()` or `check()` for the same User HID is in flight |
500
+ | `autoLoad` | `boolean` | `true` | Loads the agent right after the app is mounted; with `false`, nothing loads until `load()` from `useShieldLabs()` is called, and until then `identify()` fails fast with `not_initialized`, `check()` resolves `null` and `getAgent()` waits |
501
+
502
+ `createShieldLabs()` throws a `ShieldLabsError` with code `invalid_options` for a missing options
503
+ object or an invalid `checkOnLoad` or `autoLoad`. A second ShieldLabs plugin on the same app is
504
+ ignored with a warning.
505
+
506
+ `useShieldLabs()`
507
+
508
+ | Member | Type | Description |
509
+ |---|---|---|
510
+ | `status` | `Readonly<Ref<'loading' \| 'ready' \| 'error'>>` | Agent state. `'loading'` during server rendering and a component's first render |
511
+ | `error` | `Readonly<Ref<ShieldLabsError \| null>>` | Why the last load failed |
512
+ | `identify(options?)` | `Promise<IdentifyResult>` | Fresh identification with a new request ID; `timeout` covers the wait for the agent and its answer. Rejects with a `ShieldLabsError`; with `autoLoad: false`, with `not_initialized` right away until `load()` is called |
513
+ | `check(options?)` | `Promise<IdentifyResult \| null>` | Background check, `null` when the agent skipped it (and right away with `autoLoad: false` until `load()` is called). Waits like `identify()` |
514
+ | `load()` | `void` | Starts loading the agent unless it is loaded or loading; loads again after a failed load. Does nothing during server rendering |
515
+ | `getAgent()` | `Promise<ShieldLabsAgent>` | The agent once it has loaded, for `identifyOnInteraction()`. Loads when needed (with `autoLoad: false`, waits for `load()` with no timeout of its own); rejects with the load's `ShieldLabsError` |
516
+
517
+ `useIdentify(options?)`
518
+
519
+ | Option | Type | Default | Description |
520
+ |---|---|---|---|
521
+ | `userId` | `MaybeRefOrGetter<string \| null \| undefined>` | | User HID computed on your server, read when `identify()` runs |
522
+ | `runOnMount` | `boolean` | `false` | Calls `identify()` once after the component is mounted. With `autoLoad: false`, call `load()` before the mount: a run that starts before `load()` resolves `null` with `not_initialized` and does not run again |
523
+
524
+ | Member | Type | Description |
525
+ |---|---|---|
526
+ | `identify(options?)` | `Promise<IdentifyResult \| null>` | Fresh identification. Never rejects: `null` and `error` when there is none (`not_initialized` right away with `autoLoad: false` until `load()` is called). Returns this helper's call in flight with the same `userId` and `timeout` (an omitted `timeout` counts as the plugin's) instead of starting another |
527
+ | `result` | `Readonly<Ref<IdentifyResult \| null>>` | Result of the latest call |
528
+ | `isLoading` | `Readonly<Ref<boolean>>` | `true` while the latest call runs |
529
+ | `error` | `Readonly<Ref<ShieldLabsError \| null>>` | Why the latest call returned `null` |
530
+ | `reset()` | `void` | Clears `result`, `error` and `isLoading` |
531
+
532
+ `IdentifyOptions` is `{ userId?: string; timeout?: number }` and `IdentifyResult` is
533
+ `{ requestId: string; userId: string | null }`, as in `@shieldlabs-ai/js`. In this package `timeout`
534
+ bounds the whole call: the wait for the agent to load and the agent's answer.
535
+
536
+ Composables called outside `setup()` (or `app.runWithContext()`), or in the browser without the
537
+ plugin, throw an `Error` that names the fix. In the Options API, call them in `setup()`; outside
538
+ components (for example in a router guard), use `app.runWithContext(() => useShieldLabs())`.
539
+
540
+ ## Errors and retries
541
+
542
+ Every identification error is a `ShieldLabsError`. Branch on `error.code`:
543
+
544
+ | `code` | When | What to do |
545
+ |---|---|---|
546
+ | `invalid_options` | An option failed validation, for example an empty `publicKey` because the environment variable is missing (the plugin also logs this with `console.warn`) or a reserved `userId` | Fix the configuration or the call; retrying does not help |
547
+ | `unsupported_environment` | Server-side rendering, a worker, or a page that is not a secure context | Identify in the browser; serve the page over HTTPS (`localhost` and `127.0.0.1` also work over `http`) |
548
+ | `load_failed` | The agent could not be imported: network error, content blocker, Content Security Policy | Continue without an identification. The next `identify()`, `check()`, `getAgent()` or `load()` loads again |
549
+ | `not_initialized` | The agent did not start an identification, for example because another one is running in this or another tab. With `autoLoad: false`, also an `identify()` made before `load()` is called | Continue without an identification, or retry once later (with `autoLoad: false`, after `load()`) |
550
+ | `timeout` | The agent did not load and answer within the call's timeout (default 10 seconds) | Continue without an identification. A call that timed out while waiting for the agent never reaches it, and a late answer is ignored |
551
+
552
+ - The plugin reports a failed load in `status` (`'error'`) and `error`. A failed load is not kept:
553
+ the next `identify()`, `check()`, `getAgent()` or `load()` tries again. Mounting more components
554
+ does not.
555
+ - Identifications are never retried automatically, because every identification is billed. Call
556
+ `identify()` again on the next submission.
557
+ - Whenever there is no identification, send the protected action anyway without a `requestId`:
558
+ your backend treats a missing identification as unverified, never as clean.
559
+
560
+ More detail: [`@shieldlabs-ai/js` Errors](https://github.com/ShieldLabs-ai/shieldlabs-js#errors).
561
+
562
+ ## Compatibility
563
+
564
+ - Vue 3.3 or later (tested with 3.3 and 3.5), with Vue SSR (`createSSRApp` and
565
+ `vue/server-renderer`) and Nuxt 3.
566
+ - Browsers: as `@shieldlabs-ai/js`, current browsers with ES modules, dynamic `import()` and
567
+ WebCrypto, on a secure context (HTTPS, or `localhost` and `127.0.0.1` during development).
568
+ - Output: ES2019 syntax as ESM and CommonJS with TypeScript declarations. No runtime dependencies;
569
+ peer dependencies `vue` `^3.3.0` and `@shieldlabs-ai/js` `^1.0.0`.
570
+ - Server runtimes (Node.js 18+, Bun, Deno, edge): safe to import and render.
571
+
572
+ ## Development
573
+
574
+ ```bash
575
+ npm ci
576
+ npm install --no-save ../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz # until @shieldlabs-ai/js is on npm
577
+ npm run typecheck
578
+ npm run lint
579
+ npm test -- --coverage # builds first, then runs the tests
580
+ npm run build
581
+ ```
582
+
583
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for building the `@shieldlabs-ai/js` tarball. Documentation:
584
+ <https://docs.shieldlabs.ai>. Analytics dashboard: <https://app.shieldlabs.ai>. Support:
585
+ <contact@shieldlabs.ai>.
586
+
587
+ ## License
588
+
589
+ [MIT](./LICENSE), Copyright (c) 2026 ShieldLabs Inc.