@noirtrack/sdk 0.4.0 → 0.5.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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to `@noirtrack/sdk` are documented here. This project follows semantic versioning (while pre-1.0, a minor bump may include breaking changes).
4
4
 
5
+ ## 0.5.0
6
+
7
+ ### Changed
8
+
9
+ - **The setup is just your key.** Protection is controlled in the dashboard (Firewall settings), so the code no longer needs options for it:
10
+ - **Breaking (web client):** the firewall now runs by default. `block` defaults to `true`, and whether it only monitors or blocks is set in Firewall settings as before. To skip it, pass `block: false`.
11
+ - **Server guards (`next`, `express`, `fetch`):** without `onBlock`, a blocked visitor is redirected to the Blocked page URL from Firewall settings when one is set, otherwise gets an HTTP 403. Before, the guard always returned 403 unless you passed `onBlock: 'redirect'`. Passing `onBlock` still overrides it.
12
+ - **Fetch guard (`fetch`):** images, styles, scripts, fonts and media now pass straight through. A Cloudflare Worker in front of a site runs for every file a page loads, and each one used to count as a page view and a firewall check.
13
+
5
14
  ## 0.4.0
6
15
 
7
16
  ### Added
package/README.md CHANGED
@@ -19,14 +19,14 @@ Two keys, both on your site's Install page. The public key (`pk_live_...`) is sa
19
19
  ```ts
20
20
  import { createClient } from '@noirtrack/sdk/web';
21
21
 
22
- const noir = createClient({ publicKey: process.env.NEXT_PUBLIC_NOIRTRACK_KEY!, autoPageviews: true });
22
+ const noir = createClient({ publicKey: process.env.NEXT_PUBLIC_NOIRTRACK_KEY! });
23
23
 
24
24
  noir.event('signup', { plan: 'pro' });
25
25
  noir.identify('user_123', { email });
26
26
  noir.revenue({ checkoutId: 'cs_test_123' });
27
27
  ```
28
28
 
29
- Pageviews and route changes are automatic. Events are batched and sent with `sendBeacon`. Set `cookieless: true` for a no-cookie mode, or `block: true` to turn on the firewall. Whether it monitors or blocks, and how a block looks (redirect or overlay), are set in your Firewall settings, with no code change. Safe to import in SSR (it does nothing without a window). Also: `view(path?)`, `reset()`, `flush()`, `links.decorate(url)`.
29
+ Pageviews, route changes and the firewall are automatic. Whether the firewall only monitors or blocks, and how a block looks (redirect or overlay), are set in your Firewall settings, with no code change. Events are batched and sent with `sendBeacon`. See [Options](#options) for `cookieless` and the rest. Safe to import in SSR (it does nothing without a window). Also: `view(path?)`, `reset()`, `flush()`, `links.decorate(url)`.
30
30
 
31
31
  ## React Native
32
32
 
@@ -70,18 +70,20 @@ if (verdict?.action === 'block') {
70
70
  }
71
71
  ```
72
72
 
73
- Or use an adapter and pass the client:
73
+ Or use an adapter. It only needs your secret key:
74
74
 
75
75
  ```ts
76
76
  // Next.js: middleware.ts
77
77
  import { guard } from '@noirtrack/sdk/next';
78
- export const middleware = guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' });
78
+ export const middleware = guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! });
79
79
  export const config = { matcher: ['/((?!_next/|favicon.ico).*)'] };
