diff --git a/.changeset/event-edge.md b/.changeset/event-edge.md new file mode 100644 index 00000000..a0989879 --- /dev/null +++ b/.changeset/event-edge.md @@ -0,0 +1,6 @@ +--- +'fingerprint-pro-server-api-openapi': minor +--- + +**events**: Replace `EdgeResponse` with `EventEdge`. Add `EventDevice`. Model `Event` as a `source` discriminated union of `EventDevice` and `EventEdge` in docs and the Node SDK schema. Other SDK schemas flatten `Event` and leave `source` optional. Get-event copy describes `source` without naming generated types. `POST /edge` returns `EventEdge`. + diff --git a/.github/workflows/sync-server-side-sdks-schema.yml b/.github/workflows/sync-server-side-sdks-schema.yml index fbfb7c20..a892c509 100644 --- a/.github/workflows/sync-server-side-sdks-schema.yml +++ b/.github/workflows/sync-server-side-sdks-schema.yml @@ -110,6 +110,7 @@ jobs: language: python language-version: '3.12' generate-command: 'bash ./generate.sh' + schema-source: fingerprint-server-api-v4-flat.yaml schema-path: res/fingerprint-server-api.yaml examples-path: test/mocks app-id: ${{ vars.RUNNER_APP_ID }} @@ -152,6 +153,7 @@ jobs: language: php language-version: '8.3' generate-command: 'bash ./scripts/generate.sh' + schema-source: fingerprint-server-api-v4-flat.yaml schema-path: res/fingerprint-server-api.yaml examples-path: test/mocks app-id: ${{ vars.RUNNER_APP_ID }} diff --git a/schemas/components/schemas/EdgeRequest.yaml b/schemas/components/schemas/EdgeRequest.yaml index c31e4a7d..ff3df65b 100644 --- a/schemas/components/schemas/EdgeRequest.yaml +++ b/schemas/components/schemas/EdgeRequest.yaml @@ -2,6 +2,17 @@ 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. +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: '' additionalProperties: false required: - headers @@ -13,7 +24,7 @@ properties: 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. minItems: 1 diff --git a/schemas/components/schemas/EdgeResponse.yaml b/schemas/components/schemas/EdgeResponse.yaml deleted file mode 100644 index 33292f5f..00000000 --- a/schemas/components/schemas/EdgeResponse.yaml +++ /dev/null @@ -1,36 +0,0 @@ -type: object -description: IP and bot analysis for a request submitted through the Automation Intelligence API. -additionalProperties: false -required: - - event_id - - timestamp - - ip_info -properties: - event_id: - $ref: platform/EventId.yaml - timestamp: - $ref: platform/Timestamp.yaml - source: - $ref: platform/EventSource.yaml - linked_id: - $ref: platform/LinkedId.yaml - tags: - $ref: identification/Tags.yaml - url: - $ref: identification/Url.yaml - bot_info: - $ref: smartsignals/BotInfo.yaml - ip_info: - $ref: smartsignals/IPInfo.yaml - proxy: - $ref: smartsignals/Proxy.yaml - proxy_confidence: - $ref: smartsignals/ProxyConfidence.yaml - proxy_details: - $ref: smartsignals/ProxyDetails.yaml - vpn: - $ref: smartsignals/Vpn.yaml - vpn_confidence: - $ref: smartsignals/VpnConfidence.yaml - vpn_methods: - $ref: smartsignals/VpnMethods.yaml diff --git a/schemas/components/schemas/Event.yaml b/schemas/components/schemas/Event.yaml index 3f9f7af8..028a34d8 100644 --- a/schemas/components/schemas/Event.yaml +++ b/schemas/components/schemas/Event.yaml @@ -1,345 +1,15 @@ 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 reference](https://docs.fingerprint.com/docs/smart-signals-reference) for more details. -additionalProperties: false -required: - - event_id - - timestamp -properties: - event_id: - $ref: platform/EventId.yaml - x-platforms: - - android - - ios - - browser - timestamp: - $ref: platform/Timestamp.yaml - x-platforms: - - android - - ios - - browser - source: - $ref: platform/EventSource.yaml - x-platforms: - - android - - ios - - browser - incremental_identification_status: - $ref: platform/IncrementalIdentificationStatus.yaml - x-platforms: - - browser - linked_id: - $ref: platform/LinkedId.yaml - x-platforms: - - android - - ios - - browser - environment_id: - $ref: platform/EnvironmentId.yaml - x-platforms: - - android - - ios - - browser - suspect: - $ref: platform/Suspect.yaml - x-platforms: - - android - - ios - - browser - sdk: - $ref: platform/SDK.yaml - x-platforms: - - android - - ios - - browser - replayed: - $ref: platform/Replayed.yaml - x-platforms: - - android - - ios - - browser - triggered_by: - $ref: platform/webhook/TriggeredBy.yaml - identification: - $ref: identification/Identification.yaml - x-platforms: - - android - - ios - - browser - supplementary_id_high_recall: - $ref: identification/SupplementaryIDHighRecall.yaml - x-platforms: - - android - - ios - - browser - tags: - $ref: identification/Tags.yaml - x-platforms: - - android - - ios - - browser - url: - $ref: identification/Url.yaml - x-platforms: - - browser - bundle_id: - $ref: identification/BundleId.yaml - x-platforms: - - ios - package_name: - $ref: identification/PackageName.yaml - x-platforms: - - android - ip_address: - $ref: identification/IpAddress.yaml - x-platforms: - - android - - ios - - browser - user_agent: - $ref: identification/UserAgent.yaml - x-platforms: - - android - - ios - - browser - device: - $ref: identification/Device.yaml - x-platforms: - - android - - ios - - browser - os: - $ref: identification/Os.yaml - x-platforms: - - android - - ios - - browser - os_version: - $ref: identification/OsVersion.yaml - x-platforms: - - android - - ios - - browser - client_referrer: - $ref: identification/ClientReferrer.yaml - x-platforms: - - browser - browser_details: - $ref: identification/BrowserDetails.yaml - x-platforms: - - browser - proximity: - $ref: identification/Proximity.yaml - x-platforms: - - android - - ios - - browser - active_call: - $ref: smartsignals/ActiveCall.yaml - x-platforms: - - android - - ios - bot: - $ref: smartsignals/BotResult.yaml - x-platforms: - - browser - bot_type: - $ref: smartsignals/BotType.yaml - x-platforms: - - browser - bot_info: - $ref: smartsignals/BotInfo.yaml - x-platforms: - - browser - cloned_app: - $ref: smartsignals/ClonedApp.yaml - x-platforms: - - android - developer_tools: - $ref: smartsignals/DeveloperTools.yaml - x-platforms: - - browser - - android - - ios - emulator: - $ref: smartsignals/Emulator.yaml - x-platforms: - - android - factory_reset_timestamp: - $ref: smartsignals/FactoryReset.yaml - x-platforms: - - android - - ios - frida: - $ref: smartsignals/Frida.yaml - x-platforms: - - android - - ios - ip_blocklist: - $ref: smartsignals/IPBlockList.yaml - x-platforms: - - android - - ios - - browser - ip_info: - $ref: smartsignals/IPInfo.yaml - x-platforms: - - android - - ios - - browser - proxy: - $ref: smartsignals/Proxy.yaml - x-platforms: - - android - - ios - - browser - proxy_confidence: - $ref: smartsignals/ProxyConfidence.yaml - x-platforms: - - android - - ios - - browser - proxy_details: - $ref: smartsignals/ProxyDetails.yaml - x-platforms: - - android - - ios - - browser - proxy_ml_score: - $ref: smartsignals/ProxyMLScore.yaml - x-platforms: - - browser - incognito: - $ref: smartsignals/Incognito.yaml - x-platforms: - - browser - jailbroken: - $ref: smartsignals/Jailbroken.yaml - x-platforms: - - ios - location_spoofing: - $ref: smartsignals/LocationSpoofing.yaml - x-platforms: - - android - - ios - mitm_attack: - $ref: smartsignals/MitMAttack.yaml - x-platforms: - - android - - ios - privacy_settings: - $ref: smartsignals/PrivacySettings.yaml - x-platforms: - - browser - root_apps: - $ref: smartsignals/RootApps.yaml - x-platforms: - - android - rule_action: - $ref: platform/EventRuleAction.yaml - simulator: - $ref: smartsignals/Simulator.yaml - x-platforms: - - ios - suspect_score: - $ref: smartsignals/SuspectScore.yaml - x-platforms: - - android - - ios - - browser - tampering: - $ref: smartsignals/Tampering.yaml - x-platforms: - - android - - ios - - browser - tampering_confidence: - $ref: smartsignals/TamperingConfidence.yaml - x-platforms: - - android - - ios - - browser - tampering_ml_score: - $ref: smartsignals/TamperingMlScore.yaml - x-platforms: - - android - - ios - - browser - tampering_details: - $ref: smartsignals/TamperingDetails.yaml - x-platforms: - - android - - ios - - browser - velocity: - $ref: smartsignals/Velocity.yaml - x-platforms: - - android - - ios - - browser - virtual_machine: - $ref: smartsignals/VirtualMachine.yaml - x-platforms: - - browser - virtual_machine_ml_score: - $ref: smartsignals/VirtualMachineMLScore.yaml - x-platforms: - - browser - vpn: - $ref: smartsignals/Vpn.yaml - x-platforms: - - android - - ios - - browser - vpn_confidence: - $ref: smartsignals/VpnConfidence.yaml - x-platforms: - - android - - ios - - browser - vpn_ml_score: - $ref: smartsignals/VpnMLScore.yaml - x-platforms: - - browser - vpn_origin_timezone: - $ref: smartsignals/VpnOriginTimezone.yaml - x-platforms: - - android - - ios - - browser - vpn_origin_country: - $ref: smartsignals/VpnOriginCountry.yaml - x-platforms: - - android - - ios - vpn_methods: - $ref: smartsignals/VpnMethods.yaml - x-platforms: - - android - - ios - - browser - high_activity_device: - $ref: smartsignals/HighActivity.yaml - x-platforms: - - android - - ios - - browser - rare_device: - $ref: smartsignals/RareDevice.yaml - x-platforms: - - browser - rare_device_percentile_bucket: - $ref: smartsignals/RareDevicePercentileBucket.yaml - x-platforms: - - browser - raw_device_attributes: - $ref: smartsignals/RawDeviceAttributes.yaml - x-platforms: - - browser - - ios - - android - labels: - $ref: smartsignals/Labels.yaml - x-platforms: - - browser - - ios - - android +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: EventDevice.yaml + - $ref: EventEdge.yaml +discriminator: + propertyName: source + mapping: + device: EventDevice.yaml + edge: EventEdge.yaml diff --git a/schemas/components/schemas/EventBase.yaml b/schemas/components/schemas/EventBase.yaml new file mode 100644 index 00000000..c2cb8839 --- /dev/null +++ b/schemas/components/schemas/EventBase.yaml @@ -0,0 +1,30 @@ +type: object +description: Identifiers present on every Event. `linked_id` and `tags` are optional customer-provided values. +required: + - event_id + - timestamp +properties: + event_id: + $ref: platform/EventId.yaml + x-platforms: + - android + - ios + - browser + timestamp: + $ref: platform/Timestamp.yaml + x-platforms: + - android + - ios + - browser + linked_id: + $ref: platform/LinkedId.yaml + x-platforms: + - android + - ios + - browser + tags: + $ref: identification/Tags.yaml + x-platforms: + - android + - ios + - browser diff --git a/schemas/components/schemas/EventDevice.yaml b/schemas/components/schemas/EventDevice.yaml new file mode 100644 index 00000000..e631917b --- /dev/null +++ b/schemas/components/schemas/EventDevice.yaml @@ -0,0 +1,325 @@ +type: object +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. +additionalProperties: false +allOf: + - $ref: EventBase.yaml + - type: object + required: + - source + properties: + url: + $ref: identification/Url.yaml + x-platforms: + - browser + bot_info: + $ref: smartsignals/BotInfo.yaml + x-platforms: + - browser + ip_info: + $ref: smartsignals/IPInfo.yaml + x-platforms: + - android + - ios + - browser + proxy: + $ref: smartsignals/Proxy.yaml + x-platforms: + - android + - ios + - browser + proxy_confidence: + $ref: smartsignals/ProxyConfidence.yaml + x-platforms: + - android + - ios + - browser + proxy_details: + $ref: smartsignals/ProxyDetails.yaml + x-platforms: + - android + - ios + - browser + vpn: + $ref: smartsignals/Vpn.yaml + x-platforms: + - android + - ios + - browser + vpn_confidence: + $ref: smartsignals/VpnConfidence.yaml + x-platforms: + - android + - ios + - browser + vpn_methods: + $ref: smartsignals/VpnMethods.yaml + x-platforms: + - android + - ios + - browser + source: + $ref: platform/EventSource.yaml + const: device + x-platforms: + - android + - ios + - browser + incremental_identification_status: + $ref: platform/IncrementalIdentificationStatus.yaml + x-platforms: + - browser + environment_id: + $ref: platform/EnvironmentId.yaml + x-platforms: + - android + - ios + - browser + suspect: + $ref: platform/Suspect.yaml + x-platforms: + - android + - ios + - browser + sdk: + $ref: platform/SDK.yaml + x-platforms: + - android + - ios + - browser + replayed: + $ref: platform/Replayed.yaml + x-platforms: + - android + - ios + - browser + triggered_by: + $ref: platform/webhook/TriggeredBy.yaml + identification: + $ref: identification/Identification.yaml + x-platforms: + - android + - ios + - browser + supplementary_id_high_recall: + $ref: identification/SupplementaryIDHighRecall.yaml + x-platforms: + - android + - ios + - browser + bundle_id: + $ref: identification/BundleId.yaml + x-platforms: + - ios + package_name: + $ref: identification/PackageName.yaml + x-platforms: + - android + ip_address: + $ref: identification/IpAddress.yaml + x-platforms: + - android + - ios + - browser + user_agent: + $ref: identification/UserAgent.yaml + x-platforms: + - android + - ios + - browser + device: + $ref: identification/Device.yaml + x-platforms: + - android + - ios + - browser + os: + $ref: identification/Os.yaml + x-platforms: + - android + - ios + - browser + os_version: + $ref: identification/OsVersion.yaml + x-platforms: + - android + - ios + - browser + client_referrer: + $ref: identification/ClientReferrer.yaml + x-platforms: + - browser + browser_details: + $ref: identification/BrowserDetails.yaml + x-platforms: + - browser + proximity: + $ref: identification/Proximity.yaml + x-platforms: + - android + - ios + - browser + active_call: + $ref: smartsignals/ActiveCall.yaml + x-platforms: + - android + - ios + bot: + $ref: smartsignals/BotResult.yaml + x-platforms: + - browser + bot_type: + $ref: smartsignals/BotType.yaml + x-platforms: + - browser + cloned_app: + $ref: smartsignals/ClonedApp.yaml + x-platforms: + - android + developer_tools: + $ref: smartsignals/DeveloperTools.yaml + x-platforms: + - browser + - android + - ios + emulator: + $ref: smartsignals/Emulator.yaml + x-platforms: + - android + factory_reset_timestamp: + $ref: smartsignals/FactoryReset.yaml + x-platforms: + - android + - ios + frida: + $ref: smartsignals/Frida.yaml + x-platforms: + - android + - ios + ip_blocklist: + $ref: smartsignals/IPBlockList.yaml + x-platforms: + - android + - ios + - browser + proxy_ml_score: + $ref: smartsignals/ProxyMLScore.yaml + x-platforms: + - browser + incognito: + $ref: smartsignals/Incognito.yaml + x-platforms: + - browser + jailbroken: + $ref: smartsignals/Jailbroken.yaml + x-platforms: + - ios + location_spoofing: + $ref: smartsignals/LocationSpoofing.yaml + x-platforms: + - android + - ios + mitm_attack: + $ref: smartsignals/MitMAttack.yaml + x-platforms: + - android + - ios + privacy_settings: + $ref: smartsignals/PrivacySettings.yaml + x-platforms: + - browser + root_apps: + $ref: smartsignals/RootApps.yaml + x-platforms: + - android + rule_action: + $ref: platform/EventRuleAction.yaml + simulator: + $ref: smartsignals/Simulator.yaml + x-platforms: + - ios + suspect_score: + $ref: smartsignals/SuspectScore.yaml + x-platforms: + - android + - ios + - browser + tampering: + $ref: smartsignals/Tampering.yaml + x-platforms: + - android + - ios + - browser + tampering_confidence: + $ref: smartsignals/TamperingConfidence.yaml + x-platforms: + - android + - ios + - browser + tampering_ml_score: + $ref: smartsignals/TamperingMlScore.yaml + x-platforms: + - android + - ios + - browser + tampering_details: + $ref: smartsignals/TamperingDetails.yaml + x-platforms: + - android + - ios + - browser + velocity: + $ref: smartsignals/Velocity.yaml + x-platforms: + - android + - ios + - browser + virtual_machine: + $ref: smartsignals/VirtualMachine.yaml + x-platforms: + - browser + virtual_machine_ml_score: + $ref: smartsignals/VirtualMachineMLScore.yaml + x-platforms: + - browser + vpn_ml_score: + $ref: smartsignals/VpnMLScore.yaml + x-platforms: + - browser + vpn_origin_timezone: + $ref: smartsignals/VpnOriginTimezone.yaml + x-platforms: + - android + - ios + - browser + vpn_origin_country: + $ref: smartsignals/VpnOriginCountry.yaml + x-platforms: + - android + - ios + high_activity_device: + $ref: smartsignals/HighActivity.yaml + x-platforms: + - android + - ios + - browser + rare_device: + $ref: smartsignals/RareDevice.yaml + x-platforms: + - browser + rare_device_percentile_bucket: + $ref: smartsignals/RareDevicePercentileBucket.yaml + x-platforms: + - browser + raw_device_attributes: + $ref: smartsignals/RawDeviceAttributes.yaml + x-platforms: + - browser + - ios + - android + labels: + $ref: smartsignals/Labels.yaml + x-platforms: + - browser + - ios + - android diff --git a/schemas/components/schemas/EventEdge.yaml b/schemas/components/schemas/EventEdge.yaml new file mode 100644 index 00000000..2d65bf1a --- /dev/null +++ b/schemas/components/schemas/EventEdge.yaml @@ -0,0 +1,32 @@ +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. +additionalProperties: false +allOf: + - $ref: EventBase.yaml + - type: object + required: + - source + - ip_info + properties: + url: + $ref: identification/Url.yaml + bot_info: + $ref: smartsignals/BotInfo.yaml + ip_info: + $ref: smartsignals/IPInfo.yaml + proxy: + $ref: smartsignals/Proxy.yaml + proxy_confidence: + $ref: smartsignals/ProxyConfidence.yaml + proxy_details: + $ref: smartsignals/ProxyDetails.yaml + vpn: + $ref: smartsignals/Vpn.yaml + vpn_confidence: + $ref: smartsignals/VpnConfidence.yaml + vpn_methods: + $ref: smartsignals/VpnMethods.yaml + source: + $ref: platform/EventSource.yaml + const: edge diff --git a/schemas/paths/edge.yaml b/schemas/paths/edge.yaml index 3ac4227f..09945227 100644 --- a/schemas/paths/edge.yaml +++ b/schemas/paths/edge.yaml @@ -37,7 +37,7 @@ post: content: application/json: schema: - $ref: ../components/schemas/EdgeResponse.yaml + $ref: ../components/schemas/EventEdge.yaml examples: 200-ok-bot-detected: summary: Example response when a bot was detected. diff --git a/schemas/paths/event.yaml b/schemas/paths/event.yaml index 74bf0f3a..60117cd4 100644 --- a/schemas/paths/event.yaml +++ b/schemas/paths/event.yaml @@ -4,9 +4,11 @@ get: 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`. + + Use `source` to tell identification events (`device`) from Automation Intelligence events (`edge`). parameters: - name: event_id in: path diff --git a/schemas/paths/examples/webhook/webhook_event.json b/schemas/paths/examples/webhook/webhook_event.json index 35567917..407a005d 100644 --- a/schemas/paths/examples/webhook/webhook_event.json +++ b/schemas/paths/examples/webhook/webhook_event.json @@ -1,4 +1,5 @@ { + "source": "device", "linked_id": "somelinkedId", "tags": {}, "timestamp": 1708102555327, diff --git a/utils/replaceOneOf.spec.ts b/utils/replaceOneOf.spec.ts index b419ef03..8262e786 100644 --- a/utils/replaceOneOf.spec.ts +++ b/utils/replaceOneOf.spec.ts @@ -455,4 +455,30 @@ describe('Test replaceOneOf', () => { expect(schema.additionalProperties).toBe(true); }); + + it('keeps x-platforms from an earlier variant when a later variant omits them', () => { + const schema: OpenApiDocument = { + oneOf: [ + { + type: 'object', + properties: { + url: { type: 'string', 'x-platforms': ['browser'] }, + ip_info: { type: 'object', 'x-platforms': ['android', 'ios', 'browser'] }, + }, + }, + { + type: 'object', + properties: { + url: { type: 'string' }, + ip_info: { type: 'object' }, + }, + }, + ], + }; + + replaceOneOf(schema, {}, 'oneOf'); + + expect(schema.properties.url['x-platforms']).toEqual(['browser']); + expect(schema.properties.ip_info['x-platforms']).toEqual(['android', 'ios', 'browser']); + }); }); diff --git a/utils/replaceOneOf.ts b/utils/replaceOneOf.ts index cf83c9de..8a399c2e 100644 --- a/utils/replaceOneOf.ts +++ b/utils/replaceOneOf.ts @@ -1,6 +1,23 @@ import { resolveComponent } from './resolveComponent.ts'; import type { OpenApiDocument, JsonObject } from './openapi.ts'; +function getXPlatforms(prop: JsonObject | undefined): unknown { + if (!prop || typeof prop !== 'object') { + return undefined; + } + if (prop['x-platforms'] !== undefined) { + return prop['x-platforms']; + } + if (Array.isArray(prop.allOf)) { + for (const item of prop.allOf) { + if (item && typeof item === 'object' && item['x-platforms'] !== undefined) { + return item['x-platforms']; + } + } + } + return undefined; +} + /** * Merges multiple schemas from oneOf/anyOf into a single schema. * Properties that are only present in one schema are made optional. @@ -42,8 +59,17 @@ export function replaceOneOf( requiredCounts[propName] = (requiredCounts[propName] || 0) + 1; } - // Clone the property to avoid mutating the original schema - properties[propName] = structuredClone(currentItem.properties[propName]); + // Clone the property to avoid mutating the original schema. + // Keep x-platforms from an earlier variant when a later one omits it + // (EventEdge has no sdk.platform, EventDevice still annotates shared fields). + const incoming = structuredClone(currentItem.properties[propName]) as JsonObject; + const previous = properties[propName]; + const previousPlatforms = getXPlatforms(previous); + const incomingPlatforms = getXPlatforms(incoming); + if (previousPlatforms !== undefined && incomingPlatforms === undefined && incoming) { + incoming['x-platforms'] = structuredClone(previousPlatforms); + } + properties[propName] = incoming; if (!literalValues[propName]) { literalValues[propName] = []; diff --git a/utils/transformers/flattenNamedSchemaOneOfTransformer.spec.ts b/utils/transformers/flattenNamedSchemaOneOfTransformer.spec.ts new file mode 100644 index 00000000..453014f2 --- /dev/null +++ b/utils/transformers/flattenNamedSchemaOneOfTransformer.spec.ts @@ -0,0 +1,198 @@ +import { flattenNamedSchemaOneOfTransformer } from './flattenNamedSchemaOneOfTransformer.ts'; +import type { OpenApiDocument } from '../openapi.ts'; + +const apply = (schema: OpenApiDocument, schemaName = 'Event', optionalProperties?: string[]) => { + const options = optionalProperties === undefined ? {} : { optionalProperties }; + flattenNamedSchemaOneOfTransformer(schemaName, options)(schema); + return schema; +}; + +describe('flattenNamedSchemaOneOfTransformer', () => { + it('merges a named oneOf into one object and can leave source optional', () => { + const schema: OpenApiDocument = { + components: { + schemas: { + EventSource: { + type: 'string', + enum: ['device', 'edge'], + }, + EventDevice: { + type: 'object', + required: ['event_id', 'source'], + properties: { + event_id: { type: 'string' }, + source: { + allOf: [{ $ref: '#/components/schemas/EventSource' }, { const: 'device' }], + 'x-platforms': ['android', 'ios', 'browser'], + }, + identification: { type: 'object' }, + }, + }, + EventEdge: { + type: 'object', + required: ['event_id', 'source', 'ip_info'], + properties: { + event_id: { type: 'string' }, + source: { + allOf: [{ $ref: '#/components/schemas/EventSource' }, { const: 'edge' }], + }, + ip_info: { type: 'object' }, + }, + }, + Event: { + oneOf: [{ $ref: '#/components/schemas/EventDevice' }, { $ref: '#/components/schemas/EventEdge' }], + discriminator: { + propertyName: 'source', + mapping: { + device: '#/components/schemas/EventDevice', + edge: '#/components/schemas/EventEdge', + }, + }, + }, + }, + }, + }; + + apply(schema, 'Event', ['source']); + + const event = schema.components.schemas.Event; + expect(event.oneOf).toBeUndefined(); + expect(event.discriminator).toBeUndefined(); + expect(event.properties.identification).toBeDefined(); + expect(event.properties.ip_info).toBeDefined(); + expect(event.properties.source).toEqual({ + $ref: '#/components/schemas/EventSource', + 'x-platforms': ['android', 'ios', 'browser'], + }); + expect(event.properties.source.enum).toBeUndefined(); + expect(event.required).toEqual(['event_id']); + }); + + it('keeps x-platforms when they sit on the allOf const item', () => { + const schema: OpenApiDocument = { + components: { + schemas: { + EventSource: { type: 'string', enum: ['device', 'edge'] }, + EventDevice: { + type: 'object', + properties: { + source: { + allOf: [ + { $ref: '#/components/schemas/EventSource' }, + { const: 'device', 'x-platforms': ['android', 'ios', 'browser'] }, + ], + }, + }, + }, + EventEdge: { + type: 'object', + properties: { + source: { + allOf: [{ $ref: '#/components/schemas/EventSource' }, { const: 'edge' }], + }, + }, + }, + Event: { + oneOf: [{ $ref: '#/components/schemas/EventDevice' }, { $ref: '#/components/schemas/EventEdge' }], + }, + }, + }, + }; + + apply(schema, 'Event', ['source']); + + expect(schema.components.schemas.Event.properties.source).toEqual({ + $ref: '#/components/schemas/EventSource', + 'x-platforms': ['android', 'ios', 'browser'], + }); + }); + + it('keeps x-platforms from EventDevice when EventEdge omits them', () => { + const schema: OpenApiDocument = { + components: { + schemas: { + EventDevice: { + type: 'object', + properties: { + url: { type: 'string', 'x-platforms': ['browser'] }, + ip_info: { type: 'object', 'x-platforms': ['android', 'ios', 'browser'] }, + }, + }, + EventEdge: { + type: 'object', + properties: { + url: { type: 'string' }, + ip_info: { type: 'object' }, + }, + }, + Event: { + oneOf: [{ $ref: '#/components/schemas/EventDevice' }, { $ref: '#/components/schemas/EventEdge' }], + }, + }, + }, + }; + + apply(schema); + + const event = schema.components.schemas.Event; + expect(event.properties.url['x-platforms']).toEqual(['browser']); + expect(event.properties.ip_info['x-platforms']).toEqual(['android', 'ios', 'browser']); + }); + + it('does not modify other oneOf schemas', () => { + const schema: OpenApiDocument = { + components: { + schemas: { + Event: { + type: 'object', + properties: { event_id: { type: 'string' } }, + }, + EventRuleAction: { + oneOf: [{ $ref: '#/components/schemas/Allow' }, { $ref: '#/components/schemas/Block' }], + }, + Allow: { type: 'object', properties: { type: { const: 'allow' } } }, + Block: { type: 'object', properties: { type: { const: 'block' } } }, + }, + }, + }; + const original = structuredClone(schema); + + apply(schema); + + expect(schema).toEqual(original); + }); + + it('flattens Event without flattening EventRuleAction', () => { + const schema: OpenApiDocument = { + components: { + schemas: { + EventDevice: { + type: 'object', + properties: { event_id: { type: 'string' } }, + }, + EventEdge: { + type: 'object', + properties: { event_id: { type: 'string' } }, + }, + Event: { + oneOf: [{ $ref: '#/components/schemas/EventDevice' }, { $ref: '#/components/schemas/EventEdge' }], + }, + EventRuleAction: { + oneOf: [{ $ref: '#/components/schemas/Allow' }, { $ref: '#/components/schemas/Block' }], + }, + Allow: { type: 'object', properties: { type: { const: 'allow' } } }, + Block: { type: 'object', properties: { type: { const: 'block' } } }, + }, + }, + }; + + apply(schema); + + expect(schema.components.schemas.Event.oneOf).toBeUndefined(); + expect(schema.components.schemas.Event.properties.event_id).toBeDefined(); + expect(schema.components.schemas.EventRuleAction.oneOf).toEqual([ + { $ref: '#/components/schemas/Allow' }, + { $ref: '#/components/schemas/Block' }, + ]); + }); +}); diff --git a/utils/transformers/flattenNamedSchemaOneOfTransformer.ts b/utils/transformers/flattenNamedSchemaOneOfTransformer.ts new file mode 100644 index 00000000..5d1338c6 --- /dev/null +++ b/utils/transformers/flattenNamedSchemaOneOfTransformer.ts @@ -0,0 +1,84 @@ +import { replaceOneOf } from '../replaceOneOf.ts'; +import type { Transformer } from '../openapi.ts'; + +function isObject(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function collapseToEnumRef(property: Record): void { + if (typeof property.$ref === 'string') { + delete property.enum; + delete property.const; + return; + } + + if (!Array.isArray(property.allOf)) { + return; + } + + const refItem = property.allOf.find((item) => isObject(item) && typeof item.$ref === 'string'); + if (!isObject(refItem) || typeof refItem.$ref !== 'string') { + return; + } + + let platforms = property['x-platforms']; + for (const item of property.allOf) { + if (isObject(item) && item['x-platforms'] !== undefined) { + platforms = item['x-platforms']; + } + } + for (const key of Object.keys(property)) { + delete property[key]; + } + property.$ref = refItem.$ref; + if (platforms !== undefined) { + property['x-platforms'] = platforms; + } +} + +export type FlattenNamedSchemaOneOfOptions = { + /** + * Leave these properties optional on the flattened object even if every + * variant required them. Used so Event.source stays optional in SDK schemas. + */ + optionalProperties?: string[]; +}; + +/** + * Flatten `oneOf` on a named schema only. Used so EventDevice | EventEdge + * becomes a single Event in SDK output without flattening EventRuleAction + * and other unions. + */ +export function flattenNamedSchemaOneOfTransformer( + schemaName: string, + options: FlattenNamedSchemaOneOfOptions = {} +): Transformer { + return (apiDefinition) => { + const schema = apiDefinition.components?.schemas?.[schemaName]; + if (!schema?.oneOf) { + return; + } + replaceOneOf(schema, apiDefinition.components.schemas, 'oneOf'); + + // Discriminator fields use `allOf: [$ref Enum, const]` (EventRuleAction) or + // `$ref` + sibling `const`. Flattening would otherwise leave a nested + // SourceEnum / leftover const and change getSource()'s type. + if (isObject(schema.properties)) { + for (const property of Object.values(schema.properties)) { + if (isObject(property)) { + collapseToEnumRef(property); + } + } + } + + const optionalProperties = options.optionalProperties; + if (!optionalProperties?.length || !Array.isArray(schema.required)) { + return; + } + + schema.required = schema.required.filter((name: string) => !optionalProperties.includes(name)); + if (schema.required.length === 0) { + delete schema.required; + } + }; +} diff --git a/utils/transformers/liftOneOfSharedPropertiesTransformer.spec.ts b/utils/transformers/liftOneOfSharedPropertiesTransformer.spec.ts index c2df51e2..79819adc 100644 --- a/utils/transformers/liftOneOfSharedPropertiesTransformer.spec.ts +++ b/utils/transformers/liftOneOfSharedPropertiesTransformer.spec.ts @@ -60,6 +60,52 @@ describe('liftOneOfSharedPropertiesTransformer', () => { expect(schema).toEqual(original); }); + it('rewrites $ref + const on variants of a bare oneOf parent', () => { + const schema: OpenApiDocument = { + components: { + schemas: { + Kind: { type: 'string', enum: ['device', 'edge'] }, + EventDevice: { + type: 'object', + allOf: [ + { + type: 'object', + properties: { + url: { + $ref: '#/components/schemas/Url', + 'x-platforms': ['browser'], + }, + source: { + $ref: '#/components/schemas/Kind', + const: 'device', + 'x-platforms': ['browser'], + }, + }, + }, + ], + }, + Event: { + type: 'object', + oneOf: [{ $ref: '#/components/schemas/EventDevice' }], + }, + }, + }, + }; + + applyTransformer(schema); + + expect(schema.components.schemas.Event.properties).toBeUndefined(); + expect(schema.components.schemas.EventDevice.properties).toBeUndefined(); + expect(schema.components.schemas.EventDevice.allOf[0].properties.source).toEqual({ + allOf: [{ $ref: '#/components/schemas/Kind' }, { const: 'device' }], + 'x-platforms': ['browser'], + }); + expect(schema.components.schemas.EventDevice.allOf[0].properties.url).toEqual({ + $ref: '#/components/schemas/Url', + 'x-platforms': ['browser'], + }); + }); + it('skips variants that already have allOf', () => { const schema: OpenApiDocument = { components: { diff --git a/utils/transformers/liftOneOfSharedPropertiesTransformer.ts b/utils/transformers/liftOneOfSharedPropertiesTransformer.ts index cbadd0a0..f9daa19e 100644 --- a/utils/transformers/liftOneOfSharedPropertiesTransformer.ts +++ b/utils/transformers/liftOneOfSharedPropertiesTransformer.ts @@ -34,8 +34,8 @@ function takeKeys(schema: JsonObject, keys: string[]): JsonObject { } /** - * Convert `{ $ref: ..., ...constraints }` to `allOf` form to keep downstream - * allOf/oneOf resolvers behavior consistent. + * Convert `{ $ref, const|enum, ... }` to `allOf: [$ref, const|enum]` plus leftover + * siblings (e.g. `x-platforms`). JSON Schema ignores keywords next to `$ref`. */ function normalizeRefSiblings(node: unknown): void { if (Array.isArray(node)) { @@ -49,15 +49,18 @@ function normalizeRefSiblings(node: unknown): void { Object.values(node).forEach((value) => normalizeRefSiblings(value)); - if (Object.hasOwn(node, '$ref') && Object.keys(node).length > 1) { - const { $ref, ...siblingConstraints } = node; + if (!Object.hasOwn(node, '$ref') || (!Object.hasOwn(node, 'const') && !Object.hasOwn(node, 'enum'))) { + return; + } - Object.keys(node).forEach((key) => { - delete node[key]; - }); + const { $ref, const: constValue, enum: enumValue, ...rest } = node; - node.allOf = [{ $ref }, siblingConstraints]; - } + Object.keys(node).forEach((key) => { + delete node[key]; + }); + + node.allOf = [{ $ref }, constValue !== undefined ? { const: constValue } : { enum: enumValue }]; + Object.assign(node, rest); } function wrapVariantWithBase(schema: JsonObject, baseSchema: JsonObject): boolean { @@ -79,6 +82,10 @@ function wrapVariantWithBase(schema: JsonObject, baseSchema: JsonObject): boolea /** * For schemas that define both shared object properties and `oneOf`, extract * the shared object shape and apply it directly to each alternative via `allOf`. + * + * For a bare `oneOf` parent (no sibling properties), still rewrite `$ref` + + * sibling keywords on each variant — same `normalizeRefSiblings` step the lift + * path uses. Event is this shape; EventRuleAction is the sibling-properties shape. */ export function liftOneOfSharedPropertiesTransformer(apiDefinition: OpenApiDocument): void { const schemas = apiDefinition?.components?.schemas; @@ -88,7 +95,12 @@ export function liftOneOfSharedPropertiesTransformer(apiDefinition: OpenApiDocum } for (const schema of Object.values(schemas)) { - if (!isObject(schema) || !Array.isArray(schema.oneOf) || !isObject(schema.properties)) { + if (!isObject(schema) || !Array.isArray(schema.oneOf)) { + continue; + } + + if (!isObject(schema.properties)) { + normalizeOneOfVariants(schema, schemas); continue; } @@ -127,3 +139,26 @@ export function liftOneOfSharedPropertiesTransformer(apiDefinition: OpenApiDocum } } } + +function normalizeOneOfVariants(schema: JsonObject, schemas: JsonObject): void { + if (!Array.isArray(schema.oneOf)) { + return; + } + + for (const oneOfAlternative of schema.oneOf) { + if (!isObject(oneOfAlternative)) { + continue; + } + + if (typeof oneOfAlternative.$ref === 'string') { + const schemaName = getSchemaNameFromRef(oneOfAlternative.$ref); + const alternativeSchema = schemaName ? schemas[schemaName] : null; + if (isObject(alternativeSchema)) { + normalizeRefSiblings(alternativeSchema); + } + continue; + } + + normalizeRefSiblings(oneOfAlternative); + } +} diff --git a/utils/transformers/transformSchema.spec.ts b/utils/transformers/transformSchema.spec.ts index 3d699edb..ec74dce9 100644 --- a/utils/transformers/transformSchema.spec.ts +++ b/utils/transformers/transformSchema.spec.ts @@ -4,6 +4,7 @@ import { transformSchema, v4Transformers, v4SchemaForSdksTransformers, + v4SchemaForSdksFlatTransformers, v4SchemaForSdksNormalizedTransformers, } from './transformSchema.ts'; import type { OpenApiDocument } from '../openapi.ts'; @@ -109,17 +110,63 @@ describe('Test transformSchema pipelines for v4', () => { expect(parsed.paths['/edge']).toBeDefined(); }); - it('v4 sdk schemas remove /edge when present', () => { - const yamlWithEdge = toYaml({ - openapi: '3.1.1', - paths: { '/edge': { post: {} }, '/events': { get: {} } }, - components: { schemas: {} }, - }); + it('v4 docs and Node SDK schemas keep Event as EventDevice | EventEdge', () => { + for (const transformers of [v4Transformers, v4SchemaForSdksTransformers]) { + const parsed = parseYaml(transformSchema(v4Schema, transformers)); + const event = parsed.components.schemas.Event; + + expect(event.oneOf).toEqual([ + { $ref: '#/components/schemas/EventDevice' }, + { $ref: '#/components/schemas/EventEdge' }, + ]); + expect(event.discriminator).toEqual({ + propertyName: 'source', + mapping: { + device: '#/components/schemas/EventDevice', + edge: '#/components/schemas/EventEdge', + }, + }); + expect(parsed.components.schemas.EventDevice.properties.source).toEqual({ + allOf: [{ $ref: '#/components/schemas/EventSource' }, { const: 'device' }], + 'x-platforms': ['android', 'ios', 'browser'], + }); + expect(parsed.components.schemas.EventEdge.properties.source).toEqual({ + allOf: [{ $ref: '#/components/schemas/EventSource' }, { const: 'edge' }], + }); + expect(parsed.components.schemas.EventDevice.properties.ip_info).toBeDefined(); + expect(parsed.components.schemas.EventEdge.properties.ip_info).toBeDefined(); + expect(parsed.components.schemas.EventDevice.properties.identification).toBeDefined(); + expect(parsed.components.schemas.EventEdge.properties.identification).toBeUndefined(); + expect(parsed.paths['/edge']).toBeDefined(); + expect(parsed.paths['/events/{event_id}'].get.description).toContain( + 'Use `source` to tell identification events (`device`) from Automation Intelligence events (`edge`).' + ); + expect(parsed.paths['/events/{event_id}'].get.description).not.toContain('EventDevice'); + } + }); - for (const transformers of [v4SchemaForSdksTransformers, v4SchemaForSdksNormalizedTransformers]) { - const result = transformSchema(yamlWithEdge, transformers); - const parsed = parseYaml(result); - expect(parsed.paths['/edge']).toBeUndefined(); + it('v4 flat and normalized SDK schemas flatten Event and keep source optional', () => { + for (const transformers of [v4SchemaForSdksFlatTransformers, v4SchemaForSdksNormalizedTransformers]) { + const parsed = parseYaml(transformSchema(v4Schema, transformers)); + const event = parsed.components.schemas.Event; + + expect(event.oneOf).toBeUndefined(); + expect(event.discriminator).toBeUndefined(); + expect(event.properties.identification).toBeDefined(); + expect(event.properties.ip_info).toBeDefined(); + expect(event.properties.source).toEqual({ + $ref: '#/components/schemas/EventSource', + 'x-platforms': ['android', 'ios', 'browser'], + }); + expect(event.required).toEqual(expect.arrayContaining(['event_id', 'timestamp'])); + expect(event.required).not.toContain('source'); + expect(parsed.components.schemas.EventEdge.properties.ip_info).toBeDefined(); + expect(parsed.components.schemas.EventDevice).toBeUndefined(); + expect(parsed.paths['/edge']).toBeDefined(); + expect(parsed.paths['/events/{event_id}'].get.description).toContain( + 'Use `source` to tell identification events (`device`) from Automation Intelligence events (`edge`).' + ); + expect(parsed.paths['/events/{event_id}'].get.description).not.toContain('EventDevice'); } }); @@ -136,4 +183,21 @@ describe('Test transformSchema pipelines for v4', () => { expect(hasYamlKey(pathsYaml, 'oneOf')).toBe(false); }); + + it('v4 sdk schemas keep /edge', () => { + const yamlWithEdge = toYaml({ + openapi: '3.1.1', + paths: { '/edge': { post: {} }, '/events': { get: {} } }, + components: { schemas: {} }, + }); + + for (const transformers of [ + v4SchemaForSdksTransformers, + v4SchemaForSdksFlatTransformers, + v4SchemaForSdksNormalizedTransformers, + ]) { + const parsed = parseYaml(transformSchema(yamlWithEdge, transformers)); + expect(parsed.paths['/edge']).toBeDefined(); + } + }); }); diff --git a/utils/transformers/transformSchema.ts b/utils/transformers/transformSchema.ts index 33db8d4e..b1edfa7d 100644 --- a/utils/transformers/transformSchema.ts +++ b/utils/transformers/transformSchema.ts @@ -4,9 +4,9 @@ import { addXReadmeTransformer } from './addXReadmeTransformer.ts'; import { appendExternalSchemaRefTransformer } from './appendExternalSchemaRefTransformer.ts'; import { extractFirstParameterExampleTransformer } from './extractFirstParameterExampleTransformer.ts'; import { extractPathOperationInlineEnumsTransformer } from './extractPathOperationInlineEnumsTransformer.ts'; +import { flattenNamedSchemaOneOfTransformer } from './flattenNamedSchemaOneOfTransformer.ts'; import { parseYaml } from './parseYaml.ts'; import { removeBigExamplesTransformer } from './removeBigExamplesTransformer.ts'; -import { removeEdgeTransformer } from './removeEdgeTransformer.ts'; import { removeWebhookTransformer } from './removeWebhookTransformer.ts'; import { resolveAllOfTransformer } from './resolveAllOfTransformer.ts'; import { resolveExternalValueTransformer } from './resolveExternalValueTransformer.ts'; @@ -45,7 +45,6 @@ export const v4Transformers: Transformer[] = [ export const v4SchemaForSdksCommonTransformers: Transformer[] = [ ...v4Transformers, extractFirstParameterExampleTransformer, - removeEdgeTransformer, extractPathOperationInlineEnumsTransformer, replaceTagsTransformer, removeFieldTransformer('webhooks'), @@ -54,8 +53,17 @@ export const v4SchemaForSdksCommonTransformers: Transformer[] = [ removeBigExamplesTransformer, ]; -export const v4SchemaForSdksTransformers: Transformer[] = [ - ...v4SchemaForSdksCommonTransformers, +const flattenEventForSdks = flattenNamedSchemaOneOfTransformer('Event', { + optionalProperties: ['source'], +}); + +const flattenEventForOtherSdks: Transformer[] = [ + flattenEventForSdks, + // Flatten copies additionalProperties: false from EventDevice/EventEdge; strip it again for SDK compat. + removeFieldTransformer('additionalProperties', false), +]; + +const v4SdkBotInfoTransformers: Transformer[] = [ // Inline enums previously extracted from BotInfo to avoid breaking changes in the SDKs using this schema inlineReferencedPropertiesTransformer('BotInfo'), // Remove the added enum attribute for BotInfo.category. This must follow the inline transformer on the previous line. @@ -64,16 +72,26 @@ export const v4SchemaForSdksTransformers: Transformer[] = [ removeUnusedSchemasTransformer, ]; +// Node: Event stays EventDevice | EventEdge. start/end stay a date|int oneOf. +export const v4SchemaForSdksTransformers: Transformer[] = [ + ...v4SchemaForSdksCommonTransformers, + ...v4SdkBotInfoTransformers, +]; + +// Python, PHP: Event flattened to one object. start/end stay a date|int oneOf. +export const v4SchemaForSdksFlatTransformers: Transformer[] = [ + ...v4SchemaForSdksCommonTransformers, + ...flattenEventForOtherSdks, + ...v4SdkBotInfoTransformers, +]; + +// Go, Java, .NET: Event flattened to one object. start/end split into start + start_date_time. export const v4SchemaForSdksNormalizedTransformers: Transformer[] = [ ...v4SchemaForSdksCommonTransformers, // Expand oneOf query parameters, start and end, to avoid breaking changes in the SDKs using this schema replaceStartEndQueryParameters(), - // Inline enums previously extracted from BotInfo to avoid breaking changes in the SDKs using this schema - inlineReferencedPropertiesTransformer('BotInfo'), - // Remove the added enum attribute for BotInfo.category. This must follow the inline transformer on the previous line. - removeFieldByPathTransformer(['components', 'schemas', 'BotInfo', 'properties', 'category', 'enum']), - // This transformer should run last to ensure all unused schemas are found - removeUnusedSchemasTransformer, + ...flattenEventForOtherSdks, + ...v4SdkBotInfoTransformers, ]; export const readmeApiExplorerTransformers: Transformer[] = [ diff --git a/vite.config.ts b/vite.config.ts index 81a4aa79..aa36f816 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,7 @@ import { removeExtraDocumentationTransformers, schemaForSdksTransformers, transformSchema, + v4SchemaForSdksFlatTransformers, v4SchemaForSdksNormalizedTransformers, v4SchemaForSdksTransformers, v4Transformers, @@ -26,19 +27,20 @@ const schemaOutputs = [ }, { from: 'schemas/fingerprint-server-api-v4.yaml', - // just schema used by most SDKs - // examples are removed - // includes `oneOf` operators same as the source schema - // additionalProperties: false are removed for backward compatibility + // Node SDK. Event is EventDevice | EventEdge. start/end stay a date|int oneOf. to: 'fingerprint-server-api-v4.yaml', transformers: v4SchemaForSdksTransformers, }, { from: 'schemas/fingerprint-server-api-v4.yaml', - // normalized schema used by SDKs in weakly typed languages - // examples are removed - // `oneOf` query parameters are split into two or more parameters - // additionalProperties: false are removed for backward compatibility + // Python, PHP. Event is a single object (source optional). start/end stay a date|int oneOf. + to: 'fingerprint-server-api-v4-flat.yaml', + transformers: v4SchemaForSdksFlatTransformers, + }, + { + from: 'schemas/fingerprint-server-api-v4.yaml', + // Go, Java, .NET. Event is a single object (source optional). + // start/end are split into start + start_date_time (same as those SDKs ship today). to: 'fingerprint-server-api-v4-normalized.yaml', transformers: v4SchemaForSdksNormalizedTransformers, },