From 0d8481ff6d2eb9a054dd74aa2c2bbc1ff5410f27 Mon Sep 17 00:00:00 2001 From: Konstantin Serebryakov <97249355+KonstantinSerebryakov@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:03:04 +0300 Subject: [PATCH 1/2] Rename the public identification key to identification_id. History search and webhook parsers still accept request_id. Database columns stay request_id. The browser snippet callback remains requestID. --- CHANGELOG.md | 2 +- README.md | 48 ++--- examples/node-http/README.md | 4 +- examples/node-http/server.mjs | 30 ++-- resources/shieldlabs-api.yaml | 80 ++++----- scripts/smoke-example.mjs | 20 +-- src/constants.ts | 1 + src/generated/api.ts | 34 ++-- src/history.ts | 10 +- src/normalize.ts | 13 +- src/risk.ts | 4 +- src/types.ts | 4 +- src/validation.ts | 8 +- test/data/history-page.json | 10 +- test/data/normalization-cases.json | 32 ++-- test/data/normalizer-edge-cases.json | 168 +++++++++--------- test/data/webhook-identification-scored.json | 2 +- .../webhook-identification-scored.raw.txt | 2 +- test/data/webhook-rate-limited.json | 2 +- test/data/webhook-signature-vectors.json | 90 +++++----- test/data/webhook-test-delivery.json | 2 +- test/entries.test.ts | 2 +- test/errors.test.ts | 50 +++--- test/helpers.ts | 4 +- test/history.test.ts | 24 +-- test/identifications.test.ts | 34 ++-- test/normalize.test.ts | 6 +- test/runtime/smoke.mjs | 6 +- test/types.test.ts | 2 +- test/validation.test.ts | 18 +- test/webhooks.test.ts | 6 +- 31 files changed, 362 insertions(+), 356 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c7f9e..7c30651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ All notable changes to this package are documented in this file. The format foll ### Added - `ShieldLabs` History API client: `identifications.get` with wait-for-verdict polling, - `history.search`, and `history.iterate` with deduplication on `request_id`. + `history.search`, and `history.iterate` with deduplication on `identification_id`. - `identifications.get` waits within one total `timeout` budget (default 10 s): the first poll runs at once, then after 250 ms, 500 ms, 1 s, 1.5 s and every 2 s, and the last poll runs at the deadline. A custom `pollInterval` p gives waits of p, 2p, 4p, 6p and then 8p, each capped diff --git a/README.md b/README.md index f99bcd7..3255031 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ Read ShieldLabs identifications, verify signed webhooks and apply risk checks fr ## How it fits -1. **Browser.** The ShieldLabs agent runs an identification and hands your page a `requestId`. +1. **Browser.** The ShieldLabs agent runs an identification and hands your page a `identificationId`. The browser never sees a Risk Score, a visitor ID or a device ID. -2. **Your backend.** It receives the `requestId` together with the protected action (signup, +2. **Your backend.** It receives the `identificationId` together with the protected action (signup, login, checkout) and reads the verdict with this SDK, or receives it in a signed `identification.scored` webhook. 3. **Decision.** Your backend acts on `risk_score`, the three risk bands, `detection_flags` and @@ -38,21 +38,21 @@ const shieldlabs = new ShieldLabs({ apiKey: process.env.SHIELDLABS_API_KEY! }); // One identification authorizes one action. This in-memory set keeps the example short; in // production, claim request IDs atomically in Redis or your database (see "Apply a policy"). -const usedRequestIds = new Set(); +const usedIdentificationIds = new Set(); -function claimRequestId(requestId: string): boolean { - if (usedRequestIds.has(requestId)) return false; - usedRequestIds.add(requestId); +function claimIdentificationId(identificationId: string): boolean { + if (usedIdentificationIds.has(identificationId)) return false; + usedIdentificationIds.add(identificationId); return true; } -// 1. Call this with the requestId the browser sent together with the signup form. -export async function allowSignup(requestId: string): Promise { +// 1. Call this with the identificationId the browser sent together with the signup form. +export async function allowSignup(identificationId: string): Promise { // Scoring is asynchronous: this waits (up to 10 s by default) until the verdict is stored. - const identification = await shieldlabs.identifications.get(requestId); + const identification = await shieldlabs.identifications.get(identificationId); // 2. Missing, reused, stale, rate-limited, automated or dangerous: refuse. - const firstUse = identification !== null && claimRequestId(identification.request_id); + const firstUse = identification !== null && claimIdentificationId(identification.identification_id); const verdict = evaluateIdentification(identification, { isReplay: () => !firstUse }); return verdict.ok; } @@ -65,7 +65,7 @@ export function handleWebhook(rawBody: string | Uint8Array, signatureHeader: str process.env.SHIELDLABS_WEBHOOK_SECRET!, ); if (event.event_type === 'identification.scored') { - console.log(event.data.request_id, event.data.risk_score, event.data.detection_flags.vpn); + console.log(event.data.identification_id, event.data.risk_score, event.data.detection_flags.vpn); } } ``` @@ -88,7 +88,7 @@ The History row of an identification appears about 1 to 3 seconds after the brow be refined for up to about 10 seconds while follow-up checks finish (network checks such as the local IP arrive in later versions of the row). Start the identification when the user begins the action, for example when the signup form gets focus, so the verdict is usually stored by the time -the form is submitted. `identifications.get(requestId)` polls the History API until the row +the form is submitted. `identifications.get(identificationId)` polls the History API until the row appears and returns the first version it sees. How it waits: - **Total budget.** `timeout` (default 10 000 ms) is the time budget of the whole call, counted @@ -114,7 +114,7 @@ appears and returns the first version it sees. How it waits: stays wrong. ```ts -const identification = await shieldlabs.identifications.get(requestId, { +const identification = await shieldlabs.identifications.get(identificationId, { timeout: 5_000, // total budget: the last poll runs 5 s after the call pollInterval: 250, // waits of p, 2p, 4p, 6p, then 8p, each at most max(2 s, p) signal: AbortSignal.timeout(6_000), // hard limit, including the last request @@ -125,14 +125,14 @@ if (identification === null) { } // Read once, without waiting: -const latest = await shieldlabs.identifications.get(requestId, { wait: false }); +const latest = await shieldlabs.identifications.get(identificationId, { wait: false }); ``` `null` means no row existed for the request ID when the last poll ran. It also covers an identification that was never stored, for example when the visitor's IP went over the per-IP rate limit of identifications: the browser still gets a request ID, but no row is written for it. With `wait: false`, `identifications.get` reads once and uses the client's normal retries. To read the -refined row later, call `identifications.get(requestId, { wait: false })` again after about 10 +refined row later, call `identifications.get(identificationId, { wait: false })` again after about 10 seconds. ### Apply a policy @@ -143,7 +143,7 @@ in this order and the first failure wins: | Order | Check | `reason` | |---|---|---| | 1 | No identification (`null`) | `missing` | -| 2 | `isReplay(requestId)` returned true | `replayed` | +| 2 | `isReplay(identificationId)` returned true | `replayed` | | 3 | Older than `maxAge` (default 5 minutes, by `observed_at`; `Infinity` skips this check) | `stale` | | 4 | Risk Score above 100 (the 999 rate-limit marker) | `rate_limited` | | 5 | All-zero device ID (no usable device signals) | `no_device_signals` | @@ -155,7 +155,7 @@ in this order and the first failure wins: // requests with the same ID cannot both pass. With node-redis, SET NX answers 'OK' only once. const claimed = identification !== null && - (await redis.set(`shieldlabs:rid:${identification.request_id}`, '1', { NX: true, EX: 600 })) === + (await redis.set(`shieldlabs:rid:${identification.identification_id}`, '1', { NX: true, EX: 600 })) === 'OK'; const verdict = evaluateIdentification(identification, { @@ -187,7 +187,7 @@ A few rules that keep decisions sound: ### Search history for account-abuse checks `history.search(type, value)` reads identifications by one identifier, newest first. Lookup types -are `user_hid`, `device_id`, `visitor_id`, `ip` (IPv4), `request_id`, `session_id` and +are `user_hid`, `device_id`, `visitor_id`, `ip` (IPv4), `identification_id`, `session_id` and `cookie_id`. Arguments are validated before anything is sent, because the server does not reject an unknown type or a malformed UUID or IP. @@ -215,7 +215,7 @@ console.log(page.total, page.data.map((item) => item.public_ip.country)); // "Ge ``` `history.iterate` pages with `offset` (100 rows per request by default), skips rows repeated -across pages by `request_id` (new rows can shift offsets) and stops at `total`, at an empty page +across pages by `identification_id` (new rows can shift offsets) and stops at `total`, at an empty page or after `maxItems`. Each request counts toward the History API rate limit. A User HID is sent as one URL path segment in canonical form, so values with `@`, `+`, `=` or @@ -245,7 +245,7 @@ app.post('/webhooks/shieldlabs', express.raw({ type: 'application/json' }), (req res.sendStatus(200); // answer within 1 second, then process if (event.event_type === 'identification.scored') { - jobs.enqueue(event.data); // make the job idempotent on event.data.request_id + jobs.enqueue(event.data); // make the job idempotent on event.data.identification_id } }); ``` @@ -254,10 +254,10 @@ What to know about deliveries: - The header is `X-Shield-Signature: sha256=`, keyed with the full signing secret including its `whsec_` prefix. It is the only ShieldLabs header, so the - idempotency key comes from the body: `data.request_id`. + idempotency key comes from the body: `data.identification_id`. - Today each identification is delivered once per enabled endpoint: one attempt with a 1-second timeout and no retries. Future retries will resend identical bytes, so make handlers - idempotent on `data.request_id` now. + idempotent on `data.identification_id` now. - For guaranteed reads, use the History API: a missed delivery is not sent again, and a History row can be refined after its webhook was sent. - To rotate a secret without downtime, pass both secrets: `constructEvent(body, header, [newSecret, oldSecret])`. @@ -366,7 +366,7 @@ create one per process. | Method | Returns | Notes | |---|---|---| -| `identifications.get(requestId, { wait?, timeout?, pollInterval?, signal? })` | `Promise` | `wait` true, `timeout` 10 000 ms (total budget of the wait), `pollInterval` 250 ms by default. `pollInterval` p gives waits of p, 2p, 4p, 6p and then 8p, each at most max(2 s, p). See [Wait for the verdict](#wait-for-the-verdict) | +| `identifications.get(identificationId, { wait?, timeout?, pollInterval?, signal? })` | `Promise` | `wait` true, `timeout` 10 000 ms (total budget of the wait), `pollInterval` 250 ms by default. `pollInterval` p gives waits of p, 2p, 4p, 6p and then 8p, each at most max(2 s, p). See [Wait for the verdict](#wait-for-the-verdict) | | `history.search(type, value, { limit?, offset?, signal? })` | `Promise` | `limit` 1-100 (default 20), `offset` 0 or more (default 0). A `user_hid` value cannot contain `/` | | `history.iterate(type, value, { pageSize?, maxItems?, signal? })` | `AsyncGenerator` | `pageSize` 1-100 (default 100), `maxItems` 0 or more (default no limit; 0 yields nothing) | @@ -409,7 +409,7 @@ webhook JSON. | Property | Type | Notes | |---|---|---| -| `request_id`, `visitor_id`, `device_id`, `session_id`, `cookie_id` | `string` | UUIDs; the all-zero UUID is possible | +| `identification_id`, `visitor_id`, `device_id`, `session_id`, `cookie_id` | `string` | UUIDs; the all-zero UUID is possible | | `user_hid` | `string \| null` | `"anonymous"` for anonymous checks; `null` when it was empty | | `domain` | `string` | Registered domain of the site | | `public_ip`, `local_ip` | `{ ip, country }` | IPv4 or `""`; English country name (`"Germany"`) or `""` | diff --git a/examples/node-http/README.md b/examples/node-http/README.md index 84959ce..58538d6 100644 --- a/examples/node-http/README.md +++ b/examples/node-http/README.md @@ -3,7 +3,7 @@ A signup endpoint and a webhook receiver on plain `node:http`, with no framework and no other dependencies. -- `POST /signup` reads `requestId` from the JSON body, waits for the identification with +- `POST /signup` reads `identificationId` from the JSON body, waits for the identification with `identifications.get`, and refuses the signup when the identification is missing, reused, stale, rate limited, without device signals, automated or in the dangerous band. - `POST /webhooks/shieldlabs` verifies `X-Shield-Signature` on the raw body, answers 200 at once @@ -28,7 +28,7 @@ by a comma. ```bash curl -s -X POST localhost:3000/signup \ -H 'content-type: application/json' \ - -d '{"requestId":"7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b"}' + -d '{"identificationId":"7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b"}' ``` The request ID comes from `identify()` in the browser SDK. Each one authorizes a single signup: diff --git a/examples/node-http/server.mjs b/examples/node-http/server.mjs index c347522..0c86f9c 100644 --- a/examples/node-http/server.mjs +++ b/examples/node-http/server.mjs @@ -1,6 +1,6 @@ // A minimal ShieldLabs integration on plain node:http, with no framework and no dependencies. // -// POST /signup { "requestId": "" } +// POST /signup { "identificationId": "" } // POST /webhooks/shieldlabs signed identification.scored and webhook.ping deliveries // // Environment: @@ -38,19 +38,19 @@ const shieldlabs = new ShieldLabs({ apiKey, baseUrl: process.env.SHIELDLABS_API_ // Request IDs that already authorized a signup. With more than one process, claim them in a // shared store with an atomic insert-if-absent (Redis SET key 1 NX EX 600, or an insert into a // table with a unique key) and an expiry a little longer than the freshness window. -const usedRequestIds = new Set(); +const usedIdentificationIds = new Set(); // True for the first caller only. Nothing is awaited between the check and the add, so this is // atomic within one process. -function claimRequestId(requestId) { - if (usedRequestIds.has(requestId)) return false; - usedRequestIds.add(requestId); +function claimIdentificationId(identificationId) { + if (usedIdentificationIds.has(identificationId)) return false; + usedIdentificationIds.add(identificationId); return true; } // Deliveries already handled, by request ID. Today each identification is delivered once (one // attempt, 1-second timeout, no retries); future retries resend identical bytes, so handlers -// must be idempotent on data.request_id. +// must be idempotent on data.identification_id. const handledDeliveries = new Set(); const MAX_BODY_BYTES = 1_000_000; @@ -83,18 +83,18 @@ async function handleSignup(req, res) { try { payload = JSON.parse((await readBody(req)).toString('utf8')); } catch { - return sendJson(res, 400, { error: 'Send a JSON body with requestId.' }); + return sendJson(res, 400, { error: 'Send a JSON body with identificationId.' }); } - const requestId = typeof payload?.requestId === 'string' ? payload.requestId : ''; - if (!requestId) return sendJson(res, 400, { error: 'requestId is required.' }); + const identificationId = typeof payload?.identificationId === 'string' ? payload.identificationId : ''; + if (!identificationId) return sendJson(res, 400, { error: 'identificationId is required.' }); let identification; try { // Scoring is asynchronous: this waits (with backoff) until the verdict is stored. - identification = await shieldlabs.identifications.get(requestId); + identification = await shieldlabs.identifications.get(identificationId); } catch (error) { if (error instanceof ValidationError) - return sendJson(res, 400, { error: 'requestId must be a UUID.' }); + return sendJson(res, 400, { error: 'identificationId must be a UUID.' }); throw error; } @@ -102,7 +102,7 @@ async function handleSignup(req, res) { // never clean), a reused request ID, an identification older than 5 minutes, the rate-limit // marker, missing device signals, browser automation or disabled JavaScript, and the // dangerous band. Tune the defaults. - const firstUse = identification !== null && claimRequestId(identification.request_id); + const firstUse = identification !== null && claimIdentificationId(identification.identification_id); const verdict = evaluateIdentification(identification, { isReplay: () => !firstUse }); if (!verdict.ok) { return sendJson(res, 403, { ok: false, reason: verdict.reason }); @@ -132,10 +132,10 @@ async function handleWebhook(req, res) { switch (event.event_type) { case 'identification.scored': { const { data } = event; - if (handledDeliveries.has(data.request_id)) return; - handledDeliveries.add(data.request_id); + if (handledDeliveries.has(data.identification_id)) return; + handledDeliveries.add(data.identification_id); console.log( - `identification.scored ${data.request_id} risk_score=${data.risk_score} band=${riskBand(data.risk_score)}`, + `identification.scored ${data.identification_id} risk_score=${data.risk_score} band=${riskBand(data.risk_score)}`, ); break; } diff --git a/resources/shieldlabs-api.yaml b/resources/shieldlabs-api.yaml index 2838959..a5c763e 100644 --- a/resources/shieldlabs-api.yaml +++ b/resources/shieldlabs-api.yaml @@ -25,7 +25,7 @@ info: History row appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds as follow-up checks finish. Start the identification when the user begins the protected action (for example when the signup form opens), then either poll the History API by - `request_id` with a short backoff or wait for the webhook. Let one identification authorize one + `identification_id` with a short backoff or wait for the webhook. Let one identification authorize one protected action: reject request IDs you have already used and identifications older than your freshness window. @@ -79,7 +79,7 @@ info: ## Identifiers - - `request_id`: one identification, created in the browser (UUID v4). It joins the browser + - `identification_id`: one identification, created in the browser (UUID v4). It joins the browser call, the webhook and the History row. - `session_id`: one visit on one origin (UUID v4). - `cookie_id`: first-party browser identifier kept by the agent (UUID v4). @@ -134,7 +134,7 @@ info: ShieldLabs sends one signed `POST` for each identification to every enabled endpoint of the domain. A delivery has a 1-second timeout and is not retried today; a later release adds retries that resend identical bytes. Answer 2xx within a second, process the event - asynchronously, make the handler idempotent on `data.request_id`, and use the History API for + asynchronously, make the handler idempotent on `data.identification_id`, and use the History API for guaranteed reads. A History row can be refined after its webhook was sent (its `ver` increases); the webhook is not sent again. See the `identification.scored` and `webhook.ping` entries for the signature algorithm. @@ -160,7 +160,7 @@ servers: description: Management API (every operation also declares its own server) tags: - name: History API - description: 'Read identifications by one identifier on `https://account.shieldlabs.ai` with the Private API Key. The canonical way to read a verdict: by `request_id` right after a protected action, or by `device_id`, `user_hid`, `visitor_id` or `ip` for account-level checks.' + description: 'Read identifications by one identifier on `https://account.shieldlabs.ai` with the Private API Key. The canonical way to read a verdict: by `identification_id` right after a protected action, or by `device_id`, `user_hid`, `visitor_id` or `ip` for account-level checks.' - name: Management API description: Domain profile on `https://api.shieldlabs.ai`, authenticated with the Secret Key and the `X-Shield-Domain` header. Also serves the deprecated history endpoint until 1 January 2027. - name: Health @@ -185,7 +185,7 @@ paths: with the total number of matches. The Private API Key selects the domain; identifications from its subdomains are included (`domain` holds the host, `site_domain` the registered domain). - **Read one verdict.** After a protected action, search by `request_id` with `limit=1`. The row + **Read one verdict.** After a protected action, search by `identification_id` with `limit=1`. The row appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds as follow-up network checks finish, so start the identification when the user begins the action (for example when the signup form opens), not when the form is submitted. An empty @@ -204,7 +204,7 @@ paths: **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on - `request_id`. + `identification_id`. **Latest state.** A row can be refined after the webhook was sent, for example when late network data re-scores it; its `ver` then increases. The History API always returns the latest @@ -246,7 +246,7 @@ paths: - lang: Shell label: curl source: | - curl "https://account.shieldlabs.ai/api/v1/history/request_id/a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d?limit=1" \ + curl "https://account.shieldlabs.ai/api/v1/history/identification_id/a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d?limit=1" \ -H "Authorization: Bearer $SHIELDLABS_API_KEY" /v1/profile: servers: @@ -485,7 +485,7 @@ webhooks: **Respond fast.** Answer any 2xx status within 1 second and process the event asynchronously; do not redirect. Today each identification is delivered once per endpoint, with no retries. A later release adds retries that resend identical bytes, so make your handler idempotent on - `data.request_id`. + `data.identification_id`. **Latest state.** The event is a snapshot taken when scoring finished. The History row can still be refined afterwards (its `ver` increases) and no second event is sent. Use the History @@ -573,8 +573,8 @@ components: in: path required: true description: |- - Identifier to search by. Only these seven values are supported: - - `request_id`: one identification (read a verdict); + Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): + - `identification_id`: one identification (read a verdict); - `device_id`: every identification of one device; - `user_hid`: every identification of one account; - `visitor_id`: every identification of one visitor; @@ -587,7 +587,7 @@ components: schema: type: string enum: - - request_id + - identification_id - device_id - user_hid - visitor_id @@ -595,9 +595,9 @@ components: - session_id - cookie_id examples: - requestId: + identificationId: summary: Read one verdict - value: request_id + value: identification_id deviceId: summary: All identifications of one device value: device_id @@ -607,7 +607,7 @@ components: required: true description: |- Value of the identifier, validated on your side before sending: - - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, + - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, the nil UUID included, matching `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it lowercase. @@ -628,7 +628,7 @@ components: minLength: 1 pattern: ^(?:[^/.][^/]*|\.[^/.][^/]*|\.\.[^/]+)$ examples: - requestId: + identificationId: summary: A request ID value: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d ip: @@ -658,7 +658,7 @@ components: name: offset in: query required: false - description: 'Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`.' + description: 'Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`.' schema: type: integer minimum: 0 @@ -691,7 +691,7 @@ components: schema: type: string enum: - - request_id + - identification_id - device_id - user_hid - visitor_id @@ -699,9 +699,9 @@ components: - session_id - cookie_id examples: - requestId: + identificationId: summary: Search by request ID - value: request_id + value: identification_id DeprecatedHistoryValue: name: value in: path @@ -711,7 +711,7 @@ components: type: string minLength: 1 examples: - requestId: + identificationId: summary: A request ID value: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d DeprecatedHistoryLimit: @@ -752,7 +752,7 @@ components: summary: Signature of the webhook.ping example value: sha256=ea2685733d254f7028fb031c4214583b0650de01e6c8c93131236024edd9fdd8 schemas: - RequestId: + IdentificationId: type: string format: uuid description: Identifies one identification. The browser creates it as a UUID v4 and hands it to your page; it is the join key between the browser, the webhook and the History API. The nil UUID appears only on rate-limit marker rows that arrived with a malformed request ID. @@ -994,7 +994,7 @@ components: The `ip_mismatch` flag has no column. Rows also carry diagnostic network fields (TCP, MTU and STUN measurements) that are not part of the stable contract: ignore fields you do not know. required: - - request_id + - identification_id - session_id - cookie_id - domain @@ -1035,8 +1035,8 @@ components: - is_incognito - is_search_bot properties: - request_id: - $ref: '#/components/schemas/RequestId' + identification_id: + $ref: '#/components/schemas/IdentificationId' session_id: $ref: '#/components/schemas/SessionId' cookie_id: @@ -1327,7 +1327,7 @@ components: additionalProperties: true description: One identification as returned by the deprecated Management API history endpoint (PascalCase keys). Also carries diagnostic network fields that are not part of the stable contract. Use the History API row instead. required: - - RequestID + - IdentificationID - SessionID - CookieID - DeviceID @@ -1346,8 +1346,8 @@ components: - Details - LastRequestTime properties: - RequestID: - $ref: '#/components/schemas/RequestId' + IdentificationID: + $ref: '#/components/schemas/IdentificationId' SessionID: $ref: '#/components/schemas/SessionId' CookieID: @@ -1657,7 +1657,7 @@ components: type: object description: The scored identification. Every key is always present (no key is ever omitted); only `user_hid` can be `null`. required: - - request_id + - identification_id - visitor_id - device_id - session_id @@ -1676,8 +1676,8 @@ components: - detection_flags - observed_at properties: - request_id: - $ref: '#/components/schemas/RequestId' + identification_id: + $ref: '#/components/schemas/IdentificationId' visitor_id: $ref: '#/components/schemas/VisitorId' device_id: @@ -1764,7 +1764,7 @@ components: description: 'One page of five identifications out of 37 matches: a dangerous paid click through a proxy with an anti-detect browser, a trusted anonymous visit, a VPN visit with a local network leak and a late network correction, a rate-limit marker (999) and a search-engine crawler.' value: data: - - request_id: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d + - identification_id: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d session_id: b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e cookie_id: c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f domain: shop.example.com @@ -1822,7 +1822,7 @@ components: traffic_reason: gclid_present click_id_type: gclid is_suspicious_paid_click: true - - request_id: 7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b + - identification_id: 7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b session_id: a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d cookie_id: f0e1d2c3-b4a5-4968-8776-655443322110 domain: example.com @@ -1871,7 +1871,7 @@ components: stun_request_seen: true is_scanner_stun_passed: false stun_flow_status: ok - - request_id: 9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0 + - identification_id: 9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0 session_id: b0c1d2e3-f4a5-4b6c-9d7e-8f9a0b1c2d3e cookie_id: c3d4e5f6-a7b8-4c9d-8e0f-a1b2c3d4e5f6 domain: example.com @@ -1925,7 +1925,7 @@ components: traffic_channel: Referral traffic_channel_group: Referral traffic_reason: external_referrer - - request_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d + - identification_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d session_id: 00000000-0000-0000-0000-000000000000 cookie_id: 00000000-0000-0000-0000-000000000000 domain: example.com @@ -1974,7 +1974,7 @@ components: stun_request_seen: false is_scanner_stun_passed: false stun_flow_status: '' - - request_id: 4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f + - identification_id: 4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f session_id: 5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d cookie_id: 6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e domain: example.com @@ -2030,7 +2030,7 @@ components: total: 37 HistoryPageEmpty: summary: Nothing matched - description: No identification matched. When searching by `request_id` right after a protected action, this means "not scored yet" (or an invalid request ID), never "clean". + description: No identification matched. When searching by `identification_id` right after a protected action, this means "not scored yet" (or an invalid request ID), never "clean". value: data: [] total: 0 @@ -2095,7 +2095,7 @@ components: summary: One identification (deprecated shape) description: The PascalCase array returned by the deprecated endpoint, for the same identification as the first History API example row. value: - - RequestID: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d + - IdentificationID: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d SessionID: b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e CookieID: c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f DeviceID: d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a @@ -2159,7 +2159,7 @@ components: schema_version: '2026-06-01' created_at: '2026-09-30T12:34:57.482913041Z' data: - request_id: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d + identification_id: a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d visitor_id: e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b device_id: d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a session_id: b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e @@ -2223,7 +2223,7 @@ components: schema_version: '2026-06-01' created_at: '2026-09-30T13:10:00.5Z' data: - request_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d + identification_id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d visitor_id: 00000000-0000-0000-0000-000000000000 device_id: 00000000-0000-0000-0000-000000000000 session_id: 00000000-0000-0000-0000-000000000000 @@ -2313,7 +2313,7 @@ components: public_ip: country: BY ip: 203.0.113.10 - request_id: 13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11 + identification_id: 13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11 risk_score: 30 session_id: 3a2b1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d signals: diff --git a/scripts/smoke-example.mjs b/scripts/smoke-example.mjs index a1b8a4c..64bde74 100644 --- a/scripts/smoke-example.mjs +++ b/scripts/smoke-example.mjs @@ -12,7 +12,7 @@ const WEBHOOK_SECRET = 'whsec_00112233445566778899aabbccddeeff'; // The example accepts comma-separated secrets while a secret is rotated. const PREVIOUS_SECRET = 'whsec_ffeeddccbbaa99887766554433221100'; const LISTENING = /Listening on http:\/\/localhost:(\d+)/; -const HISTORY_PATH = /^\/api\/v1\/history\/request_id\/([0-9a-f-]+)$/; +const HISTORY_PATH = /^\/api\/v1\/history\/identification_id\/([0-9a-f-]+)$/; const ids = { clean: '11111111-1111-4111-8111-111111111111', dangerous: '22222222-2222-4222-8222-222222222222', @@ -27,9 +27,9 @@ const fixturePage = JSON.parse( ); const baseRow = fixturePage.data[1]; -function row(requestId, changes) { +function row(identificationId, changes) { const createdAt = new Date().toISOString().replace('T', ' ').replace('Z', ''); - return { ...baseRow, request_id: requestId, created_at: createdAt, ...changes }; + return { ...baseRow, identification_id: identificationId, created_at: createdAt, ...changes }; } const rows = { @@ -111,34 +111,34 @@ try { } // The missing case waits for the whole polling window (about 10 seconds), so run it alongside. - const missing = signup({ requestId: ids.missing }); + const missing = signup({ identificationId: ids.missing }); - const clean = await signup({ requestId: ids.clean }); + const clean = await signup({ identificationId: ids.clean }); check( 'a clean identification is accepted', clean.status === 201 && clean.body.band === 'trusted', ); - const replay = await signup({ requestId: ids.clean }); + const replay = await signup({ identificationId: ids.clean }); check( 'a reused request ID is refused', replay.status === 403 && replay.body.reason === 'replayed', ); - const dangerous = await signup({ requestId: ids.dangerous }); + const dangerous = await signup({ identificationId: ids.dangerous }); check( 'the dangerous band is refused', dangerous.status === 403 && dangerous.body.reason === 'blocked_band', ); - const automation = await signup({ requestId: ids.automation }); + const automation = await signup({ identificationId: ids.automation }); check( 'browser automation is refused', automation.status === 403 && automation.body.reason === 'blocked_flag', ); - const marker = await signup({ requestId: ids.marker }); + const marker = await signup({ identificationId: ids.marker }); check( 'the rate-limit marker is refused', marker.status === 403 && marker.body.reason === 'rate_limited', ); - const invalid = await signup({ requestId: 'not-a-uuid' }); + const invalid = await signup({ identificationId: 'not-a-uuid' }); check('an invalid request ID is a 400', invalid.status === 400); const absent = await signup({}); check('an absent request ID is a 400', absent.status === 400); diff --git a/src/constants.ts b/src/constants.ts index 730bf19..aab1d79 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -43,6 +43,7 @@ export const LOOKUP_TYPES: readonly LookupType[] = [ 'ip', 'user_hid', 'visitor_id', + 'identification_id', 'request_id', 'device_id', 'session_id', diff --git a/src/generated/api.ts b/src/generated/api.ts index 8bafbca..6feaca3 100644 --- a/src/generated/api.ts +++ b/src/generated/api.ts @@ -16,7 +16,7 @@ export interface paths { * with the total number of matches. The Private API Key selects the domain; identifications from * its subdomains are included (`domain` holds the host, `site_domain` the registered domain). * - * **Read one verdict.** After a protected action, search by `request_id` with `limit=1`. The row + * **Read one verdict.** After a protected action, search by `identification_id` with `limit=1`. The row * appears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds * as follow-up network checks finish, so start the identification when the user begins the * action (for example when the signup form opens), not when the form is submitted. An empty @@ -35,7 +35,7 @@ export interface paths { * * **Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at` * only, so paging while new identifications arrive can repeat or skip rows: deduplicate on - * `request_id`. + * `identification_id`. * * **Latest state.** A row can be refined after the webhook was sent, for example when late * network data re-scores it; its `ver` then increases. The History API always returns the latest @@ -167,7 +167,7 @@ export interface webhooks { * **Respond fast.** Answer any 2xx status within 1 second and process the event asynchronously; * do not redirect. Today each identification is delivered once per endpoint, with no retries. A * later release adds retries that resend identical bytes, so make your handler idempotent on - * `data.request_id`. + * `data.identification_id`. * * **Latest state.** The event is a snapshot taken when scoring finished. The History row can * still be refined afterwards (its `ver` increases) and no second event is sent. Use the History @@ -226,7 +226,7 @@ export interface components { * @description Identifies one identification. The browser creates it as a UUID v4 and hands it to your page; it is the join key between the browser, the webhook and the History API. The nil UUID appears only on rate-limit marker rows that arrived with a malformed request ID. * @example a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d */ - RequestId: string; + IdentificationId: string; /** * Format: uuid * @description One visit on one origin (UUID v4 created in the browser), shared by the open tabs of that origin. The next visit after the last tab closes gets a new session ID. The nil UUID appears on rate-limit marker rows. @@ -389,7 +389,7 @@ export interface components { * STUN measurements) that are not part of the stable contract: ignore fields you do not know. */ HistoryRow: { - request_id: components["schemas"]["RequestId"]; + identification_id: components["schemas"]["IdentificationId"]; session_id: components["schemas"]["SessionId"]; cookie_id: components["schemas"]["CookieId"]; /** @@ -604,7 +604,7 @@ export interface components { }; /** @description One identification as returned by the deprecated Management API history endpoint (PascalCase keys). Also carries diagnostic network fields that are not part of the stable contract. Use the History API row instead. */ LegacySnapshot: { - RequestID: components["schemas"]["RequestId"]; + IdentificationID: components["schemas"]["IdentificationId"]; SessionID: components["schemas"]["SessionId"]; CookieID: components["schemas"]["CookieId"]; DeviceID: components["schemas"]["DeviceId"]; @@ -824,7 +824,7 @@ export interface components { }; /** @description The scored identification. Every key is always present (no key is ever omitted); only `user_hid` can be `null`. */ IdentificationScoredData: { - request_id: components["schemas"]["RequestId"]; + identification_id: components["schemas"]["IdentificationId"]; visitor_id: components["schemas"]["VisitorId"]; device_id: components["schemas"]["DeviceId"]; session_id: components["schemas"]["SessionId"]; @@ -965,8 +965,8 @@ export interface components { }; parameters: { /** - * @description Identifier to search by. Only these seven values are supported: - * - `request_id`: one identification (read a verdict); + * @description Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): + * - `identification_id`: one identification (read a verdict); * - `device_id`: every identification of one device; * - `user_hid`: every identification of one account; * - `visitor_id`: every identification of one visitor; @@ -977,10 +977,10 @@ export interface components { * The server does not reject other values: it ignores them and returns the latest identifications * of the whole domain, so restrict the value on your side. */ - HistorySearchType: "request_id" | "device_id" | "user_hid" | "visitor_id" | "ip" | "session_id" | "cookie_id"; + HistorySearchType: "identification_id" | "request_id" | "device_id" | "user_hid" | "visitor_id" | "ip" | "session_id" | "cookie_id"; /** * @description Value of the identifier, validated on your side before sending: - * - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, + * - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, * the nil UUID included, matching * `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it * lowercase. @@ -1000,12 +1000,12 @@ export interface components { HistoryValue: string; /** @description Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side. */ HistoryLimit: number; - /** @description Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`. */ + /** @description Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`. */ HistoryOffset: number; /** @description Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. */ ShieldDomain: string; /** @description Identifier to search by. Other values get a `404` with a bare JSON string such as `"auto is not supported"`. */ - DeprecatedHistoryType: "request_id" | "device_id" | "user_hid" | "visitor_id" | "ip" | "session_id" | "cookie_id"; + DeprecatedHistoryType: "identification_id" | "request_id" | "device_id" | "user_hid" | "visitor_id" | "ip" | "session_id" | "cookie_id"; /** @description Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text. */ DeprecatedHistoryValue: string; /** @description Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`. */ @@ -1040,14 +1040,14 @@ export interface operations { query?: { /** @description Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side. */ limit?: components["parameters"]["HistoryLimit"]; - /** @description Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`. */ + /** @description Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `identification_id`. */ offset?: components["parameters"]["HistoryOffset"]; }; header?: never; path: { /** - * @description Identifier to search by. Only these seven values are supported: - * - `request_id`: one identification (read a verdict); + * @description Identifier to search by. These values are supported (`request_id` is a legacy alias of `identification_id`): + * - `identification_id`: one identification (read a verdict); * - `device_id`: every identification of one device; * - `user_hid`: every identification of one account; * - `visitor_id`: every identification of one visitor; @@ -1061,7 +1061,7 @@ export interface operations { search_type: components["parameters"]["HistorySearchType"]; /** * @description Value of the identifier, validated on your side before sending: - * - `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, + * - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, * the nil UUID included, matching * `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it * lowercase. diff --git a/src/history.ts b/src/history.ts index 9f9dc88..aadfd16 100644 --- a/src/history.ts +++ b/src/history.ts @@ -19,7 +19,7 @@ import { validateMilliseconds, validateNonNegativeInteger, validatePageSize, - validateRequestId, + validateIdentificationId, type Lookup, } from './validation.js'; @@ -208,7 +208,7 @@ export class HistoryResource { const page = await fetchPage(this.#transport, lookup, pageSize, offset, signal); if (page.data.length === 0) return; for (const identification of page.data) { - const key = identification.request_id; + const key = identification.identification_id; if (key !== '') { if (seen.has(key)) continue; seen.add(key); @@ -255,10 +255,10 @@ export class IdentificationsResource { * - 400, 401, 403 and 404 (and any other error that another poll cannot fix) are thrown at once. */ async get( - requestId: string, + identificationId: string, options: GetIdentificationOptions = {}, ): Promise { - const id = validateRequestId(requestId); + const id = validateIdentificationId(identificationId); const wait = validateBoolean(options.wait ?? true, 'wait'); const timeout = validateMilliseconds( options.timeout ?? DEFAULT_WAIT_TIMEOUT_MS, @@ -271,7 +271,7 @@ export class IdentificationsResource { ); const { signal } = options; throwIfAborted(signal); - const lookup: Lookup = { type: 'request_id', value: id, segment: id }; + const lookup: Lookup = { type: 'identification_id', value: id, segment: id }; if (!wait) { const page = await fetchPage(this.#transport, lookup, 1, 0, signal); diff --git a/src/normalize.ts b/src/normalize.ts index 231fe30..f17f847 100644 --- a/src/normalize.ts +++ b/src/normalize.ts @@ -32,9 +32,14 @@ function own(obj: JsonObject, key: string): unknown { return hasOwn(obj, key) ? obj[key] : undefined; } -/** A string property, or "" when it is absent or not a string. */ -function stringField(obj: JsonObject, key: string): string { +/** A string property, or "" when it is absent or not a string. An empty identification_id falls back to request_id. */ +function stringField(obj: JsonObject, key: string, fallbackKey?: string): string { const value = own(obj, key); + if (typeof value === 'string' && value !== '') return value; + if (fallbackKey) { + const fallback = own(obj, fallbackKey); + if (typeof fallback === 'string') return fallback; + } return typeof value === 'string' ? value : ''; } @@ -366,7 +371,7 @@ export function fromHistoryRow(row: JsonObject): Identification { } return { - request_id: stringField(row, 'request_id'), + identification_id: stringField(row, 'identification_id', 'request_id'), visitor_id: stringField(row, 'visitor_id'), device_id: stringField(row, 'device_id'), session_id: stringField(row, 'session_id'), @@ -424,7 +429,7 @@ export function fromWebhookData(data: JsonObject): Identification { : []; return { - request_id: stringField(data, 'request_id'), + identification_id: stringField(data, 'identification_id', 'request_id'), visitor_id: stringField(data, 'visitor_id'), device_id: stringField(data, 'device_id'), session_id: stringField(data, 'session_id'), diff --git a/src/risk.ts b/src/risk.ts index 1e756e0..3f11643 100644 --- a/src/risk.ts +++ b/src/risk.ts @@ -61,7 +61,7 @@ export interface EvaluateOptions { * no state: claim the request ID in your own store first (an atomic insert-if-absent) and * return the result here. Must return a boolean synchronously. */ - isReplay?: ((requestId: string) => boolean) | undefined; + isReplay?: ((identificationId: string) => boolean) | undefined; } const DEFAULT_MAX_AGE_MS = 300_000; @@ -125,7 +125,7 @@ export function evaluateIdentification( const band = riskBand(identification.risk_score); if (options.isReplay) { - const replayed: unknown = options.isReplay(identification.request_id); + const replayed: unknown = options.isReplay(identification.identification_id); if (isThenable(replayed)) { throw new ValidationError( 'isReplay must return a boolean synchronously. Claim the request ID in your store first (an atomic insert-if-absent), then pass the result, for example isReplay: () => !claimed.', diff --git a/src/types.ts b/src/types.ts index 64322c7..7f96a5c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ /** Identifier types the History API can search by. */ export type LookupType = - 'ip' | 'user_hid' | 'visitor_id' | 'request_id' | 'device_id' | 'session_id' | 'cookie_id'; + 'ip' | 'user_hid' | 'visitor_id' | 'identification_id' | 'request_id' | 'device_id' | 'session_id' | 'cookie_id'; /** The three risk bands: trusted 0-29, suspicious 30-59, dangerous 60-100. */ export type RiskBand = 'trusted' | 'suspicious' | 'dangerous'; @@ -80,7 +80,7 @@ export interface DetectionFlags { */ export interface Identification { /** UUID of the identification, created in the browser. */ - request_id: string; + identification_id: string; /** Server-side visitor identifier (sticky to the device). */ visitor_id: string; /** Server-side device identifier. The all-zero UUID means no usable device signals. */ diff --git a/src/validation.ts b/src/validation.ts index 472dcd2..8300305 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -90,11 +90,11 @@ export function validateLookup(type: unknown, value: unknown): Lookup { } /** A request ID must be a UUID; it is sent lowercase. */ -export function validateRequestId(requestId: unknown): string { - if (typeof requestId !== 'string' || !UUID.test(requestId)) { - throw new ValidationError('requestId must be a UUID (8-4-4-4-12 hexadecimal digits).'); +export function validateIdentificationId(identificationId: unknown): string { + if (typeof identificationId !== 'string' || !UUID.test(identificationId)) { + throw new ValidationError('identificationId must be a UUID (8-4-4-4-12 hexadecimal digits).'); } - return requestId.toLowerCase(); + return identificationId.toLowerCase(); } /** A boolean option. */ diff --git a/test/data/history-page.json b/test/data/history-page.json index 047866b..c67e987 100644 --- a/test/data/history-page.json +++ b/test/data/history-page.json @@ -1,7 +1,7 @@ { "data": [ { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", "cookie_id": "4449bb58-590c-444c-ae1f-d1ddc768dbdd", "domain": "shop.example.com", @@ -61,7 +61,7 @@ "is_suspicious_paid_click": true }, { - "request_id": "7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b", + "identification_id": "7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b", "session_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "cookie_id": "f0e1d2c3-b4a5-4968-8776-655443322110", "domain": "example.com", @@ -112,7 +112,7 @@ "stun_flow_status": "ok" }, { - "request_id": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0", + "identification_id": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0", "session_id": "b0c1d2e3-f4a5-4b6c-9d7e-8f9a0b1c2d3e", "cookie_id": "c3d4e5f6-a7b8-4c9d-8e0f-a1b2c3d4e5f6", "domain": "example.com", @@ -168,7 +168,7 @@ "traffic_reason": "external_referrer" }, { - "request_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "identification_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "session_id": "00000000-0000-0000-0000-000000000000", "cookie_id": "00000000-0000-0000-0000-000000000000", "domain": "example.com", @@ -219,7 +219,7 @@ "stun_flow_status": "" }, { - "request_id": "4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f", + "identification_id": "4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f", "session_id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d", "cookie_id": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e", "domain": "example.com", diff --git a/test/data/normalization-cases.json b/test/data/normalization-cases.json index 1449e9c..d311d47 100644 --- a/test/data/normalization-cases.json +++ b/test/data/normalization-cases.json @@ -5,7 +5,7 @@ "name": "history_02f1d973", "source": "history", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", "cookie_id": "4449bb58-590c-444c-ae1f-d1ddc768dbdd", "domain": "shop.example.com", @@ -65,7 +65,7 @@ "is_suspicious_paid_click": true }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -143,7 +143,7 @@ "name": "history_7c1e2f4a", "source": "history", "input": { - "request_id": "7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b", + "identification_id": "7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b", "session_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "cookie_id": "f0e1d2c3-b4a5-4968-8776-655443322110", "domain": "example.com", @@ -194,7 +194,7 @@ "stun_flow_status": "ok" }, "expected": { - "request_id": "7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b", + "identification_id": "7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b", "visitor_id": "3c4d5e6f-7a8b-5c9d-8e0f-1a2b3c4d5e6f", "device_id": "5d9a1f3e-7b2c-5e4d-8f6a-9b0c1d2e3f4a", "session_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", @@ -262,7 +262,7 @@ "name": "history_9e8d7c6b", "source": "history", "input": { - "request_id": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0", + "identification_id": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0", "session_id": "b0c1d2e3-f4a5-4b6c-9d7e-8f9a0b1c2d3e", "cookie_id": "c3d4e5f6-a7b8-4c9d-8e0f-a1b2c3d4e5f6", "domain": "example.com", @@ -318,7 +318,7 @@ "traffic_reason": "external_referrer" }, "expected": { - "request_id": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0", + "identification_id": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0", "visitor_id": "d2e3f4a5-b6c7-5d8e-9f0a-1b2c3d4e5f6a", "device_id": "e1f2a3b4-c5d6-5e7f-8a9b-0c1d2e3f4a5b", "session_id": "b0c1d2e3-f4a5-4b6c-9d7e-8f9a0b1c2d3e", @@ -401,7 +401,7 @@ "name": "history_1a2b3c4d", "source": "history", "input": { - "request_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "identification_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "session_id": "00000000-0000-0000-0000-000000000000", "cookie_id": "00000000-0000-0000-0000-000000000000", "domain": "example.com", @@ -452,7 +452,7 @@ "stun_flow_status": "" }, "expected": { - "request_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "identification_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "visitor_id": "00000000-0000-0000-0000-000000000000", "device_id": "00000000-0000-0000-0000-000000000000", "session_id": "00000000-0000-0000-0000-000000000000", @@ -520,7 +520,7 @@ "name": "history_4f5e6d7c", "source": "history", "input": { - "request_id": "4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f", + "identification_id": "4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f", "session_id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d", "cookie_id": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e", "domain": "example.com", @@ -575,7 +575,7 @@ "traffic_reason": "ip_crawler_detected" }, "expected": { - "request_id": "4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f", + "identification_id": "4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f", "visitor_id": "8d9e0f1a-2b3c-5d4e-9f5a-6b7c8d9e0f1a", "device_id": "7c8d9e0f-1a2b-5c3d-8e4f-5a6b7c8d9e0f", "session_id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d", @@ -637,7 +637,7 @@ "name": "webhook_scored", "source": "webhook", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -707,7 +707,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -785,7 +785,7 @@ "name": "webhook_rate_limited", "source": "webhook", "input": { - "request_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "identification_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "visitor_id": "00000000-0000-0000-0000-000000000000", "device_id": "00000000-0000-0000-0000-000000000000", "session_id": "00000000-0000-0000-0000-000000000000", @@ -847,7 +847,7 @@ "observed_at": "2026-09-30T13:10:00.5Z" }, "expected": { - "request_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "identification_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "visitor_id": "00000000-0000-0000-0000-000000000000", "device_id": "00000000-0000-0000-0000-000000000000", "session_id": "00000000-0000-0000-0000-000000000000", @@ -953,7 +953,7 @@ "country": "BY", "ip": "203.0.113.10" }, - "request_id": "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11", + "identification_id": "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11", "risk_score": 30, "session_id": "3a2b1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d", "signals": [ @@ -985,7 +985,7 @@ "visitor_id": "7a6b5c4d-3e2f-5a1b-9c8d-7e6f5a4b3c2d" }, "expected": { - "request_id": "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11", + "identification_id": "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11", "visitor_id": "7a6b5c4d-3e2f-5a1b-9c8d-7e6f5a4b3c2d", "device_id": "6f1e2d3c-4b5a-5968-8776-655443322110", "session_id": "3a2b1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d", diff --git a/test/data/normalizer-edge-cases.json b/test/data/normalizer-edge-cases.json index 61ec372..e747b71 100644 --- a/test/data/normalizer-edge-cases.json +++ b/test/data/normalizer-edge-cases.json @@ -338,7 +338,7 @@ { "name": "details_mixed_entries", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -362,7 +362,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -439,7 +439,7 @@ { "name": "details_invalid_json", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -463,7 +463,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -524,7 +524,7 @@ { "name": "details_object_not_list", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -548,7 +548,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -609,7 +609,7 @@ { "name": "details_missing", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -632,7 +632,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -693,7 +693,7 @@ { "name": "details_json_null", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -717,7 +717,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -778,7 +778,7 @@ { "name": "flag_truthiness", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -817,7 +817,7 @@ "is_suspicious_paid_click": false }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -878,7 +878,7 @@ { "name": "leak_source_capitalized_none", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -902,7 +902,7 @@ "webrtc_leak_source": "None" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -963,7 +963,7 @@ { "name": "leak_source_padded_none", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -987,7 +987,7 @@ "webrtc_leak_source": " none " }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1048,7 +1048,7 @@ { "name": "leak_source_shield_no_ip", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -1072,7 +1072,7 @@ "webrtc_leak_source": "shield" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1133,7 +1133,7 @@ { "name": "ip_padding_and_sentinel", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -1157,7 +1157,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1218,7 +1218,7 @@ { "name": "search_bot_suppresses_ip_mismatch", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -1243,7 +1243,7 @@ "is_search_bot": true }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1304,7 +1304,7 @@ { "name": "site_domain_empty", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "shop.example.com", @@ -1329,7 +1329,7 @@ "site_domain": "" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1390,7 +1390,7 @@ { "name": "site_domain_set", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "shop.example.com", @@ -1415,7 +1415,7 @@ "site_domain": "example.com" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1476,7 +1476,7 @@ { "name": "user_hid_empty", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -1500,7 +1500,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1561,7 +1561,7 @@ { "name": "user_hid_missing", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -1584,7 +1584,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1645,7 +1645,7 @@ { "name": "user_hid_fail_sentinel", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -1669,7 +1669,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1730,7 +1730,7 @@ { "name": "connection_type_bvp", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -1754,7 +1754,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1815,7 +1815,7 @@ { "name": "created_at_missing", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -1838,7 +1838,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1899,7 +1899,7 @@ { "name": "score_missing", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -1922,7 +1922,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -1983,7 +1983,7 @@ { "name": "unknown_fields_and_traffic", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -2019,7 +2019,7 @@ "is_suspicious_paid_click": true }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -2080,7 +2080,7 @@ { "name": "country_missing", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", "cookie_id": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a", "domain": "example.com", @@ -2103,7 +2103,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "8f9a0b1c-2d3e-5f4a-9b5c-6d7e8f9a0b1c", "device_id": "7e8f9a0b-1c2d-5e3f-8a4b-5c6d7e8f9a0b", "session_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f", @@ -2164,10 +2164,10 @@ { "name": "minimal_row", "input": { - "request_id": "4c0d2f8b-6e3a-4b9c-8d7e-0f1a2b3c4d5e" + "identification_id": "4c0d2f8b-6e3a-4b9c-8d7e-0f1a2b3c4d5e" }, "expected": { - "request_id": "4c0d2f8b-6e3a-4b9c-8d7e-0f1a2b3c4d5e", + "identification_id": "4c0d2f8b-6e3a-4b9c-8d7e-0f1a2b3c4d5e", "visitor_id": "", "device_id": "", "session_id": "", @@ -2229,7 +2229,7 @@ "name": "empty_row", "input": {}, "expected": { - "request_id": "", + "identification_id": "", "visitor_id": "", "device_id": "", "session_id": "", @@ -2290,7 +2290,7 @@ { "name": "nil_ids_rate_limited", "input": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "session_id": "00000000-0000-0000-0000-000000000000", "cookie_id": "00000000-0000-0000-0000-000000000000", "domain": "example.com", @@ -2314,7 +2314,7 @@ "webrtc_leak_source": "none" }, "expected": { - "request_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", + "identification_id": "3b9c1e7a-5d2f-4a8b-9c6d-7e8f9a0b1c2d", "visitor_id": "00000000-0000-0000-0000-000000000000", "device_id": "00000000-0000-0000-0000-000000000000", "session_id": "00000000-0000-0000-0000-000000000000", @@ -2383,7 +2383,7 @@ { "name": "flags_missing", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -2431,7 +2431,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -2508,7 +2508,7 @@ { "name": "flags_null", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -2557,7 +2557,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -2634,7 +2634,7 @@ { "name": "flags_truthiness", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -2693,7 +2693,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -2770,7 +2770,7 @@ { "name": "traffic_missing", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -2829,7 +2829,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -2906,7 +2906,7 @@ { "name": "traffic_partial", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -2970,7 +2970,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3047,7 +3047,7 @@ { "name": "signals_missing", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3103,7 +3103,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3164,7 +3164,7 @@ { "name": "signals_null", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3221,7 +3221,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3282,7 +3282,7 @@ { "name": "signals_mixed", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3361,7 +3361,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3448,7 +3448,7 @@ { "name": "public_ip_missing", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3514,7 +3514,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3591,7 +3591,7 @@ { "name": "public_ip_null", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3658,7 +3658,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3735,7 +3735,7 @@ { "name": "ip_sentinels", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3805,7 +3805,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3882,7 +3882,7 @@ { "name": "user_hid_empty", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -3952,7 +3952,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4029,7 +4029,7 @@ { "name": "user_hid_missing", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4098,7 +4098,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4175,7 +4175,7 @@ { "name": "user_hid_null", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4245,7 +4245,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4322,7 +4322,7 @@ { "name": "observed_at_offset", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4392,7 +4392,7 @@ "observed_at": "2026-09-30T14:34:57.482913041+02:00" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4469,7 +4469,7 @@ { "name": "observed_at_invalid", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4539,7 +4539,7 @@ "observed_at": "yesterday" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4616,7 +4616,7 @@ { "name": "risk_score_missing", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4685,7 +4685,7 @@ "observed_at": "2026-09-30T12:34:57.482913041Z" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4762,7 +4762,7 @@ { "name": "unknown_keys", "input": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4836,7 +4836,7 @@ "schema_extra": "y" }, "expected": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", @@ -4914,7 +4914,7 @@ "name": "empty_data", "input": {}, "expected": { - "request_id": "", + "identification_id": "", "visitor_id": "", "device_id": "", "session_id": "", diff --git a/test/data/webhook-identification-scored.json b/test/data/webhook-identification-scored.json index b42704a..fd19039 100644 --- a/test/data/webhook-identification-scored.json +++ b/test/data/webhook-identification-scored.json @@ -3,7 +3,7 @@ "schema_version": "2026-10-13", "created_at": "2026-09-30T12:34:57.482913041Z", "data": { - "request_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", + "identification_id": "02f1d973-84db-4156-a7f7-e799e6bf389b", "visitor_id": "bde0e249-20d8-4544-838c-ed9a0b6d7a36", "device_id": "ac7c303d-971b-41d1-8e25-cd5b46b46aed", "session_id": "bde78778-efd2-4c49-952f-1f11b9c05f35", diff --git a/test/data/webhook-identification-scored.raw.txt b/test/data/webhook-identification-scored.raw.txt index 77d7650..3fd1706 100644 --- a/test/data/webhook-identification-scored.raw.txt +++ b/test/data/webhook-identification-scored.raw.txt @@ -1 +1 @@ -{"event_type":"identification.scored","schema_version":"2026-10-13","created_at":"2026-09-30T12:34:57.482913041Z","data":{"request_id":"02f1d973-84db-4156-a7f7-e799e6bf389b","visitor_id":"bde0e249-20d8-4544-838c-ed9a0b6d7a36","device_id":"ac7c303d-971b-41d1-8e25-cd5b46b46aed","session_id":"bde78778-efd2-4c49-952f-1f11b9c05f35","cookie_id":"4449bb58-590c-444c-ae1f-d1ddc768dbdd","user_hid":"9f86d081884c7d659a2feaa0c55ad015","domain":"example.com","public_ip":{"ip":"203.0.113.24","country":"Netherlands"},"local_ip":{"ip":"198.51.100.23","country":"Germany"},"connection_type":"proxy","os":"Windows","browser":"Chrome","device_type":"desktop","traffic_source":{"channel":"Google Ads","referrer_domain":"google.com","landing_url":"https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123","click_id_type":"gclid","utm_source":"google","utm_medium":"cpc","utm_campaign":"","utm_content":"","utm_term":""},"risk_score":80,"signals":[{"name":"proxy","weight":10},{"name":"datacenter_ip","weight":10},{"name":"antidetect_browser","weight":60}],"detection_flags":{"vpn":false,"proxy":true,"privacy_relay":false,"check_incomplete":false,"stun_not_checked":false,"os_not_detected":false,"timezone_mismatch":false,"javascript_disabled":false,"os_mismatch":false,"ip_mismatch":true,"ws_tcp_latency":false,"anti_detect_browser":true,"device_spoofing":false,"rate_limited":false,"incognito":false,"search_bot":false,"ai_bot":false,"browser_automation":false,"ai_browser":false,"tor":false},"observed_at":"2026-09-30T12:34:57.482913041Z"}} \ No newline at end of file +{"event_type":"identification.scored","schema_version":"2026-10-13","created_at":"2026-09-30T12:34:57.482913041Z","data":{"identification_id":"02f1d973-84db-4156-a7f7-e799e6bf389b","visitor_id":"bde0e249-20d8-4544-838c-ed9a0b6d7a36","device_id":"ac7c303d-971b-41d1-8e25-cd5b46b46aed","session_id":"bde78778-efd2-4c49-952f-1f11b9c05f35","cookie_id":"4449bb58-590c-444c-ae1f-d1ddc768dbdd","user_hid":"9f86d081884c7d659a2feaa0c55ad015","domain":"example.com","public_ip":{"ip":"203.0.113.24","country":"Netherlands"},"local_ip":{"ip":"198.51.100.23","country":"Germany"},"connection_type":"proxy","os":"Windows","browser":"Chrome","device_type":"desktop","traffic_source":{"channel":"Google Ads","referrer_domain":"google.com","landing_url":"https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123","click_id_type":"gclid","utm_source":"google","utm_medium":"cpc","utm_campaign":"","utm_content":"","utm_term":""},"risk_score":80,"signals":[{"name":"proxy","weight":10},{"name":"datacenter_ip","weight":10},{"name":"antidetect_browser","weight":60}],"detection_flags":{"vpn":false,"proxy":true,"privacy_relay":false,"check_incomplete":false,"stun_not_checked":false,"os_not_detected":false,"timezone_mismatch":false,"javascript_disabled":false,"os_mismatch":false,"ip_mismatch":true,"ws_tcp_latency":false,"anti_detect_browser":true,"device_spoofing":false,"rate_limited":false,"incognito":false,"search_bot":false,"ai_bot":false,"browser_automation":false,"ai_browser":false,"tor":false},"observed_at":"2026-09-30T12:34:57.482913041Z"}} \ No newline at end of file diff --git a/test/data/webhook-rate-limited.json b/test/data/webhook-rate-limited.json index c72c8de..a6b9410 100644 --- a/test/data/webhook-rate-limited.json +++ b/test/data/webhook-rate-limited.json @@ -3,7 +3,7 @@ "schema_version": "2026-10-13", "created_at": "2026-09-30T13:10:00.5Z", "data": { - "request_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "identification_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "visitor_id": "00000000-0000-0000-0000-000000000000", "device_id": "00000000-0000-0000-0000-000000000000", "session_id": "00000000-0000-0000-0000-000000000000", diff --git a/test/data/webhook-signature-vectors.json b/test/data/webhook-signature-vectors.json index 7529cf0..5195366 100644 --- a/test/data/webhook-signature-vectors.json +++ b/test/data/webhook-signature-vectors.json @@ -5,27 +5,27 @@ { "name": "valid_lowercase_hex", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", - "signature_header": "sha256=1c2d0251d83eba5ed62c98d3758b805d227022850cdb8ddb1aa3754524345453", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", + "signature_header": "sha256=8129c91dd91ec75ba43cb4d0640c5af3b604564e077532a757ee4a1e77c4f4b0", "valid": true, "note": "Canonical header as sent by ShieldLabs" }, { "name": "valid_uppercase_hex", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", - "signature_header": "sha256=1C2D0251D83EBA5ED62C98D3758B805D227022850CDB8DDB1AA3754524345453", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", + "signature_header": "sha256=8129C91DD91EC75BA43CB4D0640C5AF3B604564E077532A757EE4A1E77C4F4B0", "valid": true, "note": "Hex digest compared case-insensitively" }, { "name": "valid_surrounding_whitespace", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", - "signature_header": " sha256=1c2d0251d83eba5ed62c98d3758b805d227022850cdb8ddb1aa3754524345453 ", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", + "signature_header": " sha256=8129c91dd91ec75ba43cb4d0640c5af3b604564e077532a757ee4a1e77c4f4b0 ", "valid": true, "note": "Leading/trailing whitespace is trimmed from the header value" }, @@ -41,71 +41,71 @@ { "name": "valid_pretty_printed_body", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\n \"event_type\": \"identification.scored\",\n \"schema_version\": \"2026-06-01\",\n \"created_at\": \"2026-09-30T10:15:00Z\",\n \"data\": {\n \"request_id\": \"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\n \"risk_score\": 35,\n \"user_hid\": null\n }\n}", - "body_base64": "ewogICJldmVudF90eXBlIjogImlkZW50aWZpY2F0aW9uLnNjb3JlZCIsCiAgInNjaGVtYV92ZXJzaW9uIjogIjIwMjYtMDYtMDEiLAogICJjcmVhdGVkX2F0IjogIjIwMjYtMDktMzBUMTA6MTU6MDBaIiwKICAiZGF0YSI6IHsKICAgICJyZXF1ZXN0X2lkIjogIjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsCiAgICAicmlza19zY29yZSI6IDM1LAogICAgInVzZXJfaGlkIjogbnVsbAogIH0KfQ==", - "signature_header": "sha256=263308293bcccb571464f60e0f8ef6076f9d23559b1658b6dc6f74ac3fdffd35", + "body": "{\n \"event_type\": \"identification.scored\",\n \"schema_version\": \"2026-06-01\",\n \"created_at\": \"2026-09-30T10:15:00Z\",\n \"data\": {\n \"identification_id\": \"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\n \"risk_score\": 35,\n \"user_hid\": null\n }\n}", + "body_base64": "ewogICJldmVudF90eXBlIjogImlkZW50aWZpY2F0aW9uLnNjb3JlZCIsCiAgInNjaGVtYV92ZXJzaW9uIjogIjIwMjYtMDYtMDEiLAogICJjcmVhdGVkX2F0IjogIjIwMjYtMDktMzBUMTA6MTU6MDBaIiwKICAiZGF0YSI6IHsKICAgICJpZGVudGlmaWNhdGlvbl9pZCI6ICIzZjJiOGMxZS05ZDRhLTRlNmItOGE3Yy0yZDFlMGY5YjZhNTMiLAogICAgInJpc2tfc2NvcmUiOiAzNSwKICAgICJ1c2VyX2hpZCI6IG51bGwKICB9Cn0=", + "signature_header": "sha256=6472e91002b86150f7d11f909347b16cac1eb0ae882aa68b35b1765fa5100dd5", "valid": true, "note": "Whatever bytes arrive are what was signed" }, { "name": "invalid_reserialized_body", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\n \"event_type\": \"identification.scored\",\n \"schema_version\": \"2026-06-01\",\n \"created_at\": \"2026-09-30T10:15:00Z\",\n \"data\": {\n \"request_id\": \"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\n \"risk_score\": 35,\n \"user_hid\": null\n }\n}", - "body_base64": "ewogICJldmVudF90eXBlIjogImlkZW50aWZpY2F0aW9uLnNjb3JlZCIsCiAgInNjaGVtYV92ZXJzaW9uIjogIjIwMjYtMDYtMDEiLAogICJjcmVhdGVkX2F0IjogIjIwMjYtMDktMzBUMTA6MTU6MDBaIiwKICAiZGF0YSI6IHsKICAgICJyZXF1ZXN0X2lkIjogIjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsCiAgICAicmlza19zY29yZSI6IDM1LAogICAgInVzZXJfaGlkIjogbnVsbAogIH0KfQ==", - "signature_header": "sha256=1c2d0251d83eba5ed62c98d3758b805d227022850cdb8ddb1aa3754524345453", + "body": "{\n \"event_type\": \"identification.scored\",\n \"schema_version\": \"2026-06-01\",\n \"created_at\": \"2026-09-30T10:15:00Z\",\n \"data\": {\n \"identification_id\": \"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\n \"risk_score\": 35,\n \"user_hid\": null\n }\n}", + "body_base64": "ewogICJldmVudF90eXBlIjogImlkZW50aWZpY2F0aW9uLnNjb3JlZCIsCiAgInNjaGVtYV92ZXJzaW9uIjogIjIwMjYtMDYtMDEiLAogICJjcmVhdGVkX2F0IjogIjIwMjYtMDktMzBUMTA6MTU6MDBaIiwKICAiZGF0YSI6IHsKICAgICJpZGVudGlmaWNhdGlvbl9pZCI6ICIzZjJiOGMxZS05ZDRhLTRlNmItOGE3Yy0yZDFlMGY5YjZhNTMiLAogICAgInJpc2tfc2NvcmUiOiAzNSwKICAgICJ1c2VyX2hpZCI6IG51bGwKICB9Cn0=", + "signature_header": "sha256=8129c91dd91ec75ba43cb4d0640c5af3b604564e077532a757ee4a1e77c4f4b0", "valid": false, "note": "Signature of the compact body does not match a re-serialized body: always verify raw bytes" }, { "name": "invalid_tampered_body", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":5,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6NSwidXNlcl9oaWQiOm51bGx9fQ==", - "signature_header": "sha256=1c2d0251d83eba5ed62c98d3758b805d227022850cdb8ddb1aa3754524345453", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":5,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjUsInVzZXJfaGlkIjpudWxsfX0=", + "signature_header": "sha256=8129c91dd91ec75ba43cb4d0640c5af3b604564e077532a757ee4a1e77c4f4b0", "valid": false, "note": "Body changed after signing" }, { "name": "invalid_wrong_secret", "secret": "whsec_test_0000000000000000000000000000000", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", - "signature_header": "sha256=1c2d0251d83eba5ed62c98d3758b805d227022850cdb8ddb1aa3754524345453", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", + "signature_header": "sha256=8129c91dd91ec75ba43cb4d0640c5af3b604564e077532a757ee4a1e77c4f4b0", "valid": false, "note": "Different endpoint secret" }, { "name": "invalid_missing_prefix", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", - "signature_header": "1c2d0251d83eba5ed62c98d3758b805d227022850cdb8ddb1aa3754524345453", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", + "signature_header": "8129c91dd91ec75ba43cb4d0640c5af3b604564e077532a757ee4a1e77c4f4b0", "valid": false, "note": "The sha256= prefix is required" }, { "name": "invalid_wrong_algorithm_prefix", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", - "signature_header": "sha1=1c2d0251d83eba5ed62c98d3758b805d227022850cdb8ddb1aa3754524345453", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", + "signature_header": "sha1=8129c91dd91ec75ba43cb4d0640c5af3b604564e077532a757ee4a1e77c4f4b0", "valid": false, "note": "Only sha256 is accepted" }, { "name": "invalid_truncated_digest", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", - "signature_header": "sha256=1c2d0251d83eba5ed62c98d3758b805d227022850cdb8ddb1aa37545243454", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", + "signature_header": "sha256=8129c91dd91ec75ba43cb4d0640c5af3b604564e077532a757ee4a1e77c4f4", "valid": false, "note": "Digest must be 64 hex characters" }, { "name": "invalid_non_hex_digest", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", "signature_header": "sha256=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "valid": false, "note": "Digest must be hexadecimal" @@ -113,8 +113,8 @@ { "name": "invalid_empty_header", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", "signature_header": "", "valid": false, "note": "Missing signature header" @@ -122,8 +122,8 @@ { "name": "invalid_prefix_only", "secret": "whsec_test_4f1a9c2e7b5d3a8f6e0c1b2d9a7e5f3c", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", "signature_header": "sha256=", "valid": false, "note": "Prefix without digest" @@ -131,9 +131,9 @@ { "name": "invalid_empty_secret", "secret": "", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"request_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJyZXF1ZXN0X2lkIjoiM2YyYjhjMWUtOWQ0YS00ZTZiLThhN2MtMmQxZTBmOWI2YTUzIiwicmlza19zY29yZSI6MzUsInVzZXJfaGlkIjpudWxsfX0=", - "signature_header": "sha256=68ae6c4becfcbc22bb017c96fec281973bc554f96afe7ba875854cd65b3368dc", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T10:15:00Z\",\"data\":{\"identification_id\":\"3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53\",\"risk_score\":35,\"user_hid\":null}}", + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTA6MTU6MDBaIiwiZGF0YSI6eyJpZGVudGlmaWNhdGlvbl9pZCI6IjNmMmI4YzFlLTlkNGEtNGU2Yi04YTdjLTJkMWUwZjliNmE1MyIsInJpc2tfc2NvcmUiOjM1LCJ1c2VyX2hpZCI6bnVsbH19", + "signature_header": "sha256=6a346decb68ad735d8421c5c8f36397d2fb34a0f3c0064816ff66cb20b459541", "valid": false, "note": "An empty secret is a configuration error and must never verify" }, @@ -149,20 +149,20 @@ { "name": "valid_scored_with_escaped_ampersand", "secret": "whsec_00112233445566778899aabbccddeeff", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T12:34:57.482913041Z\",\"data\":{\"request_id\":\"02f1d973-84db-4156-a7f7-e799e6bf389b\",\"visitor_id\":\"bde0e249-20d8-4544-838c-ed9a0b6d7a36\",\"device_id\":\"ac7c303d-971b-41d1-8e25-cd5b46b46aed\",\"session_id\":\"bde78778-efd2-4c49-952f-1f11b9c05f35\",\"cookie_id\":\"4449bb58-590c-444c-ae1f-d1ddc768dbdd\",\"user_hid\":\"9f86d081884c7d659a2feaa0c55ad015\",\"domain\":\"example.com\",\"public_ip\":{\"ip\":\"203.0.113.24\",\"country\":\"Netherlands\"},\"local_ip\":{\"ip\":\"198.51.100.23\",\"country\":\"Germany\"},\"connection_type\":\"proxy\",\"os\":\"Windows\",\"browser\":\"Chrome\",\"device_type\":\"desktop\",\"traffic_source\":{\"channel\":\"Google Ads\",\"referrer_domain\":\"google.com\",\"landing_url\":\"https://shop.example.com/signup?utm_source=google\\u0026utm_medium=cpc\\u0026gclid=abc123\",\"click_id_type\":\"gclid\",\"utm_source\":\"google\",\"utm_medium\":\"cpc\",\"utm_campaign\":\"\",\"utm_content\":\"\",\"utm_term\":\"\"},\"risk_score\":80,\"signals\":[{\"name\":\"proxy\",\"weight\":10},{\"name\":\"datacenter_ip\",\"weight\":10},{\"name\":\"antidetect_browser\",\"weight\":60}],\"detection_flags\":{\"vpn\":false,\"privacy_relay\":false,\"browser_vpn_proxy\":false,\"tor\":false,\"proxy\":true,\"datacenter_ip\":true,\"abuser\":false,\"os_mismatch\":false,\"os_not_detected\":false,\"timezone_mismatch\":false,\"anti_detect_browser\":true,\"browser_automation\":false,\"ip_mismatch\":true,\"incognito\":false,\"search_bot\":false,\"suspicious_paid_click\":true,\"javascript_disabled\":false,\"stun_not_checked\":false,\"check_incomplete\":false},\"observed_at\":\"2026-09-30T12:34:57.482913041Z\"}}", - "signature_header": "sha256=c4d44b7873625bdfda98cdb7a02d460f8492ca6a68fad8d147a5f30ad16f92a9", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T12:34:57.482913041Z\",\"data\":{\"identification_id\":\"a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d\",\"visitor_id\":\"e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b\",\"device_id\":\"d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a\",\"session_id\":\"b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e\",\"cookie_id\":\"c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f\",\"user_hid\":\"9f86d081884c7d659a2feaa0c55ad015\",\"domain\":\"example.com\",\"public_ip\":{\"ip\":\"203.0.113.24\",\"country\":\"Netherlands\"},\"local_ip\":{\"ip\":\"198.51.100.23\",\"country\":\"Germany\"},\"connection_type\":\"proxy\",\"os\":\"Windows\",\"browser\":\"Chrome\",\"device_type\":\"desktop\",\"traffic_source\":{\"channel\":\"Google Ads\",\"referrer_domain\":\"google.com\",\"landing_url\":\"https://shop.example.com/signup?utm_source=google\\u0026utm_medium=cpc\\u0026gclid=abc123\",\"click_id_type\":\"gclid\",\"utm_source\":\"google\",\"utm_medium\":\"cpc\",\"utm_campaign\":\"\",\"utm_content\":\"\",\"utm_term\":\"\"},\"risk_score\":80,\"signals\":[{\"name\":\"proxy\",\"weight\":10},{\"name\":\"datacenter_ip\",\"weight\":10},{\"name\":\"antidetect_browser\",\"weight\":60}],\"detection_flags\":{\"vpn\":false,\"privacy_relay\":false,\"browser_vpn_proxy\":false,\"tor\":false,\"proxy\":true,\"datacenter_ip\":true,\"abuser\":false,\"os_mismatch\":false,\"os_not_detected\":false,\"timezone_mismatch\":false,\"anti_detect_browser\":true,\"browser_automation\":false,\"ip_mismatch\":true,\"incognito\":false,\"search_bot\":false,\"suspicious_paid_click\":true,\"javascript_disabled\":false,\"stun_not_checked\":false,\"check_incomplete\":false},\"observed_at\":\"2026-09-30T12:34:57.482913041Z\"}}", + "signature_header": "sha256=ef87f1f65933ed7536236a3b2354c4ff657c6f7e852cb8d5a7a0ba2604951819", "valid": true, "note": "Server bodies escape & as \\u0026; verify the bytes as received", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTI6MzQ6NTcuNDgyOTEzMDQxWiIsImRhdGEiOnsicmVxdWVzdF9pZCI6IjAyZjFkOTczLTg0ZGItNDE1Ni1hN2Y3LWU3OTllNmJmMzg5YiIsInZpc2l0b3JfaWQiOiJiZGUwZTI0OS0yMGQ4LTQ1NDQtODM4Yy1lZDlhMGI2ZDdhMzYiLCJkZXZpY2VfaWQiOiJhYzdjMzAzZC05NzFiLTQxZDEtOGUyNS1jZDViNDZiNDZhZWQiLCJzZXNzaW9uX2lkIjoiYmRlNzg3NzgtZWZkMi00YzQ5LTk1MmYtMWYxMWI5YzA1ZjM1IiwiY29va2llX2lkIjoiNDQ0OWJiNTgtNTkwYy00NDRjLWFlMWYtZDFkZGM3NjhkYmRkIiwidXNlcl9oaWQiOiI5Zjg2ZDA4MTg4NGM3ZDY1OWEyZmVhYTBjNTVhZDAxNSIsImRvbWFpbiI6ImV4YW1wbGUuY29tIiwicHVibGljX2lwIjp7ImlwIjoiMjAzLjAuMTEzLjI0IiwiY291bnRyeSI6Ik5ldGhlcmxhbmRzIn0sImxvY2FsX2lwIjp7ImlwIjoiMTk4LjUxLjEwMC4yMyIsImNvdW50cnkiOiJHZXJtYW55In0sImNvbm5lY3Rpb25fdHlwZSI6InByb3h5Iiwib3MiOiJXaW5kb3dzIiwiYnJvd3NlciI6IkNocm9tZSIsImRldmljZV90eXBlIjoiZGVza3RvcCIsInRyYWZmaWNfc291cmNlIjp7ImNoYW5uZWwiOiJHb29nbGUgQWRzIiwicmVmZXJyZXJfZG9tYWluIjoiZ29vZ2xlLmNvbSIsImxhbmRpbmdfdXJsIjoiaHR0cHM6Ly9zaG9wLmV4YW1wbGUuY29tL3NpZ251cD91dG1fc291cmNlPWdvb2dsZVx1MDAyNnV0bV9tZWRpdW09Y3BjXHUwMDI2Z2NsaWQ9YWJjMTIzIiwiY2xpY2tfaWRfdHlwZSI6ImdjbGlkIiwidXRtX3NvdXJjZSI6Imdvb2dsZSIsInV0bV9tZWRpdW0iOiJjcGMiLCJ1dG1fY2FtcGFpZ24iOiIiLCJ1dG1fY29udGVudCI6IiIsInV0bV90ZXJtIjoiIn0sInJpc2tfc2NvcmUiOjgwLCJzaWduYWxzIjpbeyJuYW1lIjoicHJveHkiLCJ3ZWlnaHQiOjEwfSx7Im5hbWUiOiJkYXRhY2VudGVyX2lwIiwid2VpZ2h0IjoxMH0seyJuYW1lIjoiYW50aWRldGVjdF9icm93c2VyIiwid2VpZ2h0Ijo2MH1dLCJkZXRlY3Rpb25fZmxhZ3MiOnsidnBuIjpmYWxzZSwicHJpdmFjeV9yZWxheSI6ZmFsc2UsImJyb3dzZXJfdnBuX3Byb3h5IjpmYWxzZSwidG9yIjpmYWxzZSwicHJveHkiOnRydWUsImRhdGFjZW50ZXJfaXAiOnRydWUsImFidXNlciI6ZmFsc2UsIm9zX21pc21hdGNoIjpmYWxzZSwib3Nfbm90X2RldGVjdGVkIjpmYWxzZSwidGltZXpvbmVfbWlzbWF0Y2giOmZhbHNlLCJhbnRpX2RldGVjdF9icm93c2VyIjp0cnVlLCJicm93c2VyX2F1dG9tYXRpb24iOmZhbHNlLCJpcF9taXNtYXRjaCI6dHJ1ZSwiaW5jb2duaXRvIjpmYWxzZSwic2VhcmNoX2JvdCI6ZmFsc2UsInN1c3BpY2lvdXNfcGFpZF9jbGljayI6dHJ1ZSwiamF2YXNjcmlwdF9kaXNhYmxlZCI6ZmFsc2UsInN0dW5fbm90X2NoZWNrZWQiOmZhbHNlLCJjaGVja19pbmNvbXBsZXRlIjpmYWxzZX0sIm9ic2VydmVkX2F0IjoiMjAyNi0wOS0zMFQxMjozNDo1Ny40ODI5MTMwNDFaIn19" + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTI6MzQ6NTcuNDgyOTEzMDQxWiIsImRhdGEiOnsiaWRlbnRpZmljYXRpb25faWQiOiJhNWI3YzlkMS1lM2Y1LTRhN2ItOWMxZC0zZTVmN2E5YjFjM2QiLCJ2aXNpdG9yX2lkIjoiZTlmMWEzYjUtYzdkOS00ZTFmLThhM2ItNWM3ZDllMWYzYTViIiwiZGV2aWNlX2lkIjoiZDhlMGYyYTQtYjZjOC00ZDBlLWJmMmEtNGI2YzhkMGUyZjRhIiwic2Vzc2lvbl9pZCI6ImI2YzhkMGUyLWY0YTYtNGI4Yy04ZDBlLTJmNGE2YjhjMGQyZSIsImNvb2tpZV9pZCI6ImM3ZDllMWYzLWE1YjctNGM5ZC1hZTFmLTNhNWI3YzlkMWUzZiIsInVzZXJfaGlkIjoiOWY4NmQwODE4ODRjN2Q2NTlhMmZlYWEwYzU1YWQwMTUiLCJkb21haW4iOiJleGFtcGxlLmNvbSIsInB1YmxpY19pcCI6eyJpcCI6IjIwMy4wLjExMy4yNCIsImNvdW50cnkiOiJOZXRoZXJsYW5kcyJ9LCJsb2NhbF9pcCI6eyJpcCI6IjE5OC41MS4xMDAuMjMiLCJjb3VudHJ5IjoiR2VybWFueSJ9LCJjb25uZWN0aW9uX3R5cGUiOiJwcm94eSIsIm9zIjoiV2luZG93cyIsImJyb3dzZXIiOiJDaHJvbWUiLCJkZXZpY2VfdHlwZSI6ImRlc2t0b3AiLCJ0cmFmZmljX3NvdXJjZSI6eyJjaGFubmVsIjoiR29vZ2xlIEFkcyIsInJlZmVycmVyX2RvbWFpbiI6Imdvb2dsZS5jb20iLCJsYW5kaW5nX3VybCI6Imh0dHBzOi8vc2hvcC5leGFtcGxlLmNvbS9zaWdudXA/dXRtX3NvdXJjZT1nb29nbGVcdTAwMjZ1dG1fbWVkaXVtPWNwY1x1MDAyNmdjbGlkPWFiYzEyMyIsImNsaWNrX2lkX3R5cGUiOiJnY2xpZCIsInV0bV9zb3VyY2UiOiJnb29nbGUiLCJ1dG1fbWVkaXVtIjoiY3BjIiwidXRtX2NhbXBhaWduIjoiIiwidXRtX2NvbnRlbnQiOiIiLCJ1dG1fdGVybSI6IiJ9LCJyaXNrX3Njb3JlIjo4MCwic2lnbmFscyI6W3sibmFtZSI6InByb3h5Iiwid2VpZ2h0IjoxMH0seyJuYW1lIjoiZGF0YWNlbnRlcl9pcCIsIndlaWdodCI6MTB9LHsibmFtZSI6ImFudGlkZXRlY3RfYnJvd3NlciIsIndlaWdodCI6NjB9XSwiZGV0ZWN0aW9uX2ZsYWdzIjp7InZwbiI6ZmFsc2UsInByaXZhY3lfcmVsYXkiOmZhbHNlLCJicm93c2VyX3Zwbl9wcm94eSI6ZmFsc2UsInRvciI6ZmFsc2UsInByb3h5Ijp0cnVlLCJkYXRhY2VudGVyX2lwIjp0cnVlLCJhYnVzZXIiOmZhbHNlLCJvc19taXNtYXRjaCI6ZmFsc2UsIm9zX25vdF9kZXRlY3RlZCI6ZmFsc2UsInRpbWV6b25lX21pc21hdGNoIjpmYWxzZSwiYW50aV9kZXRlY3RfYnJvd3NlciI6dHJ1ZSwiYnJvd3Nlcl9hdXRvbWF0aW9uIjpmYWxzZSwiaXBfbWlzbWF0Y2giOnRydWUsImluY29nbml0byI6ZmFsc2UsInNlYXJjaF9ib3QiOmZhbHNlLCJzdXNwaWNpb3VzX3BhaWRfY2xpY2siOnRydWUsImphdmFzY3JpcHRfZGlzYWJsZWQiOmZhbHNlLCJzdHVuX25vdF9jaGVja2VkIjpmYWxzZSwiY2hlY2tfaW5jb21wbGV0ZSI6ZmFsc2V9LCJvYnNlcnZlZF9hdCI6IjIwMjYtMDktMzBUMTI6MzQ6NTcuNDgyOTEzMDQxWiJ9fQ==" }, { "name": "invalid_scored_unescaped_reserialization", "secret": "whsec_00112233445566778899aabbccddeeff", - "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T12:34:57.482913041Z\",\"data\":{\"request_id\":\"02f1d973-84db-4156-a7f7-e799e6bf389b\",\"visitor_id\":\"bde0e249-20d8-4544-838c-ed9a0b6d7a36\",\"device_id\":\"ac7c303d-971b-41d1-8e25-cd5b46b46aed\",\"session_id\":\"bde78778-efd2-4c49-952f-1f11b9c05f35\",\"cookie_id\":\"4449bb58-590c-444c-ae1f-d1ddc768dbdd\",\"user_hid\":\"9f86d081884c7d659a2feaa0c55ad015\",\"domain\":\"example.com\",\"public_ip\":{\"ip\":\"203.0.113.24\",\"country\":\"Netherlands\"},\"local_ip\":{\"ip\":\"198.51.100.23\",\"country\":\"Germany\"},\"connection_type\":\"proxy\",\"os\":\"Windows\",\"browser\":\"Chrome\",\"device_type\":\"desktop\",\"traffic_source\":{\"channel\":\"Google Ads\",\"referrer_domain\":\"google.com\",\"landing_url\":\"https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123\",\"click_id_type\":\"gclid\",\"utm_source\":\"google\",\"utm_medium\":\"cpc\",\"utm_campaign\":\"\",\"utm_content\":\"\",\"utm_term\":\"\"},\"risk_score\":80,\"signals\":[{\"name\":\"proxy\",\"weight\":10},{\"name\":\"datacenter_ip\",\"weight\":10},{\"name\":\"antidetect_browser\",\"weight\":60}],\"detection_flags\":{\"vpn\":false,\"privacy_relay\":false,\"browser_vpn_proxy\":false,\"tor\":false,\"proxy\":true,\"datacenter_ip\":true,\"abuser\":false,\"os_mismatch\":false,\"os_not_detected\":false,\"timezone_mismatch\":false,\"anti_detect_browser\":true,\"browser_automation\":false,\"ip_mismatch\":true,\"incognito\":false,\"search_bot\":false,\"suspicious_paid_click\":true,\"javascript_disabled\":false,\"stun_not_checked\":false,\"check_incomplete\":false},\"observed_at\":\"2026-09-30T12:34:57.482913041Z\"}}", - "signature_header": "sha256=c4d44b7873625bdfda98cdb7a02d460f8492ca6a68fad8d147a5f30ad16f92a9", + "body": "{\"event_type\":\"identification.scored\",\"schema_version\":\"2026-06-01\",\"created_at\":\"2026-09-30T12:34:57.482913041Z\",\"data\":{\"identification_id\":\"a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d\",\"visitor_id\":\"e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b\",\"device_id\":\"d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a\",\"session_id\":\"b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e\",\"cookie_id\":\"c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f\",\"user_hid\":\"9f86d081884c7d659a2feaa0c55ad015\",\"domain\":\"example.com\",\"public_ip\":{\"ip\":\"203.0.113.24\",\"country\":\"Netherlands\"},\"local_ip\":{\"ip\":\"198.51.100.23\",\"country\":\"Germany\"},\"connection_type\":\"proxy\",\"os\":\"Windows\",\"browser\":\"Chrome\",\"device_type\":\"desktop\",\"traffic_source\":{\"channel\":\"Google Ads\",\"referrer_domain\":\"google.com\",\"landing_url\":\"https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123\",\"click_id_type\":\"gclid\",\"utm_source\":\"google\",\"utm_medium\":\"cpc\",\"utm_campaign\":\"\",\"utm_content\":\"\",\"utm_term\":\"\"},\"risk_score\":80,\"signals\":[{\"name\":\"proxy\",\"weight\":10},{\"name\":\"datacenter_ip\",\"weight\":10},{\"name\":\"antidetect_browser\",\"weight\":60}],\"detection_flags\":{\"vpn\":false,\"privacy_relay\":false,\"browser_vpn_proxy\":false,\"tor\":false,\"proxy\":true,\"datacenter_ip\":true,\"abuser\":false,\"os_mismatch\":false,\"os_not_detected\":false,\"timezone_mismatch\":false,\"anti_detect_browser\":true,\"browser_automation\":false,\"ip_mismatch\":true,\"incognito\":false,\"search_bot\":false,\"suspicious_paid_click\":true,\"javascript_disabled\":false,\"stun_not_checked\":false,\"check_incomplete\":false},\"observed_at\":\"2026-09-30T12:34:57.482913041Z\"}}", + "signature_header": "sha256=ef87f1f65933ed7536236a3b2354c4ff657c6f7e852cb8d5a7a0ba2604951819", "valid": false, "note": "Re-serializing (unescaping \\u0026) changes the bytes", - "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTI6MzQ6NTcuNDgyOTEzMDQxWiIsImRhdGEiOnsicmVxdWVzdF9pZCI6IjAyZjFkOTczLTg0ZGItNDE1Ni1hN2Y3LWU3OTllNmJmMzg5YiIsInZpc2l0b3JfaWQiOiJiZGUwZTI0OS0yMGQ4LTQ1NDQtODM4Yy1lZDlhMGI2ZDdhMzYiLCJkZXZpY2VfaWQiOiJhYzdjMzAzZC05NzFiLTQxZDEtOGUyNS1jZDViNDZiNDZhZWQiLCJzZXNzaW9uX2lkIjoiYmRlNzg3NzgtZWZkMi00YzQ5LTk1MmYtMWYxMWI5YzA1ZjM1IiwiY29va2llX2lkIjoiNDQ0OWJiNTgtNTkwYy00NDRjLWFlMWYtZDFkZGM3NjhkYmRkIiwidXNlcl9oaWQiOiI5Zjg2ZDA4MTg4NGM3ZDY1OWEyZmVhYTBjNTVhZDAxNSIsImRvbWFpbiI6ImV4YW1wbGUuY29tIiwicHVibGljX2lwIjp7ImlwIjoiMjAzLjAuMTEzLjI0IiwiY291bnRyeSI6Ik5ldGhlcmxhbmRzIn0sImxvY2FsX2lwIjp7ImlwIjoiMTk4LjUxLjEwMC4yMyIsImNvdW50cnkiOiJHZXJtYW55In0sImNvbm5lY3Rpb25fdHlwZSI6InByb3h5Iiwib3MiOiJXaW5kb3dzIiwiYnJvd3NlciI6IkNocm9tZSIsImRldmljZV90eXBlIjoiZGVza3RvcCIsInRyYWZmaWNfc291cmNlIjp7ImNoYW5uZWwiOiJHb29nbGUgQWRzIiwicmVmZXJyZXJfZG9tYWluIjoiZ29vZ2xlLmNvbSIsImxhbmRpbmdfdXJsIjoiaHR0cHM6Ly9zaG9wLmV4YW1wbGUuY29tL3NpZ251cD91dG1fc291cmNlPWdvb2dsZSZ1dG1fbWVkaXVtPWNwYyZnY2xpZD1hYmMxMjMiLCJjbGlja19pZF90eXBlIjoiZ2NsaWQiLCJ1dG1fc291cmNlIjoiZ29vZ2xlIiwidXRtX21lZGl1bSI6ImNwYyIsInV0bV9jYW1wYWlnbiI6IiIsInV0bV9jb250ZW50IjoiIiwidXRtX3Rlcm0iOiIifSwicmlza19zY29yZSI6ODAsInNpZ25hbHMiOlt7Im5hbWUiOiJwcm94eSIsIndlaWdodCI6MTB9LHsibmFtZSI6ImRhdGFjZW50ZXJfaXAiLCJ3ZWlnaHQiOjEwfSx7Im5hbWUiOiJhbnRpZGV0ZWN0X2Jyb3dzZXIiLCJ3ZWlnaHQiOjYwfV0sImRldGVjdGlvbl9mbGFncyI6eyJ2cG4iOmZhbHNlLCJwcml2YWN5X3JlbGF5IjpmYWxzZSwiYnJvd3Nlcl92cG5fcHJveHkiOmZhbHNlLCJ0b3IiOmZhbHNlLCJwcm94eSI6dHJ1ZSwiZGF0YWNlbnRlcl9pcCI6dHJ1ZSwiYWJ1c2VyIjpmYWxzZSwib3NfbWlzbWF0Y2giOmZhbHNlLCJvc19ub3RfZGV0ZWN0ZWQiOmZhbHNlLCJ0aW1lem9uZV9taXNtYXRjaCI6ZmFsc2UsImFudGlfZGV0ZWN0X2Jyb3dzZXIiOnRydWUsImJyb3dzZXJfYXV0b21hdGlvbiI6ZmFsc2UsImlwX21pc21hdGNoIjp0cnVlLCJpbmNvZ25pdG8iOmZhbHNlLCJzZWFyY2hfYm90IjpmYWxzZSwic3VzcGljaW91c19wYWlkX2NsaWNrIjp0cnVlLCJqYXZhc2NyaXB0X2Rpc2FibGVkIjpmYWxzZSwic3R1bl9ub3RfY2hlY2tlZCI6ZmFsc2UsImNoZWNrX2luY29tcGxldGUiOmZhbHNlfSwib2JzZXJ2ZWRfYXQiOiIyMDI2LTA5LTMwVDEyOjM0OjU3LjQ4MjkxMzA0MVoifX0=" + "body_base64": "eyJldmVudF90eXBlIjoiaWRlbnRpZmljYXRpb24uc2NvcmVkIiwic2NoZW1hX3ZlcnNpb24iOiIyMDI2LTA2LTAxIiwiY3JlYXRlZF9hdCI6IjIwMjYtMDktMzBUMTI6MzQ6NTcuNDgyOTEzMDQxWiIsImRhdGEiOnsiaWRlbnRpZmljYXRpb25faWQiOiJhNWI3YzlkMS1lM2Y1LTRhN2ItOWMxZC0zZTVmN2E5YjFjM2QiLCJ2aXNpdG9yX2lkIjoiZTlmMWEzYjUtYzdkOS00ZTFmLThhM2ItNWM3ZDllMWYzYTViIiwiZGV2aWNlX2lkIjoiZDhlMGYyYTQtYjZjOC00ZDBlLWJmMmEtNGI2YzhkMGUyZjRhIiwic2Vzc2lvbl9pZCI6ImI2YzhkMGUyLWY0YTYtNGI4Yy04ZDBlLTJmNGE2YjhjMGQyZSIsImNvb2tpZV9pZCI6ImM3ZDllMWYzLWE1YjctNGM5ZC1hZTFmLTNhNWI3YzlkMWUzZiIsInVzZXJfaGlkIjoiOWY4NmQwODE4ODRjN2Q2NTlhMmZlYWEwYzU1YWQwMTUiLCJkb21haW4iOiJleGFtcGxlLmNvbSIsInB1YmxpY19pcCI6eyJpcCI6IjIwMy4wLjExMy4yNCIsImNvdW50cnkiOiJOZXRoZXJsYW5kcyJ9LCJsb2NhbF9pcCI6eyJpcCI6IjE5OC41MS4xMDAuMjMiLCJjb3VudHJ5IjoiR2VybWFueSJ9LCJjb25uZWN0aW9uX3R5cGUiOiJwcm94eSIsIm9zIjoiV2luZG93cyIsImJyb3dzZXIiOiJDaHJvbWUiLCJkZXZpY2VfdHlwZSI6ImRlc2t0b3AiLCJ0cmFmZmljX3NvdXJjZSI6eyJjaGFubmVsIjoiR29vZ2xlIEFkcyIsInJlZmVycmVyX2RvbWFpbiI6Imdvb2dsZS5jb20iLCJsYW5kaW5nX3VybCI6Imh0dHBzOi8vc2hvcC5leGFtcGxlLmNvbS9zaWdudXA/dXRtX3NvdXJjZT1nb29nbGUmdXRtX21lZGl1bT1jcGMmZ2NsaWQ9YWJjMTIzIiwiY2xpY2tfaWRfdHlwZSI6ImdjbGlkIiwidXRtX3NvdXJjZSI6Imdvb2dsZSIsInV0bV9tZWRpdW0iOiJjcGMiLCJ1dG1fY2FtcGFpZ24iOiIiLCJ1dG1fY29udGVudCI6IiIsInV0bV90ZXJtIjoiIn0sInJpc2tfc2NvcmUiOjgwLCJzaWduYWxzIjpbeyJuYW1lIjoicHJveHkiLCJ3ZWlnaHQiOjEwfSx7Im5hbWUiOiJkYXRhY2VudGVyX2lwIiwid2VpZ2h0IjoxMH0seyJuYW1lIjoiYW50aWRldGVjdF9icm93c2VyIiwid2VpZ2h0Ijo2MH1dLCJkZXRlY3Rpb25fZmxhZ3MiOnsidnBuIjpmYWxzZSwicHJpdmFjeV9yZWxheSI6ZmFsc2UsImJyb3dzZXJfdnBuX3Byb3h5IjpmYWxzZSwidG9yIjpmYWxzZSwicHJveHkiOnRydWUsImRhdGFjZW50ZXJfaXAiOnRydWUsImFidXNlciI6ZmFsc2UsIm9zX21pc21hdGNoIjpmYWxzZSwib3Nfbm90X2RldGVjdGVkIjpmYWxzZSwidGltZXpvbmVfbWlzbWF0Y2giOmZhbHNlLCJhbnRpX2RldGVjdF9icm93c2VyIjp0cnVlLCJicm93c2VyX2F1dG9tYXRpb24iOmZhbHNlLCJpcF9taXNtYXRjaCI6dHJ1ZSwiaW5jb2duaXRvIjpmYWxzZSwic2VhcmNoX2JvdCI6ZmFsc2UsInN1c3BpY2lvdXNfcGFpZF9jbGljayI6dHJ1ZSwiamF2YXNjcmlwdF9kaXNhYmxlZCI6ZmFsc2UsInN0dW5fbm90X2NoZWNrZWQiOmZhbHNlLCJjaGVja19pbmNvbXBsZXRlIjpmYWxzZX0sIm9ic2VydmVkX2F0IjoiMjAyNi0wOS0zMFQxMjozNDo1Ny40ODI5MTMwNDFaIn19" }, { "name": "valid_rotation_second_secret_matches", diff --git a/test/data/webhook-test-delivery.json b/test/data/webhook-test-delivery.json index 8809d3f..79fbd13 100644 --- a/test/data/webhook-test-delivery.json +++ b/test/data/webhook-test-delivery.json @@ -39,7 +39,7 @@ "country": "BY", "ip": "203.0.113.10" }, - "request_id": "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11", + "identification_id": "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11", "risk_score": 30, "session_id": "3a2b1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d", "signals": [ diff --git a/test/entries.test.ts b/test/entries.test.ts index 799d4a3..f43964c 100644 --- a/test/entries.test.ts +++ b/test/entries.test.ts @@ -148,7 +148,7 @@ describe('edge entry', () => { const fake = fakeFetch([historyPage([historyRow(REQUEST_ID)])]); const client = new edge.ShieldLabs({ apiKey: TEST_API_KEY, fetch: fake.fetch }); await expect(client.identifications.get(REQUEST_ID)).resolves.toMatchObject({ - request_id: REQUEST_ID, + identification_id: REQUEST_ID, }); }); }); diff --git a/test/errors.test.ts b/test/errors.test.ts index f88f9d5..8f7911d 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -70,7 +70,7 @@ describe('error-responses.json', () => { surface === 'history' ? (() => { const { client, fake } = historyClient([response], 1); - return { promise: client.history.search('request_id', REQUEST_ID), fake }; + return { promise: client.history.search('identification_id', REQUEST_ID), fake }; })() : (() => { const { client, fake } = managementClient([response], 1); @@ -202,7 +202,7 @@ describe('Retry-After', () => { ], 0, ); - const { error } = await settle(client.history.search('request_id', REQUEST_ID)); + const { error } = await settle(client.history.search('identification_id', REQUEST_ID)); expect((error as RateLimitError).retryAfter).toBe(4); }); }); @@ -219,7 +219,7 @@ describe('retries', () => { new TypeError('fetch failed'), historyPage([]), ]); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); expect((await result).value).toEqual({ data: [], total: 0 }); expect(times(fake.calls)).toEqual([0, 250, 750]); @@ -228,7 +228,7 @@ describe('retries', () => { it('uses the full nominal delay at the top of the jitter range', async () => { vi.spyOn(Math, 'random').mockReturnValue(0.999999); const { client, fake } = historyClient([rawResponse('{"error":"internal error"}\n', 500)], 2); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); expect((await result).error).toBeInstanceOf(ServerError); const [first, second, third] = times(fake.calls); @@ -242,7 +242,7 @@ describe('retries', () => { it('caps the backoff at 8 seconds', async () => { vi.spyOn(Math, 'random').mockReturnValue(0.999999); const { client, fake } = historyClient([new TypeError('fetch failed')], 6); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); expect((await result).error).toBeInstanceOf(ConnectionError); const gaps = times(fake.calls) @@ -261,7 +261,7 @@ describe('retries', () => { }), historyPage([]), ]); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); expect((await result).value).toEqual({ data: [], total: 0 }); expect(times(fake.calls)).toEqual([0, 2000, 12_000]); @@ -273,7 +273,7 @@ describe('retries', () => { rawResponse('{"error":"too many requests"}\n', 429), historyPage([]), ]); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); await result; expect(times(fake.calls)).toEqual([0, 1000]); @@ -290,7 +290,7 @@ describe('retries', () => { }), historyPage([]), ]); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); expect((await result).value).toEqual({ data: [], total: 0 }); expect(times(fake.calls)).toEqual([0, 0, 3000]); @@ -298,7 +298,7 @@ describe('retries', () => { it('does not retry with maxRetries 0', async () => { const { client, fake } = historyClient([new TypeError('fetch failed')], 0); - const { error } = await settle(client.history.search('request_id', REQUEST_ID)); + const { error } = await settle(client.history.search('identification_id', REQUEST_ID)); expect(error).toBeInstanceOf(ConnectionError); expect((error as ConnectionError).cause).toBeInstanceOf(TypeError); expect((error as Error).message).toContain('fetch failed'); @@ -311,7 +311,7 @@ describe('retries', () => { rawResponse('{"error":"b"}', 502), rawResponse('{"error":"c"}', 503), ]); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); const { error } = await result; expect((error as ServerError).status).toBe(503); @@ -320,7 +320,7 @@ describe('retries', () => { it.each([400, 401, 402, 403, 404])('never retries %s', async (status) => { const { client, fake } = historyClient([rawResponse('', status, null)], 5); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); expect((await result).error).toBeInstanceOf(ApiError); expect(fake.calls).toHaveLength(1); @@ -363,7 +363,7 @@ describe('retries', () => { timeout: 1500, maxRetries: 1, }); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); const { error } = await result; expect(error).toBeInstanceOf(TimeoutError); @@ -384,7 +384,7 @@ describe('retries', () => { timeout: 1000, maxRetries: 0, }); - const result = settle(client.history.search('request_id', REQUEST_ID)); + const result = settle(client.history.search('identification_id', REQUEST_ID)); await vi.runAllTimersAsync(); expect((await result).error).toBeInstanceOf(TimeoutError); }); @@ -394,7 +394,7 @@ describe('retries', () => { const controller = new AbortController(); const client = new ShieldLabs({ apiKey: TEST_API_KEY, fetch: hanging.fetch, maxRetries: 3 }); const result = settle( - client.history.search('request_id', REQUEST_ID, { signal: controller.signal }), + client.history.search('identification_id', REQUEST_ID, { signal: controller.signal }), ); await vi.advanceTimersByTimeAsync(5); controller.abort(new Error('caller gave up')); @@ -407,7 +407,7 @@ describe('retries', () => { const controller = new AbortController(); const { client, fake } = historyClient([new TypeError('fetch failed')], 3); const result = settle( - client.history.search('request_id', REQUEST_ID, { signal: controller.signal }), + client.history.search('identification_id', REQUEST_ID, { signal: controller.signal }), ); await vi.advanceTimersByTimeAsync(100); controller.abort(new Error('stop')); @@ -438,7 +438,7 @@ describe('connection errors', () => { ], ])('name the network cause %j', async (cause, message) => { const { client } = historyClient([networkError(cause)], 0); - const { error } = await settle(client.history.search('request_id', REQUEST_ID)); + const { error } = await settle(client.history.search('identification_id', REQUEST_ID)); expect(error).toBeInstanceOf(ConnectionError); expect((error as Error).message).toBe(message); expect((error as Error).cause).toBeInstanceOf(TypeError); @@ -449,7 +449,7 @@ describe('connection errors', () => { `Headers.append: "Bearer ${TEST_API_KEY}" is an invalid header value.`, ); const { client } = historyClient([leaky], 0); - const { error } = await settle(client.history.search('request_id', REQUEST_ID)); + const { error } = await settle(client.history.search('identification_id', REQUEST_ID)); expect(error).toBeInstanceOf(ConnectionError); expect((error as Error).message).toMatch(/withheld because it contained a credential/); expect((error as Error).cause).toBeUndefined(); @@ -477,13 +477,13 @@ describe('connection errors', () => { it('check causes that are plain strings', async () => { const leaky = new TypeError('fetch failed', { cause: `Bearer ${TEST_API_KEY}` }); const { client } = historyClient([leaky], 0); - const { error } = await settle(client.history.search('request_id', REQUEST_ID)); + const { error } = await settle(client.history.search('identification_id', REQUEST_ID)); expect((error as Error).message).not.toContain(TEST_API_KEY); expect((error as Error).cause).toBeUndefined(); const harmless = new TypeError('fetch failed', { cause: 'socket closed' }); const second = historyClient([harmless], 0); - const result = await settle(second.client.history.search('request_id', REQUEST_ID)); + const result = await settle(second.client.history.search('identification_id', REQUEST_ID)); expect((result.error as Error).message).toBe('History API request failed: fetch failed'); expect((result.error as Error).cause).toBe(harmless); }); @@ -501,7 +501,7 @@ describe('connection errors', () => { const looping = new Error('socket hang up') as Error & { self?: unknown }; looping.self = looping; const { client } = historyClient([looping], 0); - const { error } = await settle(client.history.search('request_id', REQUEST_ID)); + const { error } = await settle(client.history.search('identification_id', REQUEST_ID)); expect((error as Error).message).toBe('History API request failed: socket hang up'); }); }); @@ -515,7 +515,7 @@ describe('fetch implementations', () => { throw new Error('bad init'); }, }); - const { error } = await settle(client.history.search('request_id', REQUEST_ID)); + const { error } = await settle(client.history.search('identification_id', REQUEST_ID)); expect(error).toBeInstanceOf(ConnectionError); }); @@ -526,7 +526,7 @@ describe('fetch implementations', () => { // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- deliberately not an Error fetch: () => Promise.reject('offline'), }); - const { error } = await settle(client.history.search('request_id', REQUEST_ID)); + const { error } = await settle(client.history.search('identification_id', REQUEST_ID)); expect(error).toBeInstanceOf(ConnectionError); expect((error as Error).message).toContain('offline'); }); @@ -535,7 +535,7 @@ describe('fetch implementations', () => { const client = new ShieldLabs({ apiKey: TEST_API_KEY, maxRetries: 0 }); const fake = fakeFetch([historyPage([])]); vi.stubGlobal('fetch', fake.fetch); - await expect(client.history.search('request_id', REQUEST_ID)).resolves.toEqual({ + await expect(client.history.search('identification_id', REQUEST_ID)).resolves.toEqual({ data: [], total: 0, }); @@ -545,7 +545,7 @@ describe('fetch implementations', () => { it('reports a missing global fetch as a ValidationError', async () => { vi.stubGlobal('fetch', undefined); const client = new ShieldLabs({ apiKey: TEST_API_KEY, maxRetries: 0 }); - await expect(client.history.search('request_id', REQUEST_ID)).rejects.toThrow(ValidationError); + await expect(client.history.search('identification_id', REQUEST_ID)).rejects.toThrow(ValidationError); }); it('passes an AuthenticationError from custom fetch responses through unchanged', async () => { @@ -553,7 +553,7 @@ describe('fetch implementations', () => { [rawResponse('{"error":"invalid api key"}\n', 401, 'text/plain; charset=utf-8')], 0, ); - const { error } = await settle(client.history.search('request_id', REQUEST_ID)); + const { error } = await settle(client.history.search('identification_id', REQUEST_ID)); expect(error).toBeInstanceOf(AuthenticationError); expect((error as AuthenticationError).headers.get('content-type')).toBe( 'text/plain; charset=utf-8', diff --git a/test/helpers.ts b/test/helpers.ts index b396a61..d48b7b9 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -119,11 +119,11 @@ export function historyPage(rows: unknown[], total = rows.length): ResponseFacto /** A History row copied from the fixture page, with a new request ID and optional changes. */ export function historyRow( - requestId: string, + identificationId: string, overrides: Record = {}, ): Record { const page = loadJson<{ data: Record[] }>('history-page.json'); - return { ...(page.data[1] as Record), request_id: requestId, ...overrides }; + return { ...(page.data[1] as Record), identification_id: identificationId, ...overrides }; } /** Formats a Date the way History rows carry created_at. */ diff --git a/test/history.test.ts b/test/history.test.ts index 0a5f6c2..8b280fb 100644 --- a/test/history.test.ts +++ b/test/history.test.ts @@ -38,11 +38,11 @@ function client(replies: Parameters[0], baseUrl?: string) { describe('history.search requests', () => { it('sends a GET to the History API with credentials and SDK headers', async () => { const { shieldlabs, fake } = client([jsonResponse(loadJson('history-empty.json'))]); - await shieldlabs.history.search('request_id', REQUEST_ID); + await shieldlabs.history.search('identification_id', REQUEST_ID); expect(fake.calls).toHaveLength(1); const [call] = fake.calls; expect(call?.url).toBe( - `https://account.shieldlabs.ai/api/v1/history/request_id/${REQUEST_ID}?limit=20&offset=0`, + `https://account.shieldlabs.ai/api/v1/history/identification_id/${REQUEST_ID}?limit=20&offset=0`, ); expect(call?.init.method).toBe('GET'); expect(call?.init.headers).toMatchObject({ @@ -155,7 +155,7 @@ describe('history.search requests', () => { ['http://127.0.0.1:8080', 'http://127.0.0.1:8080/api/v1/history/'], ])('base URL %s never produces /api/api', async (baseUrl, prefix) => { const { shieldlabs, fake } = client([historyPage([])], baseUrl); - await shieldlabs.history.search('request_id', REQUEST_ID); + await shieldlabs.history.search('identification_id', REQUEST_ID); expect(fake.calls[0]?.url.startsWith(prefix)).toBe(true); expect(fake.calls[0]?.url).not.toContain('/api/api/'); }); @@ -180,7 +180,7 @@ describe('history.search responses', () => { it('returns an empty page for history-empty.json', async () => { const { shieldlabs } = client([jsonResponse(loadJson('history-empty.json'))]); - await expect(shieldlabs.history.search('request_id', REQUEST_ID)).resolves.toEqual({ + await expect(shieldlabs.history.search('identification_id', REQUEST_ID)).resolves.toEqual({ data: [], total: 0, }); @@ -189,7 +189,7 @@ describe('history.search responses', () => { it('parses the exact History body format (text with a trailing newline)', async () => { const body = `${JSON.stringify(loadJson('history-empty.json'))}\n`; const { shieldlabs } = client([rawResponse(body, 200, 'application/json')]); - await expect(shieldlabs.history.search('request_id', REQUEST_ID)).resolves.toEqual({ + await expect(shieldlabs.history.search('identification_id', REQUEST_ID)).resolves.toEqual({ data: [], total: 0, }); @@ -197,7 +197,7 @@ describe('history.search responses', () => { it('skips rows that are not objects and falls back to the row count for a missing total', async () => { const { shieldlabs } = client([jsonResponse({ data: [historyRow(REQUEST_ID), 5, null, 'x'] })]); - const page = await shieldlabs.history.search('request_id', REQUEST_ID); + const page = await shieldlabs.history.search('identification_id', REQUEST_ID); expect(page.data).toHaveLength(1); expect(page.total).toBe(1); }); @@ -208,14 +208,14 @@ describe('history.search responses', () => { ['a missing data field', jsonResponse({ total: 3 })], ])('rejects %s with ApiError', async (_label, response) => { const { shieldlabs } = client([response]); - const { error } = await settle(shieldlabs.history.search('request_id', REQUEST_ID)); + const { error } = await settle(shieldlabs.history.search('identification_id', REQUEST_ID)); expect(error).toBeInstanceOf(ApiError); expect((error as ApiError).status).toBe(200); }); it('rejects a 200 response that is not JSON', async () => { const { shieldlabs } = client([rawResponse('maintenance', 200, 'text/html')]); - const { error } = await settle(shieldlabs.history.search('request_id', REQUEST_ID)); + const { error } = await settle(shieldlabs.history.search('identification_id', REQUEST_ID)); expect(error).toBeInstanceOf(ApiError); expect((error as ApiError).body).toBe('maintenance'); expect((error as ApiError).message).toMatch(/not valid JSON/); @@ -231,8 +231,8 @@ describe('history.search responses', () => { '22222222-2222-4222-8222-222222222222', '33333333-3333-4333-8333-333333333333', ]; - const pages = await Promise.all(ids.map((id) => shieldlabs.history.search('request_id', id))); - expect(pages.map((p) => p.data[0]?.request_id)).toEqual(ids); + const pages = await Promise.all(ids.map((id) => shieldlabs.history.search('identification_id', id))); + expect(pages.map((p) => p.data[0]?.identification_id)).toEqual(ids); expect(fake.calls).toHaveLength(3); }); }); @@ -251,7 +251,7 @@ describe('history.iterate', () => { async function collect(iterable: AsyncIterable): Promise { const out: string[] = []; - for await (const identification of iterable) out.push(identification.request_id); + for await (const identification of iterable) out.push(identification.identification_id); return out; } @@ -335,7 +335,7 @@ describe('history.iterate', () => { pageSize: 1, signal: controller.signal, })) { - seen.push(identification.request_id); + seen.push(identification.identification_id); controller.abort(new Error('stopped by caller')); } })(); diff --git a/test/identifications.test.ts b/test/identifications.test.ts index e082ebc..a859147 100644 --- a/test/identifications.test.ts +++ b/test/identifications.test.ts @@ -118,10 +118,10 @@ describe('identifications.get without waiting', () => { it('reads once with limit 1 and returns the identification', async () => { const { shieldlabs, fake } = client([found()]); const identification = await shieldlabs.identifications.get(REQUEST_ID, { wait: false }); - expect(identification?.request_id).toBe(REQUEST_ID); + expect(identification?.identification_id).toBe(REQUEST_ID); expect(fake.calls).toHaveLength(1); const url = new URL(fake.calls[0]?.url ?? ''); - expect(url.pathname).toBe(`/api/v1/history/request_id/${REQUEST_ID}`); + expect(url.pathname).toBe(`/api/v1/history/identification_id/${REQUEST_ID}`); expect(url.search).toBe('?limit=1&offset=0'); }); @@ -135,7 +135,7 @@ describe('identifications.get without waiting', () => { const { shieldlabs, fake } = client([serverError(), found()]); const result = settle(shieldlabs.identifications.get(REQUEST_ID, { wait: false })); await vi.runAllTimersAsync(); - expect((await result).value?.request_id).toBe(REQUEST_ID); + expect((await result).value?.identification_id).toBe(REQUEST_ID); expect(fake.calls).toHaveLength(2); }); @@ -150,7 +150,7 @@ describe('identifications.get: timeout is the total budget', () => { it('returns at once when the first poll finds the row', async () => { const { shieldlabs, fake } = client([found()]); await expect(shieldlabs.identifications.get(REQUEST_ID)).resolves.toMatchObject({ - request_id: REQUEST_ID, + identification_id: REQUEST_ID, }); expect(fake.calls).toHaveLength(1); }); @@ -160,7 +160,7 @@ describe('identifications.get: timeout is the total budget', () => { const result = outcome(shieldlabs.identifications.get(REQUEST_ID)); await vi.runAllTimersAsync(); const { value, at } = await result; - expect(value?.request_id).toBe(REQUEST_ID); + expect(value?.identification_id).toBe(REQUEST_ID); expect(at).toBe(750); expect(offsets(fake.calls)).toEqual([0, 250, 750]); }); @@ -314,7 +314,7 @@ describe('identifications.get: the last poll runs at the deadline', () => { await vi.runAllTimersAsync(); const { value, at } = await result; // The 429 at 1750 asks for 1 s and 1.25 s are left: the 1.5 s step is cut to the deadline. - expect(value?.request_id).toBe(REQUEST_ID); + expect(value?.identification_id).toBe(REQUEST_ID); expect(at).toBe(3000); expect(offsets(fake.calls)).toEqual([0, 250, 750, 1750, 3000]); }); @@ -430,7 +430,7 @@ describe('identifications.get: transient errors keep polling', () => { const { value, at } = await result; // Waits of 250 ms, 500 ms, 1 s (also the minimum after a 429) and 1.5 s; the poll at 3250 // times out after its 1 s attempt and the next one follows the 2 s step. - expect(value?.request_id).toBe(REQUEST_ID); + expect(value?.identification_id).toBe(REQUEST_ID); expect(recorder.starts()).toEqual([0, 250, 750, 1750, 3250, 6250]); expect(at).toBe(6250); }); @@ -504,7 +504,7 @@ describe('identifications.get: 429 while waiting', () => { const { shieldlabs, fake } = client([tooMany(), found()]); const result = settle(shieldlabs.identifications.get(REQUEST_ID)); await vi.runAllTimersAsync(); - expect((await result).value?.request_id).toBe(REQUEST_ID); + expect((await result).value?.identification_id).toBe(REQUEST_ID); expect(offsets(fake.calls)).toEqual([0, 1000]); }); @@ -512,7 +512,7 @@ describe('identifications.get: 429 while waiting', () => { const { shieldlabs, fake } = client([empty(), empty(), empty(), tooMany(), found()]); const result = settle(shieldlabs.identifications.get(REQUEST_ID)); await vi.runAllTimersAsync(); - expect((await result).value?.request_id).toBe(REQUEST_ID); + expect((await result).value?.identification_id).toBe(REQUEST_ID); expect(offsets(fake.calls)).toEqual([0, 250, 750, 1750, 3250]); }); @@ -530,7 +530,7 @@ describe('identifications.get: 429 while waiting', () => { const { shieldlabs, fake } = client([empty(), tooMany({ 'retry-after': '3' }), found()]); const result = settle(shieldlabs.identifications.get(REQUEST_ID)); await vi.runAllTimersAsync(); - expect((await result).value?.request_id).toBe(REQUEST_ID); + expect((await result).value?.identification_id).toBe(REQUEST_ID); expect(offsets(fake.calls)).toEqual([0, 250, 3250]); }); @@ -541,7 +541,7 @@ describe('identifications.get: 429 while waiting', () => { const { shieldlabs, fake } = client([empty(), tooMany({ 'retry-after': retryAfter }), found()]); const result = settle(shieldlabs.identifications.get(REQUEST_ID)); await vi.runAllTimersAsync(); - expect((await result).value?.request_id).toBe(REQUEST_ID); + expect((await result).value?.identification_id).toBe(REQUEST_ID); // The 429 at 250 would be followed by the 500 ms step: the 1 s floor wins. expect(offsets(fake.calls)).toEqual([0, 250, 1250]); }); @@ -551,7 +551,7 @@ describe('identifications.get: 429 while waiting', () => { const { shieldlabs, fake } = client(replies); const result = settle(shieldlabs.identifications.get(REQUEST_ID)); await vi.runAllTimersAsync(); - expect((await result).value?.request_id).toBe(REQUEST_ID); + expect((await result).value?.identification_id).toBe(REQUEST_ID); // The 429 at 3250 asks for 1 s; the scheduled 2 s step is longer and applies. expect(offsets(fake.calls)).toEqual([0, 250, 750, 1750, 3250, 5250]); }); @@ -565,7 +565,7 @@ describe('identifications.get: 429 while waiting', () => { ]); const result = settle(shieldlabs.identifications.get(REQUEST_ID, { pollInterval: 100 })); await vi.runAllTimersAsync(); - expect((await result).value?.request_id).toBe(REQUEST_ID); + expect((await result).value?.identification_id).toBe(REQUEST_ID); // Waits of 100 ms, 1 s instead of 200 ms after the 429, then the 400 ms step. expect(offsets(fake.calls)).toEqual([0, 100, 1100, 1500]); }); @@ -579,7 +579,7 @@ describe('identifications.get: 429 while waiting', () => { const { shieldlabs, fake } = client([tooMany(headers), empty(), found()]); const result = settle(shieldlabs.identifications.get(REQUEST_ID, { pollInterval: 3000 })); await vi.runAllTimersAsync(); - expect((await result).value?.request_id).toBe(REQUEST_ID); + expect((await result).value?.identification_id).toBe(REQUEST_ID); expect(offsets(fake.calls)).toEqual(expected); }); @@ -593,7 +593,7 @@ describe('identifications.get: 429 while waiting', () => { await vi.runAllTimersAsync(); const { value, at } = await result; // The 429 at 750 leaves 250 ms: the 1 s wait is cut to the deadline instead of throwing. - expect(value?.request_id).toBe(REQUEST_ID); + expect(value?.identification_id).toBe(REQUEST_ID); expect(at).toBe(1000); expect(offsets(fake.calls)).toEqual([0, 250, 750, 1000]); }); @@ -614,7 +614,7 @@ describe('identifications.get: 429 while waiting', () => { const { shieldlabs, fake } = client([tooMany({ 'retry-after': '120' }), found()]); const result = settle(shieldlabs.identifications.get(REQUEST_ID, { timeout: 30_000 })); await vi.runAllTimersAsync(); - expect((await result).value?.request_id).toBe(REQUEST_ID); + expect((await result).value?.identification_id).toBe(REQUEST_ID); expect(offsets(fake.calls)).toEqual([0, 10_000]); }); @@ -644,7 +644,7 @@ describe('identifications.get: 429 while waiting', () => { const result = outcome(shieldlabs.identifications.get(REQUEST_ID, { timeout: 2000 })); await vi.runAllTimersAsync(); const { value, at } = await result; - expect(value?.request_id).toBe(REQUEST_ID); + expect(value?.identification_id).toBe(REQUEST_ID); expect(at).toBe(2000); expect(offsets(fake.calls)).toEqual([0, 2000]); }); diff --git a/test/normalize.test.ts b/test/normalize.test.ts index 256eb8e..04cf927 100644 --- a/test/normalize.test.ts +++ b/test/normalize.test.ts @@ -41,7 +41,7 @@ const edge = loadJson<{ }>('normalizer-edge-cases.json'); const IDENTIFICATION_KEYS = [ - 'request_id', + 'identification_id', 'visitor_id', 'device_id', 'session_id', @@ -192,7 +192,7 @@ describe('helpers', () => { describe('type guarantees for unexpected input', () => { it('turns values of the wrong type into empty strings, 0 weights and NaN scores', () => { const result = fromWebhookData({ - request_id: null, + identification_id: null, domain: 42, user_hid: 12345, risk_score: '80', @@ -202,7 +202,7 @@ describe('type guarantees for unexpected input', () => { traffic_source: { channel: ['x'] }, observed_at: 1790771696123, }); - expect(result.request_id).toBe(''); + expect(result.identification_id).toBe(''); expect(result.domain).toBe(''); expect(result.user_hid).toBe('12345'); expect(result.risk_score).toBeNaN(); diff --git a/test/runtime/smoke.mjs b/test/runtime/smoke.mjs index f704244..096316d 100644 --- a/test/runtime/smoke.mjs +++ b/test/runtime/smoke.mjs @@ -53,7 +53,7 @@ const history = new ShieldLabs({ fetch: async (url, init) => { if (init.headers.Authorization !== 'Bearer sec_abcd1234-efgh5678-ijkl9012') return respond(401, '{"error":"invalid api key"}\n'); - if (url.includes('/request_id/')) { + if (url.includes('/identification_id/')) { polls += 1; return respond(200, { data: polls >= 3 ? [row] : [], total: polls >= 3 ? 1 : 0 }); } @@ -70,13 +70,13 @@ check( searched.total === 37 && searched.data.length === 5, ); check('History rows keep their raw fields', searched.data[0].raw.ver === page.data[0].ver); -const identification = await history.identifications.get(row.request_id, { +const identification = await history.identifications.get(row.identification_id, { pollInterval: 20, timeout: 2000, }); check( 'identifications.get polls until the row appears', - identification?.request_id === row.request_id && polls === 3, + identification?.identification_id === row.identification_id && polls === 3, ); const verdict = evaluateIdentification(identification); check( diff --git a/test/types.test.ts b/test/types.test.ts index b361e35..6c19114 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -21,7 +21,7 @@ function describeEvent(event: WebhookEvent): string { switch (event.event_type) { case 'identification.scored': expectTypeOf(event).toEqualTypeOf(); - return `scored ${event.data.request_id}`; + return `scored ${event.data.identification_id}`; case 'webhook.ping': expectTypeOf(event).toEqualTypeOf(); return 'ping'; diff --git a/test/validation.test.ts b/test/validation.test.ts index f8ee59f..73fc744 100644 --- a/test/validation.test.ts +++ b/test/validation.test.ts @@ -25,7 +25,7 @@ describe('History lookups are validated before any request', () => { ['uppercase type', 'REQUEST_ID', REQUEST_ID], ['bad UUID', 'device_id', 'not-a-uuid'], ['UUID with braces', 'visitor_id', `{${REQUEST_ID}}`], - ['UUID without dashes', 'request_id', REQUEST_ID.replaceAll('-', '')], + ['UUID without dashes', 'identification_id', REQUEST_ID.replaceAll('-', '')], ['UUID with spaces', 'session_id', ` ${REQUEST_ID}`], ['URN UUID', 'cookie_id', `urn:uuid:${REQUEST_ID}`], ['IPv6', 'ip', '2001:db8::1'], @@ -71,14 +71,14 @@ describe('History lookups are validated before any request', () => { it('lists the valid types for an unknown type', async () => { const { shieldlabs } = client(); await expect(shieldlabs.history.search('email' as LookupType, 'x')).rejects.toThrow( - /ip, user_hid, visitor_id, request_id, device_id, session_id, cookie_id/, + /ip, user_hid, visitor_id, identification_id, request_id, device_id, session_id, cookie_id/, ); }); it.each([0, 101, 1.5, -1, Number.NaN, '20'])('rejects limit %s', async (limit) => { const { shieldlabs, fake } = client(); await expect( - shieldlabs.history.search('request_id', REQUEST_ID, { limit: limit as number }), + shieldlabs.history.search('identification_id', REQUEST_ID, { limit: limit as number }), ).rejects.toThrow(ValidationError); expect(fake.calls).toHaveLength(0); }); @@ -86,7 +86,7 @@ describe('History lookups are validated before any request', () => { it.each([-1, 1.5, Number.POSITIVE_INFINITY, '0'])('rejects offset %s', async (offset) => { const { shieldlabs, fake } = client(); await expect( - shieldlabs.history.search('request_id', REQUEST_ID, { offset: offset as number }), + shieldlabs.history.search('identification_id', REQUEST_ID, { offset: offset as number }), ).rejects.toThrow(ValidationError); expect(fake.calls).toHaveLength(0); }); @@ -107,8 +107,8 @@ describe('History lookups are validated before any request', () => { it('accepts the boundary values', async () => { const { shieldlabs, fake } = client(); - await shieldlabs.history.search('request_id', REQUEST_ID, { limit: 1, offset: 0 }); - await shieldlabs.history.search('request_id', REQUEST_ID, { limit: 100, offset: 1_000_000 }); + await shieldlabs.history.search('identification_id', REQUEST_ID, { limit: 1, offset: 0 }); + await shieldlabs.history.search('identification_id', REQUEST_ID, { limit: 100, offset: 1_000_000 }); await shieldlabs.history.search('ip', '0.0.0.0'); await shieldlabs.history.search('ip', '255.255.255.255'); await shieldlabs.history.search('device_id', '00000000-0000-0000-0000-000000000000'); @@ -155,13 +155,13 @@ describe('User HID path segments', () => { }); describe('identifications.get validation', () => { - it.each([['not-a-uuid'], [''], [42], [undefined]])('rejects request ID %s', async (requestId) => { + it.each([['not-a-uuid'], [''], [42], [undefined]])('rejects request ID %s', async (identificationId) => { const { shieldlabs, fake } = client(); - await expect(shieldlabs.identifications.get(requestId as string)).rejects.toThrow( + await expect(shieldlabs.identifications.get(identificationId as string)).rejects.toThrow( ValidationError, ); await expect( - shieldlabs.identifications.get(requestId as string, { wait: false }), + shieldlabs.identifications.get(identificationId as string, { wait: false }), ).rejects.toThrow(ValidationError); expect(fake.calls).toHaveLength(0); }); diff --git a/test/webhooks.test.ts b/test/webhooks.test.ts index 525f404..171e6ac 100644 --- a/test/webhooks.test.ts +++ b/test/webhooks.test.ts @@ -180,17 +180,17 @@ describe('constructEvent with the webhook fixtures', () => { it('narrows the event union by event_type', () => { const raw = loadBytes('webhook-identification-scored.raw.txt'); const event: WebhookEvent = webhooks.constructEvent(raw, sign(raw), SECRET); - let requestId = ''; + let identificationId = ''; switch (event.event_type) { case 'identification.scored': - requestId = event.data.request_id; + identificationId = event.data.identification_id; break; case 'webhook.ping': break; default: expect.unreachable(); } - expect(requestId).toBe('02f1d973-84db-4156-a7f7-e799e6bf389b'); + expect(identificationId).toBe('02f1d973-84db-4156-a7f7-e799e6bf389b'); }); }); From e451b0312d1489e8118123fc6c2180a3b882d02c Mon Sep 17 00:00:00 2001 From: EgorMajj <91486022+EgorMajj@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:25:44 +0300 Subject: [PATCH 2/2] fix: regenerate API types and format identifier migration --- examples/node-http/server.mjs | 6 ++++-- src/generated/api.ts | 4 ++-- src/types.ts | 9 ++++++++- test/errors.test.ts | 4 +++- test/helpers.ts | 6 +++++- test/history.test.ts | 4 +++- test/validation.test.ts | 28 +++++++++++++++++----------- 7 files changed, 42 insertions(+), 19 deletions(-) diff --git a/examples/node-http/server.mjs b/examples/node-http/server.mjs index 0c86f9c..32a8144 100644 --- a/examples/node-http/server.mjs +++ b/examples/node-http/server.mjs @@ -85,7 +85,8 @@ async function handleSignup(req, res) { } catch { return sendJson(res, 400, { error: 'Send a JSON body with identificationId.' }); } - const identificationId = typeof payload?.identificationId === 'string' ? payload.identificationId : ''; + const identificationId = + typeof payload?.identificationId === 'string' ? payload.identificationId : ''; if (!identificationId) return sendJson(res, 400, { error: 'identificationId is required.' }); let identification; @@ -102,7 +103,8 @@ async function handleSignup(req, res) { // never clean), a reused request ID, an identification older than 5 minutes, the rate-limit // marker, missing device signals, browser automation or disabled JavaScript, and the // dangerous band. Tune the defaults. - const firstUse = identification !== null && claimIdentificationId(identification.identification_id); + const firstUse = + identification !== null && claimIdentificationId(identification.identification_id); const verdict = evaluateIdentification(identification, { isReplay: () => !firstUse }); if (!verdict.ok) { return sendJson(res, 403, { ok: false, reason: verdict.reason }); diff --git a/src/generated/api.ts b/src/generated/api.ts index 6feaca3..868ae26 100644 --- a/src/generated/api.ts +++ b/src/generated/api.ts @@ -977,7 +977,7 @@ export interface components { * The server does not reject other values: it ignores them and returns the latest identifications * of the whole domain, so restrict the value on your side. */ - HistorySearchType: "identification_id" | "request_id" | "device_id" | "user_hid" | "visitor_id" | "ip" | "session_id" | "cookie_id"; + HistorySearchType: "identification_id" | "device_id" | "user_hid" | "visitor_id" | "ip" | "session_id" | "cookie_id"; /** * @description Value of the identifier, validated on your side before sending: * - `identification_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version, @@ -1005,7 +1005,7 @@ export interface components { /** @description Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body. */ ShieldDomain: string; /** @description Identifier to search by. Other values get a `404` with a bare JSON string such as `"auto is not supported"`. */ - DeprecatedHistoryType: "identification_id" | "request_id" | "device_id" | "user_hid" | "visitor_id" | "ip" | "session_id" | "cookie_id"; + DeprecatedHistoryType: "identification_id" | "device_id" | "user_hid" | "visitor_id" | "ip" | "session_id" | "cookie_id"; /** @description Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text. */ DeprecatedHistoryValue: string; /** @description Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`. */ diff --git a/src/types.ts b/src/types.ts index 7f96a5c..4b770b8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,13 @@ /** Identifier types the History API can search by. */ export type LookupType = - 'ip' | 'user_hid' | 'visitor_id' | 'identification_id' | 'request_id' | 'device_id' | 'session_id' | 'cookie_id'; + | 'ip' + | 'user_hid' + | 'visitor_id' + | 'identification_id' + | 'request_id' + | 'device_id' + | 'session_id' + | 'cookie_id'; /** The three risk bands: trusted 0-29, suspicious 30-59, dangerous 60-100. */ export type RiskBand = 'trusted' | 'suspicious' | 'dangerous'; diff --git a/test/errors.test.ts b/test/errors.test.ts index 8f7911d..b7c67b5 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -545,7 +545,9 @@ describe('fetch implementations', () => { it('reports a missing global fetch as a ValidationError', async () => { vi.stubGlobal('fetch', undefined); const client = new ShieldLabs({ apiKey: TEST_API_KEY, maxRetries: 0 }); - await expect(client.history.search('identification_id', REQUEST_ID)).rejects.toThrow(ValidationError); + await expect(client.history.search('identification_id', REQUEST_ID)).rejects.toThrow( + ValidationError, + ); }); it('passes an AuthenticationError from custom fetch responses through unchanged', async () => { diff --git a/test/helpers.ts b/test/helpers.ts index d48b7b9..f0968ce 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -123,7 +123,11 @@ export function historyRow( overrides: Record = {}, ): Record { const page = loadJson<{ data: Record[] }>('history-page.json'); - return { ...(page.data[1] as Record), identification_id: identificationId, ...overrides }; + return { + ...(page.data[1] as Record), + identification_id: identificationId, + ...overrides, + }; } /** Formats a Date the way History rows carry created_at. */ diff --git a/test/history.test.ts b/test/history.test.ts index 8b280fb..1598a8f 100644 --- a/test/history.test.ts +++ b/test/history.test.ts @@ -231,7 +231,9 @@ describe('history.search responses', () => { '22222222-2222-4222-8222-222222222222', '33333333-3333-4333-8333-333333333333', ]; - const pages = await Promise.all(ids.map((id) => shieldlabs.history.search('identification_id', id))); + const pages = await Promise.all( + ids.map((id) => shieldlabs.history.search('identification_id', id)), + ); expect(pages.map((p) => p.data[0]?.identification_id)).toEqual(ids); expect(fake.calls).toHaveLength(3); }); diff --git a/test/validation.test.ts b/test/validation.test.ts index 73fc744..d71416c 100644 --- a/test/validation.test.ts +++ b/test/validation.test.ts @@ -108,7 +108,10 @@ describe('History lookups are validated before any request', () => { it('accepts the boundary values', async () => { const { shieldlabs, fake } = client(); await shieldlabs.history.search('identification_id', REQUEST_ID, { limit: 1, offset: 0 }); - await shieldlabs.history.search('identification_id', REQUEST_ID, { limit: 100, offset: 1_000_000 }); + await shieldlabs.history.search('identification_id', REQUEST_ID, { + limit: 100, + offset: 1_000_000, + }); await shieldlabs.history.search('ip', '0.0.0.0'); await shieldlabs.history.search('ip', '255.255.255.255'); await shieldlabs.history.search('device_id', '00000000-0000-0000-0000-000000000000'); @@ -155,16 +158,19 @@ describe('User HID path segments', () => { }); describe('identifications.get validation', () => { - it.each([['not-a-uuid'], [''], [42], [undefined]])('rejects request ID %s', async (identificationId) => { - const { shieldlabs, fake } = client(); - await expect(shieldlabs.identifications.get(identificationId as string)).rejects.toThrow( - ValidationError, - ); - await expect( - shieldlabs.identifications.get(identificationId as string, { wait: false }), - ).rejects.toThrow(ValidationError); - expect(fake.calls).toHaveLength(0); - }); + it.each([['not-a-uuid'], [''], [42], [undefined]])( + 'rejects request ID %s', + async (identificationId) => { + const { shieldlabs, fake } = client(); + await expect(shieldlabs.identifications.get(identificationId as string)).rejects.toThrow( + ValidationError, + ); + await expect( + shieldlabs.identifications.get(identificationId as string, { wait: false }), + ).rejects.toThrow(ValidationError); + expect(fake.calls).toHaveLength(0); + }, + ); it.each([ [{ timeout: -1 }],