@noirtrack/sdk 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
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
+
5
+ ## 0.2.0
6
+
7
+ ### Changed
8
+
9
+ - **Breaking (web client):** `block` is now a boolean. Turn on the Traffic Filter with `createClient({ block: true })` instead of `block: 'redirect' | 'overlay'`. Whether it monitors or blocks, and how a block looks (redirect or overlay), are now set per site in Firewall settings, with no code change. If you passed `block: 'redirect'` or `block: 'overlay'`, change it to `block: true`.
10
+
11
+ ### Added
12
+
13
+ - `Verdict.style` (`'redirect' | 'overlay' | null`): the block style returned by the server. The web client applies it automatically, so the snippet and SDK no longer hardcode the style.
14
+
15
+ ## 0.1.0
16
+
17
+ - Initial release. Framework-agnostic core with browser, React Native, Next.js, Express, and fetch/edge adapters. Analytics, firewall decisions, goals, revenue, identify, and Form Shield, all from one client.
package/README.md CHANGED
@@ -4,7 +4,7 @@ One NoirTrack SDK for the browser, React Native, and your server. You set your k
4
4
 
5
5
  Works in any runtime with a global `fetch`: Node 18+, Next.js, Cloudflare Workers, Deno, Bun, the browser, and React Native. ESM only.
6
6
 
