Skip to content

Commit 2be69fa

Browse files
Release v0.3.0 (#17)
* Add CI/CD quality gates and Dependabot config Enforce coverage thresholds in CI and publish, add security audit, version-tag validation, GitHub Release creation, and Dependabot for automated dependency updates targeting develop. * Split CI into separate lint, audit, and test jobs * Remove redundant push trigger for develop in CI * Add Node.js version label to test job names * Bump actions/setup-node from 4 to 6 (#3) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](actions/setup-node@v4...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions/checkout from 4 to 6 (#4) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump esbuild from 0.25.12 to 0.27.3 in the dev-deps group (#5) Bumps the dev-deps group with 1 update: [esbuild](https://github.com/evanw/esbuild). Updates `esbuild` from 0.25.12 to 0.27.3 - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](evanw/esbuild@v0.25.12...v0.27.3) --- updated-dependencies: - dependency-name: esbuild dependency-version: 0.27.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump koa and @types/koa (#6) Bumps [koa](https://github.com/koajs/koa) and [@types/koa](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/koa). These dependencies needed to be updated together. Updates `koa` from 2.16.3 to 3.1.1 - [Release notes](https://github.com/koajs/koa/releases) - [Changelog](https://github.com/koajs/koa/blob/master/History.md) - [Commits](koajs/koa@v2.16.3...v3.1.1) Updates `@types/koa` from 2.15.0 to 3.0.1 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/koa) --- updated-dependencies: - dependency-name: koa dependency-version: 3.1.1 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: "@types/koa" dependency-version: 3.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add GPU detection via WebGL renderer string (#7) * Add GPU detection via WebGL renderer string Detect GPU capabilities using the UNMASKED_RENDERER_WEBGL string from WebGL debug info. Classifies GPUs into none/low/mid/high tiers and derives a disable3dEffects rendering hint for apps serving 3D or GPU-intensive content. * Harden GPU detection, add thresholds, extract shared demo template - Fix probe bundle size references (762 B -> 988 B) across README, docs, examples - Add WebGL context cleanup via loseContext() to free GPU resources - Use failIfMajorPerformanceCaveat to detect software renderers even when WEBGL_debug_renderer_info is blocked by privacy settings - Add GpuThresholds (softwarePattern, highEndPattern) to TierThresholds for API consistency with CPU/memory/connection thresholds - Extract duplicated HTML template from 4 example servers into examples/shared/demo-template.ts (~1000 lines removed) - Fix misaligned ASCII diagram in README - Update API docs with GPU threshold documentation - Add tests for loseContext, failIfMajorPerformanceCaveat fallback, and custom GPU thresholds * Fix ASCII diagram box alignment in README * Fix prettier formatting * Add Battery API signal to constrain rendering hints (#8) * Add Battery API signal to constrain hints on low power Battery is transient state (not a device capability), so it bypasses tier classification and feeds directly into deriveHints(). When the device is unplugged and below 15% charge, deferHeavyComponents, reduceAnimations, and disableAutoplay are forced on. Probe collects battery via navigator.getBattery() (Chromium-only), silently skipped on Firefox/Safari. Bundle stays at 1024 bytes gzipped by reclaiming bytes from cookie handling code. * Fix prettier formatting * Fix stale docs, add package READMEs, patch minimatch CVE (#10) * Replace stale probe byte counts with ~1 KB Exact byte counts (988 B, 901 B) went stale after battery signal landed. Use ~1 KB in all display references — the actual limit is enforced at build time in bundle.js and ci.yml. * Fix minimatch ReDoS vulnerability (high) Override minimatch <10.2.1 → ^10.2.1 via pnpm overrides. Vulnerable 9.0.6 was pulled in transitively by typescript-eslint. * Add README to each package for npm npm shows the README from the package directory at publish time. Each package now has a detailed README covering installation, usage, API surface, options, and compatibility. * Link npm badge to scope search page * Update README and CHANGELOG for v0.2.0 - Add battery row to signals table - Update 3 hint descriptions to mention low battery - Fix classify step to mention GPU tier - Add 0.2.0 changelog entry (GPU, battery, validation, deps, CI) * Fix prettier formatting * Bump version to 0.2.1 * Add bot/crawler filtering to probe endpoint (#12) * Add CLAUDE.md with project guidance for Claude Code * Add bot/crawler filtering to probe endpoint Reject bots, crawlers, and headless browsers before they create garbage profiles in storage. Detection uses UA pattern matching, headless GPU fingerprinting (SwiftShader/llvmpipe), and empty-signal plausibility checks. Enabled by default via rejectBots option. * Expand bot detection patterns and update docs Add AI crawlers (GPTBot, ChatGPT-User, ClaudeBot, Claude-Web, Google-Extended, Gemini, Perplexity, MistralAI, Cohere, DeepSeek, Grok, Meta, Bytedance), automation tools (Selenium), and HTTP clients (Scrapy, Apache-HttpClient). Document rejectBots option and isBotSignals() in API docs and getting-started guide. * Remove redundant grokbot and ai2bot patterns Both are already matched by the generic bot pattern. * Fix Prettier formatting * Add format-before-commit reminder to CLAUDE.md * Add first-request fallback via UA/Client Hints classification (#13) * Add first-request fallback via UA/Client Hints classification Solve the null-profile problem on first page load by adding two opt-in strategies: header-based classification (UA + Client Hints) and a fallback profile option (conservative/optimistic/custom tiers). - Add ProfileSource, FallbackProfile types and source field to ClassifiedProfile - Add classifyFromHeaders(), resolveFallback(), ACCEPT_CH_VALUE to types - Add CONSERVATIVE_TIERS / OPTIMISTIC_TIERS preset constants - Update all four middleware packages (Express, Fastify, Hono, Koa) with fallbackProfile and classifyFromHeaders options - Set Accept-CH response header when header classification is enabled * Update docs for first-request fallback and header classification Document new types (ProfileSource, FallbackProfile), functions (classifyFromHeaders, resolveFallback), constants (CONSERVATIVE_TIERS, OPTIMISTIC_TIERS, ACCEPT_CH_VALUE), the source field on ClassifiedProfile, and the classifyFromHeaders/fallbackProfile middleware options across all API docs, getting-started guide, profile schema reference, and package READMEs. * Add changelog entry and README section for first-request handling * Add missing bot-filtering docs to package READMEs and changelog (#14) The bot-filtering PR (#12) added rejectBots and isBotSignals but didn't update the package READMEs or changelog. * Add threshold validation to reject invalid configs at startup (#15) * Add cookie-parser as peer dependency for Express middleware cookie-parser is required for req.cookies to be populated, but was only listed in devDependencies. Without it the middleware silently fails to find session tokens. Mirrors how @fastify/cookie is declared in the Fastify middleware package. * Add threshold validation to reject invalid configs at startup validateThresholds() checks ordering, positivity, and RegExp types after merging with defaults. Called in all four middleware factories so inverted or nonsensical thresholds fail fast instead of silently producing wrong classifications. * Document validateThresholds in API docs, READMEs, and getting started guide * Production hardening, deployment docs, and dead signal cleanup (#16) * Add cookieSecure option and privacy documentation Add cookieSecure option to all four middleware packages, allowing the Secure flag to be set on the session cookie. Defaults to false for local dev compatibility; set to true for HTTPS deployments. Add privacy and cookie consent section to getting-started guide covering GDPR, ePrivacy Directive, UK GDPR, CCPA/CPRA, and Brazil LGPD implications of collecting device capability signals. * Add deployment guide for Docker, Cloudflare Workers, and serverless * Document streaming response limitation for probe auto-injection * Handle Redis connection failures and corrupted JSON gracefully RedisStorageAdapter now catches errors on all methods instead of letting them propagate. get() and exists() return safe defaults (null/false), set() and delete() silently degrade. * Strip userAgent from stored profiles userAgent is collected by the probe for bot/crawler filtering but serves no purpose after that check. All four endpoints now strip it before persisting to storage. JSON schema and docs updated to reflect. * Strip viewport from stored profiles and expand isValidSignals tests Viewport is only used for bot detection (presence check), not classification. Strip it alongside userAgent before persisting. Remove orphaned Viewport definition from JSON schema. Add comprehensive unit tests for isValidSignals covering all validation branches (was only testing battery paths). * Document Redis error handling and add missing changelog entries * Prepare v0.3.0 release Bump all packages to 0.3.0, finalize changelog, and switch publish workflow to extract release notes from CHANGELOG.md via ffurrer2/extract-release-notes@v3. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 3155754 commit 2be69fa

58 files changed

Lines changed: 2889 additions & 220 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/publish.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ jobs:
4646
env:
4747
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
4848

49+
- name: Extract release notes from changelog
50+
id: release-notes
51+
uses: ffurrer2/extract-release-notes@v3
52+
4953
- uses: softprops/action-gh-release@v2
5054
with:
51-
generate_release_notes: true
52-
files: |
53-
packages/probe/dist/device-router-probe.min.js
54-
packages/probe/dist/device-router-probe.min.js.gz
55+
body: ${{ steps.release-notes.outputs.release_notes }}

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
## 0.3.0 (2026-02-23)
6+
7+
### Breaking Changes
8+
9+
- **userAgent and viewport stripped from stored profiles**`userAgent` and `viewport` are no longer persisted in device profiles. They are still collected by the probe and used for bot/crawler filtering at the endpoint, but stripped before storage. Stored `RawSignals` no longer contain `userAgent` or `viewport` fields
10+
11+
### Bug Fixes
12+
13+
- **Redis error handling**`RedisStorageAdapter` now catches connection failures and corrupted JSON on all methods. `get()`/`exists()` return safe defaults (`null`/`false`), `set()`/`delete()` silently degrade instead of throwing unhandled errors
14+
15+
### Documentation
16+
17+
- **Deployment guide** — New `docs/deployment.md` covering Docker (Node.js + Redis), Cloudflare Workers (Hono + KV), and serverless platforms (Lambda, Vercel). Includes Dockerfiles, docker-compose, custom StorageAdapter examples for edge runtimes, and production checklists
18+
- **Streaming injection limitation** — Documented that probe auto-injection requires a string response body; streaming responses are silently skipped (or buffered on Hono). Added framework-specific notes to all package READMEs and getting-started guide
19+
20+
### Features
21+
22+
- **Secure cookie option** — New `cookieSecure` option on all middleware packages sets the `Secure` flag on the session cookie. Set `cookieSecure: true` for HTTPS deployments
23+
- **Threshold validation**`createDeviceRouter()` now validates custom thresholds at startup: rejects inverted bounds (e.g. `lowUpperBound >= midUpperBound`), non-positive values, and non-RegExp GPU patterns. Fails fast with descriptive errors instead of silently producing wrong classifications
24+
- **First-request fallback** — Opt-in strategies to provide a classified profile on the very first page load, before the probe has run
25+
- **Header-based classification**`classifyFromHeaders: true` classifies devices from User-Agent and Client Hints headers (`Sec-CH-UA-Mobile`, `Device-Memory`, `Save-Data`), sets `Accept-CH` response header to request hints from Chromium browsers
26+
- **Fallback profiles**`fallbackProfile` option accepts `'conservative'` (low-end defaults), `'optimistic'` (high-end defaults), or custom `DeviceTiers`
27+
- **Profile source tracking** — New `source` field on `ClassifiedProfile` indicates origin: `'probe'`, `'headers'`, or `'fallback'`
28+
- **Preset tier constants**`CONSERVATIVE_TIERS` and `OPTIMISTIC_TIERS` exported from `@device-router/types`
29+
- **Bot/crawler filtering** — Probe endpoints reject bot submissions by default (`rejectBots: true`). New `isBotSignals()` detects bots via UA patterns, headless GPU renderers, and empty signal payloads
30+
331
## 0.2.0 (2026-02-22)
432

533
### Features

CLAUDE.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
```bash
8+
pnpm install # Install dependencies (uses frozen lockfile in CI)
9+
pnpm build # Build all packages (tsc -b per package, esbuild for probe bundle)
10+
pnpm test # Run Vitest once with coverage
11+
pnpm test:watch # Run Vitest in watch mode
12+
pnpm test:coverage # Run tests with explicit coverage report
13+
pnpm lint # ESLint check
14+
pnpm format # Prettier write
15+
pnpm format:check # Prettier check
16+
pnpm clean # Remove all dist/ and build artifacts
17+
```
18+
19+
To run a single test file:
20+
21+
```bash
22+
pnpm vitest run packages/types/src/__tests__/classify.test.ts
23+
```
24+
25+
## Architecture
26+
27+
DeviceRouter is a pnpm monorepo that detects real device capabilities server-side and derives rendering hints. A tiny client probe collects browser signals, posts them to a middleware endpoint, and subsequent requests get a classified device profile attached.
28+
29+
### Package dependency graph
30+
31+
```
32+
@device-router/types ← Core: types, classify(), deriveHints(), validation
33+
34+
├── @device-router/probe ← Client-side signal collection (~1 KB gzipped, enforced in CI)
35+
├── @device-router/storage ← StorageAdapter interface + MemoryStorage / RedisStorage
36+
└── middleware-{express,fastify,hono,koa} ← Framework adapters (all depend on types + storage)
37+
```
38+
39+
### Middleware packages follow an identical pattern
40+
41+
Each middleware package exports a factory `createDeviceRouter(options)` that produces three pieces:
42+
43+
1. **middleware** — reads session cookie → retrieves profile from storage → classifies + derives hints → attaches to request
44+
2. **endpoint**`POST /device-router/probe` handler: validates payload, stores profile, sets cookie
45+
3. **inject** — intercepts HTML responses and injects the minified probe script before `</head>` (supports CSP nonce)
46+
47+
### Key flows
48+
49+
- `collectSignals()` (probe) → POST to endpoint → `isValidSignals()` + `classify()` + `deriveHints()` (types) → store via `StorageAdapter` → attach `ClassifiedProfile` to request on next hit
50+
51+
## Code Conventions
52+
53+
- **TypeScript:** ES2022 target, strict mode, Node16 module resolution, ESM throughout
54+
- **Unused params** must be prefixed with `_` (ESLint enforced)
55+
- **Prettier:** 100 char width, 2-space indent, single quotes, trailing commas, semicolons
56+
- **Tests:** Vitest, files in `src/__tests__/*.test.ts`, 80% coverage threshold (branches/functions/lines/statements)
57+
- **Probe size budget:** must be ≤ 1 KB gzipped (checked in CI via `packages/probe/scripts/bundle.js`)
58+
- **Node version:** ≥20 (.nvmrc: v20)
59+
- **Always run `pnpm format` before committing** — CI enforces `format:check` and will fail on unformatted code
60+
- **No author/co-author attribution** in commits, code, or docs

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ app.get('/', (req, res) => {
9797

9898
All signals are optional — the probe gracefully degrades based on what the browser supports.
9999

100+
> **Note:** The probe also collects `navigator.userAgent` and viewport dimensions for [bot/crawler filtering](docs/getting-started.md). They are used during probe submission and stripped before the profile is stored.
101+
100102
## Tier Classification
101103

102104
Devices are classified across three dimensions:
@@ -136,6 +138,8 @@ const { middleware, probeEndpoint } = createDeviceRouter({
136138
});
137139
```
138140

141+
Thresholds are validated at startup — inverted bounds (e.g. `lowUpperBound >= midUpperBound`), non-positive values, and non-RegExp GPU patterns throw immediately.
142+
139143
## Probe Auto-Injection
140144

141145
Automatically inject the probe script into HTML responses:
@@ -153,6 +157,24 @@ const { middleware, probeEndpoint, injectionMiddleware } = createDeviceRouter({
153157

154158
No need to manually add `<script>` tags — the probe is injected before `</head>` in every HTML response.
155159

160+
## First-Request Handling
161+
162+
By default, `deviceProfile` is `null` on the first page load before the probe runs. Two opt-in strategies provide immediate classification:
163+
164+
```typescript
165+
const { middleware, probeEndpoint } = createDeviceRouter({
166+
storage,
167+
// Option 1: Classify from User-Agent + Client Hints headers
168+
classifyFromHeaders: true,
169+
// Option 2: Fall back to preset defaults
170+
fallbackProfile: 'conservative', // or 'optimistic' or custom DeviceTiers
171+
});
172+
```
173+
174+
When `classifyFromHeaders` is enabled, mobile/tablet/desktop detection happens from the UA string, and Chromium Client Hints (`Device-Memory`, `Save-Data`) refine the result. Check `profile.source` to know the origin: `'probe'`, `'headers'`, or `'fallback'`.
175+
176+
See [Getting Started — First-Request Handling](docs/getting-started.md#first-request-handling) for details.
177+
156178
## Packages
157179

158180
| Package | Description | Size |
@@ -208,6 +230,7 @@ Open http://localhost:3000 — the probe runs on first load, refresh to see your
208230
## Documentation
209231

210232
- [Getting Started](docs/getting-started.md)
233+
- [Deployment Guide](docs/deployment.md) — Docker, Cloudflare Workers, serverless
211234
- [Profile Schema Reference](docs/profile-schema.md)
212235
- API Reference: [types](docs/api/types.md) | [probe](docs/api/probe.md) | [storage](docs/api/storage.md) | [express](docs/api/middleware-express.md) | [fastify](docs/api/middleware-fastify.md) | [hono](docs/api/middleware-hono.md) | [koa](docs/api/middleware-koa.md)
213236

docs/api/middleware-express.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,20 @@ app.use(middleware);
2121

2222
### Options
2323

24-
| Option | Type | Default | Description |
25-
| ------------- | -------------------------------------- | ------------------------ | ---------------------------------------------- |
26-
| `storage` | `StorageAdapter` | required | Storage backend |
27-
| `cookieName` | `string` | `'dr_session'` | Session cookie name |
28-
| `cookiePath` | `string` | `'/'` | Cookie path |
29-
| `ttl` | `number` | `86400` | Profile TTL in seconds |
30-
| `thresholds` | `TierThresholds` | built-in defaults | Custom tier classification thresholds |
31-
| `injectProbe` | `boolean` | `false` | Auto-inject probe script into HTML responses |
32-
| `probePath` | `string` | `'/device-router/probe'` | Custom probe endpoint path for injected script |
33-
| `probeNonce` | `string \| ((req: Request) => string)` || CSP nonce for the injected script tag |
24+
| Option | Type | Default | Description |
25+
| --------------------- | -------------------------------------- | ------------------------ | ------------------------------------------------------------ |
26+
| `storage` | `StorageAdapter` | required | Storage backend |
27+
| `cookieName` | `string` | `'dr_session'` | Session cookie name |
28+
| `cookiePath` | `string` | `'/'` | Cookie path |
29+
| `cookieSecure` | `boolean` | `false` | Set `Secure` flag on the session cookie |
30+
| `ttl` | `number` | `86400` | Profile TTL in seconds |
31+
| `rejectBots` | `boolean` | `true` | Reject bot/crawler probe submissions (returns 403) |
32+
| `thresholds` | `TierThresholds` | built-in defaults | Custom tier classification thresholds (validated at startup) |
33+
| `injectProbe` | `boolean` | `false` | Auto-inject probe script into HTML responses |
34+
| `probePath` | `string` | `'/device-router/probe'` | Custom probe endpoint path for injected script |
35+
| `probeNonce` | `string \| ((req: Request) => string)` || CSP nonce for the injected script tag |
36+
| `fallbackProfile` | `FallbackProfile` || Fallback profile for first requests without probe data |
37+
| `classifyFromHeaders` | `boolean` | `false` | Classify from UA/Client Hints on first request |
3438

3539
### Returns
3640

@@ -47,12 +51,13 @@ The middleware attaches a `ClassifiedProfile | null` to `req.deviceProfile`:
4751
```typescript
4852
interface ClassifiedProfile {
4953
profile: DeviceProfile; // Raw profile with signals
50-
tiers: DeviceTiers; // { cpu, memory, connection }
54+
tiers: DeviceTiers; // { cpu, memory, connection, gpu }
5155
hints: RenderingHints; // { deferHeavyComponents, ... }
56+
source: ProfileSource; // 'probe' | 'headers' | 'fallback'
5257
}
5358
```
5459

55-
`null` when no session cookie is present or profile has expired.
60+
`null` when no session cookie is present or profile has expired (unless `classifyFromHeaders` or `fallbackProfile` is configured).
5661

5762
## Probe Auto-Injection
5863

docs/api/middleware-fastify.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,20 @@ await app.register(plugin, pluginOptions);
2323

2424
### Options
2525

26-
| Option | Type | Default | Description |
27-
| ------------- | --------------------------------------------- | ------------------------ | ---------------------------------------------- |
28-
| `storage` | `StorageAdapter` | required | Storage backend |
29-
| `cookieName` | `string` | `'dr_session'` | Session cookie name |
30-
| `cookiePath` | `string` | `'/'` | Cookie path |
31-
| `ttl` | `number` | `86400` | Profile TTL in seconds |
32-
| `thresholds` | `TierThresholds` | built-in defaults | Custom tier classification thresholds |
33-
| `injectProbe` | `boolean` | `false` | Auto-inject probe script into HTML responses |
34-
| `probePath` | `string` | `'/device-router/probe'` | Custom probe endpoint path for injected script |
35-
| `probeNonce` | `string \| ((req: FastifyRequest) => string)` || CSP nonce for the injected script tag |
26+
| Option | Type | Default | Description |
27+
| --------------------- | --------------------------------------------- | ------------------------ | ------------------------------------------------------------ |
28+
| `storage` | `StorageAdapter` | required | Storage backend |
29+
| `cookieName` | `string` | `'dr_session'` | Session cookie name |
30+
| `cookiePath` | `string` | `'/'` | Cookie path |
31+
| `cookieSecure` | `boolean` | `false` | Set `Secure` flag on the session cookie |
32+
| `ttl` | `number` | `86400` | Profile TTL in seconds |
33+
| `rejectBots` | `boolean` | `true` | Reject bot/crawler probe submissions (returns 403) |
34+
| `thresholds` | `TierThresholds` | built-in defaults | Custom tier classification thresholds (validated at startup) |
35+
| `injectProbe` | `boolean` | `false` | Auto-inject probe script into HTML responses |
36+
| `probePath` | `string` | `'/device-router/probe'` | Custom probe endpoint path for injected script |
37+
| `probeNonce` | `string \| ((req: FastifyRequest) => string)` || CSP nonce for the injected script tag |
38+
| `fallbackProfile` | `FallbackProfile` || Fallback profile for first requests without probe data |
39+
| `classifyFromHeaders` | `boolean` | `false` | Classify from UA/Client Hints on first request |
3640

3741
### Returns
3842

@@ -50,12 +54,13 @@ The plugin attaches a `ClassifiedProfile | null` to `req.deviceProfile`:
5054
```typescript
5155
interface ClassifiedProfile {
5256
profile: DeviceProfile; // Raw profile with signals
53-
tiers: DeviceTiers; // { cpu, memory, connection }
57+
tiers: DeviceTiers; // { cpu, memory, connection, gpu }
5458
hints: RenderingHints; // { deferHeavyComponents, ... }
59+
source: ProfileSource; // 'probe' | 'headers' | 'fallback'
5560
}
5661
```
5762

58-
`null` when no session cookie is present or profile has expired.
63+
`null` when no session cookie is present or profile has expired (unless `classifyFromHeaders` or `fallbackProfile` is configured).
5964

6065
## Probe Auto-Injection
6166

0 commit comments

Comments
 (0)