@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 +21 -0
- package/README.md +107 -2
- package/examples/Assess the risk of an identity.json +119 -0
- package/examples/Look up a phone number.json +119 -0
- package/examples/Look up an IP address.json +119 -0
- package/examples/Verify an email address.json +119 -0
- package/nodes/icons/prooftell.svg +1 -0
- package/nodes/lib/client.js +120 -0
- package/nodes/prooftell.html +245 -0
- package/nodes/prooftell.js +75 -0
- package/package.json +46 -4
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
|
-
#
|
|
1
|
+
# @prooftell/node-red-prooftell
|
|
2
2
|
|
|
3
|
-
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|