7
- **Docs:** [noirtrack.com/docs](https://noirtrack.com/docs) [NPM SDK guide](https://noirtrack.com/docs/npm-sdk) · [Server SDK reference](https://noirtrack.com/docs/server-sdk)
7
+ **Docs:** [noirtrack.com/docs](https://noirtrack.com/docs) · [NPM SDK guide](https://noirtrack.com/docs/npm-sdk) · [Server SDK reference](https://noirtrack.com/docs/server-sdk)
8
8
 
9
9
  ## Install
10
10
 
@@ -26,7 +26,7 @@ noir.identify('user_123', { email });
26
26
  noir.revenue({ checkoutId: 'cs_test_123' });
27
27
  ```
28
28
 
29
- Pageviews and SPA route changes are automatic. Events are batched and sent with `sendBeacon`. Set `cookieless: true` for a no-cookie mode. Safe to import in SSR (no-op without a window). Also: `view(path?)`, `reset()`, `flush()`, `links.decorate(url)`.
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 Traffic Filter. 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)`.
30
30
 
31
31
  ## React Native
32
32
 
@@ -81,7 +81,9 @@ export const config = { matcher: ['/((?!_next/|favicon.ico).*)'] };
81
81
 
82
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.
83
83
 
84
- 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.
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.
85
+
86
+ 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.
85
87
 
86
88
  ## Options
87
89
 
@@ -93,6 +95,7 @@ The guards resolve the visitor IP from `CF-Connecting-IP`, `True-Client-IP`, the
93
95
  | `timeoutMs` | `800` | all | Abort a call after this, then fail open. |
94
96
  | `autoPageviews` | `true` | web | Capture initial + SPA pageviews. |
95
97
  | `cookieless` | `false` | web | No cookies; ids in sessionStorage. |
98
+ | `block` | `false` | web | Turn on the Traffic Filter (Monitor/Block set in dashboard). |
96
99
  | `flushIntervalMs` / `maxQueueSize` | `5000` / `10` | web, RN | Batch flush tuning. |
97
100
  | `storage` | in-memory | RN | Async store (pass AsyncStorage). |
98
101
  | `onBlock` / `blockStatus` / `blockedPage` | `block` / `403` / none | adapters | Block handling. |
@@ -111,7 +111,15 @@ export function createIngest(platform) {
111
111
  function check(path) {
112
112
  // /api/v1/check responds with { block, reason, blocked_page } — not the { action } shape that
113
113
  // /decide uses — so normalise it into a Verdict before returning.
114
- return postJson(`${endpoint}/api/v1/check`, payload('pageview', path), {}, timeoutMs).then((r) => r ? { action: r.block ? 'block' : 'allow', reason: r.reason ?? null, blocked_page: r.blocked_page ?? null, ttl: 0 } : null);
114
+ return postJson(`${endpoint}/api/v1/check`, payload('pageview', path), {}, timeoutMs).then((r) => r
115
+ ? {
116
+ action: r.block ? 'block' : 'allow',
117
+ reason: r.reason ?? null,
118
+ blocked_page: r.blocked_page ?? null,
119
+ style: r.style ?? null,
120
+ ttl: 0,
121
+ }
122
+ : null);
115
123
  }
116
124
  return { view, event, identify, revenue, reset, flush, ping, links, shield, check };
117
125
  }
@@ -9,6 +9,8 @@ export interface Verdict {
9
9
  action: 'allow' | 'block' | 'challenge';
10
10
  reason: string | null;
11
11
  blocked_page: string | null;
12
+ /** How to present a block (set in Firewall settings): redirect to a page or show an overlay. */
13
+ style?: 'redirect' | 'overlay' | null;
12
14
  ttl: number;
13
15
  }
14
16
  /** Input to a hard firewall decision (server, secret key). */
@@ -52,7 +54,11 @@ export type RevenueInput = {
52
54
  provider?: 'stripe' | 'polar' | 'lemonsqueezy';
53
55
  visitorId?: string;
54
56
  } | {
55
- /** Required for `paid`/`refunded`; omit for `cancelled`/`subscription_ended` (no money moves). */
57
+ /**
58
+ * Required for `paid`/`refunded`; omit for `cancelled`/`subscription_ended` (no money moves).
59
+ * The server rejects a `paid`/`refunded` call with no amount (422) and the SDK fails open
60
+ * silently — the revenue is dropped, not thrown — so always pass it for those two statuses.
61
+ */
56
62
  amount?: number;
57
63
  currency?: string;
58
64
  visitorId?: string;
package/dist/index.js CHANGED
@@ -31,6 +31,9 @@ export function createClient(options) {
31
31
  // cancellation — goes through the secret-key payment endpoint.
32
32
  if (!('checkoutId' in input))
33
33
  return secret.payment(input);
34
+ // Capture is authenticated by the PUBLIC key (`site.key` middleware). A guard-only client built
35
+ // without `publicKey` sends `site_key: undefined`, so the server rejects it and this fails open
36
+ // silently — set `publicKey` on the client if you capture revenue by checkout id.
34
37
  const provider = input.provider ?? 'stripe';
35
38
  return postOk(`${endpoint}/api/v1/payment/capture`, { site_key: options.publicKey, provider, external_id: input.checkoutId, visitor_id: input.visitorId }, {}, timeoutMs);
36
39
  }
package/dist/web.d.ts CHANGED
@@ -11,9 +11,10 @@ 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
- /** Soft, client-side firewall on the first view. Off by default. */
15
- block?: 'redirect' | 'overlay';
16
- /** Where to send blocked visitors when `block` is `redirect`. */
14
+ /** Turn on the Traffic Filter on the first view. Whether it monitors or blocks, and how a block
15
+ * looks (redirect or overlay), are set per site in Firewall settings. Off by default. */
16
+ block?: boolean;
17
+ /** Optional redirect target if none is set in Firewall settings. */
17
18
  blockedPage?: string;
18
19
  /** Self-hosted base URL. Defaults to the hosted service. */
19
20
  endpoint?: string;
package/dist/web.js CHANGED
@@ -230,7 +230,10 @@ export function createClient(options) {
230
230
  if (verdict?.action !== 'block')
231
231
  return;
232
232
  const page = verdict.blocked_page ?? options.blockedPage;
233
- if (options.block === 'redirect' && page)
233
+ // Style comes from Firewall settings (redirect or overlay); fall back to redirect when
234
+ // a page exists, else overlay.
235
+ const style = verdict.style ?? (page ? 'redirect' : 'overlay');
236
+ if (style === 'redirect' && page)
234
237
  location.replace(page);
235
238
  else
236
239
  overlay();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noirtrack/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": {
@@ -41,9 +41,10 @@
41
41
  "import": "./dist/form-shield.js"
42
42
  }
43
43
  },
44
- "files": ["dist"],
44
+ "files": ["dist", "README.md", "CHANGELOG.md"],
45
45
  "scripts": {
46
46
  "build": "tsc",
47
+ "prepublishOnly": "tsc",
47
48
  "test:live": "node ../docs/live-test.mjs"
48
49
  },
49
50
  "keywords": ["noirtrack", "firewall", "bot-detection", "waf", "revenue", "goals", "middleware", "nextjs", "express", "edge"],