@prooftell/node-red-prooftell 0.0.0-stage → 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ProofTell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,108 @@
1
- # Temporary Holding Version
1
+ # @prooftell/node-red-prooftell
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The [ProofTell](https://prooftell.com/integrations?utm_source=node-red&utm_medium=integration) node for
4
+ [Node-RED](https://nodered.org/): look up a phone number, verify an email address, look up an IP address, or assess
5
+ the risk of an identity, with one API key. The node calls ProofTell's public API (https://prooftell.com/docs) and
6
+ gives the answer back unchanged; nothing is stored on the way.
7
+
8
+ ## Installation
9
+
10
+ In the editor: Menu → Manage palette → Install, search for `@prooftell/node-red-prooftell`. Or, in the Node-RED
11
+ user folder (`~/.node-red`): `npm install @prooftell/node-red-prooftell`, then restart Node-RED. It needs Node-RED
12
+ 4 or later on Node.js 18 or later.
13
+
14
+ ## The connection
15
+
16
+ Create a key in the ProofTell dashboard (Developers → Integrations), named Node-RED, and paste it into a **ProofTell**
17
+ connection (the node's Connection field, then the pencil). A `pt_test_` key is the free sandbox: simulated,
18
+ deterministic answers. A `pt_live_` key spends the account's balance. The connection's label says sandbox or live
19
+ from the key. The key is kept in Node-RED's credentials, not in the flow: an exported flow does not carry it.
20
+
21
+ ## The node
22
+
23
+ One **ProofTell** node, in the palette's network section; its Action is one of the four checks. Each check is
24
+ charged per check from your balance; the sandbox is free.
25
+
26
+ | Action | Call | Fields | What comes back in `msg.payload` |
27
+ |---|---|---|---|
28
+ | Look up a phone number | `POST /v1/phone` | Phone (required); Country, for a number in national format; Level: Basic, Line type (the default), Current carrier, Line status, Fraud score, each including the ones before it | `valid`, `line_type`, `e164`, the formats, `country_code`, `original_carrier`, the level's block, `cost` |
29
+ | Verify an email address | `POST /v1/email` | Email (required); Timeout, 1000 to 45000 ms | `verdict` (deliverable, undeliverable, risky, unknown), `reason`, `flags`, `did_you_mean`, `cost` |
30
+ | Look up an IP address | `POST /v1/ip` | IP address (required) | `is_tor`, `is_vpn`, `is_datacenter`, `is_anonymous`, `country_code`, `city`, `asn`, `as_org`, `cost` |
31
+ | Assess the risk of an identity | `POST /v1/assess` | Any of Phone, Email, IP address (one at least); Country; Reference | `score` (0–100), `verdict` (allow, review, deny), `reasons[]`, `signals.*.result`, `cost` |
32
+
33
+ Each field is a Node-RED typed input: by default it reads `msg.payload.phone`, `msg.payload.email` or
34
+ `msg.payload.ip`, and it can instead hold a fixed value, another message property, a flow or global value, an
35
+ environment variable or a JSONata expression. For the three single checks, a string `msg.payload` is the value to
36
+ check when the field gives none: an inject node with `+447700900123` is enough. A field left empty is not sent.
37
+ **Answer to** sets where the answer goes (`msg.payload` by default; `msg.prooftell` keeps the payload as it was).
38
+ The node's status shows the result in a few words (`mobile`, `deliverable`, `Tor`, `allow, 15`), never the input.
39
+
40
+ ### Errors
41
+
42
+ A failed check stops the message and reports the API's own message as the node's error, which a **Catch** node
43
+ receives, with `msg.statusCode` set: 401 for a wrong key; 402 `insufficient_balance` when the balance cannot cover
44
+ the check, the message naming what the check needs, what the balance holds and what to top up; 429 `rate_limited`
45
+ above the key's rate; 503 when a source did not answer, a moment to wait. A check the API would refuse — a phone
46
+ lookup with no number, an assessment with nothing to assess — is refused before any request, with a sentence that
47
+ says what to give.
48
+
49
+ ## Examples
50
+
51
+ Menu → Import → Examples → @prooftell/node-red-prooftell has a flow per action: an inject node with the sandbox's
52
+ input, the ProofTell node, a debug node for the answer, and a Catch node with a debug node for an error. Open the
53
+ connection, paste a sandbox key, deploy, and click the inject node. "Assess the risk of an identity" answers
54
+ `score` 15, `verdict` `allow`, one reason, `geo_mismatch` (a GB number with a US address), and `cost` `0.000000`.
55
+
56
+ ## The sandbox
57
+
58
+ With a `pt_test_` key every call is simulated, deterministic and free, and every documented answer can be reached:
59
+
60
+ - `+447700900123` is a GB mobile; `+15005550002` is VoIP (fraud score: medium); `+15005550003` is not valid;
61
+ `+447700900005` is switched off (line status: not present).
62
+ - `jane@example.com` is deliverable; `undeliverable@sandbox.prooftell.com`, `disposable@…`, `catchall@…`,
63
+ `risky@…`, `role@…` and `greylisted@…` give that result.
64
+ - `203.0.113.1` is a clean US address; `203.0.113.66` is Tor, `203.0.113.77` a VPN, `203.0.113.99` a datacenter.
65
+ - An assessment of `+447700900123` with `203.0.113.1` is a `geo_mismatch`.
66
+
67
+ The full list is in the API reference: https://prooftell.com/docs.
68
+
69
+ ## Development
70
+
71
+ Plain JavaScript, no build step: `nodes/prooftell.js` (the runtime), `nodes/prooftell.html` (the editor and the
72
+ help), `nodes/lib/client.js` (the requests, tested on their own), `examples/` (a flow per action).
73
+
74
+ ```sh
75
+ npm install
76
+ npm test # the requests with a fake fetch, then the nodes in a Node-RED runtime against a local stand-in for the
77
+ # API; with PROOFTELL_TEST_KEY set to a sandbox key (read into the command, never written down),
78
+ # the four actions against staging as well
79
+ ```
80
+
81
+ To run it in a Node-RED of its own: `npm install <this folder>` in a scratch user folder, then
82
+ `node_modules/.bin/node-red --userDir <that folder>` from this folder (Node-RED is a dev dependency) and open
83
+ http://127.0.0.1:1880. `PROOFTELL_BASE_URL` points the node at another API (`https://api.prooftell.dev`, staging); it
84
+ is read from the environment only.
85
+
86
+ ## Releasing
87
+
88
+ The package is published by GitHub Actions with npm provenance, from `.github/workflows/release-node-red.yml` at the
89
+ root of [prooftell/integrations](https://github.com/prooftell/integrations).
90
+
91
+ 1. Once: the repository secret `NPM_TOKEN` publishes the first release (the package does not exist on npm before
92
+ it); then npm's trusted publishing — on npmjs.com, the package's settings, Publish access → Trusted Publishers →
93
+ GitHub Actions, owner `prooftell`, repository `integrations`, workflow `release-node-red.yml`.
94
+ 2. For each release: bump `version` in `package.json`, commit, then tag that commit `node-red-v<version>`
95
+ (`git tag node-red-v1.0.0 && git push origin node-red-v1.0.0`). The workflow tests, checks that the tag names the
96
+ version, and publishes.
97
+ 3. For the Flow Library, once: on https://flows.nodered.org, signed in with GitHub, + → Node, the package's name. It
98
+ is listed at once; a new version is picked up from its page's "request refresh" link, or by submitting again.
99
+
100
+ ## Resources
101
+
102
+ - ProofTell: https://prooftell.com/integrations?utm_source=node-red&utm_medium=integration
103
+ - The API reference: https://prooftell.com/docs
104
+ - Support: support@prooftell.com
105
+
106
+ ## License
107
+
108
+ MIT.
@@ -0,0 +1,119 @@
1
+ [
2
+ {
3
+ "id": "tassess",
4
+ "type": "tab",
5
+ "label": "Assess the risk of an identity",
6
+ "disabled": false,
7
+ "info": "A GB number with a US address: allow, with a geo_mismatch reason. Open the ProofTell connection and paste a sandbox key (pt_test_…), created in the dashboard under Developers → Integrations. Then click the inject node."
8
+ },
9
+ {
10
+ "id": "iassess",
11
+ "type": "inject",
12
+ "z": "tassess",
13
+ "name": "The sandbox's input",
14
+ "props": [
15
+ {
16
+ "p": "payload"
17
+ }
18
+ ],
19
+ "repeat": "",
20
+ "crontab": "",
21
+ "once": false,
22
+ "onceDelay": 0.1,
23
+ "topic": "",
24
+ "payload": "{\"phone\":\"+447700900123\",\"ip\":\"203.0.113.1\"}",
25
+ "payloadType": "json",
26
+ "x": 160,
27
+ "y": 100,
28
+ "wires": [
29
+ [
30
+ "passess"
31
+ ]
32
+ ]
33
+ },
34
+ {
35
+ "id": "passess",
36
+ "type": "prooftell",
37
+ "z": "tassess",
38
+ "name": "",
39
+ "connection": "c0nf1g0000000001",
40
+ "action": "assess",
41
+ "phone": "payload.phone",
42
+ "phoneType": "msg",
43
+ "email": "payload.email",
44
+ "emailType": "msg",
45
+ "ip": "payload.ip",
46
+ "ipType": "msg",
47
+ "country": "",
48
+ "countryType": "str",
49
+ "level": "line_type",
50
+ "timeout": "",
51
+ "reference": "",
52
+ "referenceType": "str",
53
+ "output": "payload",
54
+ "x": 420,
55
+ "y": 100,
56
+ "wires": [
57
+ [
58
+ "dassess"
59
+ ]
60
+ ]
61
+ },
62
+ {
63
+ "id": "dassess",
64
+ "type": "debug",
65
+ "z": "tassess",
66
+ "name": "The answer",
67
+ "active": true,
68
+ "tosidebar": true,
69
+ "console": false,
70
+ "tostatus": false,
71
+ "complete": "payload",
72
+ "targetType": "msg",
73
+ "statusVal": "",
74
+ "statusType": "auto",
75
+ "x": 680,
76
+ "y": 100,
77
+ "wires": []
78
+ },
79
+ {
80
+ "id": "kassess",
81
+ "type": "catch",
82
+ "z": "tassess",
83
+ "name": "A failed check",
84
+ "scope": [
85
+ "passess"
86
+ ],
87
+ "uncaught": false,
88
+ "x": 420,
89
+ "y": 160,
90
+ "wires": [
91
+ [
92
+ "eassess"
93
+ ]
94
+ ]
95
+ },
96
+ {
97
+ "id": "eassess",
98
+ "type": "debug",
99
+ "z": "tassess",
100
+ "name": "The error",
101
+ "active": true,
102
+ "tosidebar": true,
103
+ "console": false,
104
+ "tostatus": false,
105
+ "complete": "error",
106
+ "targetType": "msg",
107
+ "statusVal": "",
108
+ "statusType": "auto",
109
+ "x": 680,
110
+ "y": 160,
111
+ "wires": []
112
+ },
113
+ {
114
+ "id": "c0nf1g0000000001",
115
+ "type": "prooftell-config",
116
+ "name": "ProofTell sandbox",
117
+ "mode": "sandbox"
118
+ }
119
+ ]
@@ -0,0 +1,119 @@
1
+ [
2
+ {
3
+ "id": "tphone",
4
+ "type": "tab",
5
+ "label": "Look up a phone number",
6
+ "disabled": false,
7
+ "info": "A GB mobile, at the line type level. Open the ProofTell connection and paste a sandbox key (pt_test_…), created in the dashboard under Developers → Integrations. Then click the inject node."
8
+ },
9
+ {
10
+ "id": "iphone",
11
+ "type": "inject",
12
+ "z": "tphone",
13
+ "name": "The sandbox's input",
14
+ "props": [
15
+ {
16
+ "p": "payload"
17
+ }
18
+ ],
19
+ "repeat": "",
20
+ "crontab": "",
21
+ "once": false,
22
+ "onceDelay": 0.1,
23
+ "topic": "",
24
+ "payload": "+447700900123",
25
+ "payloadType": "str",
26
+ "x": 160,
27
+ "y": 100,
28
+ "wires": [
29
+ [
30
+ "pphone"
31
+ ]
32
+ ]
33
+ },
34
+ {
35
+ "id": "pphone",
36
+ "type": "prooftell",
37
+ "z": "tphone",
38
+ "name": "",
39
+ "connection": "c0nf1g0000000001",
40
+ "action": "phone",
41
+ "phone": "payload.phone",
42
+ "phoneType": "msg",
43
+ "email": "payload.email",
44
+ "emailType": "msg",
45
+ "ip": "payload.ip",
46
+ "ipType": "msg",
47
+ "country": "",
48
+ "countryType": "str",
49
+ "level": "line_type",
50
+ "timeout": "",
51
+ "reference": "",
52
+ "referenceType": "str",
53
+ "output": "payload",
54
+ "x": 420,
55
+ "y": 100,
56
+ "wires": [
57
+ [
58
+ "dphone"
59
+ ]
60
+ ]
61
+ },
62
+ {
63
+ "id": "dphone",
64
+ "type": "debug",
65
+ "z": "tphone",
66
+ "name": "The answer",
67
+ "active": true,
68
+ "tosidebar": true,
69
+ "console": false,
70
+ "tostatus": false,
71
+ "complete": "payload",
72
+ "targetType": "msg",
73
+ "statusVal": "",
74
+ "statusType": "auto",
75
+ "x": 680,
76
+ "y": 100,
77
+ "wires": []
78
+ },
79
+ {
80
+ "id": "kphone",
81
+ "type": "catch",
82
+ "z": "tphone",
83
+ "name": "A failed check",
84
+ "scope": [
85
+ "pphone"
86
+ ],
87
+ "uncaught": false,
88
+ "x": 420,
89
+ "y": 160,
90
+ "wires": [
91
+ [
92
+ "ephone"
93
+ ]
94
+ ]
95
+ },
96
+ {
97
+ "id": "ephone",
98
+ "type": "debug",
99
+ "z": "tphone",
100
+ "name": "The error",
101
+ "active": true,
102
+ "tosidebar": true,
103
+ "console": false,
104
+ "tostatus": false,
105
+ "complete": "error",
106
+ "targetType": "msg",
107
+ "statusVal": "",
108
+ "statusType": "auto",
109
+ "x": 680,
110
+ "y": 160,
111
+ "wires": []
112
+ },
113
+ {
114
+ "id": "c0nf1g0000000001",
115
+ "type": "prooftell-config",
116
+ "name": "ProofTell sandbox",
117
+ "mode": "sandbox"
118
+ }
119
+ ]
@@ -0,0 +1,119 @@
1
+ [
2
+ {
3
+ "id": "tip",
4
+ "type": "tab",
5
+ "label": "Look up an IP address",
6
+ "disabled": false,
7
+ "info": "A Tor exit. Open the ProofTell connection and paste a sandbox key (pt_test_…), created in the dashboard under Developers → Integrations. Then click the inject node."
8
+ },
9
+ {
10
+ "id": "iip",
11
+ "type": "inject",
12
+ "z": "tip",
13
+ "name": "The sandbox's input",
14
+ "props": [
15
+ {
16
+ "p": "payload"
17
+ }
18
+ ],
19
+ "repeat": "",
20
+ "crontab": "",
21
+ "once": false,
22
+ "onceDelay": 0.1,
23
+ "topic": "",
24
+ "payload": "203.0.113.66",
25
+ "payloadType": "str",
26
+ "x": 160,
27
+ "y": 100,
28
+ "wires": [
29
+ [
30
+ "pip"
31
+ ]
32
+ ]
33
+ },
34
+ {
35
+ "id": "pip",
36
+ "type": "prooftell",
37
+ "z": "tip",
38
+ "name": "",
39
+ "connection": "c0nf1g0000000001",
40
+ "action": "ip",
41
+ "phone": "payload.phone",
42
+ "phoneType": "msg",
43
+ "email": "payload.email",
44
+ "emailType": "msg",
45
+ "ip": "payload.ip",
46
+ "ipType": "msg",
47
+ "country": "",
48
+ "countryType": "str",
49
+ "level": "line_type",
50
+ "timeout": "",
51
+ "reference": "",
52
+ "referenceType": "str",
53
+ "output": "payload",
54
+ "x": 420,
55
+ "y": 100,
56
+ "wires": [
57
+ [
58
+ "dip"
59
+ ]
60
+ ]
61
+ },
62
+ {
63
+ "id": "dip",
64
+ "type": "debug",
65
+ "z": "tip",
66
+ "name": "The answer",
67
+ "active": true,
68
+ "tosidebar": true,
69
+ "console": false,
70
+ "tostatus": false,
71
+ "complete": "payload",
72
+ "targetType": "msg",
73
+ "statusVal": "",
74
+ "statusType": "auto",
75
+ "x": 680,
76
+ "y": 100,
77
+ "wires": []
78
+ },
79
+ {
80
+ "id": "kip",
81
+ "type": "catch",
82
+ "z": "tip",
83
+ "name": "A failed check",
84
+ "scope": [
85
+ "pip"
86
+ ],
87
+ "uncaught": false,
88
+ "x": 420,
89
+ "y": 160,
90
+ "wires": [
91
+ [
92
+ "eip"
93
+ ]
94
+ ]
95
+ },
96
+ {
97
+ "id": "eip",
98
+ "type": "debug",
99
+ "z": "tip",
100
+ "name": "The error",
101
+ "active": true,
102
+ "tosidebar": true,
103
+ "console": false,
104
+ "tostatus": false,
105
+ "complete": "error",
106
+ "targetType": "msg",
107
+ "statusVal": "",
108
+ "statusType": "auto",
109
+ "x": 680,
110
+ "y": 160,
111
+ "wires": []
112
+ },
113
+ {
114
+ "id": "c0nf1g0000000001",
115
+ "type": "prooftell-config",
116
+ "name": "ProofTell sandbox",
117
+ "mode": "sandbox"
118
+ }
119
+ ]
@@ -0,0 +1,119 @@
1
+ [
2
+ {
3
+ "id": "temail",
4
+ "type": "tab",
5
+ "label": "Verify an email address",
6
+ "disabled": false,
7
+ "info": "A deliverable address. Open the ProofTell connection and paste a sandbox key (pt_test_…), created in the dashboard under Developers → Integrations. Then click the inject node."
8
+ },
9
+ {
10
+ "id": "iemail",
11
+ "type": "inject",
12
+ "z": "temail",
13
+ "name": "The sandbox's input",
14
+ "props": [
15
+ {
16
+ "p": "payload"
17
+ }
18
+ ],
19
+ "repeat": "",
20
+ "crontab": "",
21
+ "once": false,
22
+ "onceDelay": 0.1,
23
+ "topic": "",
24
+ "payload": "jane@example.com",
25
+ "payloadType": "str",
26
+ "x": 160,
27
+ "y": 100,
28
+ "wires": [
29
+ [
30
+ "pemail"
31
+ ]
32
+ ]
33
+ },
34
+ {
35
+ "id": "pemail",
36
+ "type": "prooftell",
37
+ "z": "temail",
38
+ "name": "",
39
+ "connection": "c0nf1g0000000001",
40
+ "action": "email",
41
+ "phone": "payload.phone",
42
+ "phoneType": "msg",
43
+ "email": "payload.email",
44
+ "emailType": "msg",
45
+ "ip": "payload.ip",
46
+ "ipType": "msg",
47
+ "country": "",
48
+ "countryType": "str",
49
+ "level": "line_type",
50
+ "timeout": "",
51
+ "reference": "",
52
+ "referenceType": "str",
53
+ "output": "payload",
54
+ "x": 420,
55
+ "y": 100,
56
+ "wires": [
57
+ [
58
+ "demail"
59
+ ]
60
+ ]
61
+ },
62
+ {
63
+ "id": "demail",
64
+ "type": "debug",
65
+ "z": "temail",
66
+ "name": "The answer",
67
+ "active": true,
68
+ "tosidebar": true,
69
+ "console": false,
70
+ "tostatus": false,
71
+ "complete": "payload",
72
+ "targetType": "msg",
73
+ "statusVal": "",
74
+ "statusType": "auto",
75
+ "x": 680,
76
+ "y": 100,
77
+ "wires": []
78
+ },
79
+ {
80
+ "id": "kemail",
81
+ "type": "catch",
82
+ "z": "temail",
83
+ "name": "A failed check",
84
+ "scope": [
85
+ "pemail"
86
+ ],
87
+ "uncaught": false,
88
+ "x": 420,
89
+ "y": 160,
90
+ "wires": [
91
+ [
92
+ "eemail"
93
+ ]
94
+ ]
95
+ },
96
+ {
97
+ "id": "eemail",
98
+ "type": "debug",
99
+ "z": "temail",
100
+ "name": "The error",
101
+ "active": true,
102
+ "tosidebar": true,
103
+ "console": false,
104
+ "tostatus": false,
105
+ "complete": "error",
106
+ "targetType": "msg",
107
+ "statusVal": "",
108
+ "statusType": "auto",
109
+ "x": 680,
110
+ "y": 160,
111
+ "wires": []
112
+ },
113
+ {
114
+ "id": "c0nf1g0000000001",
115
+ "type": "prooftell-config",
116
+ "name": "ProofTell sandbox",
117
+ "mode": "sandbox"
118
+ }
119
+ ]
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 160" width="140" height="160" role="img" aria-label="ProofTell"><title>ProofTell</title><g transform="translate(0 0) scale(1)"><path fill="#4db38e" d="M32 0 H88 A52 52 0 0 1 88 104 H32 Z"></path><path fill="#2f6bf0" d="M32 0 A32 32 0 0 1 64 32 V128 A32 32 0 0 1 0 128 V32 A32 32 0 0 1 32 0 Z"></path><path fill="#1c3fbf" d="M32 76 A32 32 0 0 1 64 108 V128 A32 32 0 0 1 0 128 V108 A32 32 0 0 1 32 76 Z"></path><path fill="#275c99" d="M32 76 A32 32 0 0 1 63.75 104 H32 Z"></path></g></svg>
@@ -0,0 +1,120 @@
1
+ // The four checks as requests to ProofTell's public API, apart from Node-RED so that they are tested on their own.
2
+ // `bodyFor` keeps only what was given (a blank is not sent) and refuses what the api would refuse anyway, with a
3
+ // sentence a flow's author can act on; `call` sends one request and gives back the api's JSON unchanged, or throws
4
+ // a `ProoftellError` carrying the api's own message, code and status. Nothing here logs an input or the key.
5
+ 'use strict'
6
+
7
+ const { version } = require('../../package.json')
8
+
9
+ /** The shipped address; tests point the node at staging or a local server through the environment only. */
10
+ const BASE_URL = 'https://api.prooftell.com'
11
+
12
+ /** Milliseconds a request may take: an email check may take 45 s at the mail server, plus the way there and back. */
13
+ const REQUEST_TIMEOUT = 60000
14
+
15
+ /** The four actions, named as on every ProofTell connector: the path each calls and the fields it sends. */
16
+ const ACTIONS = {
17
+ phone: { label: 'Look up a phone number', path: '/v1/phone', fields: ['phone', 'country', 'level'], required: 'phone' },
18
+ email: { label: 'Verify an email address', path: '/v1/email', fields: ['email', 'timeout'], required: 'email' },
19
+ ip: { label: 'Look up an IP address', path: '/v1/ip', fields: ['ip'], required: 'ip' },
20
+ assess: { label: 'Assess the risk of an identity', path: '/v1/assess', fields: ['phone', 'email', 'ip', 'country', 'reference'] },
21
+ }
22
+
23
+ const IDENTITY_FIELDS = ['phone', 'email', 'ip']
24
+
25
+ const NAMES = { phone: 'a phone number', email: 'an email address', ip: 'an IP address' }
26
+
27
+ class ProoftellError extends Error {
28
+ constructor (message, { code, status } = {}) {
29
+ super(message)
30
+ this.name = 'ProoftellError'
31
+ this.code = code
32
+ this.status = status
33
+ }
34
+ }
35
+
36
+ function isBlank (value) {
37
+ return value === undefined || value === null || (typeof value === 'string' && value.trim() === '')
38
+ }
39
+
40
+ /** The JSON body of an action from the values given; throws a `ProoftellError` when the call cannot be made. */
41
+ function bodyFor (action, values) {
42
+ const spec = ACTIONS[action]
43
+ if (!spec) throw new ProoftellError(`Unknown action "${action}".`)
44
+ const body = {}
45
+ for (const field of spec.fields) {
46
+ const value = values[field]
47
+ if (isBlank(value)) continue
48
+ body[field] = typeof value === 'string' ? value.trim() : value
49
+ }
50
+ if (spec.required && body[spec.required] === undefined) {
51
+ throw new ProoftellError(`Give ${NAMES[spec.required]}: the node's field, or a string in msg.payload.`)
52
+ }
53
+ if (action === 'assess' && !IDENTITY_FIELDS.some((field) => body[field] !== undefined)) {
54
+ throw new ProoftellError('Give at least one of a phone number, an email address and an IP address.')
55
+ }
56
+ if (body.timeout !== undefined) {
57
+ const timeout = Number(body.timeout)
58
+ if (!Number.isInteger(timeout) || timeout < 1000 || timeout > 45000) {
59
+ throw new ProoftellError('The timeout is in milliseconds, a whole number from 1000 to 45000.')
60
+ }
61
+ body.timeout = timeout
62
+ }
63
+ return body
64
+ }
65
+
66
+ /** Sends one action and resolves with the api's answer; rejects with a `ProoftellError` whose message is the api's. */
67
+ async function call ({ key, action, body, baseUrl = process.env.PROOFTELL_BASE_URL || BASE_URL, fetchImpl = fetch }) {
68
+ if (isBlank(key)) throw new ProoftellError('No API key: open the ProofTell connection and paste a key.')
69
+ let response
70
+ try {
71
+ response = await fetchImpl(baseUrl + ACTIONS[action].path, {
72
+ method: 'POST',
73
+ headers: {
74
+ Authorization: `Bearer ${key.trim()}`,
75
+ Accept: 'application/json',
76
+ 'Content-Type': 'application/json',
77
+ 'User-Agent': `prooftell-node-red/${version}`,
78
+ },
79
+ body: JSON.stringify(body),
80
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT),
81
+ })
82
+ } catch (err) {
83
+ const reason = err && err.name === 'TimeoutError' ? 'it did not answer within 60 seconds' : 'it could not be reached'
84
+ throw new ProoftellError(`The ProofTell API did not answer: ${reason}.`, { code: 'network' })
85
+ }
86
+ const text = await response.text()
87
+ let answer
88
+ try {
89
+ answer = text ? JSON.parse(text) : {}
90
+ } catch {
91
+ answer = undefined
92
+ }
93
+ if (!response.ok) {
94
+ const error = answer && answer.error
95
+ const message = error && error.message ? error.message : `The ProofTell API answered ${response.status}.`
96
+ throw new ProoftellError(message, { code: error && error.code, status: response.status })
97
+ }
98
+ if (answer === undefined) {
99
+ throw new ProoftellError('The ProofTell API answered something other than JSON.', { status: response.status })
100
+ }
101
+ return answer
102
+ }
103
+
104
+ /** A few words on the answer for the node's status: the result, never the input. */
105
+ function summary (action, answer) {
106
+ switch (action) {
107
+ case 'phone':
108
+ return answer.valid === false ? 'not valid' : (answer.line_type || 'valid')
109
+ case 'email':
110
+ return answer.verdict || answer.result || 'checked'
111
+ case 'ip':
112
+ return answer.is_tor ? 'Tor' : answer.is_vpn ? 'VPN' : answer.is_datacenter ? 'datacenter' : (answer.country_code || 'checked')
113
+ case 'assess':
114
+ return answer.verdict ? `${answer.verdict}, ${answer.score}` : 'assessed'
115
+ default:
116
+ return 'done'
117
+ }
118
+ }
119
+
120
+ module.exports = { ACTIONS, BASE_URL, ProoftellError, bodyFor, call, summary }
@@ -0,0 +1,245 @@
1
+ <!--
2
+ The editor side of the ProofTell nodes: the connection (`prooftell-config`) and the check (`prooftell`). The action
3
+ names are the same on every ProofTell connector; the rows shown follow the action. The connection's label says live
4
+ or sandbox from the key's prefix, read when the key is typed (a saved key never comes back to the editor).
5
+ -->
6
+ <script type="text/javascript">
7
+ (function () {
8
+ var ACTIONS = {
9
+ phone: { label: 'Look up a phone number', rows: ['phone', 'country', 'level'] },
10
+ email: { label: 'Verify an email address', rows: ['email', 'timeout'] },
11
+ ip: { label: 'Look up an IP address', rows: ['ip'] },
12
+ assess: { label: 'Assess the risk of an identity', rows: ['phone', 'email', 'ip', 'country', 'reference'] },
13
+ }
14
+ var ROWS = ['phone', 'email', 'ip', 'country', 'level', 'timeout', 'reference']
15
+ var TYPES = ['msg', 'flow', 'global', 'str', 'env', 'jsonata']
16
+
17
+ RED.nodes.registerType('prooftell-config', {
18
+ category: 'config',
19
+ defaults: {
20
+ name: { value: '' },
21
+ mode: { value: '' },
22
+ },
23
+ credentials: {
24
+ apiKey: { type: 'password', required: true },
25
+ },
26
+ label: function () {
27
+ if (this.name) return this.name
28
+ return this.mode ? 'ProofTell (' + this.mode + ')' : 'ProofTell'
29
+ },
30
+ oneditsave: function () {
31
+ var key = ($('#node-config-input-apiKey').val() || '').trim()
32
+ if (key && key !== '__PWRD__') {
33
+ var mode = key.indexOf('pt_test_') === 0 ? 'sandbox' : key.indexOf('pt_live_') === 0 ? 'live' : ''
34
+ $('#node-config-input-mode').val(mode)
35
+ }
36
+ },
37
+ })
38
+
39
+ RED.nodes.registerType('prooftell', {
40
+ category: 'network',
41
+ color: '#d6e2fc',
42
+ defaults: {
43
+ name: { value: '' },
44
+ connection: { value: '', type: 'prooftell-config', required: true },
45
+ action: { value: 'phone' },
46
+ phone: { value: 'payload.phone' },
47
+ phoneType: { value: 'msg' },
48
+ email: { value: 'payload.email' },
49
+ emailType: { value: 'msg' },
50
+ ip: { value: 'payload.ip' },
51
+ ipType: { value: 'msg' },
52
+ country: { value: '' },
53
+ countryType: { value: 'str' },
54
+ level: { value: 'line_type' },
55
+ timeout: {
56
+ value: '',
57
+ validate: function (v) {
58
+ return v === '' || v === undefined || (/^\d+$/.test(String(v)) && Number(v) >= 1000 && Number(v) <= 45000)
59
+ },
60
+ },
61
+ reference: { value: '' },
62
+ referenceType: { value: 'str' },
63
+ output: { value: 'payload' },
64
+ },
65
+ inputs: 1,
66
+ outputs: 1,
67
+ icon: 'prooftell.svg',
68
+ paletteLabel: 'ProofTell',
69
+ inputLabels: 'what to check',
70
+ outputLabels: 'the answer',
71
+ label: function () {
72
+ return this.name || (ACTIONS[this.action] || ACTIONS.phone).label
73
+ },
74
+ labelStyle: function () {
75
+ return this.name ? 'node_label_italic' : ''
76
+ },
77
+ oneditprepare: function () {
78
+ ;['phone', 'email', 'ip', 'country', 'reference'].forEach(function (field) {
79
+ $('#node-input-' + field).typedInput({ default: 'msg', types: TYPES, typeField: '#node-input-' + field + 'Type' })
80
+ })
81
+ $('#node-input-output').typedInput({ default: 'msg', types: ['msg'] })
82
+ $('#node-input-action')
83
+ .on('change', function () {
84
+ var shown = (ACTIONS[$(this).val()] || ACTIONS.phone).rows
85
+ ROWS.forEach(function (row) {
86
+ $('.prooftell-row-' + row).toggle(shown.indexOf(row) !== -1)
87
+ })
88
+ $('.prooftell-required').toggle($(this).val() !== 'assess')
89
+ $('.prooftell-any').toggle($(this).val() === 'assess')
90
+ })
91
+ .trigger('change')
92
+ },
93
+ })
94
+ })()
95
+ </script>
96
+
97
+ <script type="text/html" data-template-name="prooftell-config">
98
+ <div class="form-row">
99
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
100
+ <input type="text" id="node-config-input-name" placeholder="ProofTell (live or sandbox, from the key)" />
101
+ </div>
102
+ <div class="form-row">
103
+ <label for="node-config-input-apiKey"><i class="fa fa-key"></i> API key</label>
104
+ <input type="password" id="node-config-input-apiKey" placeholder="pt_live_… or pt_test_…" />
105
+ </div>
106
+ <input type="hidden" id="node-config-input-mode" />
107
+ <div class="form-tips">
108
+ Create a key in the ProofTell dashboard (Developers → Integrations), named after this platform. A
109
+ <code>pt_test_</code> key is the free sandbox: simulated, deterministic answers. A <code>pt_live_</code> key spends
110
+ the account's balance.
111
+ </div>
112
+ </script>
113
+
114
+ <script type="text/html" data-help-name="prooftell-config">
115
+ <p>A connection to ProofTell: the API key the checks use.</p>
116
+ <h3>Details</h3>
117
+ <p>
118
+ Create a key in the <a href="https://app.prooftell.com/integrations">ProofTell dashboard</a> (Developers →
119
+ Integrations), named after this platform. A <code>pt_test_</code> key is the free sandbox: simulated, deterministic
120
+ answers. A <code>pt_live_</code> key spends the account's balance. The key is kept in Node-RED's credentials, not in
121
+ the flow, so an exported flow does not carry it.
122
+ </p>
123
+ </script>
124
+
125
+ <script type="text/html" data-template-name="prooftell">
126
+ <div class="form-row">
127
+ <label for="node-input-connection"><i class="fa fa-plug"></i> Connection</label>
128
+ <input type="text" id="node-input-connection" />
129
+ </div>
130
+ <div class="form-row">
131
+ <label for="node-input-action"><i class="fa fa-check-square-o"></i> Action</label>
132
+ <select id="node-input-action" style="width: 70%">
133
+ <option value="phone">Look up a phone number</option>
134
+ <option value="email">Verify an email address</option>
135
+ <option value="ip">Look up an IP address</option>
136
+ <option value="assess">Assess the risk of an identity</option>
137
+ </select>
138
+ </div>
139
+ <div class="form-row prooftell-row-phone">
140
+ <label for="node-input-phone"><i class="fa fa-phone"></i> Phone</label>
141
+ <input type="text" id="node-input-phone" style="width: 70%" />
142
+ <input type="hidden" id="node-input-phoneType" />
143
+ </div>
144
+ <div class="form-row prooftell-row-email">
145
+ <label for="node-input-email"><i class="fa fa-envelope"></i> Email</label>
146
+ <input type="text" id="node-input-email" style="width: 70%" />
147
+ <input type="hidden" id="node-input-emailType" />
148
+ </div>
149
+ <div class="form-row prooftell-row-ip">
150
+ <label for="node-input-ip"><i class="fa fa-globe"></i> IP address</label>
151
+ <input type="text" id="node-input-ip" style="width: 70%" />
152
+ <input type="hidden" id="node-input-ipType" />
153
+ </div>
154
+ <div class="form-row prooftell-row-country">
155
+ <label for="node-input-country"><i class="fa fa-flag"></i> Country</label>
156
+ <input type="text" id="node-input-country" style="width: 70%" placeholder="e.g. GB, for a number in national format" />
157
+ <input type="hidden" id="node-input-countryType" />
158
+ </div>
159
+ <div class="form-row prooftell-row-level">
160
+ <label for="node-input-level"><i class="fa fa-signal"></i> Level</label>
161
+ <select id="node-input-level" style="width: 70%">
162
+ <option value="basic">Basic</option>
163
+ <option value="line_type">Line type</option>
164
+ <option value="current_carrier">Current carrier</option>
165
+ <option value="line_status">Line status</option>
166
+ <option value="fraud_score">Fraud score</option>
167
+ </select>
168
+ </div>
169
+ <div class="form-row prooftell-row-timeout">
170
+ <label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout (ms)</label>
171
+ <input type="text" id="node-input-timeout" style="width: 70%" placeholder="1000 to 45000; empty for 45000" />
172
+ </div>
173
+ <div class="form-row prooftell-row-reference">
174
+ <label for="node-input-reference"><i class="fa fa-bookmark"></i> Reference</label>
175
+ <input type="text" id="node-input-reference" style="width: 70%" placeholder="Your own id, echoed back" />
176
+ <input type="hidden" id="node-input-referenceType" />
177
+ </div>
178
+ <div class="form-row">
179
+ <label for="node-input-output"><i class="fa fa-sign-out"></i> Answer to</label>
180
+ <input type="text" id="node-input-output" style="width: 70%" />
181
+ </div>
182
+ <div class="form-row">
183
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
184
+ <input type="text" id="node-input-name" />
185
+ </div>
186
+ <div class="form-tips">
187
+ <span class="prooftell-required">A string in <code>msg.payload</code> is the value to check when the field gives none.</span>
188
+ <span class="prooftell-any">Give at least one of the phone number, the email address and the IP address.</span>
189
+ Each check is charged per check from your balance; the sandbox is free.
190
+ </div>
191
+ </script>
192
+
193
+ <script type="text/html" data-help-name="prooftell">
194
+ <p>
195
+ Runs one ProofTell check per message: look up a phone number, verify an email address, look up an IP address, or
196
+ assess the risk of an identity.
197
+ </p>
198
+
199
+ <h3>Inputs</h3>
200
+ <dl class="message-properties">
201
+ <dt>payload <span class="property-type">string | object</span></dt>
202
+ <dd>
203
+ By default each field reads <code>msg.payload.phone</code>, <code>msg.payload.email</code> or
204
+ <code>msg.payload.ip</code>; any field can instead hold a fixed value, another message property, a flow or global
205
+ value, an environment variable or a JSONata expression. For the three single checks a string payload is the value
206
+ to check when the field gives none.
207
+ </dd>
208
+ </dl>
209
+
210
+ <h3>Outputs</h3>
211
+ <dl class="message-properties">
212
+ <dt>payload <span class="property-type">object</span></dt>
213
+ <dd>
214
+ The API's answer, unchanged (or the property set in <i>Answer to</i>). A phone number: <code>valid</code>,
215
+ <code>line_type</code>, <code>e164</code>, the formats, <code>country_code</code>, the level's block. An email
216
+ address: <code>verdict</code> (deliverable, undeliverable, risky, unknown), <code>reason</code>,
217
+ <code>did_you_mean</code>. An IP address: <code>is_tor</code>, <code>is_vpn</code>, <code>is_datacenter</code>,
218
+ <code>country_code</code>, <code>asn</code>. An assessment: <code>score</code> (0–100), <code>verdict</code>
219
+ (allow, review, deny), <code>reasons</code>, each signal's result. Every answer has its <code>cost</code>.
220
+ </dd>
221
+ </dl>
222
+
223
+ <h3>Details</h3>
224
+ <p>
225
+ The phone level is a ladder, each level including the ones before it and priced on its own: Basic, Line type (the
226
+ default), Current carrier, Line status, Fraud score. What is not offered for a number comes back as unavailable, at
227
+ no cost. The email timeout is how long the mail server may take, 1000 to 45000 ms; past it the address comes back
228
+ unknown, free. Each check is charged per check from your balance; the sandbox is free.
229
+ </p>
230
+ <p>
231
+ A failed check stops the message and reports the API's own message as the node's error, which a Catch node
232
+ receives with <code>msg.statusCode</code> set: 401 for a wrong key, 402 when the balance cannot cover the check (the
233
+ message names the amounts), 429 above the key's rate, 503 when a source did not answer.
234
+ </p>
235
+ <p>
236
+ The sandbox's inputs: <code>+447700900123</code> is a GB mobile, <code>jane@example.com</code> is deliverable,
237
+ <code>203.0.113.1</code> a clean US address and <code>203.0.113.66</code> Tor.
238
+ </p>
239
+
240
+ <h3>References</h3>
241
+ <ul>
242
+ <li><a href="https://prooftell.com/docs">The API reference</a>: every field and the sandbox's inputs</li>
243
+ <li><a href="https://prooftell.com/integrations#node-red">ProofTell for Node-RED</a></li>
244
+ </ul>
245
+ </script>
@@ -0,0 +1,75 @@
1
+ // The ProofTell nodes for Node-RED. `prooftell-config` is the connection: the API key, kept in Node-RED's credentials
2
+ // store (never in the flow's JSON), and a label, live or sandbox, the editor reads from the key's prefix. `prooftell`
3
+ // runs one of the four checks per message: each input is read from the node's field (a TypedInput: a fixed value, a
4
+ // message property, a flow or global context value, an environment variable), and for the three single checks a
5
+ // string `msg.payload` is the value when the field gives nothing; the api's answer, unchanged, goes into the output
6
+ // property (`msg.payload` by default). A failed check is the api's message as the node's error, for a Catch node, with
7
+ // `msg.statusCode` set; the message goes on to no output. What each call does and costs: https://prooftell.com/docs.
8
+ 'use strict'
9
+
10
+ const { ACTIONS, bodyFor, call, summary } = require('./lib/client')
11
+
12
+ const INPUTS = ['phone', 'email', 'ip', 'country', 'reference']
13
+
14
+ module.exports = function (RED) {
15
+ function ProoftellConfigNode (config) {
16
+ RED.nodes.createNode(this, config)
17
+ this.name = config.name
18
+ this.mode = config.mode
19
+ }
20
+ RED.nodes.registerType('prooftell-config', ProoftellConfigNode, {
21
+ credentials: { apiKey: { type: 'password' } },
22
+ })
23
+
24
+ /** A TypedInput's value for this message; undefined when the field is empty or the message lacks the property. */
25
+ function evaluate (node, value, type, msg) {
26
+ if (value === undefined || value === null || value === '') return Promise.resolve(undefined)
27
+ if ((type || 'str') === 'msg') {
28
+ // Node-RED throws on a path through a missing property (payload.phone with no payload): a field with nothing in it.
29
+ try {
30
+ return Promise.resolve(RED.util.getMessageProperty(msg, value))
31
+ } catch {
32
+ return Promise.resolve(undefined)
33
+ }
34
+ }
35
+ return new Promise((resolve, reject) => {
36
+ RED.util.evaluateNodeProperty(value, type || 'str', node, msg, (err, result) => (err ? reject(err) : resolve(result)))
37
+ })
38
+ }
39
+
40
+ function ProoftellNode (config) {
41
+ RED.nodes.createNode(this, config)
42
+ const node = this
43
+ const connection = RED.nodes.getNode(config.connection)
44
+ const action = ACTIONS[config.action] ? config.action : 'phone'
45
+
46
+ node.on('input', async (msg, send, done) => {
47
+ try {
48
+ const values = { level: config.level || undefined, timeout: config.timeout === '' ? undefined : config.timeout }
49
+ for (const input of INPUTS) {
50
+ values[input] = await evaluate(node, config[input], config[input + 'Type'], msg)
51
+ }
52
+ const required = ACTIONS[action].required
53
+ if (required && (values[required] === undefined || values[required] === '') && typeof msg.payload === 'string') {
54
+ values[required] = msg.payload
55
+ }
56
+ const body = bodyFor(action, values)
57
+ if (!connection) throw new Error('No ProofTell connection: pick one in the node, with its key.')
58
+ const key = connection.credentials && connection.credentials.apiKey
59
+ node.status({ fill: 'blue', shape: 'dot', text: 'checking' })
60
+ const answer = await call({ key, action, body })
61
+ RED.util.setMessageProperty(msg, config.output || 'payload', answer, true)
62
+ node.status({ fill: 'green', shape: 'dot', text: summary(action, answer) })
63
+ send(msg)
64
+ done()
65
+ } catch (err) {
66
+ if (err.status) msg.statusCode = err.status
67
+ node.status({ fill: 'red', shape: 'ring', text: err.code || 'error' })
68
+ done(err)
69
+ }
70
+ })
71
+
72
+ node.on('close', () => node.status({}))
73
+ }
74
+ RED.nodes.registerType('prooftell', ProoftellNode)
75
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@prooftell/node-red-prooftell",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "ProofTell for Node-RED: look up a phone number, verify an email address, look up an IP address, or assess the risk of an identity, with one API key.",
5
+ "license": "MIT",
6
+ "homepage": "https://prooftell.com/integrations?utm_source=node-red&utm_medium=integration",
7
+ "keywords": [
8
+ "node-red",
9
+ "prooftell",
10
+ "phone-validation",
11
+ "email-verification",
12
+ "ip-intelligence",
13
+ "fraud"
14
+ ],
15
+ "author": {
16
+ "name": "ProofTell",
17
+ "email": "support@prooftell.com"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/prooftell/integrations.git",
22
+ "directory": "node-red"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/prooftell/integrations/issues",
26
+ "email": "support@prooftell.com"
27
+ },
28
+ "files": [
29
+ "nodes",
30
+ "examples"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "node-red": {
36
+ "version": ">=4.0.0",
37
+ "nodes": {
38
+ "prooftell": "nodes/prooftell.js"
39
+ }
40
+ },
41
+ "scripts": {
42
+ "test": "node --test test/*.test.js"
43
+ },
44
+ "devDependencies": {
45
+ "node-red": "5.0.9",
46
+ "node-red-node-test-helper": "0.3.6"
47
+ }
48
+ }