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
2 changes: 2 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
precious lint --staged
24 changes: 24 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: daily
time: '14:00'
open-pull-requests-limit: 20
groups:
minor-and-patch:
patterns:
- '*'
update-types:
- minor
- patch
cooldown:
default-days: 7
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
time: '14:00'
cooldown:
default-days: 7
33 changes: 33 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: "Code scanning - action"

on:
pull_request:
push:
branches:
- main
schedule:
- cron: '0 3 * * 6'

jobs:
CodeQL-Build:

runs-on: ubuntu-latest

permissions:
security-events: write

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10

- name: Autobuild
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
41 changes: 41 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Precious
on:
pull_request:
push:
branches:
- main
schedule:
- cron: '3 0 * * SUN'
workflow_dispatch:
permissions:
contents: read
jobs:
precious:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3.6.1
- run: pnpm install --frozen-lockfile
- name: Fetch base ref
if: ${{ github.event.pull_request }}
run: git fetch origin "${BASE_REF}"
env:
BASE_REF: ${{ github.base_ref }}
- name: Select files
id: select-files
run: |
if [[ -n "${PR_NUMBER}" ]]; then
echo "precious-args=--git-diff-from origin/${BASE_REF}" >> "$GITHUB_OUTPUT"
else
echo 'precious-args=--all' >> "$GITHUB_OUTPUT"
fi
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_REF: ${{ github.base_ref }}
- name: Lint files
run: precious lint ${PRECIOUS_ARGS}
env:
PRECIOUS_ARGS: ${{ steps.select-files.outputs.precious-args }}
47 changes: 47 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Release

on:
workflow_dispatch:
pull_request:
push:
branches:
- main
release:
types: [published]

permissions: {}

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3.6.1
- run: pnpm install --frozen-lockfile
- run: pnpm test
- run: pnpm run lint
- run: pnpm run build

publish:
needs: build
if: github.event_name == 'release' && github.event.action == 'published'
runs-on: ubuntu-latest
environment: npm
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3.6.1
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: pnpm publish --provenance --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Run tests
on:
pull_request:
push:
branches:
- main
schedule:
- cron: '3 2 * * SUN'
permissions: {}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3.6.1
- run: pnpm install --frozen-lockfile
- run: pnpm test --coverage
- run: pnpm run build
23 changes: 23 additions & 0 deletions .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: GitHub Actions Security Analysis with zizmor

on:
push:
branches: ["main"]
pull_request:
branches: ["**"]

permissions: {}

jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Run zizmor
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
23 changes: 23 additions & 0 deletions .precious.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[commands.eslint]
type = "lint"
cmd = ["pnpm", "exec", "eslint"]
invoke = "once"
include = ["src/**/*.ts"]
ok-exit-codes = 0

[commands.tsc]
type = "lint"
cmd = ["pnpm", "exec", "tsc", "--noEmit"]
invoke = "once"
path-args = "none"
include = ["src/**/*.ts", "tsconfig.json"]
ok-exit-codes = 0

[commands.prettier]
type = "both"
cmd = ["pnpm", "exec", "prettier", "--parser", "typescript"]
lint-flags = ["--check"]
tidy-flags = ["--write"]
path-args = "absolute-file"
include = ["src/**/*.ts"]
ok-exit-codes = 0
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
node_modules/
pnpm-lock.yaml
66 changes: 66 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# @maxmind/device-tracking

A thin TypeScript loader that validates inputs,
dynamically imports a remote fingerprinting module from MaxMind's servers, and
returns a device tracking token. Runs in the browser.

## Commands

```sh
pnpm test # Jest (ESM via --experimental-vm-modules)
pnpm test:watch # Jest in watch mode
pnpm run lint # ESLint + tsc --noEmit
pnpm run build # Clean build to dist/
pnpm run prettier:ts # Format src/**/*.ts
```

Run a single test file:

```sh
pnpm test -- src/loader.spec.ts
```

Jest prints an `ExperimentalWarning` about VM Modules on every run. This is
expected and harmless.

## Architecture

```
src/
index.ts # Public API: trackDevice() with input validation
loader.ts # Singleton module loader with caching and timeout
dynamic-import.ts # Thin wrapper around import() for test mocking
types.ts # TrackDeviceOptions, TrackResult interfaces
*.spec.ts # Co-located test files
```

