@walkeros/server-transformer-fingerprint 4.6.0 → 4.7.0-next-1790187973605
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -38
- package/dist/dev.d.mts +23 -2
- package/dist/dev.d.ts +23 -2
- package/dist/dev.js +1 -1
- package/dist/dev.js.map +1 -1
- package/dist/dev.mjs +1 -1
- package/dist/dev.mjs.map +1 -1
- package/dist/index.d.mts +44 -17
- package/dist/index.d.ts +44 -17
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/walkerOS.json +139 -15
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
# @walkeros/server-transformer-fingerprint
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
event. No cookies, no
|
|
12
|
-
which gives session continuity and cross-domain stitching without a client-side
|
|
13
|
-
ID.
|
|
9
|
+
Cookieless, privacy-friendly visitor identification for walkerOS. Hashes the
|
|
10
|
+
anonymized IP, a reduced user agent and the site with a secret salt, rotates
|
|
11
|
+
daily, and stores the hash on the event. No cookies, no raw IP on the event.
|
|
14
12
|
|
|
15
13
|
[Documentation](https://www.walkeros.io/docs/transformers/fingerprint) •
|
|
16
14
|
[NPM Package](https://www.npmjs.com/package/@walkeros/server-transformer-fingerprint)
|
|
@@ -34,48 +32,20 @@ await startFlow({
|
|
|
34
32
|
fingerprint: {
|
|
35
33
|
code: transformerFingerprint,
|
|
36
34
|
config: {
|
|
37
|
-
settings: {
|
|
38
|
-
fields: ['ingest.ip', 'ingest.userAgent'],
|
|
39
|
-
output: 'user.hash',
|
|
40
|
-
length: 16,
|
|
41
|
-
},
|
|
35
|
+
settings: { salt: process.env.FINGERPRINT_SALT },
|
|
42
36
|
},
|
|
43
37
|
},
|
|
44
38
|
},
|
|
45
39
|
});
|
|
46
40
|
```
|
|
47
41
|
|
|
48
|
-
The
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{ "name": "page view", "user": { "hash": "158f99cc06e33fd6" } }
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Fields resolve from `{ event, ingest }` using walkerOS mapping. Strings use dot
|
|
55
|
-
notation, and function values compute dynamically. A missing field is treated as
|
|
56
|
-
an empty string, and the transformer never throws.
|
|
57
|
-
|
|
58
|
-
## Daily rotation
|
|
59
|
-
|
|
60
|
-
Without rotation the same IP and user agent produce the same hash indefinitely.
|
|
61
|
-
Add a date field to reset it each day, which limits cross-day tracking while
|
|
62
|
-
keeping session continuity within a day:
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
settings: {
|
|
66
|
-
fields: [
|
|
67
|
-
'ingest.ip',
|
|
68
|
-
'ingest.userAgent',
|
|
69
|
-
{ fn: () => new Date().toISOString().slice(0, 10) },
|
|
70
|
-
],
|
|
71
|
-
output: 'user.hash',
|
|
72
|
-
length: 16,
|
|
73
|
-
}
|
|
74
|
-
```
|
|
42
|
+
The source must extract `ip` and `userAgent` into `ingest` (its
|
|
43
|
+
`config.ingest`). The event then carries the hash at `user.hash`. Without a salt
|
|
44
|
+
the hash can be reversed to the IP, so the transformer logs a warning.
|
|
75
45
|
|
|
76
46
|
## Documentation
|
|
77
47
|
|
|
78
|
-
Full configuration,
|
|
48
|
+
Full configuration, rotation, site separation, and examples live in the docs:
|
|
79
49
|
**https://www.walkeros.io/docs/transformers/fingerprint**
|
|
80
50
|
|
|
81
51
|
## Contribute
|
package/dist/dev.d.mts
CHANGED
|
@@ -8,11 +8,32 @@ import { Flow, Hint } from '@walkeros/core';
|
|
|
8
8
|
* Mirrors: types.ts FingerprintSettings
|
|
9
9
|
*/
|
|
10
10
|
declare const SettingsSchema: z.ZodObject<{
|
|
11
|
-
|
|
11
|
+
salt: z.ZodOptional<z.ZodString>;
|
|
12
|
+
rotate: z.ZodOptional<z.ZodEnum<{
|
|
13
|
+
daily: "daily";
|
|
14
|
+
hourly: "hourly";
|
|
15
|
+
none: "none";
|
|
16
|
+
}>>;
|
|
17
|
+
ip: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
12
18
|
key: z.ZodOptional<z.ZodString>;
|
|
13
19
|
value: z.ZodOptional<z.ZodUnknown>;
|
|
14
20
|
fn: z.ZodOptional<z.ZodString>;
|
|
15
|
-
}, z.core.$strip>]>>;
|
|
21
|
+
}, z.core.$strip>]>, z.ZodLiteral<false>]>>;
|
|
22
|
+
userAgent: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
23
|
+
key: z.ZodOptional<z.ZodString>;
|
|
24
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
25
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, z.core.$strip>]>, z.ZodLiteral<false>]>>;
|
|
27
|
+
site: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
28
|
+
key: z.ZodOptional<z.ZodString>;
|
|
29
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
30
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
31
|
+
}, z.core.$strip>]>, z.ZodLiteral<false>]>>;
|
|
32
|
+
fields: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
33
|
+
key: z.ZodOptional<z.ZodString>;
|
|
34
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
35
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
36
|
+
}, z.core.$strip>]>>>;
|
|
16
37
|
output: z.ZodOptional<z.ZodString>;
|
|
17
38
|
length: z.ZodOptional<z.ZodNumber>;
|
|
18
39
|
}, z.core.$strip>;
|
package/dist/dev.d.ts
CHANGED
|
@@ -8,11 +8,32 @@ import { Flow, Hint } from '@walkeros/core';
|
|
|
8
8
|
* Mirrors: types.ts FingerprintSettings
|
|
9
9
|
*/
|
|
10
10
|
declare const SettingsSchema: z.ZodObject<{
|
|
11
|
-
|
|
11
|
+
salt: z.ZodOptional<z.ZodString>;
|
|
12
|
+
rotate: z.ZodOptional<z.ZodEnum<{
|
|
13
|
+
daily: "daily";
|
|
14
|
+
hourly: "hourly";
|
|
15
|
+
none: "none";
|
|
16
|
+
}>>;
|
|
17
|
+
ip: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
12
18
|
key: z.ZodOptional<z.ZodString>;
|
|
13
19
|
value: z.ZodOptional<z.ZodUnknown>;
|
|
14
20
|
fn: z.ZodOptional<z.ZodString>;
|
|
15
|
-
}, z.core.$strip>]>>;
|
|
21
|
+
}, z.core.$strip>]>, z.ZodLiteral<false>]>>;
|
|
22
|
+
userAgent: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
23
|
+
key: z.ZodOptional<z.ZodString>;
|
|
24
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
25
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, z.core.$strip>]>, z.ZodLiteral<false>]>>;
|
|
27
|
+
site: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
28
|
+
key: z.ZodOptional<z.ZodString>;
|
|
29
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
30
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
31
|
+
}, z.core.$strip>]>, z.ZodLiteral<false>]>>;
|
|
32
|
+
fields: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
33
|
+
key: z.ZodOptional<z.ZodString>;
|
|
34
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
35
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
36
|
+
}, z.core.$strip>]>>>;
|
|
16
37
|
output: z.ZodOptional<z.ZodString>;
|
|
17
38
|
length: z.ZodOptional<z.ZodNumber>;
|
|
18
39
|
}, z.core.$strip>;
|
package/dist/dev.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e,t=Object.defineProperty,i=Object.getOwnPropertyDescriptor,
|
|
1
|
+
"use strict";var e,t=Object.defineProperty,i=Object.getOwnPropertyDescriptor,s=Object.getOwnPropertyNames,n=Object.prototype.hasOwnProperty,r=(e,i)=>{for(var s in i)t(e,s,{get:i[s],enumerable:!0})},a={};r(a,{examples:()=>c,hints:()=>w,schemas:()=>o}),module.exports=(e=a,((e,r,a,o)=>{if(r&&"object"==typeof r||"function"==typeof r)for(let d of s(r))n.call(e,d)||d===a||t(e,d,{get:()=>r[d],enumerable:!(o=i(r,d))||o.enumerable});return e})(t({},"__esModule",{value:!0}),e));var o={};r(o,{SettingsSchema:()=>g,settings:()=>u});var d=require("@walkeros/core/dev"),l=require("@walkeros/core/dev"),h=l.z.union([l.z.string().describe('Dot-notation path: "ingest.ip", "event.data.userId"'),l.z.object({key:l.z.string().optional().describe("Source property path"),value:l.z.unknown().optional().describe("Static value or fallback"),fn:l.z.string().optional().describe("$code: function receiving { event, ingest } that returns the value. Runs only when no key is set.")}).describe("Mapping value config for computed fields")]),p=l.z.union([h,l.z.literal(!1).describe("Switch this input off")]),g=l.z.object({salt:l.z.string().optional().describe('Secret key for the hash (HMAC), e.g. "$env.FINGERPRINT_SALT". Without it the hash can be reversed to the IP; a warning is logged.'),rotate:l.z.enum(["daily","hourly","none"]).optional().describe('Rotation window in UTC: the same visitor gets a new hash each window. Default: "daily"'),ip:p.optional().describe('Client IP, anonymized to /24 (IPv4) or /48 (IPv6) before hashing. Default: "ingest.ip" when fields is not set'),userAgent:p.optional().describe('User agent, reduced to browser, major version and OS before hashing. Default: "ingest.userAgent" when fields is not set'),site:p.optional().describe('Site the visitor is on; a URL is reduced to its hostname. Default: "event.source.url" when fields is not set'),fields:l.z.array(h).optional().describe("Extra fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }. When set, named inputs are only used if set explicitly."),output:l.z.string().optional().describe('Dot-notation path where hash is stored on the event. Default: "user.hash"'),length:l.z.number().int().positive().optional().describe("Truncate hash to this length. Default: full 64-char SHA-256 hash")}).describe("Fingerprint transformer: a cookieless, privacy-friendly visitor hash"),u=(0,d.zodToSchema)(g),c={};r(c,{step:()=>f});var f={};r(f,{ipAnonymization:()=>y,missingFields:()=>m,serverFingerprint:()=>v});var v={title:"Server fingerprint",description:'Default server fingerprint: the anonymized ingest.ip, the reduced ingest.userAgent and the site, keyed with settings.salt and rotated daily. Config: { salt: "$env.FINGERPRINT_SALT", length: 16 }. Requires source config.ingest. The hash depends on the salt and the day.',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"}}}]]},m={public:!1,description:"Graceful handling when ingest is missing: each empty input logs one warning, and the 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"}}}]]},y={title:"IP anonymization",description:'The ip input anonymizes the IP before hashing (IPv4 /24, IPv6 /48), so visitors from the same 10.0.42.* subnet with the same browser, major version and OS share a hash within a day. No custom fn needed. Config: { salt: "$env.FINGERPRINT_SALT", length: 16 }',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"}}}]]},w={"ingest-prerequisite":{text:'The default inputs ip and userAgent read ingest.ip and ingest.userAgent, so the server source must 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. An input that resolves empty logs one warning ("Fingerprint input ... resolved empty") and the hash is still generated, but less distinct. Behind a load balancer the request ip is the balancer; read the client from headers.x-forwarded-for instead.',code:[{lang:"json",code:JSON.stringify({sources:{express:{package:"@walkeros/server-source-express",next:"fingerprint",config:{settings:{port:8080},ingest:{map:{ip:{key:"ip"},userAgent:{key:"headers.user-agent"}}}}}},transformers:{fingerprint:{package:"@walkeros/server-transformer-fingerprint",config:{settings:{salt:"$env.FINGERPRINT_SALT"}}}}},null,2)}]},"salt-and-rotation":{text:'The salt keys the hash (HMAC), so it cannot be recomputed or reversed to the IP without it. Pass it via $env so it stays out of the flow file, and use one salt per deployment. Without a salt the transformer runs but logs a warning at startup. Rotation is built in (daily by default, UTC windows): do not add a date field for it. For another window, set rotate: "none" and add the window as a field. Setting fields switches the default inputs off, so name them too: { rotate: "none", ip: "ingest.ip", userAgent: "ingest.userAgent", site: "event.source.url", fields: [{ fn: () => new Date().toISOString().slice(0, 7) }] } rotates monthly. rotate: "none" without such a field keeps one hash per visitor indefinitely, which is a privacy decision, not a default.'},"site-sources":{text:'site defaults to the hostname of event.source.url, which the walkerOS browser source sets on every event. Events without it (GA4 hits decoded by transformer-ga4, server-side events) need site pointed elsewhere: an ingest path such as ingest.property (a path parameter) or ingest.origin (map it in config.ingest first, e.g. origin: { key: "headers.origin" }), or a static { value: "example.com" }. site keeps one visitor apart across sites when one collector with one salt serves several of them; a single-site deployment with its own salt is already separated by the salt.'},"extra-fields":{text:"fields adds values to the hash as they resolve from { event, ingest }, in order. When fields is set, the named inputs ip, userAgent and site are only used if set explicitly, so an existing fields config keeps its input set. To transform a value before hashing, use fn alone: it receives { event, ingest } and returns the value. A fn next to a key never runs. The IP never needs a custom fn: the ip input anonymizes it (IPv4 /24, IPv6 /48)."}};//# sourceMappingURL=dev.js.map
|
package/dist/dev.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/dev.ts","../src/schemas/index.ts","../src/schemas/settings.ts","../src/examples/index.ts","../src/examples/step.ts","../src/hints.ts"],"sourcesContent":["export * as schemas from './schemas';\nexport * as examples from './examples';\nexport { hints } from './hints';\n","import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport const settings = zodToSchema(SettingsSchema);\n","import { z } from '@walkeros/core/dev';\n\n/**\n * Fingerprint transformer settings schema.\n *\n * Mirrors: types.ts FingerprintSettings\n */\nexport const SettingsSchema = z\n .object({\n fields: z\n .array(\n z.union([\n z\n .string()\n .describe('Dot-notation path: \"ingest.ip\", \"event.data.userId\"'),\n z\n .object({\n key: z.string().optional().describe('Source property path'),\n value: z\n .unknown()\n .optional()\n .describe('Static value or fallback'),\n fn: z\n .string()\n .optional()\n .describe('$code: function for value transformation'),\n })\n .describe('Mapping value config for computed fields'),\n ]),\n )\n .describe(\n 'Fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }.',\n ),\n output: z\n .string()\n .optional()\n .describe(\n 'Dot-notation path where hash is stored on the event. Default: \"user.hash\"',\n ),\n length: z\n .number()\n .int()\n .positive()\n .optional()\n .describe(\n 'Truncate hash to this length. Default: full 64-char SHA-256 hash',\n ),\n })\n .describe(\n 'Fingerprint transformer: generates deterministic user hashes from event fields',\n );\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","export * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\nexport const serverFingerprint: Flow.StepExample = {\n title: 'Server fingerprint',\n description:\n 'Standard server fingerprint using ingest.ip and ingest.userAgent. Requires source config.ingest.',\n in: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Getting Started',\n id: '/docs/getting-started',\n },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Getting Started',\n id: '/docs/getting-started',\n },\n user: { hash: '158f99cc06e33fd6' },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n\nexport const missingFields: Flow.StepExample = {\n public: false,\n description:\n 'Graceful handling when ingest is missing - fields resolve to empty strings, hash is still generated.',\n in: {\n name: 'session start',\n data: { id: 's3ss10n' },\n id: 'ev-1700000601',\n trigger: 'load',\n entity: 'session',\n action: 'start',\n timestamp: 1700000601,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'session start',\n data: { id: 's3ss10n' },\n user: { hash: 'e183220b699c10a8' },\n id: 'ev-1700000601',\n trigger: 'load',\n entity: 'session',\n action: 'start',\n timestamp: 1700000601,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n\nexport const ipAnonymization: Flow.StepExample = {\n title: 'IP anonymization',\n description:\n 'Privacy-preserving fingerprint using key+fn pattern: ' +\n 'fn truncates IP to /24 subnet before hashing, so 10.0.42.* users share a hash. ' +\n 'Config: fields: [{ key: \"ingest.ip\", fn: ip => ip.replace(/\\\\.\\\\d+$/, \".0\") }, \"ingest.userAgent\"]',\n in: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Privacy Policy',\n id: '/privacy',\n },\n id: 'ev-1700000602',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000602,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Privacy Policy',\n id: '/privacy',\n },\n user: { hash: '44d9154b9a9b3792' },\n id: 'ev-1700000602',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000602,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n","import type { Hint } from '@walkeros/core';\n\nexport const hints: Hint.Hints = {\n 'ingest-prerequisite': {\n 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.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n config: {\n settings: { port: 8080 },\n ingest: {\n map: {\n ip: { key: 'ip' },\n userAgent: { key: 'headers.user-agent' },\n origin: { key: 'headers.origin' },\n },\n },\n },\n },\n },\n transformers: {\n fingerprint: {\n package: '@walkeros/server-transformer-fingerprint',\n config: {\n settings: {\n fields: ['ingest.ip', 'ingest.userAgent'],\n output: 'user.hash',\n length: 16,\n },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'fields-overview': {\n 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).',\n },\n};\n"],"mappings":"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","names":["import_dev"]}
|
|
1
|
+
{"version":3,"sources":["../src/dev.ts","../src/schemas/index.ts","../src/schemas/settings.ts","../src/examples/index.ts","../src/examples/step.ts","../src/hints.ts"],"sourcesContent":["export * as schemas from './schemas';\nexport * as examples from './examples';\nexport { hints } from './hints';\n","import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport const settings = zodToSchema(SettingsSchema);\n","import { z } from '@walkeros/core/dev';\n\nconst FieldSchema = z.union([\n z.string().describe('Dot-notation path: \"ingest.ip\", \"event.data.userId\"'),\n z\n .object({\n key: z.string().optional().describe('Source property path'),\n value: z.unknown().optional().describe('Static value or fallback'),\n fn: z\n .string()\n .optional()\n .describe(\n '$code: function receiving { event, ingest } that returns the value. Runs only when no key is set.',\n ),\n })\n .describe('Mapping value config for computed fields'),\n]);\n\nconst InputSchema = z.union([\n FieldSchema,\n z.literal(false).describe('Switch this input off'),\n]);\n\n/**\n * Fingerprint transformer settings schema.\n *\n * Mirrors: types.ts FingerprintSettings\n */\nexport const SettingsSchema = z\n .object({\n salt: z\n .string()\n .optional()\n .describe(\n 'Secret key for the hash (HMAC), e.g. \"$env.FINGERPRINT_SALT\". Without it the hash can be reversed to the IP; a warning is logged.',\n ),\n rotate: z\n .enum(['daily', 'hourly', 'none'])\n .optional()\n .describe(\n 'Rotation window in UTC: the same visitor gets a new hash each window. Default: \"daily\"',\n ),\n ip: InputSchema.optional().describe(\n 'Client IP, anonymized to /24 (IPv4) or /48 (IPv6) before hashing. Default: \"ingest.ip\" when fields is not set',\n ),\n userAgent: InputSchema.optional().describe(\n 'User agent, reduced to browser, major version and OS before hashing. Default: \"ingest.userAgent\" when fields is not set',\n ),\n site: InputSchema.optional().describe(\n 'Site the visitor is on; a URL is reduced to its hostname. Default: \"event.source.url\" when fields is not set',\n ),\n fields: z\n .array(FieldSchema)\n .optional()\n .describe(\n 'Extra fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }. When set, named inputs are only used if set explicitly.',\n ),\n output: z\n .string()\n .optional()\n .describe(\n 'Dot-notation path where hash is stored on the event. Default: \"user.hash\"',\n ),\n length: z\n .number()\n .int()\n .positive()\n .optional()\n .describe(\n 'Truncate hash to this length. Default: full 64-char SHA-256 hash',\n ),\n })\n .describe(\n 'Fingerprint transformer: a cookieless, privacy-friendly visitor hash',\n );\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","export * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\nexport const serverFingerprint: Flow.StepExample = {\n title: 'Server fingerprint',\n description:\n 'Default server fingerprint: the anonymized ingest.ip, the reduced ingest.userAgent and the site, keyed with settings.salt and rotated daily. ' +\n 'Config: { salt: \"$env.FINGERPRINT_SALT\", length: 16 }. Requires source config.ingest. The hash depends on the salt and the day.',\n in: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Getting Started',\n id: '/docs/getting-started',\n },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Getting Started',\n id: '/docs/getting-started',\n },\n user: { hash: '158f99cc06e33fd6' },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n\nexport const missingFields: Flow.StepExample = {\n public: false,\n description:\n 'Graceful handling when ingest is missing: each empty input logs one warning, and the hash is still generated.',\n in: {\n name: 'session start',\n data: { id: 's3ss10n' },\n id: 'ev-1700000601',\n trigger: 'load',\n entity: 'session',\n action: 'start',\n timestamp: 1700000601,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'session start',\n data: { id: 's3ss10n' },\n user: { hash: 'e183220b699c10a8' },\n id: 'ev-1700000601',\n trigger: 'load',\n entity: 'session',\n action: 'start',\n timestamp: 1700000601,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n\nexport const ipAnonymization: Flow.StepExample = {\n title: 'IP anonymization',\n description:\n 'The ip input anonymizes the IP before hashing (IPv4 /24, IPv6 /48), so visitors from the same 10.0.42.* subnet ' +\n 'with the same browser, major version and OS share a hash within a day. No custom fn needed. Config: { salt: \"$env.FINGERPRINT_SALT\", length: 16 }',\n in: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Privacy Policy',\n id: '/privacy',\n },\n id: 'ev-1700000602',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000602,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Privacy Policy',\n id: '/privacy',\n },\n user: { hash: '44d9154b9a9b3792' },\n id: 'ev-1700000602',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000602,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n","import type { Hint } from '@walkeros/core';\n\nexport const hints: Hint.Hints = {\n 'ingest-prerequisite': {\n text: 'The default inputs ip and userAgent read ingest.ip and ingest.userAgent, so the server source must 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. An input that resolves empty logs one warning (\"Fingerprint input ... resolved empty\") and the hash is still generated, but less distinct. Behind a load balancer the request ip is the balancer; read the client from headers.x-forwarded-for instead.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n next: 'fingerprint',\n config: {\n settings: { port: 8080 },\n ingest: {\n map: {\n ip: { key: 'ip' },\n userAgent: { key: 'headers.user-agent' },\n },\n },\n },\n },\n },\n transformers: {\n fingerprint: {\n package: '@walkeros/server-transformer-fingerprint',\n config: {\n settings: { salt: '$env.FINGERPRINT_SALT' },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'salt-and-rotation': {\n text: 'The salt keys the hash (HMAC), so it cannot be recomputed or reversed to the IP without it. Pass it via $env so it stays out of the flow file, and use one salt per deployment. Without a salt the transformer runs but logs a warning at startup. Rotation is built in (daily by default, UTC windows): do not add a date field for it. For another window, set rotate: \"none\" and add the window as a field. Setting fields switches the default inputs off, so name them too: { rotate: \"none\", ip: \"ingest.ip\", userAgent: \"ingest.userAgent\", site: \"event.source.url\", fields: [{ fn: () => new Date().toISOString().slice(0, 7) }] } rotates monthly. rotate: \"none\" without such a field keeps one hash per visitor indefinitely, which is a privacy decision, not a default.',\n },\n 'site-sources': {\n text: 'site defaults to the hostname of event.source.url, which the walkerOS browser source sets on every event. Events without it (GA4 hits decoded by transformer-ga4, server-side events) need site pointed elsewhere: an ingest path such as ingest.property (a path parameter) or ingest.origin (map it in config.ingest first, e.g. origin: { key: \"headers.origin\" }), or a static { value: \"example.com\" }. site keeps one visitor apart across sites when one collector with one salt serves several of them; a single-site deployment with its own salt is already separated by the salt.',\n },\n 'extra-fields': {\n text: 'fields adds values to the hash as they resolve from { event, ingest }, in order. When fields is set, the named inputs ip, userAgent and site are only used if set explicitly, so an existing fields config keeps its input set. To transform a value before hashing, use fn alone: it receives { event, ingest } and returns the value. A fn next to a key never runs. The IP never needs a custom fn: the ip input anonymizes it (IPv4 /24, IPv6 /48).',\n },\n};\n"],"mappings":"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","names":["import_dev"]}
|
package/dist/dev.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=Object.defineProperty,t=(t,i)=>{for(var
|
|
1
|
+
var e=Object.defineProperty,t=(t,i)=>{for(var s in i)e(t,s,{get:i[s],enumerable:!0})},i={};t(i,{SettingsSchema:()=>o,settings:()=>d});import{zodToSchema as s}from"@walkeros/core/dev";import{z as n}from"@walkeros/core/dev";var a=n.union([n.string().describe('Dot-notation path: "ingest.ip", "event.data.userId"'),n.object({key:n.string().optional().describe("Source property path"),value:n.unknown().optional().describe("Static value or fallback"),fn:n.string().optional().describe("$code: function receiving { event, ingest } that returns the value. Runs only when no key is set.")}).describe("Mapping value config for computed fields")]),r=n.union([a,n.literal(!1).describe("Switch this input off")]),o=n.object({salt:n.string().optional().describe('Secret key for the hash (HMAC), e.g. "$env.FINGERPRINT_SALT". Without it the hash can be reversed to the IP; a warning is logged.'),rotate:n.enum(["daily","hourly","none"]).optional().describe('Rotation window in UTC: the same visitor gets a new hash each window. Default: "daily"'),ip:r.optional().describe('Client IP, anonymized to /24 (IPv4) or /48 (IPv6) before hashing. Default: "ingest.ip" when fields is not set'),userAgent:r.optional().describe('User agent, reduced to browser, major version and OS before hashing. Default: "ingest.userAgent" when fields is not set'),site:r.optional().describe('Site the visitor is on; a URL is reduced to its hostname. Default: "event.source.url" when fields is not set'),fields:n.array(a).optional().describe("Extra fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }. When set, named inputs are only used if set explicitly."),output:n.string().optional().describe('Dot-notation path where hash is stored on the event. Default: "user.hash"'),length:n.number().int().positive().optional().describe("Truncate hash to this length. Default: full 64-char SHA-256 hash")}).describe("Fingerprint transformer: a cookieless, privacy-friendly visitor hash"),d=s(o),l={};t(l,{step:()=>h});var h={};t(h,{ipAnonymization:()=>c,missingFields:()=>g,serverFingerprint:()=>p});var p={title:"Server fingerprint",description:'Default server fingerprint: the anonymized ingest.ip, the reduced ingest.userAgent and the site, keyed with settings.salt and rotated daily. Config: { salt: "$env.FINGERPRINT_SALT", length: 16 }. Requires source config.ingest. The hash depends on the salt and the day.',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"}}}]]},g={public:!1,description:"Graceful handling when ingest is missing: each empty input logs one warning, and the 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"}}}]]},c={title:"IP anonymization",description:'The ip input anonymizes the IP before hashing (IPv4 /24, IPv6 /48), so visitors from the same 10.0.42.* subnet with the same browser, major version and OS share a hash within a day. No custom fn needed. Config: { salt: "$env.FINGERPRINT_SALT", length: 16 }',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"}}}]]},u={"ingest-prerequisite":{text:'The default inputs ip and userAgent read ingest.ip and ingest.userAgent, so the server source must 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. An input that resolves empty logs one warning ("Fingerprint input ... resolved empty") and the hash is still generated, but less distinct. Behind a load balancer the request ip is the balancer; read the client from headers.x-forwarded-for instead.',code:[{lang:"json",code:JSON.stringify({sources:{express:{package:"@walkeros/server-source-express",next:"fingerprint",config:{settings:{port:8080},ingest:{map:{ip:{key:"ip"},userAgent:{key:"headers.user-agent"}}}}}},transformers:{fingerprint:{package:"@walkeros/server-transformer-fingerprint",config:{settings:{salt:"$env.FINGERPRINT_SALT"}}}}},null,2)}]},"salt-and-rotation":{text:'The salt keys the hash (HMAC), so it cannot be recomputed or reversed to the IP without it. Pass it via $env so it stays out of the flow file, and use one salt per deployment. Without a salt the transformer runs but logs a warning at startup. Rotation is built in (daily by default, UTC windows): do not add a date field for it. For another window, set rotate: "none" and add the window as a field. Setting fields switches the default inputs off, so name them too: { rotate: "none", ip: "ingest.ip", userAgent: "ingest.userAgent", site: "event.source.url", fields: [{ fn: () => new Date().toISOString().slice(0, 7) }] } rotates monthly. rotate: "none" without such a field keeps one hash per visitor indefinitely, which is a privacy decision, not a default.'},"site-sources":{text:'site defaults to the hostname of event.source.url, which the walkerOS browser source sets on every event. Events without it (GA4 hits decoded by transformer-ga4, server-side events) need site pointed elsewhere: an ingest path such as ingest.property (a path parameter) or ingest.origin (map it in config.ingest first, e.g. origin: { key: "headers.origin" }), or a static { value: "example.com" }. site keeps one visitor apart across sites when one collector with one salt serves several of them; a single-site deployment with its own salt is already separated by the salt.'},"extra-fields":{text:"fields adds values to the hash as they resolve from { event, ingest }, in order. When fields is set, the named inputs ip, userAgent and site are only used if set explicitly, so an existing fields config keeps its input set. To transform a value before hashing, use fn alone: it receives { event, ingest } and returns the value. A fn next to a key never runs. The IP never needs a custom fn: the ip input anonymizes it (IPv4 /24, IPv6 /48)."}};export{l as examples,u as hints,i as schemas};//# sourceMappingURL=dev.mjs.map
|
package/dist/dev.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schemas/index.ts","../src/schemas/settings.ts","../src/examples/index.ts","../src/examples/step.ts","../src/hints.ts"],"sourcesContent":["import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport const settings = zodToSchema(SettingsSchema);\n","import { z } from '@walkeros/core/dev';\n\n/**\n * Fingerprint transformer settings schema.\n *\n * Mirrors: types.ts FingerprintSettings\n */\nexport const SettingsSchema = z\n .object({\n fields: z\n .array(\n z.union([\n z\n .string()\n .describe('Dot-notation path: \"ingest.ip\", \"event.data.userId\"'),\n z\n .object({\n key: z.string().optional().describe('Source property path'),\n value: z\n .unknown()\n .optional()\n .describe('Static value or fallback'),\n fn: z\n .string()\n .optional()\n .describe('$code: function for value transformation'),\n })\n .describe('Mapping value config for computed fields'),\n ]),\n )\n .describe(\n 'Fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }.',\n ),\n output: z\n .string()\n .optional()\n .describe(\n 'Dot-notation path where hash is stored on the event. Default: \"user.hash\"',\n ),\n length: z\n .number()\n .int()\n .positive()\n .optional()\n .describe(\n 'Truncate hash to this length. Default: full 64-char SHA-256 hash',\n ),\n })\n .describe(\n 'Fingerprint transformer: generates deterministic user hashes from event fields',\n );\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","export * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\nexport const serverFingerprint: Flow.StepExample = {\n title: 'Server fingerprint',\n description:\n 'Standard server fingerprint using ingest.ip and ingest.userAgent. Requires source config.ingest.',\n in: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Getting Started',\n id: '/docs/getting-started',\n },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Getting Started',\n id: '/docs/getting-started',\n },\n user: { hash: '158f99cc06e33fd6' },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n\nexport const missingFields: Flow.StepExample = {\n public: false,\n description:\n 'Graceful handling when ingest is missing - fields resolve to empty strings, hash is still generated.',\n in: {\n name: 'session start',\n data: { id: 's3ss10n' },\n id: 'ev-1700000601',\n trigger: 'load',\n entity: 'session',\n action: 'start',\n timestamp: 1700000601,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'session start',\n data: { id: 's3ss10n' },\n user: { hash: 'e183220b699c10a8' },\n id: 'ev-1700000601',\n trigger: 'load',\n entity: 'session',\n action: 'start',\n timestamp: 1700000601,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n\nexport const ipAnonymization: Flow.StepExample = {\n title: 'IP anonymization',\n description:\n 'Privacy-preserving fingerprint using key+fn pattern: ' +\n 'fn truncates IP to /24 subnet before hashing, so 10.0.42.* users share a hash. ' +\n 'Config: fields: [{ key: \"ingest.ip\", fn: ip => ip.replace(/\\\\.\\\\d+$/, \".0\") }, \"ingest.userAgent\"]',\n in: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Privacy Policy',\n id: '/privacy',\n },\n id: 'ev-1700000602',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000602,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Privacy Policy',\n id: '/privacy',\n },\n user: { hash: '44d9154b9a9b3792' },\n id: 'ev-1700000602',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000602,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n","import type { Hint } from '@walkeros/core';\n\nexport const hints: Hint.Hints = {\n 'ingest-prerequisite': {\n 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.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n config: {\n settings: { port: 8080 },\n ingest: {\n map: {\n ip: { key: 'ip' },\n userAgent: { key: 'headers.user-agent' },\n origin: { key: 'headers.origin' },\n },\n },\n },\n },\n },\n transformers: {\n fingerprint: {\n package: '@walkeros/server-transformer-fingerprint',\n config: {\n settings: {\n fields: ['ingest.ip', 'ingest.userAgent'],\n output: 'user.hash',\n length: 16,\n },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'fields-overview': {\n 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).',\n },\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/schemas/index.ts","../src/schemas/settings.ts","../src/examples/index.ts","../src/examples/step.ts","../src/hints.ts"],"sourcesContent":["import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport const settings = zodToSchema(SettingsSchema);\n","import { z } from '@walkeros/core/dev';\n\nconst FieldSchema = z.union([\n z.string().describe('Dot-notation path: \"ingest.ip\", \"event.data.userId\"'),\n z\n .object({\n key: z.string().optional().describe('Source property path'),\n value: z.unknown().optional().describe('Static value or fallback'),\n fn: z\n .string()\n .optional()\n .describe(\n '$code: function receiving { event, ingest } that returns the value. Runs only when no key is set.',\n ),\n })\n .describe('Mapping value config for computed fields'),\n]);\n\nconst InputSchema = z.union([\n FieldSchema,\n z.literal(false).describe('Switch this input off'),\n]);\n\n/**\n * Fingerprint transformer settings schema.\n *\n * Mirrors: types.ts FingerprintSettings\n */\nexport const SettingsSchema = z\n .object({\n salt: z\n .string()\n .optional()\n .describe(\n 'Secret key for the hash (HMAC), e.g. \"$env.FINGERPRINT_SALT\". Without it the hash can be reversed to the IP; a warning is logged.',\n ),\n rotate: z\n .enum(['daily', 'hourly', 'none'])\n .optional()\n .describe(\n 'Rotation window in UTC: the same visitor gets a new hash each window. Default: \"daily\"',\n ),\n ip: InputSchema.optional().describe(\n 'Client IP, anonymized to /24 (IPv4) or /48 (IPv6) before hashing. Default: \"ingest.ip\" when fields is not set',\n ),\n userAgent: InputSchema.optional().describe(\n 'User agent, reduced to browser, major version and OS before hashing. Default: \"ingest.userAgent\" when fields is not set',\n ),\n site: InputSchema.optional().describe(\n 'Site the visitor is on; a URL is reduced to its hostname. Default: \"event.source.url\" when fields is not set',\n ),\n fields: z\n .array(FieldSchema)\n .optional()\n .describe(\n 'Extra fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }. When set, named inputs are only used if set explicitly.',\n ),\n output: z\n .string()\n .optional()\n .describe(\n 'Dot-notation path where hash is stored on the event. Default: \"user.hash\"',\n ),\n length: z\n .number()\n .int()\n .positive()\n .optional()\n .describe(\n 'Truncate hash to this length. Default: full 64-char SHA-256 hash',\n ),\n })\n .describe(\n 'Fingerprint transformer: a cookieless, privacy-friendly visitor hash',\n );\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","export * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\nexport const serverFingerprint: Flow.StepExample = {\n title: 'Server fingerprint',\n description:\n 'Default server fingerprint: the anonymized ingest.ip, the reduced ingest.userAgent and the site, keyed with settings.salt and rotated daily. ' +\n 'Config: { salt: \"$env.FINGERPRINT_SALT\", length: 16 }. Requires source config.ingest. The hash depends on the salt and the day.',\n in: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Getting Started',\n id: '/docs/getting-started',\n },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Getting Started',\n id: '/docs/getting-started',\n },\n user: { hash: '158f99cc06e33fd6' },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n\nexport const missingFields: Flow.StepExample = {\n public: false,\n description:\n 'Graceful handling when ingest is missing: each empty input logs one warning, and the hash is still generated.',\n in: {\n name: 'session start',\n data: { id: 's3ss10n' },\n id: 'ev-1700000601',\n trigger: 'load',\n entity: 'session',\n action: 'start',\n timestamp: 1700000601,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'session start',\n data: { id: 's3ss10n' },\n user: { hash: 'e183220b699c10a8' },\n id: 'ev-1700000601',\n trigger: 'load',\n entity: 'session',\n action: 'start',\n timestamp: 1700000601,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n\nexport const ipAnonymization: Flow.StepExample = {\n title: 'IP anonymization',\n description:\n 'The ip input anonymizes the IP before hashing (IPv4 /24, IPv6 /48), so visitors from the same 10.0.42.* subnet ' +\n 'with the same browser, major version and OS share a hash within a day. No custom fn needed. Config: { salt: \"$env.FINGERPRINT_SALT\", length: 16 }',\n in: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Privacy Policy',\n id: '/privacy',\n },\n id: 'ev-1700000602',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000602,\n source: { type: 'express', platform: 'server' },\n },\n out: [\n [\n 'return',\n {\n event: {\n name: 'page view',\n data: {\n domain: 'www.example.com',\n title: 'Privacy Policy',\n id: '/privacy',\n },\n user: { hash: '44d9154b9a9b3792' },\n id: 'ev-1700000602',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000602,\n source: { type: 'express', platform: 'server' },\n },\n },\n ],\n ],\n};\n","import type { Hint } from '@walkeros/core';\n\nexport const hints: Hint.Hints = {\n 'ingest-prerequisite': {\n text: 'The default inputs ip and userAgent read ingest.ip and ingest.userAgent, so the server source must 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. An input that resolves empty logs one warning (\"Fingerprint input ... resolved empty\") and the hash is still generated, but less distinct. Behind a load balancer the request ip is the balancer; read the client from headers.x-forwarded-for instead.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n next: 'fingerprint',\n config: {\n settings: { port: 8080 },\n ingest: {\n map: {\n ip: { key: 'ip' },\n userAgent: { key: 'headers.user-agent' },\n },\n },\n },\n },\n },\n transformers: {\n fingerprint: {\n package: '@walkeros/server-transformer-fingerprint',\n config: {\n settings: { salt: '$env.FINGERPRINT_SALT' },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'salt-and-rotation': {\n text: 'The salt keys the hash (HMAC), so it cannot be recomputed or reversed to the IP without it. Pass it via $env so it stays out of the flow file, and use one salt per deployment. Without a salt the transformer runs but logs a warning at startup. Rotation is built in (daily by default, UTC windows): do not add a date field for it. For another window, set rotate: \"none\" and add the window as a field. Setting fields switches the default inputs off, so name them too: { rotate: \"none\", ip: \"ingest.ip\", userAgent: \"ingest.userAgent\", site: \"event.source.url\", fields: [{ fn: () => new Date().toISOString().slice(0, 7) }] } rotates monthly. rotate: \"none\" without such a field keeps one hash per visitor indefinitely, which is a privacy decision, not a default.',\n },\n 'site-sources': {\n text: 'site defaults to the hostname of event.source.url, which the walkerOS browser source sets on every event. Events without it (GA4 hits decoded by transformer-ga4, server-side events) need site pointed elsewhere: an ingest path such as ingest.property (a path parameter) or ingest.origin (map it in config.ingest first, e.g. origin: { key: \"headers.origin\" }), or a static { value: \"example.com\" }. site keeps one visitor apart across sites when one collector with one salt serves several of them; a single-site deployment with its own salt is already separated by the salt.',\n },\n 'extra-fields': {\n text: 'fields adds values to the hash as they resolve from { event, ingest }, in order. When fields is set, the named inputs ip, userAgent and site are only used if set explicitly, so an existing fields config keeps its input set. To transform a value before hashing, use fn alone: it receives { event, ingest } and returns the value. A fn next to a key never runs. The IP never needs a custom fn: the ip input anonymizes it (IPv4 /24, IPv6 /48).',\n },\n};\n"],"mappings":"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","names":[]}
|
package/dist/index.d.mts
CHANGED
|
@@ -2,22 +2,56 @@ import { Mapping, Transformer } from '@walkeros/core';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Fingerprint transformer settings.
|
|
5
|
+
*
|
|
6
|
+
* Without `fields`, the hash is built from the named inputs `ip`,
|
|
7
|
+
* `userAgent` and `site`, each reduced before hashing. With `fields`, only
|
|
8
|
+
* the fields are hashed, plus any named input set explicitly.
|
|
5
9
|
*/
|
|
6
10
|
interface FingerprintSettings {
|
|
7
11
|
/**
|
|
8
|
-
*
|
|
12
|
+
* Secret key for the hash (HMAC), e.g. "$env.FINGERPRINT_SALT".
|
|
13
|
+
* Without it the hash can be reversed to the IP by brute force; the
|
|
14
|
+
* transformer then warns once and hashes unkeyed.
|
|
15
|
+
*/
|
|
16
|
+
salt?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Rotation window in UTC. The same visitor gets a new hash each window.
|
|
19
|
+
* @default 'daily'
|
|
20
|
+
*/
|
|
21
|
+
rotate?: 'daily' | 'hourly' | 'none';
|
|
22
|
+
/**
|
|
23
|
+
* Client IP, anonymized to /24 (IPv4) or /48 (IPv6) before hashing.
|
|
24
|
+
* `false` switches it off.
|
|
25
|
+
* @default 'ingest.ip' (only when `fields` is not set)
|
|
26
|
+
*/
|
|
27
|
+
ip?: Mapping.Value | false;
|
|
28
|
+
/**
|
|
29
|
+
* User agent, reduced to browser, major version and OS before hashing.
|
|
30
|
+
* `false` switches it off.
|
|
31
|
+
* @default 'ingest.userAgent' (only when `fields` is not set)
|
|
32
|
+
*/
|
|
33
|
+
userAgent?: Mapping.Value | false;
|
|
34
|
+
/**
|
|
35
|
+
* Site the visitor is on, so one visitor gets different hashes on
|
|
36
|
+
* different sites. A URL is reduced to its hostname. Use an ingest path
|
|
37
|
+
* (e.g. 'ingest.property') or a static `{ value: 'example.com' }` when
|
|
38
|
+
* events carry no page URL. `false` switches it off.
|
|
39
|
+
* @default 'event.source.url' (only when `fields` is not set)
|
|
40
|
+
*/
|
|
41
|
+
site?: Mapping.Value | false;
|
|
42
|
+
/**
|
|
43
|
+
* Extra fields to include in the hash (order matters!), hashed as resolved.
|
|
9
44
|
* Each field is resolved via getMappingValue with source object { event, ingest }.
|
|
10
45
|
*
|
|
11
46
|
* String fields use dot notation:
|
|
12
47
|
* - 'ingest.ip' -> context.ingest.ip
|
|
13
|
-
* - 'ingest.userAgent' -> context.ingest.userAgent
|
|
14
48
|
* - 'event.data.userId' -> event.data.userId
|
|
15
49
|
*
|
|
16
50
|
* Function fields via mapping config:
|
|
17
|
-
* - { fn: () =>
|
|
18
|
-
*
|
|
51
|
+
* - { fn: ({ ingest }) => ingest.region } -> computed value
|
|
52
|
+
* (fn receives { event, ingest }; a fn next to a key never runs)
|
|
19
53
|
*/
|
|
20
|
-
fields
|
|
54
|
+
fields?: Mapping.Value[];
|
|
21
55
|
/**
|
|
22
56
|
* Dot-notation path where hash is stored on the event.
|
|
23
57
|
* @default 'user.hash'
|
|
@@ -31,23 +65,16 @@ interface FingerprintSettings {
|
|
|
31
65
|
}
|
|
32
66
|
|
|
33
67
|
/**
|
|
34
|
-
* Fingerprint transformer -
|
|
68
|
+
* Fingerprint transformer - a cookieless, privacy-friendly visitor hash.
|
|
35
69
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
70
|
+
* Hashes the rotation window, the named inputs (anonymized IP, reduced user
|
|
71
|
+
* agent, site) and any extra fields with an HMAC keyed by the salt, and
|
|
72
|
+
* stores the result at the output path.
|
|
38
73
|
*
|
|
39
74
|
* @example
|
|
40
75
|
* transformerFingerprint({
|
|
41
76
|
* config: {
|
|
42
|
-
* settings: {
|
|
43
|
-
* fields: [
|
|
44
|
-
* 'ingest.ip',
|
|
45
|
-
* 'ingest.userAgent',
|
|
46
|
-
* { fn: () => new Date().getDate() },
|
|
47
|
-
* ],
|
|
48
|
-
* output: 'user.hash',
|
|
49
|
-
* length: 16,
|
|
50
|
-
* },
|
|
77
|
+
* settings: { salt: '$env.FINGERPRINT_SALT' },
|
|
51
78
|
* },
|
|
52
79
|
* })
|
|
53
80
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -2,22 +2,56 @@ import { Mapping, Transformer } from '@walkeros/core';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Fingerprint transformer settings.
|
|
5
|
+
*
|
|
6
|
+
* Without `fields`, the hash is built from the named inputs `ip`,
|
|
7
|
+
* `userAgent` and `site`, each reduced before hashing. With `fields`, only
|
|
8
|
+
* the fields are hashed, plus any named input set explicitly.
|
|
5
9
|
*/
|
|
6
10
|
interface FingerprintSettings {
|
|
7
11
|
/**
|
|
8
|
-
*
|
|
12
|
+
* Secret key for the hash (HMAC), e.g. "$env.FINGERPRINT_SALT".
|
|
13
|
+
* Without it the hash can be reversed to the IP by brute force; the
|
|
14
|
+
* transformer then warns once and hashes unkeyed.
|
|
15
|
+
*/
|
|
16
|
+
salt?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Rotation window in UTC. The same visitor gets a new hash each window.
|
|
19
|
+
* @default 'daily'
|
|
20
|
+
*/
|
|
21
|
+
rotate?: 'daily' | 'hourly' | 'none';
|
|
22
|
+
/**
|
|
23
|
+
* Client IP, anonymized to /24 (IPv4) or /48 (IPv6) before hashing.
|
|
24
|
+
* `false` switches it off.
|
|
25
|
+
* @default 'ingest.ip' (only when `fields` is not set)
|
|
26
|
+
*/
|
|
27
|
+
ip?: Mapping.Value | false;
|
|
28
|
+
/**
|
|
29
|
+
* User agent, reduced to browser, major version and OS before hashing.
|
|
30
|
+
* `false` switches it off.
|
|
31
|
+
* @default 'ingest.userAgent' (only when `fields` is not set)
|
|
32
|
+
*/
|
|
33
|
+
userAgent?: Mapping.Value | false;
|
|
34
|
+
/**
|
|
35
|
+
* Site the visitor is on, so one visitor gets different hashes on
|
|
36
|
+
* different sites. A URL is reduced to its hostname. Use an ingest path
|
|
37
|
+
* (e.g. 'ingest.property') or a static `{ value: 'example.com' }` when
|
|
38
|
+
* events carry no page URL. `false` switches it off.
|
|
39
|
+
* @default 'event.source.url' (only when `fields` is not set)
|
|
40
|
+
*/
|
|
41
|
+
site?: Mapping.Value | false;
|
|
42
|
+
/**
|
|
43
|
+
* Extra fields to include in the hash (order matters!), hashed as resolved.
|
|
9
44
|
* Each field is resolved via getMappingValue with source object { event, ingest }.
|
|
10
45
|
*
|
|
11
46
|
* String fields use dot notation:
|
|
12
47
|
* - 'ingest.ip' -> context.ingest.ip
|
|
13
|
-
* - 'ingest.userAgent' -> context.ingest.userAgent
|
|
14
48
|
* - 'event.data.userId' -> event.data.userId
|
|
15
49
|
*
|
|
16
50
|
* Function fields via mapping config:
|
|
17
|
-
* - { fn: () =>
|
|
18
|
-
*
|
|
51
|
+
* - { fn: ({ ingest }) => ingest.region } -> computed value
|
|
52
|
+
* (fn receives { event, ingest }; a fn next to a key never runs)
|
|
19
53
|
*/
|
|
20
|
-
fields
|
|
54
|
+
fields?: Mapping.Value[];
|
|
21
55
|
/**
|
|
22
56
|
* Dot-notation path where hash is stored on the event.
|
|
23
57
|
* @default 'user.hash'
|
|
@@ -31,23 +65,16 @@ interface FingerprintSettings {
|
|
|
31
65
|
}
|
|
32
66
|
|
|
33
67
|
/**
|
|
34
|
-
* Fingerprint transformer -
|
|
68
|
+
* Fingerprint transformer - a cookieless, privacy-friendly visitor hash.
|
|
35
69
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
70
|
+
* Hashes the rotation window, the named inputs (anonymized IP, reduced user
|
|
71
|
+
* agent, site) and any extra fields with an HMAC keyed by the salt, and
|
|
72
|
+
* stores the result at the output path.
|
|
38
73
|
*
|
|
39
74
|
* @example
|
|
40
75
|
* transformerFingerprint({
|
|
41
76
|
* config: {
|
|
42
|
-
* settings: {
|
|
43
|
-
* fields: [
|
|
44
|
-
* 'ingest.ip',
|
|
45
|
-
* 'ingest.userAgent',
|
|
46
|
-
* { fn: () => new Date().getDate() },
|
|
47
|
-
* ],
|
|
48
|
-
* output: 'user.hash',
|
|
49
|
-
* length: 16,
|
|
50
|
-
* },
|
|
77
|
+
* settings: { salt: '$env.FINGERPRINT_SALT' },
|
|
51
78
|
* },
|
|
52
79
|
* })
|
|
53
80
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e,
|
|
1
|
+
"use strict";var e,t=Object.defineProperty,r=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,s=Object.prototype.hasOwnProperty,o={};((e,r)=>{for(var n in r)t(e,n,{get:r[n],enumerable:!0})})(o,{default:()=>p,transformerFingerprint:()=>p}),module.exports=(e=o,((e,o,a,i)=>{if(o&&"object"==typeof o||"function"==typeof o)for(let c of n(o))s.call(e,c)||c===a||t(e,c,{get:()=>o[c],enumerable:!(i=r(o,c))||i.enumerable});return e})(t({},"__esModule",{value:!0}),e));var a=require("@walkeros/core"),i=require("@walkeros/server-core"),c=["ip","userAgent","site"],u={ip:"ingest.ip",userAgent:"ingest.userAgent",site:"event.source.url"},l={ip:a.anonymizeIP,userAgent:e=>[(0,a.getBrowser)(e),(0,a.getBrowserVersion)(e),(0,a.getOS)(e)].map(e=>e??"").join("/"),site:function(e){try{return new URL(e).hostname||e}catch{return e}}};function g(e){if("none"===e)return"";const t=(new Date).toISOString();return"hourly"===e?t.slice(0,13):t.slice(0,10)}var p=e=>{const{config:t,logger:r}=e,n=t.settings??{},s=n.fields||[],o=n.output||"user.hash",p=n.length,f=n.salt||void 0,h=n.rotate??"daily",y=[];for(const e of c){const t=n[e]??(!n.fields&&u[e]);!1!==t&&y.push({name:e,value:t})}f||r.warn('Fingerprint has no salt: the hash can be reversed to the IP by brute force. Set settings.salt, e.g. "$env.FINGERPRINT_SALT".');const w=new Set;return{type:"fingerprint",config:t,async push(e,t){const{ingest:n,collector:c}=t,u=(0,a.createMappingRoot)(n,e),m=async e=>String(await(0,a.getMappingValue)(u,e,{collector:c})??""),v=await Promise.all(y.map(async({name:e,value:t})=>{const n=await m(t),s=n?l[e](n):"";return s||w.has(e)||(w.add(e),r.warn(`Fingerprint input "${e}" resolved empty, so hashes are less distinct. Check the source's config.ingest or set settings.${e}.`)),s})),d=await Promise.all(s.map(m)),b=[g(h),...v,...d].join(""),P=await(0,i.getHashServer)(b,p,f?{key:f}:{});return{event:(0,a.setByPath)(e,o,P)}}}};//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/transformer.ts"],"sourcesContent":["export { transformerFingerprint } from './transformer';\nexport type { FingerprintSettings } from './types';\n\nexport { transformerFingerprint as default } from './transformer';\n","import type { Mapping, Transformer } from '@walkeros/core';\nimport {
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/transformer.ts"],"sourcesContent":["export { transformerFingerprint } from './transformer';\nexport type { FingerprintSettings } from './types';\n\nexport { transformerFingerprint as default } from './transformer';\n","import type { Mapping, Transformer } from '@walkeros/core';\nimport {\n anonymizeIP,\n createMappingRoot,\n getBrowser,\n getBrowserVersion,\n getMappingValue,\n getOS,\n setByPath,\n} from '@walkeros/core';\nimport { getHashServer } from '@walkeros/server-core';\nimport type { FingerprintSettings } from './types';\n\ntype InputName = 'ip' | 'userAgent' | 'site';\n\ninterface Input {\n name: InputName;\n value: Mapping.Value;\n}\n\nconst inputNames: InputName[] = ['ip', 'userAgent', 'site'];\n\nconst defaultInputs: Record<InputName, Mapping.Value> = {\n ip: 'ingest.ip',\n userAgent: 'ingest.userAgent',\n site: 'event.source.url',\n};\n\n// Each named input is reduced before hashing, so the hash never carries\n// more than the reduced value.\nconst reduceInput: Record<InputName, (value: string) => string> = {\n ip: anonymizeIP,\n userAgent: (ua) =>\n [getBrowser(ua), getBrowserVersion(ua), getOS(ua)]\n .map((part) => part ?? '')\n .join('/'),\n site: toHostname,\n};\n\nfunction toHostname(value: string): string {\n try {\n return new URL(value).hostname || value;\n } catch {\n return value; // Not a URL, e.g. a site id\n }\n}\n\nfunction rotationWindow(rotate: FingerprintSettings['rotate']): string {\n if (rotate === 'none') return '';\n const now = new Date().toISOString();\n return rotate === 'hourly' ? now.slice(0, 13) : now.slice(0, 10);\n}\n\n/**\n * Fingerprint transformer - a cookieless, privacy-friendly visitor hash.\n *\n * Hashes the rotation window, the named inputs (anonymized IP, reduced user\n * agent, site) and any extra fields with an HMAC keyed by the salt, and\n * stores the result at the output path.\n *\n * @example\n * transformerFingerprint({\n * config: {\n * settings: { salt: '$env.FINGERPRINT_SALT' },\n * },\n * })\n */\nexport const transformerFingerprint: Transformer.Init<\n Transformer.Types<FingerprintSettings>\n> = (context) => {\n const { config, logger } = context;\n const settings: Partial<FingerprintSettings> = config.settings ?? {};\n const fields: Mapping.Value[] = settings.fields || [];\n const output: string = settings.output || 'user.hash';\n const length: number | undefined = settings.length;\n const salt: string | undefined = settings.salt || undefined;\n const rotate = settings.rotate ?? 'daily';\n\n // Named inputs default on only without fields, so a fields config keeps its input set\n const inputs: Input[] = [];\n for (const name of inputNames) {\n const value =\n settings[name] ?? (settings.fields ? false : defaultInputs[name]);\n if (value !== false) inputs.push({ name, value });\n }\n\n if (!salt)\n logger.warn(\n 'Fingerprint has no salt: the hash can be reversed to the IP by brute force. Set settings.salt, e.g. \"$env.FINGERPRINT_SALT\".',\n );\n\n const warnedEmpty = new Set<InputName>();\n\n return {\n type: 'fingerprint',\n config: config as Transformer.Config<\n Transformer.Types<FingerprintSettings>\n >,\n\n async push(event, context) {\n const { ingest, collector } = context;\n\n const source = createMappingRoot(ingest, event);\n const resolve = async (value: Mapping.Value) =>\n String((await getMappingValue(source, value, { collector })) ?? '');\n\n const inputValues = await Promise.all(\n inputs.map(async ({ name, value }) => {\n const resolved = await resolve(value);\n const reduced = resolved ? reduceInput[name](resolved) : '';\n\n if (!reduced && !warnedEmpty.has(name)) {\n warnedEmpty.add(name);\n logger.warn(\n `Fingerprint input \"${name}\" resolved empty, so hashes are less distinct. Check the source's config.ingest or set settings.${name}.`,\n );\n }\n\n return reduced;\n }),\n );\n\n const fieldValues = await Promise.all(fields.map(resolve));\n\n // A separator keeps 'ab' + 'c' and 'a' + 'bc' apart\n const input = [\n rotationWindow(rotate),\n ...inputValues,\n ...fieldValues,\n ].join('\\u001f');\n\n const hash = await getHashServer(\n input,\n length,\n salt ? { key: salt } : {},\n );\n return { event: setByPath(event, output, hash) };\n },\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,kBAQO;AACP,yBAA8B;AAU9B,IAAM,aAA0B,CAAC,MAAM,aAAa,MAAM;AAE1D,IAAM,gBAAkD;AAAA,EACtD,IAAI;AAAA,EACJ,WAAW;AAAA,EACX,MAAM;AACR;AAIA,IAAM,cAA4D;AAAA,EAChE,IAAI;AAAA,EACJ,WAAW,CAAC,OACV,KAAC,wBAAW,EAAE,OAAG,+BAAkB,EAAE,OAAG,mBAAM,EAAE,CAAC,EAC9C,IAAI,CAAC,SAAS,QAAQ,EAAE,EACxB,KAAK,GAAG;AAAA,EACb,MAAM;AACR;AAEA,SAAS,WAAW,OAAuB;AACzC,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,EAAE,YAAY;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,eAAe,QAA+C;AACrE,MAAI,WAAW,OAAQ,QAAO;AAC9B,QAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,SAAO,WAAW,WAAW,IAAI,MAAM,GAAG,EAAE,IAAI,IAAI,MAAM,GAAG,EAAE;AACjE;AAgBO,IAAM,yBAET,CAAC,YAAY;AACf,QAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,QAAM,WAAyC,OAAO,YAAY,CAAC;AACnE,QAAM,SAA0B,SAAS,UAAU,CAAC;AACpD,QAAM,SAAiB,SAAS,UAAU;AAC1C,QAAM,SAA6B,SAAS;AAC5C,QAAM,OAA2B,SAAS,QAAQ;AAClD,QAAM,SAAS,SAAS,UAAU;AAGlC,QAAM,SAAkB,CAAC;AACzB,aAAW,QAAQ,YAAY;AAC7B,UAAM,QACJ,SAAS,IAAI,MAAM,SAAS,SAAS,QAAQ,cAAc,IAAI;AACjE,QAAI,UAAU,MAAO,QAAO,KAAK,EAAE,MAAM,MAAM,CAAC;AAAA,EAClD;AAEA,MAAI,CAAC;AACH,WAAO;AAAA,MACL;AAAA,IACF;AAEF,QAAM,cAAc,oBAAI,IAAe;AAEvC,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IAIA,MAAM,KAAK,OAAOA,UAAS;AACzB,YAAM,EAAE,QAAQ,UAAU,IAAIA;AAE9B,YAAM,aAAS,+BAAkB,QAAQ,KAAK;AAC9C,YAAM,UAAU,OAAO,UACrB,OAAQ,UAAM,6BAAgB,QAAQ,OAAO,EAAE,UAAU,CAAC,KAAM,EAAE;AAEpE,YAAM,cAAc,MAAM,QAAQ;AAAA,QAChC,OAAO,IAAI,OAAO,EAAE,MAAM,MAAM,MAAM;AACpC,gBAAM,WAAW,MAAM,QAAQ,KAAK;AACpC,gBAAM,UAAU,WAAW,YAAY,IAAI,EAAE,QAAQ,IAAI;AAEzD,cAAI,CAAC,WAAW,CAAC,YAAY,IAAI,IAAI,GAAG;AACtC,wBAAY,IAAI,IAAI;AACpB,mBAAO;AAAA,cACL,sBAAsB,IAAI,mGAAmG,IAAI;AAAA,YACnI;AAAA,UACF;AAEA,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAEA,YAAM,cAAc,MAAM,QAAQ,IAAI,OAAO,IAAI,OAAO,CAAC;AAGzD,YAAM,QAAQ;AAAA,QACZ,eAAe,MAAM;AAAA,QACrB,GAAG;AAAA,QACH,GAAG;AAAA,MACL,EAAE,KAAK,GAAQ;AAEf,YAAM,OAAO,UAAM;AAAA,QACjB;AAAA,QACA;AAAA,QACA,OAAO,EAAE,KAAK,KAAK,IAAI,CAAC;AAAA,MAC1B;AACA,aAAO,EAAE,WAAO,uBAAU,OAAO,QAAQ,IAAI,EAAE;AAAA,IACjD;AAAA,EACF;AACF;","names":["context"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{anonymizeIP as e,createMappingRoot as t,getBrowser as n,getBrowserVersion as r,getMappingValue as s,getOS as i,setByPath as o}from"@walkeros/core";import{getHashServer as a}from"@walkeros/server-core";var c=["ip","userAgent","site"],l={ip:"ingest.ip",userAgent:"ingest.userAgent",site:"event.source.url"},u={ip:e,userAgent:e=>[n(e),r(e),i(e)].map(e=>e??"").join("/"),site:function(e){try{return new URL(e).hostname||e}catch{return e}}};function g(e){if("none"===e)return"";const t=(new Date).toISOString();return"hourly"===e?t.slice(0,13):t.slice(0,10)}var p=e=>{const{config:n,logger:r}=e,i=n.settings??{},p=i.fields||[],h=i.output||"user.hash",f=i.length,m=i.salt||void 0,w=i.rotate??"daily",v=[];for(const e of c){const t=i[e]??(!i.fields&&l[e]);!1!==t&&v.push({name:e,value:t})}m||r.warn('Fingerprint has no salt: the hash can be reversed to the IP by brute force. Set settings.salt, e.g. "$env.FINGERPRINT_SALT".');const d=new Set;return{type:"fingerprint",config:n,async push(e,n){const{ingest:i,collector:c}=n,l=t(i,e),y=async e=>String(await s(l,e,{collector:c})??""),S=await Promise.all(v.map(async({name:e,value:t})=>{const n=await y(t),s=n?u[e](n):"";return s||d.has(e)||(d.add(e),r.warn(`Fingerprint input "${e}" resolved empty, so hashes are less distinct. Check the source's config.ingest or set settings.${e}.`)),s})),A=await Promise.all(p.map(y)),k=[g(w),...S,...A].join(""),I=await a(k,f,m?{key:m}:{});return{event:o(e,h,I)}}}};export{p as default,p as transformerFingerprint};//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/transformer.ts"],"sourcesContent":["import type { Mapping, Transformer } from '@walkeros/core';\nimport {
|
|
1
|
+
{"version":3,"sources":["../src/transformer.ts"],"sourcesContent":["import type { Mapping, Transformer } from '@walkeros/core';\nimport {\n anonymizeIP,\n createMappingRoot,\n getBrowser,\n getBrowserVersion,\n getMappingValue,\n getOS,\n setByPath,\n} from '@walkeros/core';\nimport { getHashServer } from '@walkeros/server-core';\nimport type { FingerprintSettings } from './types';\n\ntype InputName = 'ip' | 'userAgent' | 'site';\n\ninterface Input {\n name: InputName;\n value: Mapping.Value;\n}\n\nconst inputNames: InputName[] = ['ip', 'userAgent', 'site'];\n\nconst defaultInputs: Record<InputName, Mapping.Value> = {\n ip: 'ingest.ip',\n userAgent: 'ingest.userAgent',\n site: 'event.source.url',\n};\n\n// Each named input is reduced before hashing, so the hash never carries\n// more than the reduced value.\nconst reduceInput: Record<InputName, (value: string) => string> = {\n ip: anonymizeIP,\n userAgent: (ua) =>\n [getBrowser(ua), getBrowserVersion(ua), getOS(ua)]\n .map((part) => part ?? '')\n .join('/'),\n site: toHostname,\n};\n\nfunction toHostname(value: string): string {\n try {\n return new URL(value).hostname || value;\n } catch {\n return value; // Not a URL, e.g. a site id\n }\n}\n\nfunction rotationWindow(rotate: FingerprintSettings['rotate']): string {\n if (rotate === 'none') return '';\n const now = new Date().toISOString();\n return rotate === 'hourly' ? now.slice(0, 13) : now.slice(0, 10);\n}\n\n/**\n * Fingerprint transformer - a cookieless, privacy-friendly visitor hash.\n *\n * Hashes the rotation window, the named inputs (anonymized IP, reduced user\n * agent, site) and any extra fields with an HMAC keyed by the salt, and\n * stores the result at the output path.\n *\n * @example\n * transformerFingerprint({\n * config: {\n * settings: { salt: '$env.FINGERPRINT_SALT' },\n * },\n * })\n */\nexport const transformerFingerprint: Transformer.Init<\n Transformer.Types<FingerprintSettings>\n> = (context) => {\n const { config, logger } = context;\n const settings: Partial<FingerprintSettings> = config.settings ?? {};\n const fields: Mapping.Value[] = settings.fields || [];\n const output: string = settings.output || 'user.hash';\n const length: number | undefined = settings.length;\n const salt: string | undefined = settings.salt || undefined;\n const rotate = settings.rotate ?? 'daily';\n\n // Named inputs default on only without fields, so a fields config keeps its input set\n const inputs: Input[] = [];\n for (const name of inputNames) {\n const value =\n settings[name] ?? (settings.fields ? false : defaultInputs[name]);\n if (value !== false) inputs.push({ name, value });\n }\n\n if (!salt)\n logger.warn(\n 'Fingerprint has no salt: the hash can be reversed to the IP by brute force. Set settings.salt, e.g. \"$env.FINGERPRINT_SALT\".',\n );\n\n const warnedEmpty = new Set<InputName>();\n\n return {\n type: 'fingerprint',\n config: config as Transformer.Config<\n Transformer.Types<FingerprintSettings>\n >,\n\n async push(event, context) {\n const { ingest, collector } = context;\n\n const source = createMappingRoot(ingest, event);\n const resolve = async (value: Mapping.Value) =>\n String((await getMappingValue(source, value, { collector })) ?? '');\n\n const inputValues = await Promise.all(\n inputs.map(async ({ name, value }) => {\n const resolved = await resolve(value);\n const reduced = resolved ? reduceInput[name](resolved) : '';\n\n if (!reduced && !warnedEmpty.has(name)) {\n warnedEmpty.add(name);\n logger.warn(\n `Fingerprint input \"${name}\" resolved empty, so hashes are less distinct. Check the source's config.ingest or set settings.${name}.`,\n );\n }\n\n return reduced;\n }),\n );\n\n const fieldValues = await Promise.all(fields.map(resolve));\n\n // A separator keeps 'ab' + 'c' and 'a' + 'bc' apart\n const input = [\n rotationWindow(rotate),\n ...inputValues,\n ...fieldValues,\n ].join('\\u001f');\n\n const hash = await getHashServer(\n input,\n length,\n salt ? { key: salt } : {},\n );\n return { event: setByPath(event, output, hash) };\n },\n };\n};\n"],"mappings":";AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAU9B,IAAM,aAA0B,CAAC,MAAM,aAAa,MAAM;AAE1D,IAAM,gBAAkD;AAAA,EACtD,IAAI;AAAA,EACJ,WAAW;AAAA,EACX,MAAM;AACR;AAIA,IAAM,cAA4D;AAAA,EAChE,IAAI;AAAA,EACJ,WAAW,CAAC,OACV,CAAC,WAAW,EAAE,GAAG,kBAAkB,EAAE,GAAG,MAAM,EAAE,CAAC,EAC9C,IAAI,CAAC,SAAS,QAAQ,EAAE,EACxB,KAAK,GAAG;AAAA,EACb,MAAM;AACR;AAEA,SAAS,WAAW,OAAuB;AACzC,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,EAAE,YAAY;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,eAAe,QAA+C;AACrE,MAAI,WAAW,OAAQ,QAAO;AAC9B,QAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,SAAO,WAAW,WAAW,IAAI,MAAM,GAAG,EAAE,IAAI,IAAI,MAAM,GAAG,EAAE;AACjE;AAgBO,IAAM,yBAET,CAAC,YAAY;AACf,QAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,QAAM,WAAyC,OAAO,YAAY,CAAC;AACnE,QAAM,SAA0B,SAAS,UAAU,CAAC;AACpD,QAAM,SAAiB,SAAS,UAAU;AAC1C,QAAM,SAA6B,SAAS;AAC5C,QAAM,OAA2B,SAAS,QAAQ;AAClD,QAAM,SAAS,SAAS,UAAU;AAGlC,QAAM,SAAkB,CAAC;AACzB,aAAW,QAAQ,YAAY;AAC7B,UAAM,QACJ,SAAS,IAAI,MAAM,SAAS,SAAS,QAAQ,cAAc,IAAI;AACjE,QAAI,UAAU,MAAO,QAAO,KAAK,EAAE,MAAM,MAAM,CAAC;AAAA,EAClD;AAEA,MAAI,CAAC;AACH,WAAO;AAAA,MACL;AAAA,IACF;AAEF,QAAM,cAAc,oBAAI,IAAe;AAEvC,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IAIA,MAAM,KAAK,OAAOA,UAAS;AACzB,YAAM,EAAE,QAAQ,UAAU,IAAIA;AAE9B,YAAM,SAAS,kBAAkB,QAAQ,KAAK;AAC9C,YAAM,UAAU,OAAO,UACrB,OAAQ,MAAM,gBAAgB,QAAQ,OAAO,EAAE,UAAU,CAAC,KAAM,EAAE;AAEpE,YAAM,cAAc,MAAM,QAAQ;AAAA,QAChC,OAAO,IAAI,OAAO,EAAE,MAAM,MAAM,MAAM;AACpC,gBAAM,WAAW,MAAM,QAAQ,KAAK;AACpC,gBAAM,UAAU,WAAW,YAAY,IAAI,EAAE,QAAQ,IAAI;AAEzD,cAAI,CAAC,WAAW,CAAC,YAAY,IAAI,IAAI,GAAG;AACtC,wBAAY,IAAI,IAAI;AACpB,mBAAO;AAAA,cACL,sBAAsB,IAAI,mGAAmG,IAAI;AAAA,YACnI;AAAA,UACF;AAEA,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAEA,YAAM,cAAc,MAAM,QAAQ,IAAI,OAAO,IAAI,OAAO,CAAC;AAGzD,YAAM,QAAQ;AAAA,QACZ,eAAe,MAAM;AAAA,QACrB,GAAG;AAAA,QACH,GAAG;AAAA,MACL,EAAE,KAAK,GAAQ;AAEf,YAAM,OAAO,MAAM;AAAA,QACjB;AAAA,QACA;AAAA,QACA,OAAO,EAAE,KAAK,KAAK,IAAI,CAAC;AAAA,MAC1B;AACA,aAAO,EAAE,OAAO,UAAU,OAAO,QAAQ,IAAI,EAAE;AAAA,IACjD;AAAA,EACF;AACF;","names":["context"]}
|
package/dist/walkerOS.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$meta": {
|
|
3
3
|
"package": "@walkeros/server-transformer-fingerprint",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.7.0-next-1790187973605",
|
|
5
5
|
"type": "transformer",
|
|
6
6
|
"platform": [
|
|
7
7
|
"server"
|
|
@@ -14,7 +14,129 @@
|
|
|
14
14
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
15
15
|
"type": "object",
|
|
16
16
|
"properties": {
|
|
17
|
+
"salt": {
|
|
18
|
+
"description": "Secret key for the hash (HMAC), e.g. \"$env.FINGERPRINT_SALT\". Without it the hash can be reversed to the IP; a warning is logged.",
|
|
19
|
+
"type": "string"
|
|
20
|
+
},
|
|
21
|
+
"rotate": {
|
|
22
|
+
"description": "Rotation window in UTC: the same visitor gets a new hash each window. Default: \"daily\"",
|
|
23
|
+
"type": "string",
|
|
24
|
+
"enum": [
|
|
25
|
+
"daily",
|
|
26
|
+
"hourly",
|
|
27
|
+
"none"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"ip": {
|
|
31
|
+
"description": "Client IP, anonymized to /24 (IPv4) or /48 (IPv6) before hashing. Default: \"ingest.ip\" when fields is not set",
|
|
32
|
+
"anyOf": [
|
|
33
|
+
{
|
|
34
|
+
"anyOf": [
|
|
35
|
+
{
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Dot-notation path: \"ingest.ip\", \"event.data.userId\""
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"key": {
|
|
43
|
+
"description": "Source property path",
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"value": {
|
|
47
|
+
"description": "Static value or fallback"
|
|
48
|
+
},
|
|
49
|
+
"fn": {
|
|
50
|
+
"description": "$code: function receiving { event, ingest } that returns the value. Runs only when no key is set.",
|
|
51
|
+
"type": "string"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"description": "Mapping value config for computed fields"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"const": false,
|
|
62
|
+
"description": "Switch this input off"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"userAgent": {
|
|
67
|
+
"description": "User agent, reduced to browser, major version and OS before hashing. Default: \"ingest.userAgent\" when fields is not set",
|
|
68
|
+
"anyOf": [
|
|
69
|
+
{
|
|
70
|
+
"anyOf": [
|
|
71
|
+
{
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Dot-notation path: \"ingest.ip\", \"event.data.userId\""
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"type": "object",
|
|
77
|
+
"properties": {
|
|
78
|
+
"key": {
|
|
79
|
+
"description": "Source property path",
|
|
80
|
+
"type": "string"
|
|
81
|
+
},
|
|
82
|
+
"value": {
|
|
83
|
+
"description": "Static value or fallback"
|
|
84
|
+
},
|
|
85
|
+
"fn": {
|
|
86
|
+
"description": "$code: function receiving { event, ingest } that returns the value. Runs only when no key is set.",
|
|
87
|
+
"type": "string"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"additionalProperties": false,
|
|
91
|
+
"description": "Mapping value config for computed fields"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"type": "boolean",
|
|
97
|
+
"const": false,
|
|
98
|
+
"description": "Switch this input off"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"site": {
|
|
103
|
+
"description": "Site the visitor is on; a URL is reduced to its hostname. Default: \"event.source.url\" when fields is not set",
|
|
104
|
+
"anyOf": [
|
|
105
|
+
{
|
|
106
|
+
"anyOf": [
|
|
107
|
+
{
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Dot-notation path: \"ingest.ip\", \"event.data.userId\""
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"type": "object",
|
|
113
|
+
"properties": {
|
|
114
|
+
"key": {
|
|
115
|
+
"description": "Source property path",
|
|
116
|
+
"type": "string"
|
|
117
|
+
},
|
|
118
|
+
"value": {
|
|
119
|
+
"description": "Static value or fallback"
|
|
120
|
+
},
|
|
121
|
+
"fn": {
|
|
122
|
+
"description": "$code: function receiving { event, ingest } that returns the value. Runs only when no key is set.",
|
|
123
|
+
"type": "string"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"additionalProperties": false,
|
|
127
|
+
"description": "Mapping value config for computed fields"
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"type": "boolean",
|
|
133
|
+
"const": false,
|
|
134
|
+
"description": "Switch this input off"
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
},
|
|
17
138
|
"fields": {
|
|
139
|
+
"description": "Extra fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }. When set, named inputs are only used if set explicitly.",
|
|
18
140
|
"type": "array",
|
|
19
141
|
"items": {
|
|
20
142
|
"anyOf": [
|
|
@@ -33,7 +155,7 @@
|
|
|
33
155
|
"description": "Static value or fallback"
|
|
34
156
|
},
|
|
35
157
|
"fn": {
|
|
36
|
-
"description": "$code: function
|
|
158
|
+
"description": "$code: function receiving { event, ingest } that returns the value. Runs only when no key is set.",
|
|
37
159
|
"type": "string"
|
|
38
160
|
}
|
|
39
161
|
},
|
|
@@ -41,8 +163,7 @@
|
|
|
41
163
|
"description": "Mapping value config for computed fields"
|
|
42
164
|
}
|
|
43
165
|
]
|
|
44
|
-
}
|
|
45
|
-
"description": "Fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }."
|
|
166
|
+
}
|
|
46
167
|
},
|
|
47
168
|
"output": {
|
|
48
169
|
"description": "Dot-notation path where hash is stored on the event. Default: \"user.hash\"",
|
|
@@ -55,18 +176,15 @@
|
|
|
55
176
|
"maximum": 9007199254740991
|
|
56
177
|
}
|
|
57
178
|
},
|
|
58
|
-
"required": [
|
|
59
|
-
"fields"
|
|
60
|
-
],
|
|
61
179
|
"additionalProperties": false,
|
|
62
|
-
"description": "Fingerprint transformer:
|
|
180
|
+
"description": "Fingerprint transformer: a cookieless, privacy-friendly visitor hash"
|
|
63
181
|
}
|
|
64
182
|
},
|
|
65
183
|
"examples": {
|
|
66
184
|
"step": {
|
|
67
185
|
"ipAnonymization": {
|
|
68
186
|
"title": "IP anonymization",
|
|
69
|
-
"description": "
|
|
187
|
+
"description": "The ip input anonymizes the IP before hashing (IPv4 /24, IPv6 /48), so visitors from the same 10.0.42.* subnet with the same browser, major version and OS share a hash within a day. No custom fn needed. Config: { salt: \"$env.FINGERPRINT_SALT\", length: 16 }",
|
|
70
188
|
"in": {
|
|
71
189
|
"name": "page view",
|
|
72
190
|
"data": {
|
|
@@ -114,7 +232,7 @@
|
|
|
114
232
|
},
|
|
115
233
|
"missingFields": {
|
|
116
234
|
"public": false,
|
|
117
|
-
"description": "Graceful handling when ingest is missing
|
|
235
|
+
"description": "Graceful handling when ingest is missing: each empty input logs one warning, and the hash is still generated.",
|
|
118
236
|
"in": {
|
|
119
237
|
"name": "session start",
|
|
120
238
|
"data": {
|
|
@@ -158,7 +276,7 @@
|
|
|
158
276
|
},
|
|
159
277
|
"serverFingerprint": {
|
|
160
278
|
"title": "Server fingerprint",
|
|
161
|
-
"description": "
|
|
279
|
+
"description": "Default server fingerprint: the anonymized ingest.ip, the reduced ingest.userAgent and the site, keyed with settings.salt and rotated daily. Config: { salt: \"$env.FINGERPRINT_SALT\", length: 16 }. Requires source config.ingest. The hash depends on the salt and the day.",
|
|
162
280
|
"in": {
|
|
163
281
|
"name": "page view",
|
|
164
282
|
"data": {
|
|
@@ -208,16 +326,22 @@
|
|
|
208
326
|
},
|
|
209
327
|
"hints": {
|
|
210
328
|
"ingest-prerequisite": {
|
|
211
|
-
"text": "
|
|
329
|
+
"text": "The default inputs ip and userAgent read ingest.ip and ingest.userAgent, so the server source must 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. An input that resolves empty logs one warning (\"Fingerprint input ... resolved empty\") and the hash is still generated, but less distinct. Behind a load balancer the request ip is the balancer; read the client from headers.x-forwarded-for instead.",
|
|
212
330
|
"code": [
|
|
213
331
|
{
|
|
214
332
|
"lang": "json",
|
|
215
|
-
"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 }
|
|
333
|
+
"code": "{\n \"sources\": {\n \"express\": {\n \"package\": \"@walkeros/server-source-express\",\n \"next\": \"fingerprint\",\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 }\n }\n }\n }\n },\n \"transformers\": {\n \"fingerprint\": {\n \"package\": \"@walkeros/server-transformer-fingerprint\",\n \"config\": {\n \"settings\": {\n \"salt\": \"$env.FINGERPRINT_SALT\"\n }\n }\n }\n }\n}"
|
|
216
334
|
}
|
|
217
335
|
]
|
|
218
336
|
},
|
|
219
|
-
"
|
|
220
|
-
"text": "
|
|
337
|
+
"salt-and-rotation": {
|
|
338
|
+
"text": "The salt keys the hash (HMAC), so it cannot be recomputed or reversed to the IP without it. Pass it via $env so it stays out of the flow file, and use one salt per deployment. Without a salt the transformer runs but logs a warning at startup. Rotation is built in (daily by default, UTC windows): do not add a date field for it. For another window, set rotate: \"none\" and add the window as a field. Setting fields switches the default inputs off, so name them too: { rotate: \"none\", ip: \"ingest.ip\", userAgent: \"ingest.userAgent\", site: \"event.source.url\", fields: [{ fn: () => new Date().toISOString().slice(0, 7) }] } rotates monthly. rotate: \"none\" without such a field keeps one hash per visitor indefinitely, which is a privacy decision, not a default."
|
|
339
|
+
},
|
|
340
|
+
"site-sources": {
|
|
341
|
+
"text": "site defaults to the hostname of event.source.url, which the walkerOS browser source sets on every event. Events without it (GA4 hits decoded by transformer-ga4, server-side events) need site pointed elsewhere: an ingest path such as ingest.property (a path parameter) or ingest.origin (map it in config.ingest first, e.g. origin: { key: \"headers.origin\" }), or a static { value: \"example.com\" }. site keeps one visitor apart across sites when one collector with one salt serves several of them; a single-site deployment with its own salt is already separated by the salt."
|
|
342
|
+
},
|
|
343
|
+
"extra-fields": {
|
|
344
|
+
"text": "fields adds values to the hash as they resolve from { event, ingest }, in order. When fields is set, the named inputs ip, userAgent and site are only used if set explicitly, so an existing fields config keeps its input set. To transform a value before hashing, use fn alone: it receives { event, ingest } and returns the value. A fn next to a key never runs. The IP never needs a custom fn: the ip input anonymizes it (IPv4 /24, IPv6 /48)."
|
|
221
345
|
}
|
|
222
346
|
}
|
|
223
347
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walkeros/server-transformer-fingerprint",
|
|
3
3
|
"description": "Fingerprint transformer for walkerOS server - hash configurable fields for session continuity",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.7.0-next-1790187973605",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.mjs",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"update": "npx npm-check-updates -u && npm update"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@walkeros/core": "4.
|
|
31
|
-
"@walkeros/server-core": "4.
|
|
30
|
+
"@walkeros/core": "4.7.0-next-1790187973605",
|
|
31
|
+
"@walkeros/server-core": "4.7.0-next-1790187973605"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"url": "git+https://github.com/elbwalker/walkerOS.git",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
}
|
|
61
61
|
],
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@walkeros/core": "4.
|
|
64
|
-
"@walkeros/server-core": "4.
|
|
63
|
+
"@walkeros/core": "4.7.0-next-1790187973605",
|
|
64
|
+
"@walkeros/server-core": "4.7.0-next-1790187973605"
|
|
65
65
|
}
|
|
66
66
|
}
|