From fd9d31e6840839993d21f1802a94658e661cf872 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Mon, 31 Aug 2026 15:51:31 +0100 Subject: [PATCH 01/10] chore: generate event as device/edge union INTER-2457 option C. Node Event is EventDevice | EventEdge. --- resources/fingerprint-server-api.yaml | 511 ++++++++++++++---- src/generatedApiTypes.ts | 326 +++++++++-- .../webhook/webhook_event.json | 1 + 3 files changed, 704 insertions(+), 134 deletions(-) diff --git a/resources/fingerprint-server-api.yaml b/resources/fingerprint-server-api.yaml index 3c2626a5..88835dd7 100644 --- a/resources/fingerprint-server-api.yaml +++ b/resources/fingerprint-server-api.yaml @@ -37,6 +37,97 @@ servers: security: - bearerAuth: [] paths: + /edge: + post: + tags: + - Fingerprint + operationId: analyzeRequestForAutomationIntelligence + summary: Collect Automation Intelligence. + description: > + The Automation Intelligence API gives you the tools to determine whether + traffic is legitimate and should be accepted by your application. + + + This feature is currently in a Public Preview testing phase. All + feedback is welcome! If you encounter any issues, please [contact our + support team](https://fingerprint.com/support/). + + + The API detects automation tools like AI Agents, AI Assistants, AI + Browsers, and other bots. Additionally, it provides IP intelligence like + geolocation, residential proxy, VPN and data center detection. + + + Automation Intelligence is derived from HTTP request metadata that + reaches your server. It does not require the use of a JavaScript + client-side agent or mobile SDKs to collect device context. + + + The API is fast, with average response times of less than 30ms, making + it a great fit for edge, pre-origin or middleware contexts. The API is + platform-agnostic and can be used with different CDN providers, cloud + platforms, or any server backend. + + + Because this API doesn’t require the use of a client-side device + collection agent, it doesn’t support device identification via + `visitor_id` and a few Smart Signals derived from deep device telemetry. + + + ### Event Retrieval + + + Events created by the Automation Intelligence API can be fetched via the + [`/v4/events/{event_id}`](https://docs.fingerprint.com/reference/server-api-get-event) + API using the `event_id` present in the API response. + + + Fetch all Automation Intelligence API events via the + [`/v4/events?source=edge`](https://docs.fingerprint.com/reference/server-api-search-events#parameter-source) + API. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EdgeRequest' + responses: + '200': + description: OK. + content: + application/json: + schema: + $ref: '#/components/schemas/EventEdge' + '400': + description: Bad request. The request payload is not valid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden. Access to this API is denied. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '413': + description: Bad request. The request payload is too large. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '429': + description: Too Many Requests. The request is throttled. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Workspace error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /events/{event_id}: get: tags: @@ -44,12 +135,15 @@ paths: operationId: getEvent summary: Get an event by event ID description: > - Get a detailed analysis of an individual identification event, including - Smart Signals. + Get a detailed analysis of an individual event, including Smart Signals. Use `event_id` as the URL path parameter. This API method is scoped to a request, i.e. all returned information is by `event_id`. + + + Returns `EventDevice` when `source` is `device`, and `EventEdge` when + `source` is `edge`. parameters: - name: event_id in: path @@ -61,7 +155,7 @@ paths: example: 1708102555327.NLOjmg description: >- The unique - [identifier](https://docs.fingerprint.com/reference/js-agent-v4-get-function#event_id) + [identifier](https://docs.fingerprint.com/reference/js-agent-get-function#event_id) of each identification request (`requestId` can be used in its place). - name: ruleset_id @@ -165,7 +259,7 @@ paths: example: 1708102555327.NLOjmg description: >- The unique event - [identifier](https://docs.fingerprint.com/reference/js-agent-v4-get-function#event_id). + [identifier](https://docs.fingerprint.com/reference/js-agent-get-function#event_id). requestBody: required: true content: @@ -317,7 +411,7 @@ paths: example: Ibk1527CUFmcnjLwIs4A9 description: > Unique [visitor - identifier](https://docs.fingerprint.com/reference/js-agent-v4-get-function#visitor_id) + identifier](https://docs.fingerprint.com/reference/js-agent-get-function#visitor_id) issued by Fingerprint Identification and all active Smart Signals. @@ -492,7 +586,7 @@ paths: You can use [linked - Ids](https://docs.fingerprint.com/reference/js-agent-v4-get-function#linkedid) + Ids](https://docs.fingerprint.com/reference/js-agent-get-function#linkedid) to associate identification requests with your own identifier, for example, session Id, purchase Id, or transaction Id. You can then use this `linked_id` parameter to retrieve all events associated @@ -1110,7 +1204,7 @@ paths: example: Ibk1527CUFmcnjLwIs4A9 description: >- The [visitor - ID](https://docs.fingerprint.com/reference/js-agent-v4-get-function#visitor_id) + ID](https://docs.fingerprint.com/reference/js-agent-get-function#visitor_id) you want to delete. responses: '200': @@ -1163,6 +1257,127 @@ components: identification request or updated later. additionalProperties: true required: [] + EdgeRequest: + type: object + description: >- + HTTP request metadata (including the HTTP method, headers and IP + address) sent by you (your server) to the Fingerprint API for IP and bot + analysis. To improve accuracy, retain as much of the original semantics + of the HTTP request as possible. For example, preserve the order of the + request headers and their capitalization. + + At least one of `ipv4_address` or `ipv6_address` must be provided; a + request with neither is rejected with a `400` error. If both IPv4 and + IPv6 are provided, IP intelligence will be provided for each address. If + an IPv4-mapped IPv6 address is provided in the `ipv6_address` request + property, the IP intelligence will be provided in the `ipv4_address` + property of the response. + required: + - headers + - method + - url + properties: + headers: + type: array + description: > + Ordered header entries from the request made to your server. Each + entry represents one header line. If one header name appears as + multiple lines, send each as a separate item in the array. + + + Headers that contain authentication or session data must still be + included, but with with their value set to an empty string. This + includes headers like `Authorization` and `Cookie`, but may contain + more depending on your specific project, for instance + `Proxy-Authenticate` or `X-Api-Key`. Omitting the headers entirely + changes the shape of the request and can affect detection. Never + forward the real secret values. + + + Whenever possible, we recommend preserving header order and + capitalization to provide the best accuracy, however it’s not a + strict requirement if your runtime does not maintain http header + order or canonicalizes header names. + minItems: 1 + items: + type: object + required: + - name + - value + properties: + name: + type: string + examples: + - User-Agent + description: >- + Header name as forwarded by your server. Headers must be valid + according to RFC 7230 and will be canonicalized according to + RFC 9112. + value: + type: string + examples: + - Mozilla/5.0 + description: >- + Value of a single forwarded header entry. Be careful to + preserve the original encoding and escaping. For example, do + not double escape quotes. + examples: + - - name: Host + value: example.com + - name: User-Agent + value: Mozilla/5.0 + - name: Accept-Language + value: en-US,en;q=0.9 + - - name: Host + value: example.com + - name: User-Agent + value: Mozilla/5.0 + - name: Accept-Encoding + value: gzip + - name: Accept-Encoding + value: deflate + method: + type: string + description: >- + The original HTTP method of the request. If supported in your + runtime, preserve the original casing. + examples: + - GET + - POST + - PUT + - PATCH + - DELETE + url: + type: string + description: >- + Absolute URL of the request, without a \#fragment suffix. Only HTTP + and HTTPS schemes are supported. + format: uri + examples: + - http://example.com + - https://example.com/checkout?method=card + ipv4_address: + type: string + description: Client IPv4 address observed by your server. + format: ipv4 + examples: + - 34.162.244.71 + - 3.208.0.3 + - 173.56.0.4 + ipv6_address: + type: string + description: Client IPv6 address observed by your server. + format: ipv6 + examples: + - 2001:4860:4801:10::1 + - 2600:1f42:abcd:5678:9876:fedc:1357:2468 + - 2001:4868:85f:1a2b:3c4d:5e6f:7890:abcd + - ::ffff:22a2:f447 + - ::ffff:34.162.244.71 + linked_id: + $ref: '#/components/schemas/LinkedId' + tags: + $ref: '#/components/schemas/Tags' EventId: type: string examples: @@ -1176,19 +1391,6 @@ components: format: int64 examples: - 1708102555327 - EventSource: - type: string - description: > - Identifies how the event was generated. - - - `device` - the event was generated by the JS agent or a mobile SDK - running on an end-user device. - - - `edge` - the event was generated by the Automation Intelligence API - (`/edge` endpoint), analyzing a request intercepted at the edge. - enum: - - device - - edge Url: type: string examples: @@ -1596,6 +1798,109 @@ components: description: > `true` if the request came from a device running a VPN, `false` otherwise. + EventSource: + type: string + description: > + Identifies how the event was generated. + + - `device` - the event was generated by the JS agent or a mobile SDK + running on an end-user device. + + - `edge` - the event was generated by the Automation Intelligence API + (`/edge` endpoint), analyzing a request intercepted at the edge. + enum: + - device + - edge + EventEdge: + type: object + description: > + IP and bot analysis for an event generated by the Automation + Intelligence API (`/edge` endpoint). No client-side collection agent is + involved, so Identification (`visitor_id`) and device-telemetry-derived + Smart Signals are not available. + properties: + event_id: + $ref: '#/components/schemas/EventId' + x-platforms: + - android + - ios + - browser + timestamp: + $ref: '#/components/schemas/Timestamp' + x-platforms: + - android + - ios + - browser + linked_id: + $ref: '#/components/schemas/LinkedId' + x-platforms: + - android + - ios + - browser + tags: + $ref: '#/components/schemas/Tags' + x-platforms: + - android + - ios + - browser + url: + $ref: '#/components/schemas/Url' + x-platforms: + - browser + bot_info: + $ref: '#/components/schemas/BotInfo' + x-platforms: + - browser + ip_info: + $ref: '#/components/schemas/IPInfo' + x-platforms: + - android + - ios + - browser + proxy: + $ref: '#/components/schemas/Proxy' + x-platforms: + - android + - ios + - browser + proxy_confidence: + $ref: '#/components/schemas/ProxyConfidence' + x-platforms: + - android + - ios + - browser + proxy_details: + $ref: '#/components/schemas/ProxyDetails' + x-platforms: + - android + - ios + - browser + vpn: + $ref: '#/components/schemas/Vpn' + x-platforms: + - android + - ios + - browser + vpn_confidence: + $ref: '#/components/schemas/VpnConfidence' + x-platforms: + - android + - ios + - browser + vpn_methods: + $ref: '#/components/schemas/VpnMethods' + x-platforms: + - android + - ios + - browser + source: + $ref: '#/components/schemas/EventSource' + const: edge + required: + - event_id + - timestamp + - source + - ip_info ErrorCode: type: string enum: @@ -1715,7 +2020,7 @@ components: description: >- Field is `true` if you have previously set the `suspect` flag for this event using the [Server API Update event - endpoint](https://docs.fingerprint.com/reference/server-api-v4-update-event). + endpoint](https://docs.fingerprint.com/reference/server-api-update-event). Integration: type: object required: [] @@ -3082,18 +3387,13 @@ components: case. This field is in the beta phase and only available to select customers. If you are interested, please [contact our support team](https://fingerprint.com/support/). - Event: + EventDevice: type: object - description: >- - Contains results from Fingerprint Identification and all active Smart - Signals. Some Smart Signals are only supported for certain device types, - these fields will be omitted for events not generated from the supported - devices. Consult the [Smart Signals + description: > + Contains results from Fingerprint Identification and Smart Signals + derived from client-side device telemetry. Consult the [Smart Signals reference](https://docs.fingerprint.com/docs/smart-signals-reference) for more details. - required: - - event_id - - timestamp properties: event_id: $ref: '#/components/schemas/EventId' @@ -3107,22 +3407,79 @@ components: - android - ios - browser - source: - $ref: '#/components/schemas/EventSource' + linked_id: + $ref: '#/components/schemas/LinkedId' x-platforms: - android - ios - browser - incremental_identification_status: - $ref: '#/components/schemas/IncrementalIdentificationStatus' + tags: + $ref: '#/components/schemas/Tags' x-platforms: + - android + - ios - browser - linked_id: - $ref: '#/components/schemas/LinkedId' + url: + $ref: '#/components/schemas/Url' + x-platforms: + - browser + bot_info: + $ref: '#/components/schemas/BotInfo' + x-platforms: + - browser + ip_info: + $ref: '#/components/schemas/IPInfo' + x-platforms: + - android + - ios + - browser + proxy: + $ref: '#/components/schemas/Proxy' + x-platforms: + - android + - ios + - browser + proxy_confidence: + $ref: '#/components/schemas/ProxyConfidence' + x-platforms: + - android + - ios + - browser + proxy_details: + $ref: '#/components/schemas/ProxyDetails' + x-platforms: + - android + - ios + - browser + vpn: + $ref: '#/components/schemas/Vpn' + x-platforms: + - android + - ios + - browser + vpn_confidence: + $ref: '#/components/schemas/VpnConfidence' x-platforms: - android - ios - browser + vpn_methods: + $ref: '#/components/schemas/VpnMethods' + x-platforms: + - android + - ios + - browser + source: + $ref: '#/components/schemas/EventSource' + const: device + x-platforms: + - android + - ios + - browser + incremental_identification_status: + $ref: '#/components/schemas/IncrementalIdentificationStatus' + x-platforms: + - browser environment_id: $ref: '#/components/schemas/EnvironmentId' x-platforms: @@ -3159,16 +3516,6 @@ components: - android - ios - browser - tags: - $ref: '#/components/schemas/Tags' - x-platforms: - - android - - ios - - browser - url: - $ref: '#/components/schemas/Url' - x-platforms: - - browser bundle_id: $ref: '#/components/schemas/BundleId' x-platforms: @@ -3234,10 +3581,6 @@ components: $ref: '#/components/schemas/BotType' x-platforms: - browser - bot_info: - $ref: '#/components/schemas/BotInfo' - x-platforms: - - browser cloned_app: $ref: '#/components/schemas/ClonedApp' x-platforms: @@ -3268,30 +3611,6 @@ components: - android - ios - browser - ip_info: - $ref: '#/components/schemas/IPInfo' - x-platforms: - - android - - ios - - browser - proxy: - $ref: '#/components/schemas/Proxy' - x-platforms: - - android - - ios - - browser - proxy_confidence: - $ref: '#/components/schemas/ProxyConfidence' - x-platforms: - - android - - ios - - browser - proxy_details: - $ref: '#/components/schemas/ProxyDetails' - x-platforms: - - android - - ios - - browser proxy_ml_score: $ref: '#/components/schemas/ProxyMLScore' x-platforms: @@ -3372,18 +3691,6 @@ components: $ref: '#/components/schemas/VirtualMachineMLScore' x-platforms: - browser - vpn: - $ref: '#/components/schemas/Vpn' - x-platforms: - - android - - ios - - browser - vpn_confidence: - $ref: '#/components/schemas/VpnConfidence' - x-platforms: - - android - - ios - - browser vpn_ml_score: $ref: '#/components/schemas/VpnMLScore' x-platforms: @@ -3399,12 +3706,6 @@ components: x-platforms: - android - ios - vpn_methods: - $ref: '#/components/schemas/VpnMethods' - x-platforms: - - android - - ios - - browser high_activity_device: $ref: '#/components/schemas/HighActivity' x-platforms: @@ -3431,6 +3732,32 @@ components: - browser - ios - android + required: + - event_id + - timestamp + - source + Event: + type: object + description: > + An identification event (`source: device`) or an Automation Intelligence + event (`source: edge`). + + + Use `source` to tell them apart. Device events include Identification + and device-derived Smart Signals. Edge events do not. + + + Consult the [Smart Signals + reference](https://docs.fingerprint.com/docs/smart-signals-reference) + for more details. + oneOf: + - $ref: '#/components/schemas/EventDevice' + - $ref: '#/components/schemas/EventEdge' + discriminator: + propertyName: source + mapping: + device: '#/components/schemas/EventDevice' + edge: '#/components/schemas/EventEdge' EventUpdate: type: object required: [] diff --git a/src/generatedApiTypes.ts b/src/generatedApiTypes.ts index 5134e088..892366c3 100644 --- a/src/generatedApiTypes.ts +++ b/src/generatedApiTypes.ts @@ -1,4 +1,40 @@ export interface paths { + '/edge': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * Collect Automation Intelligence. + * @description The Automation Intelligence API gives you the tools to determine whether traffic is legitimate and should be accepted by your application. + * + * This feature is currently in a Public Preview testing phase. All feedback is welcome! If you encounter any issues, please [contact our support team](https://fingerprint.com/support/). + * + * The API detects automation tools like AI Agents, AI Assistants, AI Browsers, and other bots. Additionally, it provides IP intelligence like geolocation, residential proxy, VPN and data center detection. + * + * Automation Intelligence is derived from HTTP request metadata that reaches your server. It does not require the use of a JavaScript client-side agent or mobile SDKs to collect device context. + * + * The API is fast, with average response times of less than 30ms, making it a great fit for edge, pre-origin or middleware contexts. The API is platform-agnostic and can be used with different CDN providers, cloud platforms, or any server backend. + * + * Because this API doesn’t require the use of a client-side device collection agent, it doesn’t support device identification via `visitor_id` and a few Smart Signals derived from deep device telemetry. + * + * ### Event Retrieval + * + * Events created by the Automation Intelligence API can be fetched via the [`/v4/events/{event_id}`](https://docs.fingerprint.com/reference/server-api-get-event) API using the `event_id` present in the API response. + * + * Fetch all Automation Intelligence API events via the [`/v4/events?source=edge`](https://docs.fingerprint.com/reference/server-api-search-events#parameter-source) API. + */ + post: operations['analyzeRequestForAutomationIntelligence'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/events/{event_id}': { parameters: { query?: never @@ -8,9 +44,11 @@ export interface paths { } /** * Get an event by event ID - * @description Get a detailed analysis of an individual identification event, including Smart Signals. + * @description Get a detailed analysis of an individual event, including Smart Signals. * * Use `event_id` as the URL path parameter. This API method is scoped to a request, i.e. all returned information is by `event_id`. + * + * Returns `EventDevice` when `source` is `device`, and `EventEdge` when `source` is `edge`. */ get: operations['getEvent'] put?: never @@ -137,6 +175,101 @@ export interface components { Tags: { [key: string]: unknown } + /** + * @description HTTP request metadata (including the HTTP method, headers and IP address) sent by you (your server) to the Fingerprint API for IP and bot analysis. To improve accuracy, retain as much of the original semantics of the HTTP request as possible. For example, preserve the order of the request headers and their capitalization. + * At least one of `ipv4_address` or `ipv6_address` must be provided; a request with neither is rejected with a `400` error. If both IPv4 and IPv6 are provided, IP intelligence will be provided for each address. If an IPv4-mapped IPv6 address is provided in the `ipv6_address` request property, the IP intelligence will be provided in the `ipv4_address` property of the response. + */ + EdgeRequest: { + /** + * @description Ordered header entries from the request made to your server. Each entry represents one header line. If one header name appears as multiple lines, send each as a separate item in the array. + * + * Headers that contain authentication or session data must still be included, but with with their value set to an empty string. This includes headers like `Authorization` and `Cookie`, but may contain more depending on your specific project, for instance `Proxy-Authenticate` or `X-Api-Key`. Omitting the headers entirely changes the shape of the request and can affect detection. Never forward the real secret values. + * + * Whenever possible, we recommend preserving header order and capitalization to provide the best accuracy, however it’s not a strict requirement if your runtime does not maintain http header order or canonicalizes header names. + * @example [ + * { + * "name": "Host", + * "value": "example.com" + * }, + * { + * "name": "User-Agent", + * "value": "Mozilla/5.0" + * }, + * { + * "name": "Accept-Language", + * "value": "en-US,en;q=0.9" + * } + * ] + * @example [ + * { + * "name": "Host", + * "value": "example.com" + * }, + * { + * "name": "User-Agent", + * "value": "Mozilla/5.0" + * }, + * { + * "name": "Accept-Encoding", + * "value": "gzip" + * }, + * { + * "name": "Accept-Encoding", + * "value": "deflate" + * } + * ] + */ + headers: { + /** + * @description Header name as forwarded by your server. Headers must be valid according to RFC 7230 and will be canonicalized according to RFC 9112. + * @example User-Agent + */ + name: string + /** + * @description Value of a single forwarded header entry. Be careful to preserve the original encoding and escaping. For example, do not double escape quotes. + * @example Mozilla/5.0 + */ + value: string + }[] + /** + * @description The original HTTP method of the request. If supported in your runtime, preserve the original casing. + * @example GET + * @example POST + * @example PUT + * @example PATCH + * @example DELETE + */ + method: string + /** + * Format: uri + * @description Absolute URL of the request, without a \#fragment suffix. Only HTTP and HTTPS schemes are supported. + * @example http://example.com + * @example https://example.com/checkout?method=card + */ + url: string + /** + * Format: ipv4 + * @description Client IPv4 address observed by your server. + * @example 34.162.244.71 + * @example 3.208.0.3 + * @example 173.56.0.4 + */ + ipv4_address?: string + /** + * Format: ipv6 + * @description Client IPv6 address observed by your server. + * @example 2001:4860:4801:10::1 + * @example 2600:1f42:abcd:5678:9876:fedc:1357:2468 + * @example 2001:4868:85f:1a2b:3c4d:5e6f:7890:abcd + * @example ::ffff:22a2:f447 + * @example ::ffff:34.162.244.71 + */ + ipv6_address?: string + /** @description A customer-provided id that was sent with the request. */ + linked_id?: components['schemas']['LinkedId'] + /** @description A customer-provided value or an object that was sent with the identification request or updated later. */ + tags?: components['schemas']['Tags'] + } /** * @description Unique identifier of the user's request. The first portion of the event_id is a unix epoch milliseconds timestamp. * @example 1708102555327.NLOjmg @@ -148,13 +281,6 @@ export interface components { * @example 1708102555327 */ Timestamp: number - /** - * @description Identifies how the event was generated. - * - `device` - the event was generated by the JS agent or a mobile SDK running on an end-user device. - * - `edge` - the event was generated by the Automation Intelligence API (`/edge` endpoint), analyzing a request intercepted at the edge. - * @enum {string} - */ - EventSource: 'device' | 'edge' /** * @description Page URL from which the request was sent. * @example https://www.example.com/login @@ -419,6 +545,46 @@ export interface components { /** @description `true` if the request came from a device running a VPN, `false` otherwise. */ ml_prediction?: boolean } + /** + * @description Identifies how the event was generated. + * - `device` - the event was generated by the JS agent or a mobile SDK running on an end-user device. + * - `edge` - the event was generated by the Automation Intelligence API (`/edge` endpoint), analyzing a request intercepted at the edge. + * @enum {string} + */ + EventSource: 'device' | 'edge' + /** @description IP and bot analysis for an event generated by the Automation Intelligence API (`/edge` endpoint). No client-side collection agent is involved, so Identification (`visitor_id`) and device-telemetry-derived Smart Signals are not available. */ + EventEdge: { + /** @description Unique identifier of the user's request. The first portion of the event_id is a unix epoch milliseconds timestamp. */ + event_id: components['schemas']['EventId'] + /** @description Timestamp of the event with millisecond precision in Unix time. */ + timestamp: components['schemas']['Timestamp'] + /** @description A customer-provided id that was sent with the request. */ + linked_id?: components['schemas']['LinkedId'] + /** @description A customer-provided value or an object that was sent with the identification request or updated later. */ + tags?: components['schemas']['Tags'] + /** @description Page URL from which the request was sent. */ + url?: components['schemas']['Url'] + /** @description Extended bot information. */ + bot_info?: components['schemas']['BotInfo'] + /** @description Details about the request IP address. Has separate fields for v4 and v6 IP address versions. */ + ip_info: components['schemas']['IPInfo'] + /** @description IP address was used by a public proxy provider or belonged to a known recent residential proxy */ + proxy?: components['schemas']['Proxy'] + /** @description Confidence level of the proxy detection. If a proxy is not detected, confidence is "high". If it's detected, can be "low", "medium", or "high". */ + proxy_confidence?: components['schemas']['ProxyConfidence'] + /** @description Proxy detection details (present if `proxy` is `true`) */ + proxy_details?: components['schemas']['ProxyDetails'] + /** @description VPN or other anonymizing service has been used when sending the request. */ + vpn?: components['schemas']['Vpn'] + /** @description A confidence rating for the VPN detection result — "low", "medium", or "high". Depends on the combination of results returned from all VPN detection methods. */ + vpn_confidence?: components['schemas']['VpnConfidence'] + vpn_methods?: components['schemas']['VpnMethods'] + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + source: 'edge' + } /** * @description Error code: * * `request_cannot_be_parsed` - The query parameters or JSON payload contains some errors @@ -511,7 +677,7 @@ export interface components { * @example ae_47abaca3db2c7c43 */ EnvironmentId: string - /** @description Field is `true` if you have previously set the `suspect` flag for this event using the [Server API Update event endpoint](https://docs.fingerprint.com/reference/server-api-v4-update-event). */ + /** @description Field is `true` if you have previously set the `suspect` flag for this event using the [Server API Update event endpoint](https://docs.fingerprint.com/reference/server-api-update-event). */ Suspect: boolean Integration: { /** @@ -1385,29 +1551,47 @@ export interface components { */ ml_score?: number }[] - /** @description Contains results from Fingerprint Identification and all active Smart Signals. Some Smart Signals are only supported for certain device types, these fields will be omitted for events not generated from the supported devices. Consult the [Smart Signals reference](https://docs.fingerprint.com/docs/smart-signals-reference) for more details. */ - Event: { + /** @description Contains results from Fingerprint Identification and Smart Signals derived from client-side device telemetry. Consult the [Smart Signals reference](https://docs.fingerprint.com/docs/smart-signals-reference) for more details. */ + EventDevice: { /** @description Unique identifier of the user's request. The first portion of the event_id is a unix epoch milliseconds timestamp. */ event_id: components['schemas']['EventId'] /** @description Timestamp of the event with millisecond precision in Unix time. */ timestamp: components['schemas']['Timestamp'] + /** @description A customer-provided id that was sent with the request. */ + linked_id?: components['schemas']['LinkedId'] + /** @description A customer-provided value or an object that was sent with the identification request or updated later. */ + tags?: components['schemas']['Tags'] + /** @description Page URL from which the request was sent. */ + url?: components['schemas']['Url'] + /** @description Extended bot information. */ + bot_info?: components['schemas']['BotInfo'] + /** @description Details about the request IP address. Has separate fields for v4 and v6 IP address versions. */ + ip_info?: components['schemas']['IPInfo'] + /** @description IP address was used by a public proxy provider or belonged to a known recent residential proxy */ + proxy?: components['schemas']['Proxy'] + /** @description Confidence level of the proxy detection. If a proxy is not detected, confidence is "high". If it's detected, can be "low", "medium", or "high". */ + proxy_confidence?: components['schemas']['ProxyConfidence'] + /** @description Proxy detection details (present if `proxy` is `true`) */ + proxy_details?: components['schemas']['ProxyDetails'] + /** @description VPN or other anonymizing service has been used when sending the request. */ + vpn?: components['schemas']['Vpn'] + /** @description A confidence rating for the VPN detection result — "low", "medium", or "high". Depends on the combination of results returned from all VPN detection methods. */ + vpn_confidence?: components['schemas']['VpnConfidence'] + vpn_methods?: components['schemas']['VpnMethods'] /** - * @description Identifies how the event was generated. - * - `device` - the event was generated by the JS agent or a mobile SDK running on an end-user device. - * - `edge` - the event was generated by the Automation Intelligence API (`/edge` endpoint), analyzing a request intercepted at the edge. + * @description discriminator enum property added by openapi-typescript + * @enum {string} */ - source?: components['schemas']['EventSource'] + source: 'device' /** * @description Only included for requests using incremental identification. * - `partially_completed` - Indicates this event corresponds to a 'minimal' request. Smart Signals, even if included in your plan, are not computed; hence, their values must be ignored. * - `completed` - Indicates this event corresponds to a 'complete' request. Smart Signals, if included in your plan, are computed; hence, their values are valid and relevant. */ incremental_identification_status?: components['schemas']['IncrementalIdentificationStatus'] - /** @description A customer-provided id that was sent with the request. */ - linked_id?: components['schemas']['LinkedId'] /** @description Environment Id of the event. */ environment_id?: components['schemas']['EnvironmentId'] - /** @description Field is `true` if you have previously set the `suspect` flag for this event using the [Server API Update event endpoint](https://docs.fingerprint.com/reference/server-api-v4-update-event). */ + /** @description Field is `true` if you have previously set the `suspect` flag for this event using the [Server API Update event endpoint](https://docs.fingerprint.com/reference/server-api-update-event). */ suspect?: components['schemas']['Suspect'] /** @description Contains information about the SDK used to perform the request. */ sdk?: components['schemas']['SDK'] @@ -1416,10 +1600,6 @@ export interface components { identification?: components['schemas']['Identification'] /** @description The High Recall ID is a supplementary browser identifier designed for use cases that require wider coverage over precision. Compared to the standard visitor ID, the High Recall ID strives to match incoming browsers more generously (rather than precisely) with existing browsers and thus identifies fewer browsers as new. The High Recall ID is best suited for use cases that are sensitive to browsers being identified as new and where mismatched browsers are not detrimental. */ supplementary_id_high_recall?: components['schemas']['SupplementaryIDHighRecall'] - /** @description A customer-provided value or an object that was sent with the identification request or updated later. */ - tags?: components['schemas']['Tags'] - /** @description Page URL from which the request was sent. */ - url?: components['schemas']['Url'] /** @description Bundle Id of the iOS application integrated with the Fingerprint SDK for the event. */ bundle_id?: components['schemas']['BundleId'] /** @description Package name of the Android application integrated with the Fingerprint SDK for the event. */ @@ -1450,8 +1630,6 @@ export interface components { bot?: components['schemas']['BotResult'] /** @description Additional classification of the bot type if detected. */ bot_type?: components['schemas']['BotType'] - /** @description Extended bot information. */ - bot_info?: components['schemas']['BotInfo'] /** * @description Android specific cloned application detection. There are 2 values: * * `true` - Presence of app cloners work detected (e.g. fully cloned application found or launch of it inside of a not main working profile detected). @@ -1475,14 +1653,6 @@ export interface components { */ frida?: components['schemas']['Frida'] ip_blocklist?: components['schemas']['IPBlockList'] - /** @description Details about the request IP address. Has separate fields for v4 and v6 IP address versions. */ - ip_info?: components['schemas']['IPInfo'] - /** @description IP address was used by a public proxy provider or belonged to a known recent residential proxy */ - proxy?: components['schemas']['Proxy'] - /** @description Confidence level of the proxy detection. If a proxy is not detected, confidence is "high". If it's detected, can be "low", "medium", or "high". */ - proxy_confidence?: components['schemas']['ProxyConfidence'] - /** @description Proxy detection details (present if `proxy` is `true`) */ - proxy_details?: components['schemas']['ProxyDetails'] /** @description Machine learning–based proxy score, represented as a floating-point value between 0 and 1 (inclusive), with up to three decimal places of precision. A higher score means a higher confidence in the positive `proxy` detection result. This Smart Signal is currently in beta and only available to select customers. If you are interested, please [contact our support team](https://fingerprint.com/support/). */ proxy_ml_score?: components['schemas']['ProxyMLScore'] /** @description `true` if we detected incognito mode used in the browser, `false` otherwise. */ @@ -1564,17 +1734,12 @@ export interface components { virtual_machine?: components['schemas']['VirtualMachine'] /** @description Machine learning–based virtual machine score, represented as a floating-point value between 0 and 1 (inclusive), with up to three decimal places of precision. A higher score means a higher confidence in the positive `virtual_machine` detection result. This Smart Signal is currently in beta and only available to select customers. If you are interested, please [contact our support team](https://fingerprint.com/support/). */ virtual_machine_ml_score?: components['schemas']['VirtualMachineMLScore'] - /** @description VPN or other anonymizing service has been used when sending the request. */ - vpn?: components['schemas']['Vpn'] - /** @description A confidence rating for the VPN detection result — "low", "medium", or "high". Depends on the combination of results returned from all VPN detection methods. */ - vpn_confidence?: components['schemas']['VpnConfidence'] /** @description Machine learning–based VPN score, represented as a floating-point value between 0 and 1 (inclusive), with up to three decimal places of precision. A higher score means a higher confidence in the positive `vpn` detection result. This Smart Signal is currently in beta and only available to select customers. If you are interested, please [contact our support team](https://fingerprint.com/support/). */ vpn_ml_score?: components['schemas']['VpnMLScore'] /** @description Local timezone which is used in timezone_mismatch method. */ vpn_origin_timezone?: components['schemas']['VpnOriginTimezone'] /** @description Country of the request (Android SDK version >= 2.4.0, iOS SDK version >= 2.9.0, JS agent >= 3.12.9 / 4.0.2), ISO 3166 format or unknown. */ vpn_origin_country?: components['schemas']['VpnOriginCountry'] - vpn_methods?: components['schemas']['VpnMethods'] /** @description Flag indicating if the request came from a high-activity visitor. */ high_activity_device?: components['schemas']['HighActivity'] /** @@ -1592,6 +1757,14 @@ export interface components { /** @description Each label returns a prediction (true or false) for a specific use case (label field) based on a machine learning score. The machine learning score is determined by a model trained on customer data for that use case. This field is in the beta phase and only available to select customers. If you are interested, please [contact our support team](https://fingerprint.com/support/). */ labels?: components['schemas']['Labels'] } + /** + * @description An identification event (`source: device`) or an Automation Intelligence event (`source: edge`). + * + * Use `source` to tell them apart. Device events include Identification and device-derived Smart Signals. Edge events do not. + * + * Consult the [Smart Signals reference](https://docs.fingerprint.com/docs/smart-signals-reference) for more details. + */ + Event: components['schemas']['EventDevice'] | components['schemas']['EventEdge'] EventUpdate: { /** * @description Linked ID value to assign to the existing event @@ -1683,6 +1856,75 @@ export interface components { } export type $defs = Record export interface operations { + analyzeRequestForAutomationIntelligence: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['EdgeRequest'] + } + } + responses: { + /** @description OK. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['EventEdge'] + } + } + /** @description Bad request. The request payload is not valid. */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Forbidden. Access to this API is denied. */ + 403: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Bad request. The request payload is too large. */ + 413: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Too Many Requests. The request is throttled. */ + 429: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + /** @description Workspace error. */ + 500: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ErrorResponse'] + } + } + } + } getEvent: { parameters: { query?: { @@ -1696,7 +1938,7 @@ export interface operations { header?: never path: { /** - * @description The unique [identifier](https://docs.fingerprint.com/reference/js-agent-v4-get-function#event_id) of each identification request (`requestId` can be used in its place). + * @description The unique [identifier](https://docs.fingerprint.com/reference/js-agent-get-function#event_id) of each identification request (`requestId` can be used in its place). * @example 1708102555327.NLOjmg */ event_id: string @@ -1779,7 +2021,7 @@ export interface operations { header?: never path: { /** - * @description The unique event [identifier](https://docs.fingerprint.com/reference/js-agent-v4-get-function#event_id). + * @description The unique event [identifier](https://docs.fingerprint.com/reference/js-agent-get-function#event_id). * @example 1708102555327.NLOjmg */ event_id: string @@ -1856,7 +2098,7 @@ export interface operations { */ pagination_key?: string /** - * @description Unique [visitor identifier](https://docs.fingerprint.com/reference/js-agent-v4-get-function#visitor_id) issued by Fingerprint Identification and all active Smart Signals. + * @description Unique [visitor identifier](https://docs.fingerprint.com/reference/js-agent-get-function#visitor_id) issued by Fingerprint Identification and all active Smart Signals. * * Filter events by matching Visitor ID (`identification.visitor_id` property). * @example Ibk1527CUFmcnjLwIs4A9 @@ -1931,7 +2173,7 @@ export interface operations { /** * @description Filter events by your custom identifier. * - * You can use [linked Ids](https://docs.fingerprint.com/reference/js-agent-v4-get-function#linkedid) to associate identification requests with your own identifier, for example, session Id, purchase Id, or transaction Id. You can then use this `linked_id` parameter to retrieve all events associated with your custom identifier. + * You can use [linked Ids](https://docs.fingerprint.com/reference/js-agent-get-function#linkedid) to associate identification requests with your own identifier, for example, session Id, purchase Id, or transaction Id. You can then use this `linked_id` parameter to retrieve all events associated with your custom identifier. * @example somelinkedId */ linked_id?: string @@ -2225,7 +2467,7 @@ export interface operations { header?: never path: { /** - * @description The [visitor ID](https://docs.fingerprint.com/reference/js-agent-v4-get-function#visitor_id) you want to delete. + * @description The [visitor ID](https://docs.fingerprint.com/reference/js-agent-get-function#visitor_id) you want to delete. * @example Ibk1527CUFmcnjLwIs4A9 */ visitor_id: string diff --git a/tests/mocked-responses-tests/mocked-responses-data/webhook/webhook_event.json b/tests/mocked-responses-tests/mocked-responses-data/webhook/webhook_event.json index 35567917..407a005d 100644 --- a/tests/mocked-responses-tests/mocked-responses-data/webhook/webhook_event.json +++ b/tests/mocked-responses-tests/mocked-responses-data/webhook/webhook_event.json @@ -1,4 +1,5 @@ { + "source": "device", "linked_id": "somelinkedId", "tags": {}, "timestamp": 1708102555327, From ae2bdfc1f6c5c3add38e7bf6e4408f6295b9862e Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Mon, 31 Aug 2026 17:07:55 +0100 Subject: [PATCH 02/10] chore: document v4 union schema in sync.sh --- sync.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/sync.sh b/sync.sh index 290d8db4..2f25e559 100755 --- a/sync.sh +++ b/sync.sh @@ -7,6 +7,7 @@ if [[ "${TRACE:-}" != "true" && "${ACTIONS_STEP_DEBUG:-}" != "true" ]]; then fi mkdir -p ./resources +# Node consumes the union schema (Event is EventDevice | EventEdge). start/end stay a date|int oneOf. curl "${CURL_OPTS[@]}" -o ./resources/fingerprint-server-api.yaml \ https://fingerprintjs.github.io/fingerprint-pro-server-api-openapi/schemas/fingerprint-server-api-v4.yaml From 7e07cd5a4b02822fd84a50d23f5ad5742a7e7cfb Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Mon, 31 Aug 2026 17:24:07 +0100 Subject: [PATCH 03/10] chore: sync schema with Event flatten x-platforms --- resources/fingerprint-server-api.yaml | 32 --------------------------- 1 file changed, 32 deletions(-) diff --git a/resources/fingerprint-server-api.yaml b/resources/fingerprint-server-api.yaml index 88835dd7..8859f4d2 100644 --- a/resources/fingerprint-server-api.yaml +++ b/resources/fingerprint-server-api.yaml @@ -1845,54 +1845,22 @@ components: - browser url: $ref: '#/components/schemas/Url' - x-platforms: - - browser bot_info: $ref: '#/components/schemas/BotInfo' - x-platforms: - - browser ip_info: $ref: '#/components/schemas/IPInfo' - x-platforms: - - android - - ios - - browser proxy: $ref: '#/components/schemas/Proxy' - x-platforms: - - android - - ios - - browser proxy_confidence: $ref: '#/components/schemas/ProxyConfidence' - x-platforms: - - android - - ios - - browser proxy_details: $ref: '#/components/schemas/ProxyDetails' - x-platforms: - - android - - ios - - browser vpn: $ref: '#/components/schemas/Vpn' - x-platforms: - - android - - ios - - browser vpn_confidence: $ref: '#/components/schemas/VpnConfidence' - x-platforms: - - android - - ios - - browser vpn_methods: $ref: '#/components/schemas/VpnMethods' - x-platforms: - - android - - ios - - browser source: $ref: '#/components/schemas/EventSource' const: edge From 0c41131d945089e6fa506c6f77d6da53cbb944d9 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Mon, 31 Aug 2026 18:45:27 +0100 Subject: [PATCH 04/10] feat: add makeEdgeEvent() for Automation Intelligence API --- .changeset/add-make-edge-event.md | 5 + example/makeEdgeEvent.mjs | 45 ++++++ readme.md | 15 ++ src/index.ts | 2 + src/serverApiClient.ts | 53 +++++++ src/types.ts | 8 ++ .../makeEdgeEventTests.spec.ts | 129 ++++++++++++++++++ tests/unit-tests/serverApiClientTests.spec.ts | 12 ++ tests/unit-tests/urlUtilsTests.spec.ts | 13 ++ 9 files changed, 282 insertions(+) create mode 100644 .changeset/add-make-edge-event.md create mode 100644 example/makeEdgeEvent.mjs create mode 100644 tests/mocked-responses-tests/makeEdgeEventTests.spec.ts diff --git a/.changeset/add-make-edge-event.md b/.changeset/add-make-edge-event.md new file mode 100644 index 00000000..f191f483 --- /dev/null +++ b/.changeset/add-make-edge-event.md @@ -0,0 +1,5 @@ +--- +"@fingerprint/node-sdk": minor +--- + +Add `makeEdgeEvent()` to collect Automation Intelligence via `POST /v4/edge` diff --git a/example/makeEdgeEvent.mjs b/example/makeEdgeEvent.mjs new file mode 100644 index 00000000..bf97580e --- /dev/null +++ b/example/makeEdgeEvent.mjs @@ -0,0 +1,45 @@ +// @ts-check +import { FingerprintServerApiClient, Region, RequestError } from '@fingerprint/node-sdk' +import { config } from 'dotenv' +config() + +const apiKey = process.env.API_KEY +const envRegion = process.env.REGION + +if (!apiKey) { + console.error('API key not defined') + process.exit(1) +} + +/** @type {Region} */ +let region = Region.Global +if (envRegion === 'eu') { + region = Region.EU +} else if (envRegion === 'ap') { + region = Region.AP +} + +const client = new FingerprintServerApiClient({ region, apiKey }) + +try { + const event = await client.makeEdgeEvent({ + method: 'GET', + url: 'https://example.com/login', + ipv4_address: '34.162.244.71', + headers: [ + { name: 'Host', value: 'example.com' }, + { name: 'User-Agent', value: 'Mozilla/5.0' }, + { name: 'Authorization', value: '' }, + ], + }) + console.log(JSON.stringify(event, null, 2)) +} catch (error) { + if (error instanceof RequestError) { + console.log(`error ${error.statusCode}: `, error.message) + // You can also access the raw response + console.log(error.response.statusText) + } else { + console.log('unknown error: ', error) + } + process.exit(1) +} diff --git a/readme.md b/readme.md index d63131a3..5fb6dc0a 100644 --- a/readme.md +++ b/readme.md @@ -107,6 +107,21 @@ client .then((events) => { console.log(events) }) + +// Collect Automation Intelligence for a request intercepted at the edge +client + .makeEdgeEvent({ + method: 'GET', + url: 'https://example.com/login', + ipv4_address: '34.162.244.71', + headers: [ + { name: 'Host', value: 'example.com' }, + { name: 'User-Agent', value: 'Mozilla/5.0' }, + ], + }) + .then((event) => { + console.log(event.event_id, event.bot_info) + }) ``` See the [Examples](https://github.com/fingerprintjs/node-sdk/tree/main/example) folder for more detailed examples. diff --git a/src/index.ts b/src/index.ts index 2c9f2954..ce5dc5e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ export * from './serverApiClient' export { + EdgeRequest, ErrorCode, ErrorResponse, Event, + EventEdge, EventRuleAction, EventUpdate, GetEventOptions, diff --git a/src/serverApiClient.ts b/src/serverApiClient.ts index 46c00a7f..12ee913a 100644 --- a/src/serverApiClient.ts +++ b/src/serverApiClient.ts @@ -1,6 +1,8 @@ import { getRequestPath, GetRequestPathOptions } from './urlUtils' import { + EdgeRequest, Event, + EventEdge, EventUpdate, FingerprintApi, GetEventOptions, @@ -161,6 +163,57 @@ export class FingerprintServerApiClient implements FingerprintApi { }) } + /** + * Collect Automation Intelligence for an HTTP request intercepted at the edge, pre-origin, or middleware. + * + * The API detects automation tools (AI agents, AI assistants, AI browsers, and other bots) + * and provides IP intelligence such as geolocation, residential proxy, VPN, and data center detection. + * It does not require a client-side JS agent or mobile SDK, so `visitor_id` and device-telemetry + * Smart Signals are not available. + * + * This feature is currently in Public Preview. Average response times are typically under 30ms. + * + * At least one of `ipv4_address` or `ipv6_address` is required. Include original request headers + * (order and capitalization preserved when possible), but redact secret values such as + * `Authorization` and `Cookie`. Send the header names with empty values rather than omitting them. + * + * Created events can later be fetched with {@link getEvent} or searched with + * {@link searchEvents} using `source: 'edge'`. + * + * @param body - HTTP request metadata to analyze. + * + * @returns {Promise} Promise with the Automation Intelligence event. + * + * @example + * ```javascript + * const event = await client.makeEdgeEvent({ + * method: 'GET', + * url: 'https://example.com/login', + * ipv4_address: '34.162.244.71', + * headers: [ + * { name: 'Host', value: 'example.com' }, + * { name: 'User-Agent', value: 'Mozilla/5.0' }, + * { name: 'Authorization', value: '' }, + * ], + * }) + * console.log(event.event_id, event.bot_info) + * ``` + */ + public async makeEdgeEvent(body: EdgeRequest): Promise { + // Runtime guard for untyped callers even though TypeScript treats body as required. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions -- runtime validation + if (!body) { + throw new TypeError('body is not set') + } + + return this.callApi({ + path: '/edge', + method: 'post', + body: JSON.stringify(body), + expect: 'json', + }) + } + /** * Request deletion of all data associated with the specified visitor ID. * diff --git a/src/types.ts b/src/types.ts index 41d97723..cc7ce9e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,15 +50,23 @@ export type SearchEventsResponse = components['schemas']['EventSearch'] */ export type Event = components['schemas']['Event'] +export type EventEdge = components['schemas']['EventEdge'] + export type GetEventOptions = paths['/events/{event_id}']['get']['parameters']['query'] export type EventUpdate = components['schemas']['EventUpdate'] export type EventRuleAction = components['schemas']['EventRuleAction'] +/** + * HTTP request metadata sent to the Automation Intelligence API (`POST /v4/edge`). + */ +export type EdgeRequest = components['schemas']['EdgeRequest'] + export interface FingerprintApi { getEvent(eventId: string, options?: GetEventOptions): Promise updateEvent(eventId: string, body: EventUpdate): Promise + makeEdgeEvent(body: EdgeRequest): Promise searchEvents(filter: SearchEventsFilter): Promise deleteVisitorData(visitorId: string): Promise } diff --git a/tests/mocked-responses-tests/makeEdgeEventTests.spec.ts b/tests/mocked-responses-tests/makeEdgeEventTests.spec.ts new file mode 100644 index 00000000..98470ec2 --- /dev/null +++ b/tests/mocked-responses-tests/makeEdgeEventTests.spec.ts @@ -0,0 +1,129 @@ +import { ServerApiError, FingerprintServerApiClient, Region, RequestError, TooManyRequestsError } from '../../src' +import makeEdgeEventResponse from './mocked-responses-data/events/get_event_with_edge_200.json' +import Error400IpRequired from './mocked-responses-data/errors/400_edge_ip_required.json' +import Error400UnknownField from './mocked-responses-data/errors/400_edge_unknown_field.json' +import Error403 from './mocked-responses-data/errors/403_feature_not_enabled.json' +import Error413 from './mocked-responses-data/errors/413_payload_too_large.json' +import Error429 from './mocked-responses-data/errors/429_too_many_requests.json' +import { createJsonResponse } from './utils' +import { getIntegrationInfo } from '../../src/urlUtils' +import { describe, expect, it } from 'vitest' +import { mockFetch } from './mockFetch' + +const body = { + method: 'GET', + url: 'https://example.com/login?foo=bar', + ipv4_address: '104.210.139.192', + headers: [ + { name: 'Host', value: 'example.com' }, + { name: 'User-Agent', value: 'Mozilla/5.0' }, + ], +} + +describe('[Mocked response] Make Edge Event', () => { + const apiKey = 'dummy_api_key' + + const client = new FingerprintServerApiClient({ region: Region.EU, apiKey }) + + it('creates an edge event', async () => { + mockFetch.mockReturnValue(Promise.resolve(createJsonResponse(makeEdgeEventResponse))) + + const response = await client.makeEdgeEvent(body) + + expect(mockFetch).toHaveBeenCalledWith( + `https://eu.api.fpjs.io/v4/edge?ii=${encodeURIComponent(getIntegrationInfo())}`, + { + headers: { Authorization: `Bearer ${apiKey}` }, + method: 'POST', + body: JSON.stringify(body), + } + ) + expect(response).toEqual(makeEdgeEventResponse) + }) + + it('400 error when IP is missing', async () => { + mockFetch.mockReturnValue(Promise.resolve(createJsonResponse(Error400IpRequired, 400))) + + const caught = await client.makeEdgeEvent(body).catch((e: unknown) => e) + expect(caught).toBeInstanceOf(ServerApiError) + expect(caught).toMatchObject({ + message: Error400IpRequired.error.message, + errorCode: Error400IpRequired.error.code, + }) + }) + + it('400 error when body contains an unknown field', async () => { + mockFetch.mockReturnValue(Promise.resolve(createJsonResponse(Error400UnknownField, 400))) + + const caught = await client.makeEdgeEvent(body).catch((e: unknown) => e) + expect(caught).toBeInstanceOf(ServerApiError) + expect(caught).toMatchObject({ + message: Error400UnknownField.error.message, + errorCode: Error400UnknownField.error.code, + }) + }) + + it('403 error', async () => { + mockFetch.mockReturnValue(Promise.resolve(createJsonResponse(Error403, 403))) + + const caught = await client.makeEdgeEvent(body).catch((e: unknown) => e) + expect(caught).toBeInstanceOf(ServerApiError) + expect(caught).toMatchObject({ + message: Error403.error.message, + errorCode: Error403.error.code, + }) + }) + + it('413 error', async () => { + mockFetch.mockReturnValue(Promise.resolve(createJsonResponse(Error413, 413))) + + const caught = await client.makeEdgeEvent(body).catch((e: unknown) => e) + expect(caught).toBeInstanceOf(ServerApiError) + expect(caught).toMatchObject({ + message: Error413.error.message, + errorCode: Error413.error.code, + }) + }) + + it('429 error', async () => { + mockFetch.mockReturnValue(Promise.resolve(createJsonResponse(Error429, 429))) + + const caught = await client.makeEdgeEvent(body).catch((e: unknown) => e) + expect(caught).toBeInstanceOf(TooManyRequestsError) + expect(caught).toMatchObject({ + message: Error429.error.message, + errorCode: Error429.error.code, + }) + }) + + it('Error with bad JSON throws a RequestError with the raw body preserved', async () => { + mockFetch.mockReturnValue(Promise.resolve(new Response('(Some bad JSON)', { status: 400 }))) + + const caught = await client.makeEdgeEvent(body).catch((e: unknown) => e) + expect(caught).toBeInstanceOf(RequestError) + expect(caught).not.toBeInstanceOf(ServerApiError) + expect(caught).toMatchObject({ + statusCode: 400, + message: 'Unknown error', + responseBody: '(Some bad JSON)', + }) + }) + + it('Error with bad shape', async () => { + mockFetch.mockReturnValue( + Promise.resolve( + createJsonResponse( + { + error: 'Unexpected error format', + }, + 400 + ) + ) + ) + + const caught = await client.makeEdgeEvent(body).catch((e: unknown) => e) + expect(caught).toBeInstanceOf(RequestError) + expect(caught).not.toBeInstanceOf(ServerApiError) + expect(caught).toMatchObject({ message: 'Unknown error' }) + }) +}) diff --git a/tests/unit-tests/serverApiClientTests.spec.ts b/tests/unit-tests/serverApiClientTests.spec.ts index 1df284b1..77161481 100644 --- a/tests/unit-tests/serverApiClientTests.spec.ts +++ b/tests/unit-tests/serverApiClientTests.spec.ts @@ -5,6 +5,7 @@ import { Region, Options, EventUpdate, + EdgeRequest, SdkError, ErrorResponse, } from '../../src' @@ -221,6 +222,17 @@ describe('ServerApiClient', () => { ) }) + it('should throw error when using makeEdgeEvent if body is empty', async () => { + const client = new FingerprintServerApiClient({ + apiKey: 'test', + region: 'Global', + }) + + await expect(client.makeEdgeEvent(undefined as unknown as EdgeRequest)).rejects.toThrow( + new TypeError('body is not set') + ) + }) + it('should throw error when using deleteVisitorData if visitorId is empty', async () => { const client = new FingerprintServerApiClient({ apiKey: 'test', diff --git a/tests/unit-tests/urlUtilsTests.spec.ts b/tests/unit-tests/urlUtilsTests.spec.ts index f281a13a..e06514db 100644 --- a/tests/unit-tests/urlUtilsTests.spec.ts +++ b/tests/unit-tests/urlUtilsTests.spec.ts @@ -112,6 +112,19 @@ describe('Get Event Search path', () => { }) }) +describe('Make Edge Event path', () => { + it('returns correct path', () => { + const url = getRequestPath({ + path: '/edge', + method: 'post', + region: Region.Global, + }) + const expectedPath = `https://api.fpjs.io/v4/edge?${ii}` + + expect(url).toEqual(expectedPath) + }) +}) + describe('Delete visitor path', () => { it('eu region', () => { const actualPath = getRequestPath({ From a394ff37f40a823076728708cbc1afa9c2a87759 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Mon, 31 Aug 2026 18:57:10 +0100 Subject: [PATCH 05/10] fix: refine event rule action condition to check for device source --- example/getEvent.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/getEvent.mjs b/example/getEvent.mjs index 0500d26b..b29b8ad1 100644 --- a/example/getEvent.mjs +++ b/example/getEvent.mjs @@ -32,7 +32,7 @@ try { const event = await client.getEvent(eventId, { ruleset_id: rulesetId }) console.log(JSON.stringify(event, null, 2)) - if (rulesetId && event.rule_action) { + if (rulesetId && event.source === 'device' && event.rule_action) { const { type, ruleset_id, rule_id, rule_expression } = event.rule_action console.log(`Rule action: ${type} (ruleset: ${ruleset_id}, rule: ${rule_id}, expression: ${rule_expression})`) From fb78a2bcbf84bc64f292e164636b2ef106b76e8c Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Mon, 31 Aug 2026 19:25:24 +0100 Subject: [PATCH 06/10] chore: sync schema get-event copy and EdgeRequest example --- resources/fingerprint-server-api.yaml | 21 ++++++++++++++++----- src/generatedApiTypes.ts | 23 +++++++++++++++++++++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/resources/fingerprint-server-api.yaml b/resources/fingerprint-server-api.yaml index 8859f4d2..e7bd9380 100644 --- a/resources/fingerprint-server-api.yaml +++ b/resources/fingerprint-server-api.yaml @@ -142,8 +142,8 @@ paths: request, i.e. all returned information is by `event_id`. - Returns `EventDevice` when `source` is `device`, and `EventEdge` when - `source` is `edge`. + Use `source` to tell identification events (`device`) from Automation + Intelligence events (`edge`). parameters: - name: event_id in: path @@ -1272,6 +1272,17 @@ components: an IPv4-mapped IPv6 address is provided in the `ipv6_address` request property, the IP intelligence will be provided in the `ipv4_address` property of the response. + example: + method: GET + url: https://example.com/login + ipv4_address: 34.162.244.71 + headers: + - name: Host + value: example.com + - name: User-Agent + value: Mozilla/5.0 + - name: Authorization + value: '' required: - headers - method @@ -1286,9 +1297,9 @@ components: Headers that contain authentication or session data must still be - included, but with with their value set to an empty string. This - includes headers like `Authorization` and `Cookie`, but may contain - more depending on your specific project, for instance + included, but with their value set to an empty string. This includes + headers like `Authorization` and `Cookie`, but may contain more + depending on your specific project, for instance `Proxy-Authenticate` or `X-Api-Key`. Omitting the headers entirely changes the shape of the request and can affect detection. Never forward the real secret values. diff --git a/src/generatedApiTypes.ts b/src/generatedApiTypes.ts index 892366c3..adb9cf66 100644 --- a/src/generatedApiTypes.ts +++ b/src/generatedApiTypes.ts @@ -48,7 +48,7 @@ export interface paths { * * Use `event_id` as the URL path parameter. This API method is scoped to a request, i.e. all returned information is by `event_id`. * - * Returns `EventDevice` when `source` is `device`, and `EventEdge` when `source` is `edge`. + * Use `source` to tell identification events (`device`) from Automation Intelligence events (`edge`). */ get: operations['getEvent'] put?: never @@ -178,12 +178,31 @@ export interface components { /** * @description HTTP request metadata (including the HTTP method, headers and IP address) sent by you (your server) to the Fingerprint API for IP and bot analysis. To improve accuracy, retain as much of the original semantics of the HTTP request as possible. For example, preserve the order of the request headers and their capitalization. * At least one of `ipv4_address` or `ipv6_address` must be provided; a request with neither is rejected with a `400` error. If both IPv4 and IPv6 are provided, IP intelligence will be provided for each address. If an IPv4-mapped IPv6 address is provided in the `ipv6_address` request property, the IP intelligence will be provided in the `ipv4_address` property of the response. + * @example { + * "method": "GET", + * "url": "https://example.com/login", + * "ipv4_address": "34.162.244.71", + * "headers": [ + * { + * "name": "Host", + * "value": "example.com" + * }, + * { + * "name": "User-Agent", + * "value": "Mozilla/5.0" + * }, + * { + * "name": "Authorization", + * "value": "" + * } + * ] + * } */ EdgeRequest: { /** * @description Ordered header entries from the request made to your server. Each entry represents one header line. If one header name appears as multiple lines, send each as a separate item in the array. * - * Headers that contain authentication or session data must still be included, but with with their value set to an empty string. This includes headers like `Authorization` and `Cookie`, but may contain more depending on your specific project, for instance `Proxy-Authenticate` or `X-Api-Key`. Omitting the headers entirely changes the shape of the request and can affect detection. Never forward the real secret values. + * Headers that contain authentication or session data must still be included, but with their value set to an empty string. This includes headers like `Authorization` and `Cookie`, but may contain more depending on your specific project, for instance `Proxy-Authenticate` or `X-Api-Key`. Omitting the headers entirely changes the shape of the request and can affect detection. Never forward the real secret values. * * Whenever possible, we recommend preserving header order and capitalization to provide the best accuracy, however it’s not a strict requirement if your runtime does not maintain http header order or canonicalizes header names. * @example [ From d7f664142f24645405dcc3b78a5b0aa2ed6e3b10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:34:44 +0000 Subject: [PATCH 07/10] fix: send edge requests as JSON --- src/serverApiClient.ts | 1 + tests/mocked-responses-tests/makeEdgeEventTests.spec.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/serverApiClient.ts b/src/serverApiClient.ts index 12ee913a..10e77ca6 100644 --- a/src/serverApiClient.ts +++ b/src/serverApiClient.ts @@ -209,6 +209,7 @@ export class FingerprintServerApiClient implements FingerprintApi { return this.callApi({ path: '/edge', method: 'post', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), expect: 'json', }) diff --git a/tests/mocked-responses-tests/makeEdgeEventTests.spec.ts b/tests/mocked-responses-tests/makeEdgeEventTests.spec.ts index 98470ec2..a118d3c6 100644 --- a/tests/mocked-responses-tests/makeEdgeEventTests.spec.ts +++ b/tests/mocked-responses-tests/makeEdgeEventTests.spec.ts @@ -19,6 +19,7 @@ const body = { { name: 'User-Agent', value: 'Mozilla/5.0' }, ], } +const headersKey: string = 'headers' describe('[Mocked response] Make Edge Event', () => { const apiKey = 'dummy_api_key' @@ -36,6 +37,10 @@ describe('[Mocked response] Make Edge Event', () => { headers: { Authorization: `Bearer ${apiKey}` }, method: 'POST', body: JSON.stringify(body), + [headersKey]: expect.objectContaining({ + Authorization: expect.any(String), + 'Content-Type': 'application/json', + }), } ) expect(response).toEqual(makeEdgeEventResponse) From 2dd9df05073ba6523e33387b6020b010fbde2116 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Fri, 4 Sep 2026 14:01:29 +0100 Subject: [PATCH 08/10] fix: regenerate Event union after source allOf + const --- resources/fingerprint-server-api.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/fingerprint-server-api.yaml b/resources/fingerprint-server-api.yaml index e7bd9380..765bc77b 100644 --- a/resources/fingerprint-server-api.yaml +++ b/resources/fingerprint-server-api.yaml @@ -1873,8 +1873,9 @@ components: vpn_methods: $ref: '#/components/schemas/VpnMethods' source: - $ref: '#/components/schemas/EventSource' - const: edge + allOf: + - $ref: '#/components/schemas/EventSource' + - const: edge required: - event_id - timestamp @@ -3449,8 +3450,9 @@ components: - ios - browser source: - $ref: '#/components/schemas/EventSource' - const: device + allOf: + - $ref: '#/components/schemas/EventSource' + - const: device x-platforms: - android - ios From c5e14ed957011700597dabfe7600b903d9bc07b9 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Sun, 6 Sep 2026 19:54:08 +0100 Subject: [PATCH 09/10] fix: hydrate missing Event source to device --- src/hydrateEvent.ts | 27 ++++++++++++++++ src/index.ts | 1 + src/sealedResults.ts | 9 ++---- .../castWebhookEventTest.spec.ts | 7 ++--- .../__snapshots__/sealedResults.spec.ts.snap | 1 + tests/unit-tests/hydrateEvent.spec.ts | 31 +++++++++++++++++++ 6 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 src/hydrateEvent.ts create mode 100644 tests/unit-tests/hydrateEvent.spec.ts diff --git a/src/hydrateEvent.ts b/src/hydrateEvent.ts new file mode 100644 index 00000000..99d1b96b --- /dev/null +++ b/src/hydrateEvent.ts @@ -0,0 +1,27 @@ +import type { Event } from './types' + +export function isEventPayload(data: unknown): data is Event { + return typeof data === 'object' && data !== null && !Array.isArray(data) && 'event_id' in data && 'timestamp' in data +} + +/** + * Treat a missing `source` as `device`. + * + * Identification events stored before `source` existed omit the field. `Event` + * is `EventDevice | EventEdge` discriminated by `source`, so a missing key + * matches neither variant. Live GET, search, and new webhook payloads already + * include `source`. Call this when replaying stored JSON. + * + * Never rewrites `edge` or an existing `device`. + */ +export function hydrateEvent(event: unknown): Event { + if (!isEventPayload(event)) { + throw new Error('event JSON must be an object') + } + + if (Object.hasOwn(event, 'source')) { + return event + } + + return { ...event, source: 'device' } +} diff --git a/src/index.ts b/src/index.ts index ce5dc5e8..1651f087 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ export { SearchEventsFilter, SearchEventsResponse, } from './types' +export { hydrateEvent } from './hydrateEvent' export * from './sealedResults' export * from './errors/unsealError' export * from './webhook' diff --git a/src/sealedResults.ts b/src/sealedResults.ts index 2dbb4eaa..9e97160a 100644 --- a/src/sealedResults.ts +++ b/src/sealedResults.ts @@ -2,6 +2,7 @@ import { createDecipheriv } from 'crypto' import { inflateRaw } from 'zlib' import { promisify } from 'util' import { Event } from './types' +import { hydrateEvent, isEventPayload } from './hydrateEvent' import { UnsealAggregateError, UnsealError } from './errors/unsealError' import { toError } from './errors/toError' import { Buffer } from 'buffer' @@ -19,21 +20,17 @@ export interface DecryptionKey { const SEALED_HEADER = Buffer.from([0x9e, 0x85, 0xdc, 0xed]) -function isEventResponse(data: unknown): data is Event { - return typeof data === 'object' && data !== null && 'event_id' in data && 'timestamp' in data -} - /** * @private * */ export function parseEventsResponse(unsealed: string): Event { const json: unknown = JSON.parse(unsealed) - if (!isEventResponse(json)) { + if (!isEventPayload(json)) { throw new Error('Sealed data is not valid events response') } - return json + return hydrateEvent(json) } /** diff --git a/tests/mocked-responses-tests/castWebhookEventTest.spec.ts b/tests/mocked-responses-tests/castWebhookEventTest.spec.ts index 51f786ff..9e587e40 100644 --- a/tests/mocked-responses-tests/castWebhookEventTest.spec.ts +++ b/tests/mocked-responses-tests/castWebhookEventTest.spec.ts @@ -1,12 +1,11 @@ import { describe, expect, it } from 'vitest' -import { Event } from '../../src' +import { hydrateEvent } from '../../src' import eventWebhookBody from './mocked-responses-data/webhook/webhook_event.json' describe('[Mocked body] Cast webhook event', () => { it('with sample request body', () => { - const event = eventWebhookBody as Event + const event = hydrateEvent(eventWebhookBody) - // Assertion just to use the `event` variable. The goal of this test is to assume that Typescript won't throw an error here. - expect(event).toBeTruthy() + expect(event.source).toBe('device') }) }) diff --git a/tests/unit-tests/__snapshots__/sealedResults.spec.ts.snap b/tests/unit-tests/__snapshots__/sealedResults.spec.ts.snap index 0b046536..b733f513 100644 --- a/tests/unit-tests/__snapshots__/sealedResults.spec.ts.snap +++ b/tests/unit-tests/__snapshots__/sealedResults.spec.ts.snap @@ -105,6 +105,7 @@ exports[`Unseal event response > unseals sealed data using aes256gcm 1`] = ` "platform": "js", "version": "3.11.10", }, + "source": "device", "supplementary_id_high_recall": { "confidence": { "score": 0.97, diff --git a/tests/unit-tests/hydrateEvent.spec.ts b/tests/unit-tests/hydrateEvent.spec.ts new file mode 100644 index 00000000..d26342c9 --- /dev/null +++ b/tests/unit-tests/hydrateEvent.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' +import { hydrateEvent } from '../../src' + +describe('hydrateEvent', () => { + it('inserts device when source is absent', () => { + const event = hydrateEvent({ event_id: '1708102555327.NLOjmg', timestamp: 1 }) + + expect(event.source).toBe('device') + expect(event.event_id).toBe('1708102555327.NLOjmg') + }) + + it('leaves device unchanged', () => { + const event = hydrateEvent({ event_id: 'x', timestamp: 1, source: 'device' }) + + expect(event.source).toBe('device') + }) + + it('never rewrites edge', () => { + const event = hydrateEvent({ event_id: 'x', timestamp: 1, source: 'edge', ip_info: {} }) + + expect(event.source).toBe('edge') + }) + + it('rejects non-object JSON', () => { + expect(() => hydrateEvent(['not', 'an', 'event'])).toThrow('event JSON must be an object') + }) + + it('rejects null JSON', () => { + expect(() => hydrateEvent(null)).toThrow('event JSON must be an object') + }) +}) From a6d3efa753ffb23d3300aa852facd583df4c22c6 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Sun, 6 Sep 2026 21:16:28 +0100 Subject: [PATCH 10/10] fix: hydrate empty Event source and fail unknown values --- src/hydrateEvent.ts | 26 +++++++++++++++++++++----- tests/unit-tests/hydrateEvent.spec.ts | 14 ++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/hydrateEvent.ts b/src/hydrateEvent.ts index 99d1b96b..bba3ea64 100644 --- a/src/hydrateEvent.ts +++ b/src/hydrateEvent.ts @@ -1,27 +1,43 @@ import type { Event } from './types' +const EVENT_SOURCES = new Set(['device', 'edge']) + export function isEventPayload(data: unknown): data is Event { return typeof data === 'object' && data !== null && !Array.isArray(data) && 'event_id' in data && 'timestamp' in data } +function readSource(event: object): unknown { + if (!Object.hasOwn(event, 'source')) { + return undefined + } + + return Reflect.get(event, 'source') +} + /** - * Treat a missing `source` as `device`. + * Treat a missing, null, or empty `source` as `device`. * * Identification events stored before `source` existed omit the field. `Event` * is `EventDevice | EventEdge` discriminated by `source`, so a missing key * matches neither variant. Live GET, search, and new webhook payloads already * include `source`. Call this when replaying stored JSON. * - * Never rewrites `edge` or an existing `device`. + * Never rewrites `edge` or an existing `device`. Unknown non-empty values fail. */ export function hydrateEvent(event: unknown): Event { if (!isEventPayload(event)) { throw new Error('event JSON must be an object') } - if (Object.hasOwn(event, 'source')) { - return event + const source = readSource(event) + + if (source === undefined || source === null || source === '') { + return { ...event, source: 'device' } + } + + if (typeof source !== 'string' || !EVENT_SOURCES.has(source)) { + throw new Error(`unknown Event source: ${JSON.stringify(source)}`) } - return { ...event, source: 'device' } + return event } diff --git a/tests/unit-tests/hydrateEvent.spec.ts b/tests/unit-tests/hydrateEvent.spec.ts index d26342c9..fb8ee26a 100644 --- a/tests/unit-tests/hydrateEvent.spec.ts +++ b/tests/unit-tests/hydrateEvent.spec.ts @@ -28,4 +28,18 @@ describe('hydrateEvent', () => { it('rejects null JSON', () => { expect(() => hydrateEvent(null)).toThrow('event JSON must be an object') }) + + it('inserts device when source is empty', () => { + expect(hydrateEvent({ event_id: 'x', timestamp: 1, source: '' }).source).toBe('device') + }) + + it('inserts device when source is null', () => { + expect(hydrateEvent({ event_id: 'x', timestamp: 1, source: null }).source).toBe('device') + }) + + it('rejects an unknown non-empty source', () => { + expect(() => hydrateEvent({ event_id: 'x', timestamp: 1, source: 'webhook' })).toThrow( + 'unknown Event source: "webhook"' + ) + }) })