diff --git a/README.md b/README.md index f63294b..c6f6d81 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ export function parseEvent(rawBody) { - The key is the whole signing secret, `whsec_` prefix included, as UTF-8 bytes. The message is the raw body: re-serialized JSON does not match (for example, `&` arrives as `\u0026`). - Delivery attempts have a 1-second timeout. Network errors, timeout, 429 and 5xx retry with backoff within a bounded window (8 failed sends or 15-minute retry age); worker/Redis waits can extend wall time. Retries preserve signed bytes and `event_id`, and duplicate deliveries remain possible. Verify the raw-body signature, durably store by signed `event_id` (legacy fallback: `data.request_id`), then acknowledge 2xx and process asynchronously. Redirects are not followed. -- Current Portal Test sends a complete `2026-10-06` Core-generated sample with fp21/HRE and a distinct event ID per click. Vendored historical Test fixtures retain their two missing flags as explicit legacy compatibility cases. +- Current Portal Test sends a complete `2026-10-07` Core-generated sample with HRE cluster references and a distinct event ID per click. Vendored historical Test fixtures retain their two missing flags as explicit legacy compatibility cases. - A History row can be refined after its webhook was sent; the webhook is not sent again. Read the History API when you need the latest state. ### Read identifications from the History API @@ -210,7 +210,7 @@ access the typed field. ## Compatibility - OpenAPI 3.1.0; the JSON Schemas use JSON Schema 2020-12 and compile with a strict validator. -- `info.version` follows the package version (semantic versioning). Webhook payloads carry `schema_version` (`2026-10-06` current, `2026-06-01` legacy). +- `info.version` follows the package version (semantic versioning). Webhook payloads carry `schema_version` (`2026-10-07` current; `2026-10-06` and `2026-06-01` legacy). - Parse tolerantly: ignore unknown fields, keep unknown values of string fields and accept other `schema_version` values. Response fields with a known set of values are open strings in the spec and in the JSON Schemas, so a value added later never fails validation. - Tooling requires Node.js 20.19 or later; CI runs on Node.js 20, 22 and 24. diff --git a/dist/schemas/identification-scored-event.schema.json b/dist/schemas/identification-scored-event.schema.json index 647eba2..62b7827 100644 --- a/dist/schemas/identification-scored-event.schema.json +++ b/dist/schemas/identification-scored-event.schema.json @@ -79,6 +79,121 @@ "type": "object", "additionalProperties": true } + }, + { + "if": { + "type": "object", + "required": [ + "schema_version" + ], + "properties": { + "schema_version": { + "const": "2026-10-07" + } + }, + "additionalProperties": true + }, + "then": { + "type": "object", + "required": [ + "event_id" + ], + "properties": { + "event_id": { + "type": "string", + "minLength": 1, + "description": "Logical site/request/final-result-version/type identity. Stable across retries and endpoints." + }, + "data": { + "type": "object", + "required": [ + "result_version", + "scoring_version", + "hre" + ], + "properties": { + "result_version": {}, + "scoring_version": {}, + "risk_events": {}, + "fingerprint": {}, + "detection_flags": { + "type": "object", + "not": { + "type": "object", + "required": [ + "os_mismatch2" + ], + "properties": { + "os_mismatch2": {} + }, + "additionalProperties": true + } + }, + "hre": { + "type": "object", + "properties": { + "account_sharing": { + "type": "object", + "required": [ + "cluster_id" + ], + "properties": { + "cluster_id": {} + }, + "additionalProperties": true + }, + "account_takeover": { + "type": "object", + "required": [ + "cluster_id" + ], + "properties": { + "cluster_id": {} + }, + "additionalProperties": true + }, + "impossible_travel": { + "type": "object", + "required": [ + "cluster_id" + ], + "properties": { + "cluster_id": {} + }, + "additionalProperties": true + } + } + } + }, + "additionalProperties": true, + "not": { + "anyOf": [ + { + "type": "object", + "required": [ + "risk_events" + ], + "properties": { + "risk_events": {} + }, + "additionalProperties": true + }, + { + "type": "object", + "required": [ + "fingerprint" + ], + "properties": { + "fingerprint": {} + }, + "additionalProperties": true + } + ] + } + } + }, + "additionalProperties": true + } } ], "$defs": { @@ -367,7 +482,7 @@ }, "os_mismatch": { "type": "boolean", - "description": "The operating system seen on the network differs from the one the browser reports." + "description": "Either internal OS consistency check detected a mismatch. The public flag combines browser/network and TCP behaviour checks." }, "os_not_detected": { "type": "boolean", @@ -413,9 +528,6 @@ "type": "boolean", "description": "Part of the browser checks timed out, so the verdict rests on partial data. Informational." }, - "os_mismatch2": { - "type": "boolean" - }, "device_spoofing": { "type": "boolean" }, @@ -432,6 +544,11 @@ "ai_browser": { "type": "boolean", "description": "A browser is identified by separate verified browser infrastructure. A generic browser User-Agent or a verified provider alone is insufficient. Weight 0. Optional on older payloads; absence is not an evaluated negative." + }, + "os_mismatch2": { + "type": "boolean", + "deprecated": true, + "description": "Legacy 2026-10-06 only; current payloads combine this check into os_mismatch." } } }, @@ -511,12 +628,19 @@ "min_devices": { "type": "integer", "minimum": 1 + }, + "cluster_id": { + "type": [ + "string", + "null" + ], + "description": "Authoritative cluster ID for this HRE result. Null when no cluster was published for the result, including old stored verdicts; never a device ID. Reused on retries." } } }, "IdentificationScoredData": { "type": "object", - "description": "Final identification. risk_score is this scan only; no all-time entity risk. New extension fields are required by version 2026-10-06; legacy bodies remain accepted.", + "description": "Final identification. risk_score is this scan only. Version 2026-10-07 uses signals for score contributions, detection_flags for final decisions, and hre for account results. Older bodies remain accepted.", "required": [ "request_id", "visitor_id", @@ -615,6 +739,8 @@ "description": "Core build source revision; core:unversioned on local builds." }, "risk_events": { + "deprecated": true, + "description": "Legacy 2026-10-06 only; absent from current scored events.", "type": "array", "items": { "$ref": "#/$defs/RiskEvent" @@ -622,6 +748,24 @@ }, "hre": { "$ref": "#/$defs/HRE" + }, + "search_bot_owner": { + "type": "string", + "description": "Owner label from accepted search-bot detection; omitted when unknown or inactive.", + "examples": [ + "Google" + ] + }, + "ai_bot_owner": { + "type": "string", + "description": "Provider company label from accepted AI-bot detection; omitted when unknown or inactive.", + "examples": [ + "OpenAI" + ] + }, + "ai_browser_owner": { + "type": "string", + "description": "Owner label from accepted AI-browser detection; omitted when unknown or inactive." } } }, diff --git a/dist/schemas/identification.schema.json b/dist/schemas/identification.schema.json index 668ff58..cf5cd78 100644 --- a/dist/schemas/identification.schema.json +++ b/dist/schemas/identification.schema.json @@ -435,7 +435,7 @@ }, "os_mismatch": { "type": "boolean", - "description": "The operating system seen on the network differs from the one the browser reports." + "description": "Either internal OS consistency check detected a mismatch. The public flag combines browser/network and TCP behaviour checks." }, "os_not_detected": { "type": "boolean", @@ -481,9 +481,6 @@ "type": "boolean", "description": "Part of the browser checks timed out, so the verdict rests on partial data. Informational." }, - "os_mismatch2": { - "type": "boolean" - }, "device_spoofing": { "type": "boolean" }, @@ -500,6 +497,11 @@ "ai_browser": { "type": "boolean", "description": "A browser is identified by separate verified browser infrastructure. A generic browser User-Agent or a verified provider alone is insufficient. Weight 0. Optional on older payloads; absence is not an evaluated negative." + }, + "os_mismatch2": { + "type": "boolean", + "deprecated": true, + "description": "Legacy 2026-10-06 only; current payloads combine this check into os_mismatch." } } }, diff --git a/dist/schemas/webhook-event.schema.json b/dist/schemas/webhook-event.schema.json index cf82ae3..d9b7de1 100644 --- a/dist/schemas/webhook-event.schema.json +++ b/dist/schemas/webhook-event.schema.json @@ -358,7 +358,7 @@ }, "os_mismatch": { "type": "boolean", - "description": "The operating system seen on the network differs from the one the browser reports." + "description": "Either internal OS consistency check detected a mismatch. The public flag combines browser/network and TCP behaviour checks." }, "os_not_detected": { "type": "boolean", @@ -404,9 +404,6 @@ "type": "boolean", "description": "Part of the browser checks timed out, so the verdict rests on partial data. Informational." }, - "os_mismatch2": { - "type": "boolean" - }, "device_spoofing": { "type": "boolean" }, @@ -423,6 +420,11 @@ "ai_browser": { "type": "boolean", "description": "A browser is identified by separate verified browser infrastructure. A generic browser User-Agent or a verified provider alone is insufficient. Weight 0. Optional on older payloads; absence is not an evaluated negative." + }, + "os_mismatch2": { + "type": "boolean", + "deprecated": true, + "description": "Legacy 2026-10-06 only; current payloads combine this check into os_mismatch." } } }, @@ -502,12 +504,19 @@ "min_devices": { "type": "integer", "minimum": 1 + }, + "cluster_id": { + "type": [ + "string", + "null" + ], + "description": "Authoritative cluster ID for this HRE result. Null when no cluster was published for the result, including old stored verdicts; never a device ID. Reused on retries." } } }, "IdentificationScoredData": { "type": "object", - "description": "Final identification. risk_score is this scan only; no all-time entity risk. New extension fields are required by version 2026-10-06; legacy bodies remain accepted.", + "description": "Final identification. risk_score is this scan only. Version 2026-10-07 uses signals for score contributions, detection_flags for final decisions, and hre for account results. Older bodies remain accepted.", "required": [ "request_id", "visitor_id", @@ -606,6 +615,8 @@ "description": "Core build source revision; core:unversioned on local builds." }, "risk_events": { + "deprecated": true, + "description": "Legacy 2026-10-06 only; absent from current scored events.", "type": "array", "items": { "$ref": "#/$defs/RiskEvent" @@ -613,6 +624,24 @@ }, "hre": { "$ref": "#/$defs/HRE" + }, + "search_bot_owner": { + "type": "string", + "description": "Owner label from accepted search-bot detection; omitted when unknown or inactive.", + "examples": [ + "Google" + ] + }, + "ai_bot_owner": { + "type": "string", + "description": "Provider company label from accepted AI-bot detection; omitted when unknown or inactive.", + "examples": [ + "OpenAI" + ] + }, + "ai_browser_owner": { + "type": "string", + "description": "Owner label from accepted AI-browser detection; omitted when unknown or inactive." } } }, @@ -694,6 +723,121 @@ "type": "object", "additionalProperties": true } + }, + { + "if": { + "type": "object", + "required": [ + "schema_version" + ], + "properties": { + "schema_version": { + "const": "2026-10-07" + } + }, + "additionalProperties": true + }, + "then": { + "type": "object", + "required": [ + "event_id" + ], + "properties": { + "event_id": { + "type": "string", + "minLength": 1, + "description": "Logical site/request/final-result-version/type identity. Stable across retries and endpoints." + }, + "data": { + "type": "object", + "required": [ + "result_version", + "scoring_version", + "hre" + ], + "properties": { + "result_version": {}, + "scoring_version": {}, + "risk_events": {}, + "fingerprint": {}, + "detection_flags": { + "type": "object", + "not": { + "type": "object", + "required": [ + "os_mismatch2" + ], + "properties": { + "os_mismatch2": {} + }, + "additionalProperties": true + } + }, + "hre": { + "type": "object", + "properties": { + "account_sharing": { + "type": "object", + "required": [ + "cluster_id" + ], + "properties": { + "cluster_id": {} + }, + "additionalProperties": true + }, + "account_takeover": { + "type": "object", + "required": [ + "cluster_id" + ], + "properties": { + "cluster_id": {} + }, + "additionalProperties": true + }, + "impossible_travel": { + "type": "object", + "required": [ + "cluster_id" + ], + "properties": { + "cluster_id": {} + }, + "additionalProperties": true + } + } + } + }, + "additionalProperties": true, + "not": { + "anyOf": [ + { + "type": "object", + "required": [ + "risk_events" + ], + "properties": { + "risk_events": {} + }, + "additionalProperties": true + }, + { + "type": "object", + "required": [ + "fingerprint" + ], + "properties": { + "fingerprint": {} + }, + "additionalProperties": true + } + ] + } + } + }, + "additionalProperties": true + } } ] }, diff --git a/dist/shieldlabs-api.json b/dist/shieldlabs-api.json index 7a4c874..177a510 100644 --- a/dist/shieldlabs-api.json +++ b/dist/shieldlabs-api.json @@ -2462,7 +2462,7 @@ }, "os_mismatch": { "type": "boolean", - "description": "The operating system seen on the network differs from the one the browser reports." + "description": "Either internal OS consistency check detected a mismatch. The public flag combines browser/network and TCP behaviour checks." }, "os_not_detected": { "type": "boolean", @@ -2508,9 +2508,6 @@ "type": "boolean", "description": "Part of the browser checks timed out, so the verdict rests on partial data. Informational." }, - "os_mismatch2": { - "type": "boolean" - }, "device_spoofing": { "type": "boolean" }, @@ -2527,6 +2524,11 @@ "ai_browser": { "type": "boolean", "description": "A browser is identified by separate verified browser infrastructure. A generic browser User-Agent or a verified provider alone is insufficient. Weight 0. Optional on older payloads; absence is not an evaluated negative." + }, + "os_mismatch2": { + "type": "boolean", + "deprecated": true, + "description": "Legacy 2026-10-06 only; current payloads combine this check into os_mismatch." } } }, @@ -2602,6 +2604,13 @@ "min_devices": { "type": "integer", "minimum": 1 + }, + "cluster_id": { + "type": [ + "string", + "null" + ], + "description": "Authoritative cluster ID for this HRE result. Null when no cluster was published for the result, including old stored verdicts; never a device ID. Reused on retries." } } }, @@ -2630,7 +2639,7 @@ }, "IdentificationScoredData": { "type": "object", - "description": "Final identification. risk_score is this scan only; no all-time entity risk. New extension fields are required by version 2026-10-06; legacy bodies remain accepted.", + "description": "Final identification. risk_score is this scan only. Version 2026-10-07 uses signals for score contributions, detection_flags for final decisions, and hre for account results. Older bodies remain accepted.", "required": [ "request_id", "visitor_id", @@ -2729,6 +2738,8 @@ "description": "Core build source revision; core:unversioned on local builds." }, "risk_events": { + "deprecated": true, + "description": "Legacy 2026-10-06 only; absent from current scored events.", "type": "array", "items": { "$ref": "#/components/schemas/RiskEvent" @@ -2736,6 +2747,24 @@ }, "hre": { "$ref": "#/components/schemas/HRE" + }, + "search_bot_owner": { + "type": "string", + "description": "Owner label from accepted search-bot detection; omitted when unknown or inactive.", + "examples": [ + "Google" + ] + }, + "ai_bot_owner": { + "type": "string", + "description": "Provider company label from accepted AI-bot detection; omitted when unknown or inactive.", + "examples": [ + "OpenAI" + ] + }, + "ai_browser_owner": { + "type": "string", + "description": "Owner label from accepted AI-browser detection; omitted when unknown or inactive." } } }, @@ -2817,6 +2846,121 @@ "type": "object", "additionalProperties": true } + }, + { + "if": { + "type": "object", + "required": [ + "schema_version" + ], + "properties": { + "schema_version": { + "const": "2026-10-07" + } + }, + "additionalProperties": true + }, + "then": { + "type": "object", + "required": [ + "event_id" + ], + "properties": { + "event_id": { + "type": "string", + "minLength": 1, + "description": "Logical site/request/final-result-version/type identity. Stable across retries and endpoints." + }, + "data": { + "type": "object", + "required": [ + "result_version", + "scoring_version", + "hre" + ], + "properties": { + "result_version": {}, + "scoring_version": {}, + "risk_events": {}, + "fingerprint": {}, + "detection_flags": { + "type": "object", + "not": { + "type": "object", + "required": [ + "os_mismatch2" + ], + "properties": { + "os_mismatch2": {} + }, + "additionalProperties": true + } + }, + "hre": { + "type": "object", + "properties": { + "account_sharing": { + "type": "object", + "required": [ + "cluster_id" + ], + "properties": { + "cluster_id": {} + }, + "additionalProperties": true + }, + "account_takeover": { + "type": "object", + "required": [ + "cluster_id" + ], + "properties": { + "cluster_id": {} + }, + "additionalProperties": true + }, + "impossible_travel": { + "type": "object", + "required": [ + "cluster_id" + ], + "properties": { + "cluster_id": {} + }, + "additionalProperties": true + } + } + } + }, + "additionalProperties": true, + "not": { + "anyOf": [ + { + "type": "object", + "required": [ + "risk_events" + ], + "properties": { + "risk_events": {} + }, + "additionalProperties": true + }, + { + "type": "object", + "required": [ + "fingerprint" + ], + "properties": { + "fingerprint": {} + }, + "additionalProperties": true + } + ] + } + } + }, + "additionalProperties": true + } } ] }, diff --git a/dist/shieldlabs-api.yaml b/dist/shieldlabs-api.yaml index 09f334d..9cbe011 100644 --- a/dist/shieldlabs-api.yaml +++ b/dist/shieldlabs-api.yaml @@ -2153,7 +2153,7 @@ components: description: The public IP has a record of abuse in IP intelligence. os_mismatch: type: boolean - description: The operating system seen on the network differs from the one the browser reports. + description: Either internal OS consistency check detected a mismatch. The public flag combines browser/network and TCP behaviour checks. os_not_detected: type: boolean description: The operating system could not be determined from the User-Agent or the network. @@ -2187,8 +2187,6 @@ components: check_incomplete: type: boolean description: Part of the browser checks timed out, so the verdict rests on partial data. Informational. - os_mismatch2: - type: boolean device_spoofing: type: boolean latency_test: @@ -2201,6 +2199,10 @@ components: ai_browser: type: boolean description: A browser is identified by separate verified browser infrastructure. A generic browser User-Agent or a verified provider alone is insufficient. Weight 0. Optional on older payloads; absence is not an evaluated negative. + os_mismatch2: + type: boolean + deprecated: true + description: Legacy 2026-10-06 only; current payloads combine this check into os_mismatch. RiskEvent: type: object required: @@ -2256,6 +2258,11 @@ components: min_devices: type: integer minimum: 1 + cluster_id: + type: + - string + - 'null' + description: Authoritative cluster ID for this HRE result. Null when no cluster was published for the result, including old stored verdicts; never a device ID. Reused on retries. HRE: type: object required: @@ -2274,7 +2281,7 @@ components: description: Three completed on-demand handler results. Technical errors block emission; no_history/skipped are explicit not_evaluated results. Anonymous checks are not_applicable. Multiaccount is separate. IdentificationScoredData: type: object - description: Final identification. risk_score is this scan only; no all-time entity risk. New extension fields are required by version 2026-10-06; legacy bodies remain accepted. + description: Final identification. risk_score is this scan only. Version 2026-10-07 uses signals for score contributions, detection_flags for final decisions, and hre for account results. Older bodies remain accepted. required: - request_id - visitor_id @@ -2349,11 +2356,26 @@ components: type: string description: Core build source revision; core:unversioned on local builds. risk_events: + deprecated: true + description: Legacy 2026-10-06 only; absent from current scored events. type: array items: $ref: '#/components/schemas/RiskEvent' hre: $ref: '#/components/schemas/HRE' + search_bot_owner: + type: string + description: Owner label from accepted search-bot detection; omitted when unknown or inactive. + examples: + - Google + ai_bot_owner: + type: string + description: Provider company label from accepted AI-bot detection; omitted when unknown or inactive. + examples: + - OpenAI + ai_browser_owner: + type: string + description: Owner label from accepted AI-browser detection; omitted when unknown or inactive. IdentificationScoredEvent: type: object description: 'Body of an `identification.scored` delivery. The signature is not part of the body: it arrives in the `X-Shield-Signature` header.' @@ -2412,6 +2434,83 @@ components: description: Logical site/request/final-result-version/type identity. Stable across retries and endpoints. type: object additionalProperties: true + - if: + type: object + required: + - schema_version + properties: + schema_version: + const: '2026-10-07' + additionalProperties: true + then: + type: object + required: + - event_id + properties: + event_id: + type: string + minLength: 1 + description: Logical site/request/final-result-version/type identity. Stable across retries and endpoints. + data: + type: object + required: + - result_version + - scoring_version + - hre + properties: + result_version: {} + scoring_version: {} + risk_events: {} + fingerprint: {} + detection_flags: + type: object + not: + type: object + required: + - os_mismatch2 + properties: + os_mismatch2: {} + additionalProperties: true + hre: + type: object + properties: + account_sharing: + type: object + required: + - cluster_id + properties: + cluster_id: {} + additionalProperties: true + account_takeover: + type: object + required: + - cluster_id + properties: + cluster_id: {} + additionalProperties: true + impossible_travel: + type: object + required: + - cluster_id + properties: + cluster_id: {} + additionalProperties: true + additionalProperties: true + not: + anyOf: + - type: object + required: + - risk_events + properties: + risk_events: {} + additionalProperties: true + - type: object + required: + - fingerprint + properties: + fingerprint: {} + additionalProperties: true + additionalProperties: true WebhookPingEvent: type: object description: Body of a `webhook.ping` delivery, sent when you verify an endpoint. It has no `data`. The keys arrive sorted alphabetically and `created_at` has second precision. diff --git a/spec/components/schemas/DetectionFlags.yaml b/spec/components/schemas/DetectionFlags.yaml index 30c51db..175ee5a 100644 --- a/spec/components/schemas/DetectionFlags.yaml +++ b/spec/components/schemas/DetectionFlags.yaml @@ -56,8 +56,8 @@ properties: description: The public IP has a record of abuse in IP intelligence. os_mismatch: type: boolean - description: The operating system seen on the network differs from the one the - browser reports. + description: Either internal OS consistency check detected a mismatch. The public + flag combines browser/network and TCP behaviour checks. os_not_detected: type: boolean description: The operating system could not be determined from the User-Agent or @@ -99,8 +99,6 @@ properties: type: boolean description: Part of the browser checks timed out, so the verdict rests on partial data. Informational. - os_mismatch2: - type: boolean device_spoofing: type: boolean latency_test: @@ -119,3 +117,8 @@ properties: description: A browser is identified by separate verified browser infrastructure. A generic browser User-Agent or a verified provider alone is insufficient. Weight 0. Optional on older payloads; absence is not an evaluated negative. + + os_mismatch2: + type: boolean + deprecated: true + description: Legacy 2026-10-06 only; current payloads combine this check into os_mismatch. diff --git a/spec/components/schemas/HREResult.yaml b/spec/components/schemas/HREResult.yaml index 81b9cf3..1564ce4 100644 --- a/spec/components/schemas/HREResult.yaml +++ b/spec/components/schemas/HREResult.yaml @@ -26,3 +26,7 @@ properties: min_devices: type: integer minimum: 1 + + cluster_id: + type: [string, "null"] + description: Authoritative cluster ID for this HRE result. Null when no cluster was published for the result, including old stored verdicts; never a device ID. Reused on retries. diff --git a/spec/components/schemas/IdentificationScoredData.yaml b/spec/components/schemas/IdentificationScoredData.yaml index 6ab9c2a..a9122d6 100644 --- a/spec/components/schemas/IdentificationScoredData.yaml +++ b/spec/components/schemas/IdentificationScoredData.yaml @@ -1,7 +1,7 @@ type: object -description: Final identification. risk_score is this scan only; no all-time - entity risk. New extension fields are required by version 2026-10-06; legacy - bodies remain accepted. +description: Final identification. risk_score is this scan only. Version 2026-10-07 + uses signals for score contributions, detection_flags for final decisions, + and hre for account results. Older bodies remain accepted. required: - request_id - visitor_id @@ -83,8 +83,22 @@ properties: type: string description: Core build source revision; core:unversioned on local builds. risk_events: + deprecated: true + description: Legacy 2026-10-06 only; absent from current scored events. type: array items: $ref: ./RiskEvent.yaml hre: $ref: ./HRE.yaml + + search_bot_owner: + type: string + description: Owner label from accepted search-bot detection; omitted when unknown or inactive. + examples: [Google] + ai_bot_owner: + type: string + description: Provider company label from accepted AI-bot detection; omitted when unknown or inactive. + examples: [OpenAI] + ai_browser_owner: + type: string + description: Owner label from accepted AI-browser detection; omitted when unknown or inactive. diff --git a/spec/components/schemas/IdentificationScoredEvent.yaml b/spec/components/schemas/IdentificationScoredEvent.yaml index 4b4b5e7..fb4b44d 100644 --- a/spec/components/schemas/IdentificationScoredEvent.yaml +++ b/spec/components/schemas/IdentificationScoredEvent.yaml @@ -55,3 +55,77 @@ allOf: event_id: *a1 type: object additionalProperties: true + - if: + type: object + required: + - schema_version + properties: + schema_version: + const: 2026-10-07 + additionalProperties: true + then: + type: object + required: + - event_id + properties: + event_id: *a1 + data: + type: object + required: + - result_version + - scoring_version + - hre + properties: + result_version: {} + scoring_version: {} + risk_events: {} + fingerprint: {} + detection_flags: + type: object + not: + type: object + required: + - os_mismatch2 + properties: + os_mismatch2: {} + additionalProperties: true + hre: + type: object + properties: + account_sharing: + type: object + required: + - cluster_id + properties: + cluster_id: {} + additionalProperties: true + account_takeover: + type: object + required: + - cluster_id + properties: + cluster_id: {} + additionalProperties: true + impossible_travel: + type: object + required: + - cluster_id + properties: + cluster_id: {} + additionalProperties: true + additionalProperties: true + not: + anyOf: + - type: object + required: + - risk_events + properties: + risk_events: {} + additionalProperties: true + - type: object + required: + - fingerprint + properties: + fingerprint: {} + additionalProperties: true + additionalProperties: true diff --git a/test/build.test.mjs b/test/build.test.mjs index ddae35c..4d2edb1 100644 --- a/test/build.test.mjs +++ b/test/build.test.mjs @@ -217,7 +217,7 @@ describe('schemas', () => { const { doc } = freshBuild(); const data = doc.components.schemas.IdentificationScoredData; assert.deepEqual([...data.required].sort(), [...WEBHOOK_DATA_KEYS].sort()); - assert.deepEqual(Object.keys(data.properties).sort(), [...WEBHOOK_DATA_KEYS, "result_version", "scoring_version", "risk_events", "hre", "client_identity"].sort()); + assert.deepEqual(Object.keys(data.properties).sort(), [...WEBHOOK_DATA_KEYS, "result_version", "scoring_version", "risk_events", "hre", "client_identity", "search_bot_owner", "ai_bot_owner", "ai_browser_owner"].sort()); assert.deepEqual(doc.components.schemas.UserHid.type, ['string', 'null']); }); diff --git a/test/contract-current.test.mjs b/test/contract-current.test.mjs index e18a511..aca8838 100644 --- a/test/contract-current.test.mjs +++ b/test/contract-current.test.mjs @@ -3,18 +3,19 @@ import {readFileSync} from 'node:fs'; import {describe,it} from 'node:test'; import {freshBuild,ROOT} from './helpers.mjs'; import path from 'node:path'; -describe('Core 2026-10-06 fixtures',()=>{ +describe('Core 2026-10-07 fixtures',()=>{ for(const name of ['named','anonymous','no-history','banned','incomplete'])it(name,()=>{ const fixture=JSON.parse(readFileSync(path.join(ROOT,'test/contracts',name+'.json'),'utf8')); const {doc,validator}=freshBuild();const schema=doc.components.schemas.IdentificationScoredEvent; const result=validator.validate(schema,fixture);assert.ok(result.valid,JSON.stringify(result.errors)); - assert.equal(fixture.data.risk_events.length,19); - assert.equal(new Set(fixture.data.risk_events.map(r=>r.code)).size,19); - assert.ok(!fixture.data.risk_events.some(r=>r.code.startsWith('ai_'))); + assert.equal(fixture.data.risk_events,undefined); + assert.equal(fixture.data.fingerprint,undefined); + assert.equal(fixture.data.detection_flags.os_mismatch2,undefined); + for(const result of Object.values(fixture.data.hre).filter(v=>v && typeof v==='object')) assert.ok('cluster_id' in result); assert.equal(fixture.data.entity_risks,undefined); const missing=structuredClone(fixture);delete missing.event_id; assert.equal(validator.validate(schema,missing).valid,false); - for(const key of ['risk_events','hre','result_version','scoring_version']){ + for(const key of ['hre','result_version','scoring_version']){ const broken=structuredClone(fixture);delete broken.data[key]; assert.equal(validator.validate(schema,broken).valid,false,key+' required'); } diff --git a/test/contracts/anonymous.json b/test/contracts/anonymous.json index 7c52ee8..e359a25 100644 --- a/test/contracts/anonymous.json +++ b/test/contracts/anonymous.json @@ -2,158 +2,26 @@ "event_id": "ce256c0f66536957adc5f719250f5b10ce331bd1", "site_id": 7, "event_type": "identification.scored", - "schema_version": "2026-10-06", + "schema_version": "2026-10-07", "created_at": "2026-10-06T00:00:02Z", "data": { "result_version": "2026-10-06T00:00:01Z", "scoring_version": "core:fixture", - "risk_events": [ - { - "code": "vpn", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "proxy", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "privacy_relay", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "check_incomplete", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "network_check_incomplete", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_not_detected", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "timezone_mismatch", - "detected": false, - "weight": 10, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "javascript_disabled", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch2", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "ip_mismatch", - "detected": false, - "weight": 45, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "latency_test", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "anti_detect_browser", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "device_spoofing", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "banned_ip", - "detected": false, - "weight": 999, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "incognito", - "detected": true, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "search_bot", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "browser_automation", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "tor", - "detected": false, - "weight": 99, - "contribution": 0, - "status": "evaluated" - } - ], "hre": { "account_sharing": { + "cluster_id": null, "status": "not_applicable", "level": null, "reason": "anonymous_user" }, "account_takeover": { + "cluster_id": null, "status": "not_applicable", "level": null, "reason": "anonymous_user" }, "impossible_travel": { + "cluster_id": null, "status": "not_applicable", "level": null, "reason": "anonymous_user" @@ -192,7 +60,6 @@ "risk_score": 0, "signals": [], "detection_flags": { - "os_mismatch2": false, "device_spoofing": false, "latency_test": false, "banned_ip": false, @@ -211,6 +78,8 @@ "ip_mismatch": false, "incognito": true, "search_bot": false, + "ai_bot": false, + "ai_browser": false, "suspicious_paid_click": false, "javascript_disabled": false, "stun_not_checked": false, diff --git a/test/contracts/banned.json b/test/contracts/banned.json index fa633ed..7257f74 100644 --- a/test/contracts/banned.json +++ b/test/contracts/banned.json @@ -2,149 +2,15 @@ "event_id": "ce256c0f66536957adc5f719250f5b10ce331bd1", "site_id": 7, "event_type": "identification.scored", - "schema_version": "2026-10-06", + "schema_version": "2026-10-07", "created_at": "2026-10-06T00:00:02Z", "data": { "result_version": "2026-10-06T00:00:01Z", "scoring_version": "core:fixture", - "risk_events": [ - { - "code": "vpn", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "proxy", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "privacy_relay", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "check_incomplete", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "network_check_incomplete", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_not_detected", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "timezone_mismatch", - "detected": false, - "weight": 10, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "javascript_disabled", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch2", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "ip_mismatch", - "detected": false, - "weight": 45, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "latency_test", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "anti_detect_browser", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "device_spoofing", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "banned_ip", - "detected": true, - "weight": 999, - "contribution": 999, - "status": "evaluated" - }, - { - "code": "incognito", - "detected": true, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "search_bot", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "browser_automation", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "tor", - "detected": false, - "weight": 99, - "contribution": 0, - "status": "evaluated" - } - ], "hre": { "rules_version": "fp21-fixture", "account_sharing": { + "cluster_id": "11111111-1111-4111-8111-111111111111", "status": "evaluated", "level": "high", "reason": "", @@ -152,11 +18,13 @@ "min_devices": 4 }, "account_takeover": { + "cluster_id": "22222222-2222-4222-8222-222222222222", "status": "evaluated", "level": "medium", "reason": "fixture" }, "impossible_travel": { + "cluster_id": null, "status": "evaluated", "level": null, "reason": "below_distance" @@ -200,7 +68,6 @@ } ], "detection_flags": { - "os_mismatch2": false, "device_spoofing": false, "latency_test": false, "banned_ip": true, @@ -219,6 +86,8 @@ "ip_mismatch": false, "incognito": true, "search_bot": false, + "ai_bot": false, + "ai_browser": false, "suspicious_paid_click": false, "javascript_disabled": false, "stun_not_checked": false, diff --git a/test/contracts/incomplete.json b/test/contracts/incomplete.json index 2dd7aa5..301087a 100644 --- a/test/contracts/incomplete.json +++ b/test/contracts/incomplete.json @@ -2,149 +2,15 @@ "event_id": "ce256c0f66536957adc5f719250f5b10ce331bd1", "site_id": 7, "event_type": "identification.scored", - "schema_version": "2026-10-06", + "schema_version": "2026-10-07", "created_at": "2026-10-06T00:00:02Z", "data": { "result_version": "2026-10-06T00:00:01Z", "scoring_version": "core:fixture", - "risk_events": [ - { - "code": "vpn", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "proxy", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "privacy_relay", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "check_incomplete", - "detected": true, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "network_check_incomplete", - "detected": true, - "weight": 30, - "contribution": 30, - "status": "evaluated" - }, - { - "code": "os_not_detected", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "timezone_mismatch", - "detected": false, - "weight": 10, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "javascript_disabled", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch2", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "ip_mismatch", - "detected": false, - "weight": 45, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "latency_test", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "anti_detect_browser", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "device_spoofing", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "banned_ip", - "detected": false, - "weight": 999, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "incognito", - "detected": true, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "search_bot", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "browser_automation", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "tor", - "detected": false, - "weight": 99, - "contribution": 0, - "status": "evaluated" - } - ], "hre": { "rules_version": "fp21-fixture", "account_sharing": { + "cluster_id": "11111111-1111-4111-8111-111111111111", "status": "evaluated", "level": "high", "reason": "", @@ -152,11 +18,13 @@ "min_devices": 4 }, "account_takeover": { + "cluster_id": "22222222-2222-4222-8222-222222222222", "status": "evaluated", "level": "medium", "reason": "fixture" }, "impossible_travel": { + "cluster_id": null, "status": "evaluated", "level": null, "reason": "below_distance" @@ -200,7 +68,6 @@ } ], "detection_flags": { - "os_mismatch2": false, "device_spoofing": false, "latency_test": false, "banned_ip": false, @@ -219,6 +86,8 @@ "ip_mismatch": false, "incognito": true, "search_bot": false, + "ai_bot": false, + "ai_browser": false, "suspicious_paid_click": false, "javascript_disabled": false, "stun_not_checked": true, diff --git a/test/contracts/named.json b/test/contracts/named.json index 1a653a4..9d51b09 100644 --- a/test/contracts/named.json +++ b/test/contracts/named.json @@ -2,149 +2,15 @@ "event_id": "ce256c0f66536957adc5f719250f5b10ce331bd1", "site_id": 7, "event_type": "identification.scored", - "schema_version": "2026-10-06", + "schema_version": "2026-10-07", "created_at": "2026-10-06T00:00:02Z", "data": { "result_version": "2026-10-06T00:00:01Z", "scoring_version": "core:fixture", - "risk_events": [ - { - "code": "vpn", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "proxy", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "privacy_relay", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "check_incomplete", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "network_check_incomplete", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_not_detected", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "timezone_mismatch", - "detected": false, - "weight": 10, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "javascript_disabled", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch2", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "ip_mismatch", - "detected": false, - "weight": 45, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "latency_test", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "anti_detect_browser", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "device_spoofing", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "banned_ip", - "detected": false, - "weight": 999, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "incognito", - "detected": true, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "search_bot", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "browser_automation", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "tor", - "detected": false, - "weight": 99, - "contribution": 0, - "status": "evaluated" - } - ], "hre": { "rules_version": "fp21-fixture", "account_sharing": { + "cluster_id": "11111111-1111-4111-8111-111111111111", "status": "evaluated", "level": "high", "reason": "", @@ -152,11 +18,13 @@ "min_devices": 4 }, "account_takeover": { + "cluster_id": "22222222-2222-4222-8222-222222222222", "status": "evaluated", "level": "medium", "reason": "fixture" }, "impossible_travel": { + "cluster_id": null, "status": "evaluated", "level": null, "reason": "below_distance" @@ -195,7 +63,6 @@ "risk_score": 0, "signals": [], "detection_flags": { - "os_mismatch2": false, "device_spoofing": false, "latency_test": false, "banned_ip": false, @@ -214,6 +81,8 @@ "ip_mismatch": false, "incognito": true, "search_bot": false, + "ai_bot": false, + "ai_browser": false, "suspicious_paid_click": false, "javascript_disabled": false, "stun_not_checked": false, diff --git a/test/contracts/no-history.json b/test/contracts/no-history.json index cd7bc6a..f2f77c1 100644 --- a/test/contracts/no-history.json +++ b/test/contracts/no-history.json @@ -2,149 +2,15 @@ "event_id": "ce256c0f66536957adc5f719250f5b10ce331bd1", "site_id": 7, "event_type": "identification.scored", - "schema_version": "2026-10-06", + "schema_version": "2026-10-07", "created_at": "2026-10-06T00:00:02Z", "data": { "result_version": "2026-10-06T00:00:01Z", "scoring_version": "core:fixture", - "risk_events": [ - { - "code": "vpn", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "proxy", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "privacy_relay", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "check_incomplete", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "network_check_incomplete", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_not_detected", - "detected": false, - "weight": 30, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "timezone_mismatch", - "detected": false, - "weight": 10, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "javascript_disabled", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "os_mismatch2", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "ip_mismatch", - "detected": false, - "weight": 45, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "latency_test", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "anti_detect_browser", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "device_spoofing", - "detected": false, - "weight": 90, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "banned_ip", - "detected": false, - "weight": 999, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "incognito", - "detected": true, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "search_bot", - "detected": false, - "weight": 0, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "browser_automation", - "detected": false, - "weight": 60, - "contribution": 0, - "status": "evaluated" - }, - { - "code": "tor", - "detected": false, - "weight": 99, - "contribution": 0, - "status": "evaluated" - } - ], "hre": { "rules_version": "fp21-fixture", "account_sharing": { + "cluster_id": null, "status": "evaluated", "level": null, "reason": "", @@ -152,11 +18,13 @@ "min_devices": 4 }, "account_takeover": { + "cluster_id": null, "status": "not_evaluated", "level": null, "reason": "no_history" }, "impossible_travel": { + "cluster_id": null, "status": "not_evaluated", "level": null, "reason": "skipped" @@ -195,7 +63,6 @@ "risk_score": 0, "signals": [], "detection_flags": { - "os_mismatch2": false, "device_spoofing": false, "latency_test": false, "banned_ip": false, @@ -214,6 +81,8 @@ "ip_mismatch": false, "incognito": true, "search_bot": false, + "ai_bot": false, + "ai_browser": false, "suspicious_paid_click": false, "javascript_disabled": false, "stun_not_checked": false,