diff --git a/CHANGELOG.md b/CHANGELOG.md index 601ce03..679a200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +- Webhook client_identity and every nested field are optional and nullable. Attribution is best-effort; receivers must work without it. + +## Unreleased + - Scored webhook 2026-10-08 removes browser_vpn_proxy, datacenter_ip, abuser and suspicious_paid_click flags. vpn includes Browser VPN/Proxy in flags, connection type and signals; site_id remains. Legacy schemas remain readable. # Changelog diff --git a/dist/schemas/identification-scored-event.schema.json b/dist/schemas/identification-scored-event.schema.json index b2ec322..047f47f 100644 --- a/dist/schemas/identification-scored-event.schema.json +++ b/dist/schemas/identification-scored-event.schema.json @@ -302,196 +302,6 @@ "" ] }, - "ClientIdentity": { - "type": "object", - "additionalProperties": true, - "required": [ - "schema_version", - "classification_revision", - "classifier_version", - "registry_revision", - "availability", - "observed_at", - "decided_at", - "claims", - "verified", - "assessments", - "evidence" - ], - "properties": { - "schema_version": { - "type": "string", - "description": "Open version identifier. Version 1 is currently emitted." - }, - "classification_revision": { - "type": "integer", - "minimum": 1 - }, - "classifier_version": { - "type": "string" - }, - "registry_revision": { - "type": "string" - }, - "availability": { - "type": "string", - "description": "available or input_unavailable. Missing object means no stored identity data; it does not mean human." - }, - "observed_at": { - "type": "string", - "format": "date-time" - }, - "decided_at": { - "type": "string", - "format": "date-time" - }, - "claims": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "required": [ - "profile_id", - "provider_id", - "provider_name", - "agent_name", - "client_kind", - "purpose", - "source" - ], - "properties": { - "profile_id": { - "type": "string" - }, - "provider_id": { - "type": "string" - }, - "provider_name": { - "type": "string" - }, - "agent_name": { - "type": "string" - }, - "client_kind": { - "type": "string" - }, - "purpose": { - "type": "string" - }, - "source": { - "type": "string" - } - } - } - }, - "verified": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "required": [ - "subject", - "value_id", - "evidence_ids" - ], - "properties": { - "subject": { - "type": "string", - "description": "The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes." - }, - "value_id": { - "type": "string" - }, - "evidence_ids": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "assessments": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "required": [ - "status", - "reason" - ], - "properties": { - "candidate_profile_id": { - "type": "string" - }, - "status": { - "type": "string" - }, - "reason": { - "type": "string" - } - } - } - }, - "evidence": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "required": [ - "id", - "method", - "source_id", - "source_revision", - "checked_at", - "evaluated_at", - "covered_attributes" - ], - "properties": { - "id": { - "type": "string" - }, - "method": { - "type": "string" - }, - "source_id": { - "type": "string" - }, - "source_revision": { - "type": "string" - }, - "checked_at": { - "type": "string", - "format": "date-time" - }, - "evaluated_at": { - "type": "string", - "format": "date-time" - }, - "covered_attributes": { - "type": "array", - "items": { - "type": "string" - } - }, - "covered_components": { - "type": "array", - "items": { - "type": "string" - } - }, - "request_binding": { - "type": "string" - }, - "replay_policy": { - "type": "string" - } - } - } - } - }, - "description": "Stored ingest attribution, independent of risk score and browser automation. Claimed names are unverified. A verified provider does not verify the agent name, service or AI mode. Read proof scope from each attribution and its referenced evidence. Unknown string values and fields must be accepted." - }, "ConnectionType": { "type": "string", "description": "How the visitor connected. Known values:\n- `direct`: a regular connection;\n- `mobile`: a mobile carrier network;\n- `vpn`: a VPN;\n- `proxy`: a proxy, datacenter or hosting network (search-engine crawlers are reported here too);\n- `tor`: the Tor network;\n- `privacy_relay`: a privacy relay such as iCloud Private Relay;\n- `browser_vpn_proxy`: a VPN or proxy built into the browser or one of its extensions;\n- `unknown`: not enough data.\n\nThe value can say `vpn` while `detection_flags.vpn` is `false` (IP intelligence classified the\nnetwork, but the scored VPN check did not fire). Branch on `detection_flags` for decisions.\nThe set is open: keep values added in later versions and treat them as `unknown`.", @@ -752,7 +562,8 @@ ], "properties": { "client_identity": { - "$ref": "#/$defs/ClientIdentity" + "$ref": "#/$defs/WebhookClientIdentity", + "description": "Optional best-effort attribution. May be omitted or null; every nested field is also optional and nullable. Integrations must work without these values." }, "request_id": { "$ref": "#/$defs/RequestId" @@ -1101,6 +912,282 @@ "examples": [ "e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b" ] + }, + "WebhookClientIdentity": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "schema_version": { + "type": [ + "string", + "null" + ], + "description": "Open version identifier. Version 1 is currently emitted." + }, + "classification_revision": { + "type": [ + "integer", + "null" + ], + "minimum": 1 + }, + "classifier_version": { + "type": [ + "string", + "null" + ] + }, + "registry_revision": { + "type": [ + "string", + "null" + ] + }, + "availability": { + "type": [ + "string", + "null" + ], + "description": "available or input_unavailable. Missing object means no stored identity data; it does not mean human." + }, + "observed_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "decided_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "claims": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "profile_id": { + "type": [ + "string", + "null" + ] + }, + "provider_id": { + "type": [ + "string", + "null" + ] + }, + "provider_name": { + "type": [ + "string", + "null" + ] + }, + "agent_name": { + "type": [ + "string", + "null" + ] + }, + "client_kind": { + "type": [ + "string", + "null" + ] + }, + "purpose": { + "type": [ + "string", + "null" + ] + }, + "source": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "verified": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "subject": { + "type": [ + "string", + "null" + ], + "description": "The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes." + }, + "value_id": { + "type": [ + "string", + "null" + ] + }, + "evidence_ids": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + } + } + } + }, + "assessments": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "candidate_profile_id": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "reason": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "evidence": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "method": { + "type": [ + "string", + "null" + ] + }, + "source_id": { + "type": [ + "string", + "null" + ] + }, + "source_revision": { + "type": [ + "string", + "null" + ] + }, + "checked_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "evaluated_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "covered_attributes": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "covered_components": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "request_binding": { + "type": [ + "string", + "null" + ] + }, + "replay_policy": { + "type": [ + "string", + "null" + ] + } + } + } + } + }, + "description": "Optional best-effort client attribution for webhooks. The object and every nested field may be omitted or null. Availability and completeness are not guaranteed; integrations must work without this block or any of its fields. Missing or null values do not mean human or a negative verification result. Claimed names are unverified; a verified provider does not prove a service or agent. Unknown fields and string values must be accepted." } } } diff --git a/dist/schemas/webhook-event.schema.json b/dist/schemas/webhook-event.schema.json index 18acd7a..181cc0d 100644 --- a/dist/schemas/webhook-event.schema.json +++ b/dist/schemas/webhook-event.schema.json @@ -89,196 +89,6 @@ "" ] }, - "ClientIdentity": { - "type": "object", - "additionalProperties": true, - "required": [ - "schema_version", - "classification_revision", - "classifier_version", - "registry_revision", - "availability", - "observed_at", - "decided_at", - "claims", - "verified", - "assessments", - "evidence" - ], - "properties": { - "schema_version": { - "type": "string", - "description": "Open version identifier. Version 1 is currently emitted." - }, - "classification_revision": { - "type": "integer", - "minimum": 1 - }, - "classifier_version": { - "type": "string" - }, - "registry_revision": { - "type": "string" - }, - "availability": { - "type": "string", - "description": "available or input_unavailable. Missing object means no stored identity data; it does not mean human." - }, - "observed_at": { - "type": "string", - "format": "date-time" - }, - "decided_at": { - "type": "string", - "format": "date-time" - }, - "claims": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "required": [ - "profile_id", - "provider_id", - "provider_name", - "agent_name", - "client_kind", - "purpose", - "source" - ], - "properties": { - "profile_id": { - "type": "string" - }, - "provider_id": { - "type": "string" - }, - "provider_name": { - "type": "string" - }, - "agent_name": { - "type": "string" - }, - "client_kind": { - "type": "string" - }, - "purpose": { - "type": "string" - }, - "source": { - "type": "string" - } - } - } - }, - "verified": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "required": [ - "subject", - "value_id", - "evidence_ids" - ], - "properties": { - "subject": { - "type": "string", - "description": "The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes." - }, - "value_id": { - "type": "string" - }, - "evidence_ids": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "assessments": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "required": [ - "status", - "reason" - ], - "properties": { - "candidate_profile_id": { - "type": "string" - }, - "status": { - "type": "string" - }, - "reason": { - "type": "string" - } - } - } - }, - "evidence": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "required": [ - "id", - "method", - "source_id", - "source_revision", - "checked_at", - "evaluated_at", - "covered_attributes" - ], - "properties": { - "id": { - "type": "string" - }, - "method": { - "type": "string" - }, - "source_id": { - "type": "string" - }, - "source_revision": { - "type": "string" - }, - "checked_at": { - "type": "string", - "format": "date-time" - }, - "evaluated_at": { - "type": "string", - "format": "date-time" - }, - "covered_attributes": { - "type": "array", - "items": { - "type": "string" - } - }, - "covered_components": { - "type": "array", - "items": { - "type": "string" - } - }, - "request_binding": { - "type": "string" - }, - "replay_policy": { - "type": "string" - } - } - } - } - }, - "description": "Stored ingest attribution, independent of risk score and browser automation. Claimed names are unverified. A verified provider does not verify the agent name, service or AI mode. Read proof scope from each attribution and its referenced evidence. Unknown string values and fields must be accepted." - }, "ConnectionType": { "type": "string", "description": "How the visitor connected. Known values:\n- `direct`: a regular connection;\n- `mobile`: a mobile carrier network;\n- `vpn`: a VPN;\n- `proxy`: a proxy, datacenter or hosting network (search-engine crawlers are reported here too);\n- `tor`: the Tor network;\n- `privacy_relay`: a privacy relay such as iCloud Private Relay;\n- `browser_vpn_proxy`: a VPN or proxy built into the browser or one of its extensions;\n- `unknown`: not enough data.\n\nThe value can say `vpn` while `detection_flags.vpn` is `false` (IP intelligence classified the\nnetwork, but the scored VPN check did not fire). Branch on `detection_flags` for decisions.\nThe set is open: keep values added in later versions and treat them as `unknown`.", @@ -539,7 +349,8 @@ ], "properties": { "client_identity": { - "$ref": "#/$defs/ClientIdentity" + "$ref": "#/$defs/WebhookClientIdentity", + "description": "Optional best-effort attribution. May be omitted or null; every nested field is also optional and nullable. Integrations must work without these values." }, "request_id": { "$ref": "#/$defs/RequestId" @@ -1459,6 +1270,282 @@ "e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b" ] }, + "WebhookClientIdentity": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "schema_version": { + "type": [ + "string", + "null" + ], + "description": "Open version identifier. Version 1 is currently emitted." + }, + "classification_revision": { + "type": [ + "integer", + "null" + ], + "minimum": 1 + }, + "classifier_version": { + "type": [ + "string", + "null" + ] + }, + "registry_revision": { + "type": [ + "string", + "null" + ] + }, + "availability": { + "type": [ + "string", + "null" + ], + "description": "available or input_unavailable. Missing object means no stored identity data; it does not mean human." + }, + "observed_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "decided_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "claims": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "profile_id": { + "type": [ + "string", + "null" + ] + }, + "provider_id": { + "type": [ + "string", + "null" + ] + }, + "provider_name": { + "type": [ + "string", + "null" + ] + }, + "agent_name": { + "type": [ + "string", + "null" + ] + }, + "client_kind": { + "type": [ + "string", + "null" + ] + }, + "purpose": { + "type": [ + "string", + "null" + ] + }, + "source": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "verified": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "subject": { + "type": [ + "string", + "null" + ], + "description": "The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes." + }, + "value_id": { + "type": [ + "string", + "null" + ] + }, + "evidence_ids": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + } + } + } + }, + "assessments": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "candidate_profile_id": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "reason": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "evidence": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "method": { + "type": [ + "string", + "null" + ] + }, + "source_id": { + "type": [ + "string", + "null" + ] + }, + "source_revision": { + "type": [ + "string", + "null" + ] + }, + "checked_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "evaluated_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "covered_attributes": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "covered_components": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "request_binding": { + "type": [ + "string", + "null" + ] + }, + "replay_policy": { + "type": [ + "string", + "null" + ] + } + } + } + } + }, + "description": "Optional best-effort client attribution for webhooks. The object and every nested field may be omitted or null. Availability and completeness are not guaranteed; integrations must work without this block or any of its fields. Missing or null values do not mean human or a negative verification result. Claimed names are unverified; a verified provider does not prove a service or agent. Unknown fields and string values must be accepted." + }, "WebhookPingEvent": { "type": "object", "description": "Body of a `webhook.ping` delivery, sent when you verify an endpoint. It has no `data`. The keys arrive sorted alphabetically and `created_at` has second precision.", diff --git a/dist/shieldlabs-api.json b/dist/shieldlabs-api.json index fff5271..6f6d335 100644 --- a/dist/shieldlabs-api.json +++ b/dist/shieldlabs-api.json @@ -2321,6 +2321,282 @@ "2026-06-01" ] }, + "WebhookClientIdentity": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "schema_version": { + "type": [ + "string", + "null" + ], + "description": "Open version identifier. Version 1 is currently emitted." + }, + "classification_revision": { + "type": [ + "integer", + "null" + ], + "minimum": 1 + }, + "classifier_version": { + "type": [ + "string", + "null" + ] + }, + "registry_revision": { + "type": [ + "string", + "null" + ] + }, + "availability": { + "type": [ + "string", + "null" + ], + "description": "available or input_unavailable. Missing object means no stored identity data; it does not mean human." + }, + "observed_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "decided_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "claims": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "profile_id": { + "type": [ + "string", + "null" + ] + }, + "provider_id": { + "type": [ + "string", + "null" + ] + }, + "provider_name": { + "type": [ + "string", + "null" + ] + }, + "agent_name": { + "type": [ + "string", + "null" + ] + }, + "client_kind": { + "type": [ + "string", + "null" + ] + }, + "purpose": { + "type": [ + "string", + "null" + ] + }, + "source": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "verified": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "subject": { + "type": [ + "string", + "null" + ], + "description": "The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes." + }, + "value_id": { + "type": [ + "string", + "null" + ] + }, + "evidence_ids": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + } + } + } + }, + "assessments": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "candidate_profile_id": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "reason": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "evidence": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "object", + "null" + ], + "additionalProperties": true, + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "method": { + "type": [ + "string", + "null" + ] + }, + "source_id": { + "type": [ + "string", + "null" + ] + }, + "source_revision": { + "type": [ + "string", + "null" + ] + }, + "checked_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "evaluated_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "covered_attributes": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "covered_components": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "request_binding": { + "type": [ + "string", + "null" + ] + }, + "replay_policy": { + "type": [ + "string", + "null" + ] + } + } + } + } + }, + "description": "Optional best-effort client attribution for webhooks. The object and every nested field may be omitted or null. Availability and completeness are not guaranteed; integrations must work without this block or any of its fields. Missing or null values do not mean human or a negative verification result. Claimed names are unverified; a verified provider does not prove a service or agent. Unknown fields and string values must be accepted." + }, "UserHid": { "type": [ "string", @@ -2712,7 +2988,8 @@ ], "properties": { "client_identity": { - "$ref": "#/components/schemas/ClientIdentity" + "$ref": "#/components/schemas/WebhookClientIdentity", + "description": "Optional best-effort attribution. May be omitted or null; every nested field is also optional and nullable. Integrations must work without these values." }, "request_id": { "$ref": "#/components/schemas/RequestId" diff --git a/dist/shieldlabs-api.yaml b/dist/shieldlabs-api.yaml index 616373a..d3b3b63 100644 --- a/dist/shieldlabs-api.yaml +++ b/dist/shieldlabs-api.yaml @@ -1997,6 +1997,193 @@ components: - '2026-10-07' - '2026-10-06' - '2026-06-01' + WebhookClientIdentity: + type: + - object + - 'null' + additionalProperties: true + properties: + schema_version: + type: + - string + - 'null' + description: Open version identifier. Version 1 is currently emitted. + classification_revision: + type: + - integer + - 'null' + minimum: 1 + classifier_version: + type: + - string + - 'null' + registry_revision: + type: + - string + - 'null' + availability: + type: + - string + - 'null' + description: available or input_unavailable. Missing object means no stored identity data; it does not mean human. + observed_at: + type: + - string + - 'null' + format: date-time + decided_at: + type: + - string + - 'null' + format: date-time + claims: + type: + - array + - 'null' + items: + type: + - object + - 'null' + additionalProperties: true + properties: + profile_id: + type: + - string + - 'null' + provider_id: + type: + - string + - 'null' + provider_name: + type: + - string + - 'null' + agent_name: + type: + - string + - 'null' + client_kind: + type: + - string + - 'null' + purpose: + type: + - string + - 'null' + source: + type: + - string + - 'null' + verified: + type: + - array + - 'null' + items: + type: + - object + - 'null' + additionalProperties: true + properties: + subject: + type: + - string + - 'null' + description: 'The proved attribute: provider, service or infrastructure. Proof does not extend to other attributes.' + value_id: + type: + - string + - 'null' + evidence_ids: + type: + - array + - 'null' + items: + type: + - string + - 'null' + assessments: + type: + - array + - 'null' + items: + type: + - object + - 'null' + additionalProperties: true + properties: + candidate_profile_id: + type: + - string + - 'null' + status: + type: + - string + - 'null' + reason: + type: + - string + - 'null' + evidence: + type: + - array + - 'null' + items: + type: + - object + - 'null' + additionalProperties: true + properties: + id: + type: + - string + - 'null' + method: + type: + - string + - 'null' + source_id: + type: + - string + - 'null' + source_revision: + type: + - string + - 'null' + checked_at: + type: + - string + - 'null' + format: date-time + evaluated_at: + type: + - string + - 'null' + format: date-time + covered_attributes: + type: + - array + - 'null' + items: + type: + - string + - 'null' + covered_components: + type: + - array + - 'null' + items: + type: + - string + - 'null' + request_binding: + type: + - string + - 'null' + replay_policy: + type: + - string + - 'null' + description: Optional best-effort client attribution for webhooks. The object and every nested field may be omitted or null. Availability and completeness are not guaranteed; integrations must work without this block or any of its fields. Missing or null values do not mean human or a negative verification result. Claimed names are unverified; a verified provider does not prove a service or agent. Unknown fields and string values must be accepted. UserHid: type: - string @@ -2337,7 +2524,8 @@ components: - observed_at properties: client_identity: - $ref: '#/components/schemas/ClientIdentity' + $ref: '#/components/schemas/WebhookClientIdentity' + description: Optional best-effort attribution. May be omitted or null; every nested field is also optional and nullable. Integrations must work without these values. request_id: $ref: '#/components/schemas/RequestId' visitor_id: diff --git a/spec/components/schemas/IdentificationScoredData.yaml b/spec/components/schemas/IdentificationScoredData.yaml index 0c47e46..85b384d 100644 --- a/spec/components/schemas/IdentificationScoredData.yaml +++ b/spec/components/schemas/IdentificationScoredData.yaml @@ -23,7 +23,8 @@ required: - observed_at properties: client_identity: - $ref: ./ClientIdentity.yaml + $ref: ./WebhookClientIdentity.yaml + description: Optional best-effort attribution. May be omitted or null; every nested field is also optional and nullable. Integrations must work without these values. request_id: $ref: ./RequestId.yaml visitor_id: diff --git a/spec/components/schemas/WebhookClientIdentity.yaml b/spec/components/schemas/WebhookClientIdentity.yaml new file mode 100644 index 0000000..0710714 --- /dev/null +++ b/spec/components/schemas/WebhookClientIdentity.yaml @@ -0,0 +1,122 @@ +type: + - object + - "null" +additionalProperties: true +properties: + schema_version: + type: + - string + - "null" + description: Open version identifier. Version 1 is currently emitted. + classification_revision: + type: + - integer + - "null" + minimum: 1 + classifier_version: &a1 + type: + - string + - "null" + registry_revision: *a1 + availability: + type: + - string + - "null" + description: available or input_unavailable. Missing object means no stored + identity data; it does not mean human. + observed_at: + type: + - string + - "null" + format: date-time + decided_at: + type: + - string + - "null" + format: date-time + claims: + type: + - array + - "null" + items: + type: + - object + - "null" + additionalProperties: true + properties: + profile_id: *a1 + provider_id: *a1 + provider_name: *a1 + agent_name: *a1 + client_kind: *a1 + purpose: *a1 + source: *a1 + verified: + type: + - array + - "null" + items: + type: + - object + - "null" + additionalProperties: true + properties: + subject: + type: + - string + - "null" + description: "The proved attribute: provider, service or infrastructure. Proof + does not extend to other attributes." + value_id: *a1 + evidence_ids: &a2 + type: + - array + - "null" + items: *a1 + assessments: + type: + - array + - "null" + items: + type: + - object + - "null" + additionalProperties: true + properties: + candidate_profile_id: *a1 + status: *a1 + reason: *a1 + evidence: + type: + - array + - "null" + items: + type: + - object + - "null" + additionalProperties: true + properties: + id: *a1 + method: *a1 + source_id: *a1 + source_revision: *a1 + checked_at: + type: + - string + - "null" + format: date-time + evaluated_at: + type: + - string + - "null" + format: date-time + covered_attributes: *a2 + covered_components: *a2 + request_binding: *a1 + replay_policy: *a1 +description: Optional best-effort client attribution for webhooks. The object + and every nested field may be omitted or null. Availability and completeness + are not guaranteed; integrations must work without this block or any of its + fields. Missing or null values do not mean human or a negative verification + result. Claimed names are unverified; a verified provider does not prove a + service or agent. Unknown fields and string values must be accepted. diff --git a/test/webhook-client-identity.test.mjs b/test/webhook-client-identity.test.mjs new file mode 100644 index 0000000..48ce006 --- /dev/null +++ b/test/webhook-client-identity.test.mjs @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { it } from 'node:test'; +import YAML from 'yaml'; +import { freshBuild, ROOT } from './helpers.mjs'; + +it('webhook client identity and every nested field accept missing and null values', () => { + const { doc, validator } = freshBuild(); + const eventSchema = doc.components.schemas.IdentificationScoredEvent; + const fixture = JSON.parse(readFileSync(path.join(ROOT, 'test/contracts/named.json'), 'utf8')); + const check = identity => { + const event = structuredClone(fixture); + if (identity !== undefined) event.data.client_identity = identity; + const result = validator.validate(eventSchema, event); + assert.ok(result.valid, JSON.stringify(result.errors)); + }; + check(undefined); + check(null); + check({}); + const identitySchema = YAML.parse(readFileSync(path.join(ROOT, 'spec/components/schemas/WebhookClientIdentity.yaml'), 'utf8')); + function allNulls(schema) { + assert.equal(schema.required, undefined); + assert.ok(schema.type.includes('null')); + for (const child of Object.values(schema.properties ?? {})) allNulls(child); + if (schema.items) allNulls(schema.items); + return Object.fromEntries(Object.keys(schema.properties ?? {}).map(key => [key, null])); + } + check(allNulls(identitySchema)); + for (const key of ['claims', 'verified', 'assessments', 'evidence']) { + check({ [key]: [null, {}, allNulls(identitySchema.properties[key].items)] }); + } + const invalid = structuredClone(fixture); + invalid.data.client_identity = { classification_revision: 'invalid' }; + assert.equal(validator.validate(eventSchema, invalid).valid, false); +});