- `index.ts` is the only public entry point (`package.json` exports map).
- `loader.ts` caches module promises per-host; clears cache on failure for retry.
- `dynamic-import.ts` exists solely to make `import()` mockable in Jest.
- Tests use `jest.unstable_mockModule` (ESM-compatible mocking).

## Conventions

- **ESM only** — `"type": "module"` in package.json, `.js` extensions in imports.
- **Strict TypeScript** — `strict: true`, target ES2022, module Node16.
- **Prettier** — single quotes, trailing commas (es5).
- **Formatting separate from logic** — keep style-only changes in their own commits.
- **`fixup!` commits** — prefix fixup commits with `fixup! <original subject>` for autosquash.
- **Error messages include context** — URLs, received values, types.
- **`@internal` JSDoc tag** — marks exports that exist only for testing (e.g. `resetModuleCache`).

## Testing notes

- Test environment is jsdom (browser globals).
- Tests co-locate next to source files (`*.spec.ts`).
- The `moduleNameMapper` in jest.config.js strips `.js` extensions for ts-jest.
- Loader tests use real dynamic import (which fails in Node) to exercise error paths.
- Mocked-module tests use `jest.unstable_mockModule` + dynamic `import()` to get fresh modules.
- Fake timers are used for timeout tests — always call `jest.useRealTimers()` in `afterEach`.

## Tooling

- **mise** manages Node, pnpm, and precious versions (see `mise.toml`).
- **precious** is the code-quality runner (config: `.precious.toml`).
- **Git hooks** live in `.githooks/`. Enable with: `git config core.hooksPath .githooks`
- GitHub Actions: test, lint, CodeQL, zizmor, release workflows.
65 changes: 58 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,69 @@
# MaxMind Device Tracking Add-On

A thin loader package for MaxMind's [minFraud device tracking](https://dev.maxmind.com/minfraud/track-devices)
system. This package dynamically loads the device fingerprinting module from
MaxMind's servers at runtime, ensuring you always get the latest version without
updating the npm package.

The package itself contains no fingerprinting logic — it validates inputs, loads
the remote module, and returns the tracking token.

## Installation

```bash
npm install @maxmind/device-tracking
```

## License
## Usage

```typescript
import { trackDevice } from '@maxmind/device-tracking';

const result = await trackDevice({ accountId: 123456 });
console.log(result.trackingToken);
```

### Ad-blocker bypass

If you proxy MaxMind's device tracking through your own subdomain (to avoid
ad-blockers), pass the `host` option. The module will be loaded from your
custom host, and the host value is passed to the remote module for its own use:

```typescript
const result = await trackDevice({
accountId: 123456,
host: 'tracking.yourdomain.com',
});
```

### Disable WebGL hash

For performance or compatibility, you can disable WebGL hash collection:

```typescript
const result = await trackDevice({
accountId: 123456,
disableWebglHash: true,
});
```

## API reference

### `trackDevice(options: TrackDeviceOptions): Promise<TrackResult>`

Loads the device tracking module (if not already cached) and collects a device
fingerprint.

### `TrackDeviceOptions`

Licensed under either of
| Property | Type | Required | Description |
| ------------------ | --------- | -------- | ----------------------------------------------- |
| `accountId` | `number` | Yes | Your MaxMind account ID (positive integer) |
| `host` | `string` | No | Custom hostname for ad-blocker bypass |
| `disableWebglHash` | `boolean` | No | Disable WebGL hash collection |

- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to drop this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! My thought is that it was confusing as this is just a very thin wrapper around a script that isn't under that license.

https://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or
https://opensource.org/licenses/MIT)
### `TrackResult`

at your option.
| Property | Type | Description |
| --------------- | -------- | ------------------------------ |
| `trackingToken` | `string` | Opaque device tracking token |
27 changes: 27 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettierConfig from 'eslint-config-prettier';
import globals from 'globals';

export default tseslint.config(
{
ignores: ['dist/', 'node_modules/', 'coverage/', 'jest.config.js', '*.mjs'],
},
js.configs.recommended,
...tseslint.configs.recommended,
prettierConfig,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
projectService: {
allowDefaultProject: ['src/*.spec.ts'],
},
tsconfigRootDir: import.meta.dirname,
},
},
}
);
Loading