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: 9 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ src/shared/constants.ts SIGNALS_HEADER ("x-mgpl-signals"), SIGNAL_LIMITS, ID_PA
src/shared/crypto.ts base64url, randomBytes/randomId, sha256Hex, constantTimeEqual, canonicalJson (Web Crypto only)
src/shared/signals.ts encodeSignals, signalHeaders, parseSignals/decodeSignals (validate untrusted input)
src/browser/ "/browser": storage.ts (web/memory/custom), device.ts (deviceInfo, fingerprint inputs),
collect.ts (createSignalCollector)
collect.ts (createSignalCollector: collect, headers, subscribe/getSnapshot),
adapters.ts (signalFetch, mergeSignalHeaders, axiosSignals, kySignals, apolloSignals, signalStore)
src/react.ts "/react": useDeviceSignals (React is an optional peer)
src/server/ "/server": signals.ts (readSignals, bindSignals, checkSignals, decide), ip.ts (clientIp),
headers.ts (securityHeaders, corsPolicy), redact.ts (redact, maskEmail, maskPhone)
headers.ts (securityHeaders, corsPolicy), redact.ts (redact, maskEmail, maskPhone),
adapters.ts (requestSecurity, fetchSecurity, expressSecurity, fastifySecurity)
scripts/test.ts every part; browser code runs on simulated environments
scripts/check-dist.mjs the built main and /browser bundles import no Node.js modules
```
Expand All @@ -25,7 +28,10 @@ scripts/check-dist.mjs the built main and /browser bundles import no Node.js m
1. **The shared and browser parts use no Node.js APIs** (no `node:*`, no `Buffer`): only Web Crypto,
`TextEncoder`, `btoa`/`atob`. `npm run typecheck` compiles them without `@types/node`, and CI runs
`scripts/check-dist.mjs` on the bundles.
2. **No runtime dependencies.** Frameworks (React, TanStack Query, Fastify…) stay in the consumers.
2. **No runtime dependencies; works with every library.** Adapters match a library's hook shape
structurally and never import it (axios, ky, Apollo, Express, Fastify…). React is the one optional
peer, used only by `/react`. Prefer a generic contract (`fetch`, headers, `subscribe`/`getSnapshot`)
over a new per-library adapter, and test every adapter (a real Express and Fastify app in `npm test`).
3. **Signals stay coarse.** No canvas, audio, WebGL, font or IP probing, nothing sent to third parties.
Anything new that is collected goes in the README table "What is collected" in the same PR.
4. **Never store raw ids or tokens.** The server keeps `bindSignals()` output (SHA-256 of ids).
Expand Down
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Device signals collected in any frontend and checked on the backend, plus the server helpers every
endpoint needs. Zero dependencies.

![version](https://img.shields.io/badge/version-0.1.0-E9A23B) ![checks](https://img.shields.io/badge/checks-32-14264A) ![dependencies](https://img.shields.io/badge/dependencies-0-14264A)
![version](https://img.shields.io/badge/version-0.2.0-E9A23B) ![checks](https://img.shields.io/badge/checks-44-14264A) ![dependencies](https://img.shields.io/badge/dependencies-0-14264A)

</div>

Expand All @@ -21,10 +21,41 @@ npm install @mgpl-diagnostics/security-kit
| --- | --- | --- |
| `@mgpl-diagnostics/security-kit` | everywhere (browser, Node.js, workers, React Native) | `DeviceSignals` types, `SIGNALS_HEADER`, `encodeSignals` / `decodeSignals` (validated), `randomId`, `sha256Hex`, `constantTimeEqual`, base64url, `canonicalJson` |
| `@mgpl-diagnostics/security-kit/browser` | any frontend, any framework | `createSignalCollector`, `deviceInfo`, pluggable storage (`webStorage`, `memoryStorage`, or your own sync/async store) |
| `@mgpl-diagnostics/security-kit/server` | Node.js ≥ 20 | `readSignals`, `bindSignals`, `checkSignals`, `decide`; `clientIp` behind proxies; `securityHeaders`, `corsPolicy`; `redact`, `maskEmail`, `maskPhone` |
| `@mgpl-diagnostics/security-kit/server` | Node.js ≥ 20 | `readSignals`, `bindSignals`, `checkSignals`, `decide`; `clientIp` behind proxies; `securityHeaders`, `corsPolicy`; `redact`, `maskEmail`, `maskPhone`; adapters for Express, Fastify and Fetch-standard servers |
| `@mgpl-diagnostics/security-kit/react` | React ≥ 18 (optional peer) | `useDeviceSignals(collector)` |

Both `/browser` and `/server` re-export the shared entry.

## Works with any library

Nothing here imports a framework. The adapters match each library's hook shape, so they add no
dependency, and anything not listed uses `collector.headers()` or `subscribe` / `getSnapshot` directly.

| You use | Plug in |
| --- | --- |
| `@mgpl-diagnostics/client` | `createMgplGraphQLClient({ headers: () => signals.headers() })` |
| plain `fetch`, graphql-request, urql, Apollo `HttpLink`, ofetch, wretch | `fetch: signalFetch(signals)` |
| TanStack Query, SWR, RTK Query, Vue Query | call your client (or `signalFetch(signals)`) inside the query function; nothing else |
| axios | `axios.interceptors.request.use(axiosSignals(signals))` |
| ky | `ky.create({ hooks: { beforeRequest: [kySignals(signals)] } })` |
| Apollo Client | `setContext(apolloSignals(signals))` |
| graphql-ws, socket.io, Angular `HttpClient`, anything with a headers object | `await mergeSignalHeaders(signals, headers)` |
| React | `const s = useDeviceSignals(signals)` from `/react` |
| Svelte | `const store = signalStore(signals)`, then `$store` |
| Vue | `const s = shallowRef(signals.getSnapshot()); signals.subscribe((v) => (s.value = v))` |
| Solid, Angular signals, RxJS, MobX | wrap `signals.subscribe` / `signals.getSnapshot` (or `signalStore`) |
| React Native, Electron, Capacitor | pass `storage` / `environment` (see below) |

| Server | Plug in |
| --- | --- |
| Fastify | `await app.register(fastifySecurity({ cors: { origins }, trustedHops: 1 }))` → `request.security` |
| Express, Connect, node:http | `app.use(expressSecurity({ cors: { origins } }))` → `req.security` |
| Hono, Next.js route handlers, Bun, Deno, workers, GraphQL Yoga | `fetchSecurity(opts).handle(request, remoteAddress)` → `{ security, headers, preflight }` |
| anything else, or a GraphQL context | `requestSecurity({ headers, remoteAddress, trustedHops })` → `{ ip, signals, userAgent, origin }` |

`security.signals` is already validated (or `null`). Leave `cors` unset when another plugin
(`@fastify/cors`, Yoga's own CORS) already handles it.

## Device signals

```mermaid
Expand Down Expand Up @@ -133,7 +164,7 @@ logger.info(redact({ body, headers })); // passwords, tokens, cookies, keys →

```bash
npm run typecheck # also checks that the shared and browser parts compile without Node.js types
npm test # 32 checks
npm test # 44 checks, including real Express and Fastify apps
npm run build && node scripts/check-dist.mjs
npm run llms # regenerate llms-full.txt (CI fails when it is stale)
```
Expand Down
Loading
Loading