{
  "$meta": {
    "package": "@walkeros/server-transformer-fingerprint",
    "version": "4.2.1",
    "type": "transformer",
    "platform": [
      "server"
    ],
    "docs": "https://www.walkeros.io/docs/transformers/fingerprint",
    "source": "https://github.com/elbwalker/walkerOS/tree/main/packages/server/transformers/fingerprint/src"
  },
  "schemas": {
    "settings": {
      "$schema": "http://json-schema.org/draft-07/schema#",
      "type": "object",
      "properties": {
        "fields": {
          "type": "array",
          "items": {
            "anyOf": [
              {
                "type": "string",
                "description": "Dot-notation path: \"ingest.ip\", \"event.data.userId\""
              },
              {
                "type": "object",
                "properties": {
                  "key": {
                    "description": "Source property path",
                    "type": "string"
                  },
                  "value": {
                    "description": "Static value or fallback"
                  },
                  "fn": {
                    "description": "$code: function for value transformation",
                    "type": "string"
                  }
                },
                "additionalProperties": false,
                "description": "Mapping value config for computed fields"
              }
            ]
          },
          "description": "Fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }."
        },
        "output": {
          "description": "Dot-notation path where hash is stored on the event. Default: \"user.hash\"",
          "type": "string"
        },
        "length": {
          "description": "Truncate hash to this length. Default: full 64-char SHA-256 hash",
          "type": "integer",
          "exclusiveMinimum": 0,
          "maximum": 9007199254740991
        }
      },
      "required": [
        "fields"
      ],
      "additionalProperties": false,
      "description": "Fingerprint transformer: generates deterministic user hashes from event fields"
    }
  },
  "examples": {
    "step": {
      "ipAnonymization": {
        "title": "IP anonymization",
        "description": "Privacy-preserving fingerprint using key+fn pattern: fn truncates IP to /24 subnet before hashing, so 10.0.42.* users share a hash. Config: fields: [{ key: \"ingest.ip\", fn: ip => ip.replace(/\\.\\d+$/, \".0\") }, \"ingest.userAgent\"]",
        "in": {
          "name": "page view",
          "data": {
            "domain": "www.example.com",
            "title": "Privacy Policy",
            "id": "/privacy"
          },
          "id": "ev-1700000602",
          "trigger": "load",
          "entity": "page",
          "action": "view",
          "timestamp": 1700000602,
          "source": {
            "type": "express",
            "platform": "server"
          }
        },
        "out": [
          [
            "return",
            {
              "event": {
                "name": "page view",
                "data": {
                  "domain": "www.example.com",
                  "title": "Privacy Policy",
                  "id": "/privacy"
                },
                "user": {
                  "hash": "44d9154b9a9b3792"
                },
                "id": "ev-1700000602",
                "trigger": "load",
                "entity": "page",
                "action": "view",
                "timestamp": 1700000602,
                "source": {
                  "type": "express",
                  "platform": "server"
                }
              }
            }
          ]
        ]
      },
      "missingFields": {
        "public": false,
        "description": "Graceful handling when ingest is missing - fields resolve to empty strings, hash is still generated.",
        "in": {
          "name": "session start",
          "data": {
            "id": "s3ss10n"
          },
          "id": "ev-1700000601",
          "trigger": "load",
          "entity": "session",
          "action": "start",
          "timestamp": 1700000601,
          "source": {
            "type": "express",
            "platform": "server"
          }
        },
        "out": [
          [
            "return",
            {
              "event": {
                "name": "session start",
                "data": {
                  "id": "s3ss10n"
                },
                "user": {
                  "hash": "e183220b699c10a8"
                },
                "id": "ev-1700000601",
                "trigger": "load",
                "entity": "session",
                "action": "start",
                "timestamp": 1700000601,
                "source": {
                  "type": "express",
                  "platform": "server"
                }
              }
            }
          ]
        ]
      },
      "serverFingerprint": {
        "title": "Server fingerprint",
        "description": "Standard server fingerprint using ingest.ip and ingest.userAgent. Requires source config.ingest.",
        "in": {
          "name": "page view",
          "data": {
            "domain": "www.example.com",
            "title": "Getting Started",
            "id": "/docs/getting-started"
          },
          "id": "ev-1700000600",
          "trigger": "load",
          "entity": "page",
          "action": "view",
          "timestamp": 1700000600,
          "source": {
            "type": "express",
            "platform": "server"
          }
        },
        "out": [
          [
            "return",
            {
              "event": {
                "name": "page view",
                "data": {
                  "domain": "www.example.com",
                  "title": "Getting Started",
                  "id": "/docs/getting-started"
                },
                "user": {
                  "hash": "158f99cc06e33fd6"
                },
                "id": "ev-1700000600",
                "trigger": "load",
                "entity": "page",
                "action": "view",
                "timestamp": 1700000600,
                "source": {
                  "type": "express",
                  "platform": "server"
                }
              }
            }
          ]
        ]
      }
    }
  },
  "hints": {
    "ingest-prerequisite": {
      "text": "Fields starting with ingest.* require the server source to have config.ingest configured. config.ingest MUST use the map operator with direct request field paths (no req. prefix); a bare object like { ip: \"req.ip\" } is silently inert and leaves ingest empty. Without populated ingest, all ingest.* fields resolve to empty strings, the hash is still generated but not unique. Always pair this transformer with a source that extracts request metadata.",
      "code": [
        {
          "lang": "json",
          "code": "{\n  \"sources\": {\n    \"express\": {\n      \"package\": \"@walkeros/server-source-express\",\n      \"config\": {\n        \"settings\": {\n          \"port\": 8080\n        },\n        \"ingest\": {\n          \"map\": {\n            \"ip\": {\n              \"key\": \"ip\"\n            },\n            \"userAgent\": {\n              \"key\": \"headers.user-agent\"\n            },\n            \"origin\": {\n              \"key\": \"headers.origin\"\n            }\n          }\n        }\n      }\n    }\n  },\n  \"transformers\": {\n    \"fingerprint\": {\n      \"package\": \"@walkeros/server-transformer-fingerprint\",\n      \"config\": {\n        \"settings\": {\n          \"fields\": [\n            \"ingest.ip\",\n            \"ingest.userAgent\"\n          ],\n          \"output\": \"user.hash\",\n          \"length\": 16\n        }\n      }\n    }\n  }\n}"
        }
      ]
    },
    "fields-overview": {
      "text": "Fields resolve from { event, ingest } via walkerOS mapping. Common patterns: ingest.ip (client IP), ingest.userAgent (browser UA), event.data.* (any event property). For time-based rotation use fn fields: daily rotation with toISOString().slice(0,10), monthly with getDate(). Order matters — same fields in different order produce different hashes. Use { key, fn } objects to transform before hashing (e.g., IP anonymization via the ipAnonymization step example)."
    }
  }
}