@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 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!, 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,21 +70,27 @@ 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
 
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` | `false` | web | Turn on the firewall (Monitor/Block set in dashboard). |
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` | `block` / `403` / none | adapters | Block handling. |
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
 
@@ -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;
@@ -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,2 @@
1
+ /** The body for a key-file request, or null when the path is not this site's key file. */
2
+ export declare function indexNowKeyFile(pathname: string, lookup: () => Promise<string | null>): Promise<string | null>;
@@ -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
+ }
@@ -12,6 +12,8 @@ export interface IngestContext {
12
12
  source: string | null;
13
13
  medium: string | null;
14
14
  campaign: string | null;
15
+ content: string | null;
16
+ term: string | null;
15
17
  };
16
18
  }
17
19
  /** What each platform plugs into the shared ingest core. */
@@ -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;
@@ -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
- return { decide, goal, payment, identify, shield };
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
  }
@@ -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,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
- * const noir = createClient({ publicKey: '...', secretKey: process.env.NOIRTRACK_SECRET! });
9
- * app.use(guard(noir, { onBlock: 'redirect', blockedPage: '/blocked' }));
8
+ * app.use(guard({ secretKey: process.env.NOIRTRACK_SECRET_KEY! }));
10
9
  *
11
- * 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' })`.
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 ?? 'block') === 'redirect' && page) {
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
- * 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. 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
- * 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); } };
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 ?? 'block') === 'redirect' && page) {
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
@@ -45,6 +45,7 @@ export function createClient(options) {
45
45
  decide: secret.decide,
46
46
  goal: secret.goal,
47
47
  shield: secret.shield,
48
+ indexNowKey: secret.indexNowKey,
48
49
  };
49
50
  }
50
51
  /**
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,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 noir = createClient({ publicKey: '...', secretKey: process.env.NOIRTRACK_SECRET! });
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
- * You can also pass options directly: `guard({ publicKey, secretKey, onBlock: 'redirect' })`.
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) {
@@ -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
- /** 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
@@ -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 = !!options.block;
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.4.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": {