Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
102 changes: 102 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
name: Test (Node ${{ matrix.node-version }}, React ${{ matrix.react }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [20, 22, 24]
react: [18, 19]
defaults:
run:
working-directory: shieldlabs-react
steps:
- uses: actions/checkout@v5
with:
path: shieldlabs-react
persist-credentials: false
# @shieldlabs-ai/js is not on npm yet, so it is built from its repository next to this one.
- uses: actions/checkout@v5
with:
repository: ShieldLabs-ai/shieldlabs-js
ref: main
path: shieldlabs-js
persist-credentials: false
- uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
cache: npm
cache-dependency-path: |
shieldlabs-react/package-lock.json
shieldlabs-js/package-lock.json
- name: Build and pack @shieldlabs-ai/js
working-directory: shieldlabs-js
run: |
npm ci
npm run build
npm pack
- run: npm ci
- name: Install the packed @shieldlabs-ai/js and React ${{ matrix.react }}
env:
REACT_MAJOR: ${{ matrix.react }}
run: |
set -- ../shieldlabs-js/shieldlabs-ai-js-*.tgz
if [ "$REACT_MAJOR" = "18" ]; then
set -- "$@" react@18 react-dom@18 @types/react@18 @types/react-dom@18
fi
npm install --no-save "$@"
node -p "'React ' + require('react/package.json').version"
- run: npm run typecheck
- run: npm run lint
- run: npm test -- --coverage
- run: npm run build

example-vite:
name: Vite example
runs-on: ubuntu-latest
defaults:
run:
working-directory: shieldlabs-react
steps:
- uses: actions/checkout@v5
with:
path: shieldlabs-react
persist-credentials: false
- uses: actions/checkout@v5
with:
repository: ShieldLabs-ai/shieldlabs-js
ref: main
path: shieldlabs-js
persist-credentials: false
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
cache-dependency-path: |
shieldlabs-react/package-lock.json
shieldlabs-js/package-lock.json
- name: Build and pack @shieldlabs-ai/js
working-directory: shieldlabs-js
run: |
npm ci
npm run build
npm pack
- run: npm ci
- run: npm install --no-save ../shieldlabs-js/shieldlabs-ai-js-*.tgz
- run: npm run build
- run: npm pack
- name: Build examples/vite against both packed packages
working-directory: shieldlabs-react/examples/vite
run: |
npm install --no-save --no-package-lock ../../shieldlabs-ai-react-*.tgz ../../../shieldlabs-js/shieldlabs-ai-js-*.tgz
npm run build
67 changes: 67 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Release

# Publishes @shieldlabs-ai/react to npm when a maintainer pushes a tag such as v1.0.0.
# Needs the NPM_TOKEN repository secret. Safe to re-run: a version that is already on npm is skipped.
on:
push:
tags: ['v*']

permissions:
contents: read

jobs:
check:
name: Check and pack
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
- name: Check that the tag matches package.json
run: test "v$(node -p "require('./package.json').version")" = "$GITHUB_REF_NAME"
- run: npm ci
# Fails until @shieldlabs-ai/js is on npm: publish it first, then re-run this workflow.
- name: Install the published @shieldlabs-ai/js
run: npm install --no-save "@shieldlabs-ai/js@$(node -p "require('./package.json').peerDependencies['@shieldlabs-ai/js']")"
- run: npm run typecheck
- run: npm run lint
- run: npm test -- --coverage
- run: npm run build
- run: npm pack
- uses: actions/upload-artifact@v4
with:
name: package
path: shieldlabs-ai-react-*.tgz
if-no-files-found: error

publish:
name: Publish to npm
needs: check
runs-on: ubuntu-latest
# Add required reviewers to this environment in the repository settings to approve each release.
environment: npm
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: package
- uses: actions/setup-node@v5
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Publish, unless this version is already on npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
version="${GITHUB_REF_NAME#v}"
if [ "$(npm view "@shieldlabs-ai/react@$version" version 2>/dev/null)" = "$version" ]; then
echo "@shieldlabs-ai/react@$version is already on npm."
else
npm publish "./shieldlabs-ai-react-$version.tgz" --provenance --access public
fi
16 changes: 12 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# Dependencies and build output
node_modules/
dist/
build/
*.log
coverage/
*.tgz
*.log
.turbo/

# Local environment
.env
.env.*
!.env.example

# Editors and OS
.DS_Store
coverage/
.turbo/
.history
.history/
.idea/
.vscode/
7 changes: 7 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Development settings only: npm never includes .npmrc in the published package.
# @shieldlabs-ai/js, a peer dependency, is not on npm yet, so npm cannot install it by itself.
# legacy-peer-deps: npm ci and npm install leave peer dependencies to you.
# save-dev: `npm install --no-save <tarball>` still installs a local pack of @shieldlabs-ai/js.
# Remove this file once @shieldlabs-ai/js 1.x is published (see CONTRIBUTING.md).
legacy-peer-deps=true
save-dev=true
54 changes: 52 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,55 @@
# Changelog

## 2026-09-06
All notable changes to `@shieldlabs-ai/react` are documented in this file. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- Minor improvements and bug fixes
## [Unreleased]

## [1.0.0] - 2026-09-30

### Added

- `ShieldLabsProvider` with the props `publicKey`, `environment`, `scriptUrl`, `timeout`,
`autoLoad` and `checkOnLoad`: loads the agent once with `load()` of `@shieldlabs-ai/js`, in an effect
after the first render, and reports `status` (`loading`, `ready` or `error`) and `error`. Invalid
props and pages that are not a secure context also log a console warning. Changing an option
loads the agent for the new options.
- `autoLoad={false}` defers loading, for example until consent: nothing loads until `load()` is
called or `autoLoad` becomes `true`. Until then `useIdentify().identify()` resolves `null` at
once with a `not_initialized` error, and `runOnMount` ends the same way without running again
after `load()`. `useShieldLabs().identify()` rejects with `not_initialized`, `check()` resolves
`null` and `getAgent()` waits, with no timeout of its own.
- `useShieldLabs()`: `status`, `error`, `identify()`, `check()`, `load()` and `getAgent()` of the
closest provider. `getAgent()` resolves the loaded agent of `@shieldlabs-ai/js`, for early
identification with `agent.identifyOnInteraction(form)`. Calls made while the agent loads wait
for it, and the call timeout covers the whole call: that wait, then the agent's answer in the
time that is left. A failed or timed-out load is tried again by the next call.
- `useIdentify({ userId, runOnMount })`: `identify()`, `result`, `isLoading`, `error` and
`reset()`. `identify()` resolves the result, or `null` with the reason in `error`, and never
rejects. A call with the same User HID and `timeout` as a running call of the same hook returns
that call, so a double submit costs one identification; a call without `timeout` counts as one
with the provider `timeout`. Options of `identify()` with a `userId` key override the hook
option, also with `undefined` or `null`, which both identify anonymously; only options without
the key use the User HID of the hook. The state follows the call made last.
- `checkOnLoad` runs `check()` once per provider mount when the agent is ready, unless an
`identify()` or `check()` for the same User HID is running at that moment, and `runOnMount` runs
`identify()` once per component mount. Both run once in StrictMode.
- Server rendering that touches neither `window` nor `document`, no state updates after unmount,
state that stays current inside a hidden `<Activity>` (React 19.2 and later), and hooks outside
the provider throw an error that names the hook.
- Re-exports of `ShieldLabsError` and the types `IdentifyOptions`, `IdentifyResult`,
`InteractionIdentifier`, `LoadOptions`, `ShieldLabsAgent` and `ShieldLabsErrorCode` from
`@shieldlabs-ai/js`.
- ESM, CommonJS and TypeScript declarations with a `"use client"` directive. Peer dependencies:
`react` 18 or 19 and `@shieldlabs-ai/js` 1.x.
- `examples/vite`: a signup form that starts an identification on the first interaction with
`identifyOnInteraction()` and sends the `requestId` with the submit.

### Removed

- The placeholder `useShieldLabs(options)` hook of the pre-release scaffold, which returned `data`.
The browser receives a request ID; results are read on your server.

[Unreleased]: https://github.com/ShieldLabs-ai/shieldlabs-react/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/ShieldLabs-ai/shieldlabs-react/releases/tag/v1.0.0
83 changes: 83 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Contributing to @shieldlabs-ai/react

Thank you for improving the ShieldLabs React bindings.

## Set up

You need Node.js 20 or later. The package builds on `@shieldlabs-ai/js`, a peer dependency. Until
`@shieldlabs-ai/js` 1.0.0 is published to npm, work with a local pack of it, built from a working copy
of [shieldlabs-js](https://github.com/ShieldLabs-ai/shieldlabs-js) next to this repository:

```bash
# in shieldlabs-js
npm ci && npm run build && npm pack

# in this repository
npm ci
npm install --no-save ../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz
```

Repeat the last command after every `npm ci`. `--no-save` leaves `package.json` and
`package-lock.json` unchanged. Never commit a `file:` dependency or a tarball. When the pack is
missing, `npm run typecheck`, `npm run lint` and `npm run build` stop with these instructions
(`scripts/check-core.mjs`).

### Why the repository has an `.npmrc`

npm installs required peer dependencies by itself. It would try to download `@shieldlabs-ai/js` from
the registry, where it does not exist yet, so `npm ci` would fail. The committed `.npmrc` sets
`legacy-peer-deps=true`, so `npm ci` and `npm install` leave peer dependencies to you, and
`save-dev=true`, so `npm install --no-save <pack>` still installs the pack. Both settings apply
only to work in this repository: npm never publishes `.npmrc`, and `package.json` lists
`@shieldlabs-ai/js` as a required peer dependency, so npm installs it for the users of the package.

Once `@shieldlabs-ai/js` 1.0.0 is on npm, `npm install --no-save @shieldlabs-ai/js` works instead of the
pack. To tidy up later, delete `.npmrc`, add `@shieldlabs-ai/js` to `devDependencies`, run
`npm install` to refresh `package-lock.json`, and remove the pack steps from the CI workflow and
from this file.

## Checks

Run these before you open a pull request. CI runs them on Node.js 20, 22 and 24 with React 18
and 19.

```bash
npm run typecheck
npm run lint
npm test -- --coverage # builds first; coverage must stay at 90 % or more
npm run build
```

To test with React 18 locally (afterwards, `npm ci` and the pack install bring back React 19):

```bash
npm install --no-save react@18 react-dom@18 @types/react@18 @types/react-dom@18 ../shieldlabs-js/shieldlabs-ai-js-1.0.0.tgz
npm run typecheck && npm test
```

## Guidelines

- Stay a thin layer over `@shieldlabs-ai/js`: load the agent only through its `load()`, never touch
`window` or `document` during render, and never identify on re-renders or route changes.
- Every change comes with tests. The tests mock `load()` with the stand-in agent in
`test/support/agent.ts`, and any `console.error` (such as a React update outside `act()`) fails
a test.
- Use conventional commit messages (`feat:`, `fix:`, `docs:`, `test:`, `ci:`, `chore:`) and add a
line to `CHANGELOG.md` under "Unreleased".
- Documentation style: plain technical English, "risk signals", and the three risk bands trusted
0-29, suspicious 30-59 and dangerous 60-100.

## Releasing

Publish `@shieldlabs-ai/js` first. Then update the version in `package.json`, move the "Unreleased"
changelog entries under the new version, and push a tag such as `v1.0.1`. The release workflow runs
all checks against the published `@shieldlabs-ai/js`, packs the package and publishes the pack to npm
with provenance, using the `NPM_TOKEN` repository secret. The publish job runs in the `npm`
environment: add required reviewers to it in the repository settings to approve each release.

Re-running the workflow is safe. When it stopped because `@shieldlabs-ai/js` was not on npm yet,
publish it and re-run the workflow; a version that is already on npm is not published again.

## Security

Please report security issues privately to <contact@shieldlabs.ai> rather than in a public issue.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 ShieldLabs
Copyright (c) 2026 ShieldLabs Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
Loading
Loading