@noirtrack/sdk 0.4.0 → 0.6.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 +16 -0
- package/README.md +14 -8
- package/dist/core/endpoint.d.ts +5 -0
- package/dist/core/endpoint.js +7 -0
- package/dist/core/http.d.ts +2 -0
- package/dist/core/http.js +15 -0
- package/dist/core/indexnow.d.ts +2 -0
- package/dist/core/indexnow.js +14 -0
- package/dist/core/ingest.d.ts +2 -0
- package/dist/core/secret.d.ts +2 -0
- package/dist/core/secret.js +13 -2
- package/dist/core/types.d.ts +4 -1
- package/dist/express.d.ts +3 -4
- package/dist/express.js +12 -6
- package/dist/fetch.d.ts +9 -4
- package/dist/fetch.js +20 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/next.d.ts +3 -4
- package/dist/next.js +10 -6
- package/dist/react-native.js +1 -1
- package/dist/web.d.ts +3 -2
- package/dist/web.js +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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.6.0
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **IndexNow key file.** With Indexing on (Site → Settings → Indexing), the `next`, `express` and `fetch` guards answer `/{key}.txt` with your site's IndexNow key, so Bing, Yandex and other search engines accept the new pages NoirTrack sends them. Nothing to configure. `createClient()` also gains `indexNowKey()`.
|
|
10
|
+
- **UTM content and term (web client).** `utm_content` and `utm_term` are sent with page views and events, so the dashboard's Campaigns menu breaks traffic down by them too.
|
|
11
|
+
|
|
12
|
+
## 0.5.0
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **The setup is just your key.** Protection is controlled in the dashboard (Firewall settings), so the code no longer needs options for it:
|
|
17
|
+
- **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`.
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
5
21
|
## 0.4.0
|
|
6
22
|
|
|
7
23
|
### 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,21 +70,27 @@ 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
|
|
|
90
|
+
### IndexNow key file
|
|
91
|
+
|
|
92
|
+
With Indexing on (Site → Settings → Indexing), the `next`, `express` and `fetch` guards also answer `/{key}.txt` with your site's IndexNow key, so Bing, Yandex and other search engines accept the new pages NoirTrack sends them. There is nothing to configure.
|
|
93
|
+
|
|
88
94
|
## Options
|
|
89
95
|
|
|
90
96
|
| Option | Default | Where | Description |
|
|
@@ -95,10 +101,10 @@ The guards resolve the visitor IP from `CF-Connecting-IP`, `True-Client-IP`, the
|
|
|
95
101
|
| `timeoutMs` | `800` | all | Abort a call after this, then fail open. |
|
|
96
102
|
| `autoPageviews` | `true` | web | Capture initial + SPA pageviews. |
|
|
97
103
|
| `cookieless` | `false` | web | No cookies; ids in sessionStorage. |
|
|
98
|
-
| `block` | `
|
|
104
|
+
| `block` | `true` | web | Run the firewall (Monitor/Block set in dashboard). `false` skips it. |
|
|
99
105
|
| `flushIntervalMs` / `maxQueueSize` | `5000` / `10` | web, RN | Batch flush tuning. |
|
|
100
106
|
| `storage` | in-memory | RN | Async store (pass AsyncStorage). |
|
|
101
|
-
| `onBlock` / `blockStatus` / `blockedPage` |
|
|
107
|
+
| `onBlock` / `blockStatus` / `blockedPage` | from dashboard / `403` / none | adapters | Block handling. Default: redirect to the dashboard's Blocked page URL, else 403. |
|
|
102
108
|
|
|
103
109
|
Full reference: the [Server SDK page](https://noirtrack.com/docs/server-sdk) in the NoirTrack docs.
|
|
104
110
|
|
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/http.d.ts
CHANGED
|
@@ -6,5 +6,7 @@
|
|
|
6
6
|
export declare function postJson<T>(url: string, body: unknown, headers: Record<string, string>, timeoutMs: number): Promise<T | null>;
|
|
7
7
|
/** POST JSON and return whether the server accepted it (2xx). */
|
|
8
8
|
export declare function postOk(url: string, body: unknown, headers: Record<string, string>, timeoutMs: number): Promise<boolean>;
|
|
9
|
+
/** GET JSON and return the parsed body, or null on non-2xx / timeout / error. */
|
|
10
|
+
export declare function getJson<T>(url: string, headers: Record<string, string>, timeoutMs: number): Promise<T | null>;
|
|
9
11
|
/** Bearer header for the secret-key endpoints. */
|
|
10
12
|
export declare function bearer(secretKey: string): Record<string, string>;
|
package/dist/core/http.js
CHANGED
|
@@ -37,6 +37,21 @@ async function rawPost(url, body, headers, timeoutMs) {
|
|
|
37
37
|
clearTimeout(timer);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
/** GET JSON and return the parsed body, or null on non-2xx / timeout / error. */
|
|
41
|
+
export async function getJson(url, headers, timeoutMs) {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(url, { headers, signal: controller.signal });
|
|
46
|
+
return res.ok ? (await res.json()) : null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
40
55
|
/** Bearer header for the secret-key endpoints. */
|
|
41
56
|
export function bearer(secretKey) {
|
|
42
57
|
return { Authorization: `Bearer ${secretKey}` };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IndexNow key file. Search engines confirm a site sent them URLs by fetching https://{host}/{key}.txt, so the
|
|
3
|
+
* server guards answer that one path with the site's key from NoirTrack (Settings → Indexing). Nothing is served while
|
|
4
|
+
* Indexing is off, and any other .txt file passes through untouched.
|
|
5
|
+
*/
|
|
6
|
+
const KEY_FILE = /^\/([a-f0-9]{32})\.txt$/;
|
|
7
|
+
/** The body for a key-file request, or null when the path is not this site's key file. */
|
|
8
|
+
export async function indexNowKeyFile(pathname, lookup) {
|
|
9
|
+
const match = KEY_FILE.exec(pathname);
|
|
10
|
+
if (!match)
|
|
11
|
+
return null;
|
|
12
|
+
const key = await lookup();
|
|
13
|
+
return key !== null && key === match[1] ? key : null;
|
|
14
|
+
}
|
package/dist/core/ingest.d.ts
CHANGED
package/dist/core/secret.d.ts
CHANGED
|
@@ -26,5 +26,7 @@ export interface SecretApi {
|
|
|
26
26
|
visitorId: string;
|
|
27
27
|
}): Promise<boolean>;
|
|
28
28
|
shield(input: ShieldInput): Promise<ShieldResult>;
|
|
29
|
+
/** The site's IndexNow key, or null while Indexing is off. Cached for an hour. */
|
|
30
|
+
indexNowKey(): Promise<string | null>;
|
|
29
31
|
}
|
|
30
32
|
export declare function createSecretApi(config: SecretConfig): SecretApi;
|
package/dist/core/secret.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The secret-key API: trusted server-to-server methods. Authenticated with the secret key
|
|
3
3
|
* (Bearer). Every method fails open. Used by the server client and the firewall adapters.
|
|
4
4
|
*/
|
|
5
|
-
import { bearer, postJson, postOk } from './http.js';
|
|
5
|
+
import { bearer, getJson, postJson, postOk } from './http.js';
|
|
6
6
|
export function createSecretApi(config) {
|
|
7
7
|
const { secretKey, endpoint } = config;
|
|
8
8
|
const timeoutMs = config.timeoutMs;
|
|
@@ -60,5 +60,16 @@ export function createSecretApi(config) {
|
|
|
60
60
|
return { ok: true, reason: null }; // fail open
|
|
61
61
|
return { ok: data.ok !== false, reason: data.reason ?? null };
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
// Only asked for when a search engine fetches /{key}.txt, and the key almost never changes, so one answer an hour is
|
|
64
|
+
// plenty. A failed lookup is cached for a minute, so an outage can't turn every key-file request into a network call.
|
|
65
|
+
let keyCache = null;
|
|
66
|
+
async function indexNowKey() {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (keyCache && keyCache.expires > now)
|
|
69
|
+
return keyCache.key;
|
|
70
|
+
const data = await getJson(`${endpoint}/api/v1/indexnow-key`, head, Math.max(timeoutMs, 1500));
|
|
71
|
+
keyCache = { key: data?.key ?? null, expires: now + (data ? 3_600_000 : 60_000) };
|
|
72
|
+
return keyCache.key;
|
|
73
|
+
}
|
|
74
|
+
return { decide, goal, payment, identify, shield, indexNowKey };
|
|
64
75
|
}
|
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,19 +1,25 @@
|
|
|
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
|
+
import { indexNowKeyFile } from './core/indexnow.js';
|
|
3
4
|
import { resolveGuard } from './index.js';
|
|
4
5
|
/**
|
|
5
|
-
* import { createClient } from '@noirtrack/sdk';
|
|
6
6
|
* import { guard } from '@noirtrack/sdk/express';
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* app.use(guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' }));
|
|
8
|
+
* app.use(guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! }));
|
|
10
9
|
*
|
|
11
|
-
*
|
|
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' })`.
|
|
12
12
|
* Fails open: a NoirTrack error/timeout calls next() and never breaks your app.
|
|
13
13
|
*/
|
|
14
14
|
export function guard(target, blockOptions) {
|
|
15
15
|
const { client, block } = resolveGuard(target, blockOptions);
|
|
16
16
|
return async function (request, response, next) {
|
|
17
|
+
// With Indexing on, search engines read the IndexNow key here (docs: Indexing).
|
|
18
|
+
const keyFile = await indexNowKeyFile(request.path, () => client.indexNowKey());
|
|
19
|
+
if (keyFile) {
|
|
20
|
+
response.type('text/plain').send(keyFile);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
17
23
|
const ip = clientIp((name) => {
|
|
18
24
|
const value = request.headers[name];
|
|
19
25
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -38,7 +44,7 @@ export function guard(target, blockOptions) {
|
|
|
38
44
|
return;
|
|
39
45
|
}
|
|
40
46
|
const page = verdict.blocked_page ?? block.blockedPage;
|
|
41
|
-
if ((block.onBlock
|
|
47
|
+
if (blockMode(block.onBlock, page) === 'redirect' && page) {
|
|
42
48
|
// Don't redirect the blocked page to itself — that loops forever.
|
|
43
49
|
if (isBlockedPagePath(path, page)) {
|
|
44
50
|
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. With Indexing on, `/{key}.txt` answers with your IndexNow key.
|
|
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,26 @@
|
|
|
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
|
+
import { indexNowKeyFile } from './core/indexnow.js';
|
|
4
5
|
import { resolveGuard } from './index.js';
|
|
6
|
+
// A Worker in front of a site runs for every file a page loads. Checking those would record each image and stylesheet as
|
|
7
|
+
// 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
|
|
8
|
+
const STATIC_FILE = /\.(?:css|js|mjs|map|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|mp4|webm|mp3)$/i;
|
|
5
9
|
/**
|
|
6
10
|
* Returns a guard that, given a standard Request, resolves to a blocking Response, or null when
|
|
7
11
|
* the request should continue.
|
|
8
12
|
*
|
|
9
|
-
* import { createClient } from '@noirtrack/sdk';
|
|
10
13
|
* import { createGuard } from '@noirtrack/sdk/fetch';
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* export default {
|
|
16
|
+
* async fetch(request, env) {
|
|
17
|
+
* const guard = createGuard({ secretKey: env.NOIRTRACK_SECRET_KEY });
|
|
18
|
+
* return (await guard(request)) ?? fetch(request);
|
|
19
|
+
* },
|
|
20
|
+
* };
|
|
21
|
+
*
|
|
22
|
+
* Monitor or Block, and where a blocked visitor goes, come from Firewall settings. Images, styles, scripts, fonts
|
|
23
|
+
* and media pass straight through without a check. With Indexing on, `/{key}.txt` answers with your IndexNow key.
|
|
15
24
|
*
|
|
16
25
|
* Fails open: a NoirTrack error/timeout returns null (let the request through).
|
|
17
26
|
*/
|
|
@@ -19,6 +28,11 @@ export function createGuard(target, blockOptions) {
|
|
|
19
28
|
const { client, block } = resolveGuard(target, blockOptions);
|
|
20
29
|
return async function guard(request) {
|
|
21
30
|
const url = new URL(request.url);
|
|
31
|
+
const keyFile = await indexNowKeyFile(url.pathname, () => client.indexNowKey());
|
|
32
|
+
if (keyFile)
|
|
33
|
+
return new Response(keyFile, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
|
|
34
|
+
if (STATIC_FILE.test(url.pathname))
|
|
35
|
+
return null;
|
|
22
36
|
const ip = clientIp((name) => request.headers.get(name));
|
|
23
37
|
const ua = request.headers.get('user-agent') ?? '';
|
|
24
38
|
const path = url.pathname;
|
|
@@ -37,7 +51,7 @@ export function createGuard(target, blockOptions) {
|
|
|
37
51
|
if (verdict?.action !== 'block')
|
|
38
52
|
return null;
|
|
39
53
|
const page = verdict.blocked_page ?? block.blockedPage;
|
|
40
|
-
if ((block.onBlock
|
|
54
|
+
if (blockMode(block.onBlock, page) === 'redirect' && page) {
|
|
41
55
|
// Already on the blocked page → let it through, don't loop.
|
|
42
56
|
if (isBlockedPagePath(path, page))
|
|
43
57
|
return null;
|
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface ServerClient {
|
|
|
40
40
|
goal(name: string, options?: Omit<GoalInput, 'name'>): Promise<boolean>;
|
|
41
41
|
/** Check a form submission for spam and bots. */
|
|
42
42
|
shield(input: ShieldInput): Promise<ShieldResult>;
|
|
43
|
+
/** The site's IndexNow key, or null while Indexing is off. The guards use it to serve `/{key}.txt`. */
|
|
44
|
+
indexNowKey(): Promise<string | null>;
|
|
43
45
|
}
|
|
44
46
|
export declare function createClient(options: ServerClientOptions): ServerClient;
|
|
45
47
|
/** What a firewall adapter accepts: an existing client, or options to build one. */
|
package/dist/index.js
CHANGED
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,23 +1,27 @@
|
|
|
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
|
+
import { indexNowKeyFile } from './core/indexnow.js';
|
|
5
6
|
import { resolveGuard } from './index.js';
|
|
6
7
|
/**
|
|
7
8
|
* // middleware.ts
|
|
8
|
-
* import { createClient } from '@noirtrack/sdk';
|
|
9
9
|
* import { guard } from '@noirtrack/sdk/next';
|
|
10
10
|
*
|
|
11
|
-
* const
|
|
12
|
-
* export const middleware = guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' });
|
|
11
|
+
* export const middleware = guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! });
|
|
13
12
|
* export const config = { matcher: ['/((?!_next/|favicon.ico).*)'] };
|
|
14
13
|
*
|
|
15
|
-
*
|
|
14
|
+
* Monitor or Block, and where a blocked visitor goes, come from Firewall settings. You can also pass a client:
|
|
15
|
+
* `guard(noir, { onBlock: 'rewrite' })`.
|
|
16
16
|
* Fails open: a NoirTrack error/timeout never blocks or breaks your site.
|
|
17
17
|
*/
|
|
18
18
|
export function guard(target, blockOptions) {
|
|
19
19
|
const { client, block } = resolveGuard(target, blockOptions);
|
|
20
20
|
return async function middleware(request) {
|
|
21
|
+
// With Indexing on, search engines read the IndexNow key here (docs: Indexing).
|
|
22
|
+
const keyFile = await indexNowKeyFile(request.nextUrl.pathname, () => client.indexNowKey());
|
|
23
|
+
if (keyFile)
|
|
24
|
+
return new NextResponse(keyFile, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
|
|
21
25
|
const ip = clientIp((name) => request.headers.get(name));
|
|
22
26
|
const ua = request.headers.get('user-agent') ?? '';
|
|
23
27
|
const path = request.nextUrl.pathname;
|
|
@@ -35,8 +39,8 @@ export function guard(target, blockOptions) {
|
|
|
35
39
|
});
|
|
36
40
|
if (verdict?.action !== 'block')
|
|
37
41
|
return NextResponse.next();
|
|
38
|
-
const mode = block.onBlock ?? 'block';
|
|
39
42
|
const page = verdict.blocked_page ?? block.blockedPage;
|
|
43
|
+
const mode = blockMode(block.onBlock, page);
|
|
40
44
|
// The blocked page must stay reachable, or a still-blocked visitor loops forever
|
|
41
45
|
// when that page also runs this middleware.
|
|
42
46
|
if ((mode === 'redirect' || mode === 'rewrite') && page) {
|
package/dist/react-native.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function createClient(options) {
|
|
|
42
42
|
path: pathOverride ?? '/',
|
|
43
43
|
referrer: null,
|
|
44
44
|
screen: null,
|
|
45
|
-
utm: { source: null, medium: null, campaign: null },
|
|
45
|
+
utm: { source: null, medium: null, campaign: null, content: null, term: null },
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
const platform = {
|
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
|
|
@@ -193,6 +193,8 @@ export function createClient(options) {
|
|
|
193
193
|
source: params.get('utm_source'),
|
|
194
194
|
medium: params.get('utm_medium'),
|
|
195
195
|
campaign: params.get('utm_campaign'),
|
|
196
|
+
content: params.get('utm_content'),
|
|
197
|
+
term: params.get('utm_term'),
|
|
196
198
|
},
|
|
197
199
|
};
|
|
198
200
|
}
|
|
@@ -250,7 +252,7 @@ export function createClient(options) {
|
|
|
250
252
|
// Soft block on the first view: ask the server, then enforce. /api/v1/check already RECORDS
|
|
251
253
|
// this pageview (stamped with the verdict), so when blocking is on we must NOT also call
|
|
252
254
|
// ingest.view() for the first view, or it would be counted twice.
|
|
253
|
-
const wantsBlock =
|
|
255
|
+
const wantsBlock = options.block !== false;
|
|
254
256
|
if (wantsBlock) {
|
|
255
257
|
void ingest.check().then((verdict) => {
|
|
256
258
|
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.6.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": {
|