80
80
  ```
81
81
 
82
- `@noirtrack/sdk/express` (`guard(noir, { onBlock })`) and `@noirtrack/sdk/fetch` (`createGuard(noir, { onBlock })`) work the same way. You can also pass `{ publicKey, secretKey, onBlock }` instead of a client.
82
+ `@noirtrack/sdk/express` (`app.use(guard({ secretKey }))`) and `@noirtrack/sdk/fetch` (`createGuard({ secretKey })`) work the same way. You can also pass a client you already created: `guard(noir)`.
83
83
 
84
- **Monitor or Block is set in the dashboard.** Adding a guard turns the firewall on, but whether it blocks or just monitors is your Firewall Mode, not the code. New sites start in **Monitor** (detect and log, don't block anyone) until you switch to **Block** in Firewall settings. `onBlock` only changes how a block is delivered once you are in Block mode.
84
+ On Cloudflare Workers, read the key from the `env` argument inside `fetch` (`createGuard({ secretKey: env.NOIRTRACK_SECRET_KEY })`), since `process.env` is only there with Node.js compatibility on. The fetch guard lets images, CSS, JavaScript, fonts and videos through without a check. Step-by-step setup: [Cloudflare Workers](https://noirtrack.com/docs/cloudflare-workers).
85
+
86
+ **Everything else is set in the dashboard.** Adding a guard turns the firewall on. Whether it blocks or just monitors is your Firewall Mode, and a blocked visitor is sent to the Blocked page URL from Firewall settings (or gets a 403 when none is set). New sites start in **Monitor** (detect and log, don't block anyone) until you switch to **Block**. To handle a block differently in code, pass `onBlock` (see [Options](#options)).
85
87
 
86
88
  The guards resolve the visitor IP from `CF-Connecting-IP`, `True-Client-IP`, then the left-most `X-Forwarded-For`. Run them **behind a trusted proxy/CDN that sets those headers**. If requests can reach your app directly, a client can spoof `X-Forwarded-For` to forge its IP. Keep your secret key server-side only; it is never needed in the browser or app bundle.
87
89
 
@@ -95,10 +97,10 @@ The guards resolve the visitor IP from `CF-Connecting-IP`, `True-Client-IP`, the
95
97
  | `timeoutMs` | `800` | all | Abort a call after this, then fail open. |
96
98
  | `autoPageviews` | `true` | web | Capture initial + SPA pageviews. |
97
99
  | `cookieless` | `false` | web | No cookies; ids in sessionStorage. |
98
- | `block` | `false` | web | Turn on the firewall (Monitor/Block set in dashboard). |
100
+ | `block` | `true` | web | Run the firewall (Monitor/Block set in dashboard). `false` skips it. |
99
101
  | `flushIntervalMs` / `maxQueueSize` | `5000` / `10` | web, RN | Batch flush tuning. |
100
102
  | `storage` | in-memory | RN | Async store (pass AsyncStorage). |
101
- | `onBlock` / `blockStatus` / `blockedPage` | `block` / `403` / none | adapters | Block handling. |
103
+ | `onBlock` / `blockStatus` / `blockedPage` | from dashboard / `403` / none | adapters | Block handling. Default: redirect to the dashboard's Blocked page URL, else 403. |
102
104
 
103
105
  Full reference: the [Server SDK page](https://noirtrack.com/docs/server-sdk) in the NoirTrack docs.
104
106
 
@@ -14,4 +14,9 @@ export declare function resolveEndpoint(explicit?: string): string;
14
14
  * enforcement on the blocked page so a still-blocked visitor isn't redirected to it forever.
15
15
  * `page` may be a path ("/blocked") or an absolute URL ("https://site.com/blocked").
16
16
  */
17
+ /**
18
+ * What a guard does with a block verdict when the code doesn't say: redirect to the Blocked page URL set in Firewall
19
+ * settings when there is one, otherwise return an HTTP error. So a block is set up in the dashboard, not in code.
20
+ */
21
+ export declare function blockMode(onBlock: 'redirect' | 'rewrite' | 'block' | undefined, page: string | null | undefined): 'redirect' | 'rewrite' | 'block';
17
22
  export declare function isBlockedPagePath(path: string, page: string | null | undefined): boolean;
@@ -18,6 +18,13 @@ export function resolveEndpoint(explicit) {
18
18
  * enforcement on the blocked page so a still-blocked visitor isn't redirected to it forever.
19
19
  * `page` may be a path ("/blocked") or an absolute URL ("https://site.com/blocked").
20
20
  */
21
+ /**
22
+ * What a guard does with a block verdict when the code doesn't say: redirect to the Blocked page URL set in Firewall
23
+ * settings when there is one, otherwise return an HTTP error. So a block is set up in the dashboard, not in code.
24
+ */
25
+ export function blockMode(onBlock, page) {
26
+ return onBlock ?? (page ? 'redirect' : 'block');
27
+ }
21
28
  export function isBlockedPagePath(path, page) {
22
29
  if (!page)
23
30
  return false;
@@ -105,7 +105,10 @@ export interface ShieldResult {
105
105
  }
106
106
  /** Block-handling shared by the firewall adapters. */
107
107
  export interface BlockOptions {
108
- /** What to do on a block verdict. Default 'block' (return an HTTP error). */
108
+ /**
109
+ * What to do on a block verdict. By default it follows Firewall settings: a redirect to the Blocked page URL when
110
+ * one is set there, otherwise 'block' (an HTTP error).
111
+ */
109
112
  onBlock?: 'redirect' | 'rewrite' | 'block';
110
113
  /** Page to redirect/rewrite to (falls back to the verdict's blocked_page). */
111
114
  blockedPage?: string;
package/dist/express.d.ts CHANGED
@@ -3,13 +3,12 @@ import type { RequestHandler } from 'express';
3
3
  import type { BlockOptions } from './core/types.js';
4
4
  import { type GuardTarget } from './index.js';
5
5
  /**
6
- * import { createClient } from '@noirtrack/sdk';
7
6
  * import { guard } from '@noirtrack/sdk/express';
8
7
  *
9
- * const noir = createClient({ publicKey: '...', secretKey: process.env.NOIRTRACK_SECRET! });
10
- * app.use(guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' }));
8
+ * app.use(guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! }));
11
9
  *
12
- * Or pass options: `guard({ publicKey, secretKey, onBlock: 'block' })`.
10
+ * Monitor or Block, and where a blocked visitor goes, come from Firewall settings. You can also pass a client:
11
+ * `guard(noir, { onBlock: 'block' })`.
13
12
  * Fails open: a NoirTrack error/timeout calls next() and never breaks your app.
14
13
  */
15
14
  export declare function guard(target: GuardTarget, blockOptions?: BlockOptions): RequestHandler;
package/dist/express.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import { clientIp } from './core/client-ip.js';
2
- import { isBlockedPagePath } from './core/endpoint.js';
2
+ import { blockMode, isBlockedPagePath } from './core/endpoint.js';
3
3
  import { resolveGuard } from './index.js';
4
4
  /**
5
- * import { createClient } from '@noirtrack/sdk';
6
5
  * import { guard } from '@noirtrack/sdk/express';
7
6
  *
8
- * const noir = createClient({ publicKey: '...', secretKey: process.env.NOIRTRACK_SECRET! });
9
- * app.use(guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' }));
7
+ * app.use(guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! }));
10
8
  *
11
- * Or pass options: `guard({ publicKey, secretKey, onBlock: 'block' })`.
9
+ * Monitor or Block, and where a blocked visitor goes, come from Firewall settings. You can also pass a client:
10
+ * `guard(noir, { onBlock: 'block' })`.
12
11
  * Fails open: a NoirTrack error/timeout calls next() and never breaks your app.
13
12
  */
14
13
  export function guard(target, blockOptions) {
@@ -38,7 +37,7 @@ export function guard(target, blockOptions) {
38
37
  return;
39
38
  }
40
39
  const page = verdict.blocked_page ?? block.blockedPage;
41
- if ((block.onBlock ?? 'block') === 'redirect' && page) {
40
+ if (blockMode(block.onBlock, page) === 'redirect' && page) {
42
41
  // Don't redirect the blocked page to itself — that loops forever.
43
42
  if (isBlockedPagePath(path, page)) {
44
43
  next();
package/dist/fetch.d.ts CHANGED
@@ -4,12 +4,17 @@ import { type GuardTarget } from './index.js';
4
4
  * Returns a guard that, given a standard Request, resolves to a blocking Response, or null when
5
5
  * the request should continue.
6
6
  *
7
- * import { createClient } from '@noirtrack/sdk';
8
7
  * import { createGuard } from '@noirtrack/sdk/fetch';
9
8
  *
10
- * const noir = createClient({ publicKey: '...', secretKey: env.NOIRTRACK_SECRET });
11
- * const guard = createGuard(noir, { onBlock: 'redirect', blockedPage: '/blocked' });
12
- * export default { async fetch(req) { return (await guard(req)) ?? fetch(req); } };
9
+ * export default {
10
+ * async fetch(request, env) {
11
+ * const guard = createGuard({ secretKey: env.NOIRTRACK_SECRET_KEY });
12
+ * return (await guard(request)) ?? fetch(request);
13
+ * },
14
+ * };
15
+ *
16
+ * Monitor or Block, and where a blocked visitor goes, come from Firewall settings. Images, styles, scripts, fonts
17
+ * and media pass straight through without a check.
13
18
  *
14
19
  * Fails open: a NoirTrack error/timeout returns null (let the request through).
15
20
  */
package/dist/fetch.js CHANGED
@@ -1,17 +1,25 @@
1
1
  /** @noirtrack/sdk/fetch — guard for standard Request/Response runtimes (Hono, Cloudflare Workers, Deno). */
2
2
  import { clientIp } from './core/client-ip.js';
3
- import { isBlockedPagePath } from './core/endpoint.js';
3
+ import { blockMode, isBlockedPagePath } from './core/endpoint.js';
4
4
  import { resolveGuard } from './index.js';
5
+ // A Worker in front of a site runs for every file a page loads. Checking those would record each image and stylesheet as
6
+ // a page view and spend a firewall check on it, so they skip the guard, like the Next.js matcher skips Next's own files. // why
7
+ const STATIC_FILE = /\.(?:css|js|mjs|map|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|mp4|webm|mp3)$/i;
5
8
  /**
6
9
  * Returns a guard that, given a standard Request, resolves to a blocking Response, or null when
7
10
  * the request should continue.
8
11
  *
9
- * import { createClient } from '@noirtrack/sdk';
10
12
  * import { createGuard } from '@noirtrack/sdk/fetch';
11
13
  *
12
- * const noir = createClient({ publicKey: '...', secretKey: env.NOIRTRACK_SECRET });
13
- * const guard = createGuard(noir, { onBlock: 'redirect', blockedPage: '/blocked' });
14
- * export default { async fetch(req) { return (await guard(req)) ?? fetch(req); } };
14
+ * export default {
15
+ * async fetch(request, env) {
16
+ * const guard = createGuard({ secretKey: env.NOIRTRACK_SECRET_KEY });
17
+ * return (await guard(request)) ?? fetch(request);
18
+ * },
19
+ * };
20
+ *
21
+ * Monitor or Block, and where a blocked visitor goes, come from Firewall settings. Images, styles, scripts, fonts
22
+ * and media pass straight through without a check.
15
23
  *
16
24
  * Fails open: a NoirTrack error/timeout returns null (let the request through).
17
25
  */
@@ -19,6 +27,8 @@ export function createGuard(target, blockOptions) {
19
27
  const { client, block } = resolveGuard(target, blockOptions);
20
28
  return async function guard(request) {
21
29
  const url = new URL(request.url);
30
+ if (STATIC_FILE.test(url.pathname))
31
+ return null;
22
32
  const ip = clientIp((name) => request.headers.get(name));
23
33
  const ua = request.headers.get('user-agent') ?? '';
24
34
  const path = url.pathname;
@@ -37,7 +47,7 @@ export function createGuard(target, blockOptions) {
37
47
  if (verdict?.action !== 'block')
38
48
  return null;
39
49
  const page = verdict.blocked_page ?? block.blockedPage;
40
- if ((block.onBlock ?? 'block') === 'redirect' && page) {
50
+ if (blockMode(block.onBlock, page) === 'redirect' && page) {
41
51
  // Already on the blocked page → let it through, don't loop.
42
52
  if (isBlockedPagePath(path, page))
43
53
  return null;
package/dist/next.d.ts CHANGED
@@ -3,14 +3,13 @@ import { NextRequest, NextResponse } from 'next/server';
3
3
  import { type GuardTarget } from './index.js';
4
4
  /**
5
5
  * // middleware.ts
6
- * import { createClient } from '@noirtrack/sdk';
7
6
  * import { guard } from '@noirtrack/sdk/next';
8
7
  *
9
- * const noir = createClient({ publicKey: '...', secretKey: process.env.NOIRTRACK_SECRET! });
10
- * export const middleware = guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' });
8
+ * export const middleware = guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! });
11
9
  * export const config = { matcher: ['/((?!_next/|favicon.ico).*)'] };
12
10
  *
13
- * You can also pass options directly: `guard({ publicKey, secretKey, onBlock: 'redirect' })`.
11
+ * Monitor or Block, and where a blocked visitor goes, come from Firewall settings. You can also pass a client:
12
+ * `guard(noir, { onBlock: 'rewrite' })`.
14
13
  * Fails open: a NoirTrack error/timeout never blocks or breaks your site.
15
14
  */
16
15
  export declare function guard(target: GuardTarget, blockOptions?: import('./core/types.js').BlockOptions): (request: NextRequest) => Promise<NextResponse>;
package/dist/next.js CHANGED
@@ -1,18 +1,17 @@
1
1
  /** @noirtrack/sdk/next — firewall middleware for Next.js (Edge runtime). */
2
2
  import { NextResponse } from 'next/server';
3
3
  import { clientIp } from './core/client-ip.js';
4
- import { isBlockedPagePath } from './core/endpoint.js';
4
+ import { blockMode, isBlockedPagePath } from './core/endpoint.js';
5
5
  import { resolveGuard } from './index.js';
6
6
  /**
7
7
  * // middleware.ts
8
- * import { createClient } from '@noirtrack/sdk';
9
8
  * import { guard } from '@noirtrack/sdk/next';
10
9
  *
11
- * const noir = createClient({ publicKey: '...', secretKey: process.env.NOIRTRACK_SECRET! });
12
- * export const middleware = guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' });
10
+ * export const middleware = guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! });
13
11
  * export const config = { matcher: ['/((?!_next/|favicon.ico).*)'] };
14
12
  *
15
- * You can also pass options directly: `guard({ publicKey, secretKey, onBlock: 'redirect' })`.
13
+ * Monitor or Block, and where a blocked visitor goes, come from Firewall settings. You can also pass a client:
14
+ * `guard(noir, { onBlock: 'rewrite' })`.
16
15
  * Fails open: a NoirTrack error/timeout never blocks or breaks your site.
17
16
  */
18
17
  export function guard(target, blockOptions) {
@@ -35,8 +34,8 @@ export function guard(target, blockOptions) {
35
34
  });
36
35
  if (verdict?.action !== 'block')
37
36
  return NextResponse.next();
38
- const mode = block.onBlock ?? 'block';
39
37
  const page = verdict.blocked_page ?? block.blockedPage;
38
+ const mode = blockMode(block.onBlock, page);
40
39
  // The blocked page must stay reachable, or a still-blocked visitor loops forever
41
40
  // when that page also runs this middleware.
42
41
  if ((mode === 'redirect' || mode === 'rewrite') && page) {
package/dist/web.d.ts CHANGED
@@ -11,8 +11,9 @@ export interface WebClientOptions {
11
11
  cookieless?: boolean;
12
12
  /** Track on localhost too (off by default, like the hosted script). */
13
13
  allowLocalhost?: boolean;
14
- /** Turn on the firewall on the first view. Whether it monitors or blocks, and how a block looks
15
- * (redirect or overlay), are set per site in Firewall settings. Off by default. */
14
+ /** Run the firewall on the first view. On by default: whether it only monitors or blocks, and how a block
15
+ * looks (redirect or overlay), are set per site in Firewall settings, so nothing here needs changing. Set
16
+ * false to skip the check entirely. */
16
17
  block?: boolean;
17
18
  /** Optional redirect target if none is set in Firewall settings. */
18
19
  blockedPage?: string;
package/dist/web.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @noirtrack/sdk/web — browser analytics for frameworks (React, Vue, Svelte, and friends).
3
3
  *
4
4
  * import { createClient } from '@noirtrack/sdk/web';
5
- * const noir = createClient({ publicKey: 'pk_live_...', autoPageviews: true });
5
+ * const noir = createClient({ publicKey: 'pk_live_...' });
6
6
  * noir.event('signup', { plan: 'pro' });
7
7
  *
8
8
  * Uses the public key only (browser code can't hold a secret). Safe to import in SSR: when there
@@ -250,7 +250,7 @@ export function createClient(options) {
250
250
  // Soft block on the first view: ask the server, then enforce. /api/v1/check already RECORDS
251
251
  // this pageview (stamped with the verdict), so when blocking is on we must NOT also call
252
252
  // ingest.view() for the first view, or it would be counted twice.
253
- const wantsBlock = !!options.block;
253
+ const wantsBlock = options.block !== false;
254
254
  if (wantsBlock) {
255
255
  void ingest.check().then((verdict) => {
256
256
  if (verdict?.action !== 'block')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noirtrack/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "NoirTrack server SDK. Block bots and bad traffic before your app renders, and record goals, revenue, and identify from your backend with one secret key. Framework-agnostic core plus Next.js, Express, and fetch/edge adapters.",
5
5
  "homepage": "https://noirtrack.com/docs",
6
6
  "bugs": {