@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 +9 -0
- package/README.md +10 -8
- package/dist/core/endpoint.d.ts +5 -0
- package/dist/core/endpoint.js +7 -0
- package/dist/core/types.d.ts +4 -1
- package/dist/express.d.ts +3 -4
- package/dist/express.js +5 -6
- package/dist/fetch.d.ts +9 -4
- package/dist/fetch.js +16 -6
- package/dist/next.d.ts +3 -4
- package/dist/next.js +5 -6
- package/dist/web.d.ts +3 -2
- package/dist/web.js +2 -2
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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` | `
|
|
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` |
|
|
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
|
|
package/dist/core/endpoint.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/endpoint.js
CHANGED
|
@@ -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;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -105,7 +105,10 @@ export interface ShieldResult {
|
|
|
105
105
|
}
|
|
106
106
|
/** Block-handling shared by the firewall adapters. */
|
|
107
107
|
export interface BlockOptions {
|
|
108
|
-
/**
|
|
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
|
-
*
|
|
10
|
-
* app.use(guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' }));
|
|
8
|
+
* app.use(guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! }));
|
|
11
9
|
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
9
|
-
* app.use(guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' }));
|
|
7
|
+
* app.use(guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! }));
|
|
10
8
|
*
|
|
11
|
-
*
|
|
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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
15
|
-
* (redirect or overlay), are set per site in Firewall settings
|
|
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_...'
|
|
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 =
|
|
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.
|
|
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": {
|