erlc-v2 1.0.0 → 1.1.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/README.md +608 -380
- package/index.d.ts +143 -2
- package/package.json +7 -4
- package/src/Client.js +74 -1
- package/src/api/LocalApiServer.js +538 -0
- package/src/events/Poller.js +14 -0
- package/src/events/diff.js +10 -1
- package/src/util/constants.js +11 -0
- package/src/util/errors.js +1 -1
- package/src/util/normalize.js +1 -0
- package/src/util/vehicleSearch.js +120 -0
- package/src/util/webhook.js +204 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
function cleanText(value) {
|
|
2
|
+
return String(value ?? "")
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function cleanPlate(value) {
|
|
8
|
+
return cleanText(value).replace(/[^a-z0-9]/g, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toVehicleTerms(vehicle) {
|
|
12
|
+
return {
|
|
13
|
+
raw: vehicle,
|
|
14
|
+
name: String(vehicle?.Name ?? "").trim(),
|
|
15
|
+
owner: String(vehicle?.Owner ?? "").trim(),
|
|
16
|
+
plate: String(vehicle?.Plate ?? "").trim(),
|
|
17
|
+
texture: String(vehicle?.Texture ?? "").trim(),
|
|
18
|
+
colorName: String(vehicle?.ColorName ?? "").trim(),
|
|
19
|
+
colorHex: String(vehicle?.ColorHex ?? "").trim(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matchText(value, query, exact) {
|
|
24
|
+
if (!query) return true;
|
|
25
|
+
if (exact) return value === query;
|
|
26
|
+
return value.includes(query);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function scoreVehicle(terms, query) {
|
|
30
|
+
if (!query) return 0;
|
|
31
|
+
if (terms.plate && cleanPlate(terms.plate) === query) return 100;
|
|
32
|
+
if (terms.plate && cleanPlate(terms.plate).startsWith(query)) return 80;
|
|
33
|
+
if (terms.owner && cleanText(terms.owner) === query) return 75;
|
|
34
|
+
if (terms.name && cleanText(terms.name) === query) return 70;
|
|
35
|
+
if (terms.plate && cleanPlate(terms.plate).includes(query)) return 60;
|
|
36
|
+
if (terms.owner && cleanText(terms.owner).includes(query)) return 50;
|
|
37
|
+
if (terms.name && cleanText(terms.name).includes(query)) return 40;
|
|
38
|
+
if (terms.colorName && cleanText(terms.colorName).includes(query)) return 30;
|
|
39
|
+
if (terms.texture && cleanText(terms.texture).includes(query)) return 20;
|
|
40
|
+
return 10;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function searchVehicles(list, filters = {}) {
|
|
44
|
+
const vehicles = Array.isArray(list) ? list : [];
|
|
45
|
+
const input =
|
|
46
|
+
typeof filters === "string" ? { query: filters } : filters || {};
|
|
47
|
+
|
|
48
|
+
const exact = input.exact === true;
|
|
49
|
+
const queryText = cleanText(input.query ?? input.search ?? "");
|
|
50
|
+
const plateQuery = cleanPlate(input.plate ?? "");
|
|
51
|
+
const ownerQuery = cleanText(input.owner ?? "");
|
|
52
|
+
const nameQuery = cleanText(input.name ?? "");
|
|
53
|
+
const colorQuery = cleanText(input.color ?? input.colorName ?? "");
|
|
54
|
+
const textureQuery = cleanText(input.texture ?? "");
|
|
55
|
+
const limit =
|
|
56
|
+
input.limit === undefined || input.limit === null
|
|
57
|
+
? null
|
|
58
|
+
: Math.max(0, Number(input.limit) || 0);
|
|
59
|
+
|
|
60
|
+
const matched = [];
|
|
61
|
+
for (const vehicle of vehicles) {
|
|
62
|
+
const terms = toVehicleTerms(vehicle);
|
|
63
|
+
const plateValue = cleanPlate(terms.plate);
|
|
64
|
+
const ownerValue = cleanText(terms.owner);
|
|
65
|
+
const nameValue = cleanText(terms.name);
|
|
66
|
+
const colorValue = cleanText(terms.colorName);
|
|
67
|
+
const textureValue = cleanText(terms.texture);
|
|
68
|
+
|
|
69
|
+
if (!matchText(plateValue, plateQuery, exact)) continue;
|
|
70
|
+
if (!matchText(ownerValue, ownerQuery, exact)) continue;
|
|
71
|
+
if (!matchText(nameValue, nameQuery, exact)) continue;
|
|
72
|
+
if (!matchText(colorValue, colorQuery, exact)) continue;
|
|
73
|
+
if (!matchText(textureValue, textureQuery, exact)) continue;
|
|
74
|
+
|
|
75
|
+
if (queryText) {
|
|
76
|
+
const hit =
|
|
77
|
+
plateValue.includes(queryText) ||
|
|
78
|
+
ownerValue.includes(queryText) ||
|
|
79
|
+
nameValue.includes(queryText) ||
|
|
80
|
+
colorValue.includes(queryText) ||
|
|
81
|
+
textureValue.includes(queryText) ||
|
|
82
|
+
cleanText(terms.colorHex).includes(queryText);
|
|
83
|
+
if (!hit) continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
matched.push({
|
|
87
|
+
vehicle,
|
|
88
|
+
score: scoreVehicle(terms, queryText || plateQuery || ownerQuery || nameQuery),
|
|
89
|
+
plate: plateValue,
|
|
90
|
+
owner: ownerValue,
|
|
91
|
+
name: nameValue,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
matched.sort((a, b) => {
|
|
96
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
97
|
+
if (a.owner !== b.owner) return a.owner.localeCompare(b.owner);
|
|
98
|
+
if (a.name !== b.name) return a.name.localeCompare(b.name);
|
|
99
|
+
return a.plate.localeCompare(b.plate);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const output = matched.map((item) => item.vehicle);
|
|
103
|
+
if (limit === null) return output;
|
|
104
|
+
return output.slice(0, limit);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function findVehicleByPlate(list, plate) {
|
|
108
|
+
const results = searchVehicles(list, {
|
|
109
|
+
plate,
|
|
110
|
+
exact: true,
|
|
111
|
+
limit: 1,
|
|
112
|
+
});
|
|
113
|
+
return results[0] ?? null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
searchVehicles,
|
|
118
|
+
findVehicleByPlate,
|
|
119
|
+
cleanPlate,
|
|
120
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
const PUBLIC_KEY = crypto.createPublicKey({
|
|
4
|
+
key: Buffer.from(
|
|
5
|
+
"MCowBQYDK2VwAyEAjSICb9pp0kHizGQtdG8ySWsDChfGqi+gyFCttigBNOA=",
|
|
6
|
+
"base64",
|
|
7
|
+
),
|
|
8
|
+
format: "der",
|
|
9
|
+
type: "spki",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function getHeader(req, name) {
|
|
13
|
+
const value = req.headers?.[name.toLowerCase()];
|
|
14
|
+
if (Array.isArray(value)) return value[0] ?? "";
|
|
15
|
+
return typeof value === "string" ? value : "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function verifyWebhookSignature(timestamp, sigHex, rawBody) {
|
|
19
|
+
if (!timestamp || !sigHex || !Buffer.isBuffer(rawBody)) return false;
|
|
20
|
+
if (!/^[a-f0-9]+$/i.test(sigHex) || sigHex.length % 2 !== 0) return false;
|
|
21
|
+
|
|
22
|
+
const signature = Buffer.from(sigHex, "hex");
|
|
23
|
+
const message = Buffer.concat([Buffer.from(String(timestamp), "utf8"), rawBody]);
|
|
24
|
+
return crypto.verify(null, message, PUBLIC_KEY, signature);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function detectWebhookType(body = {}) {
|
|
28
|
+
if (!body || typeof body !== "object") {
|
|
29
|
+
return "verification";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Object.keys(body).length === 0) {
|
|
33
|
+
return "verification";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const events = Array.isArray(body.events)
|
|
37
|
+
? body.events
|
|
38
|
+
: Array.isArray(body.Events)
|
|
39
|
+
? body.Events
|
|
40
|
+
: [];
|
|
41
|
+
|
|
42
|
+
for (const entry of events) {
|
|
43
|
+
const eventName = String(entry?.event ?? entry?.Event ?? "")
|
|
44
|
+
.trim()
|
|
45
|
+
.toLowerCase();
|
|
46
|
+
const data = entry?.data ?? entry?.Data ?? {};
|
|
47
|
+
|
|
48
|
+
if (!eventName && (!data || typeof data !== "object")) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
eventName.includes("probe") ||
|
|
54
|
+
eventName.includes("verify") ||
|
|
55
|
+
eventName.includes("validation") ||
|
|
56
|
+
eventName.includes("challenge")
|
|
57
|
+
) {
|
|
58
|
+
return "verification";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (eventName.includes("emergency")) {
|
|
62
|
+
return "emergencyCall";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (eventName.includes("command")) {
|
|
66
|
+
return "command";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const dataCommand =
|
|
70
|
+
data.command ??
|
|
71
|
+
data.Command ??
|
|
72
|
+
data.message ??
|
|
73
|
+
data.Message ??
|
|
74
|
+
data.content ??
|
|
75
|
+
data.Content;
|
|
76
|
+
if (typeof dataCommand === "string" && dataCommand.trim().startsWith(";")) {
|
|
77
|
+
return "command";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const hints = [
|
|
82
|
+
body.type,
|
|
83
|
+
body.Type,
|
|
84
|
+
body.event,
|
|
85
|
+
body.Event,
|
|
86
|
+
body.eventType,
|
|
87
|
+
body.EventType,
|
|
88
|
+
body.kind,
|
|
89
|
+
body.Kind,
|
|
90
|
+
]
|
|
91
|
+
.filter((value) => typeof value === "string")
|
|
92
|
+
.map((value) => value.trim().toLowerCase());
|
|
93
|
+
|
|
94
|
+
for (const hint of hints) {
|
|
95
|
+
if (
|
|
96
|
+
hint.includes("verify") ||
|
|
97
|
+
hint.includes("validation") ||
|
|
98
|
+
hint.includes("challenge")
|
|
99
|
+
) {
|
|
100
|
+
return "verification";
|
|
101
|
+
}
|
|
102
|
+
if (hint.includes("emergency")) return "emergencyCall";
|
|
103
|
+
if (hint.includes("command")) return "command";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const commandText =
|
|
107
|
+
body.command ??
|
|
108
|
+
body.Command ??
|
|
109
|
+
body.message ??
|
|
110
|
+
body.Message ??
|
|
111
|
+
body.content ??
|
|
112
|
+
body.Content;
|
|
113
|
+
if (typeof commandText === "string" && commandText.trim().startsWith(";")) {
|
|
114
|
+
return "command";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
body.CallNumber !== undefined ||
|
|
119
|
+
body.callNumber !== undefined ||
|
|
120
|
+
body.PositionDescriptor !== undefined ||
|
|
121
|
+
body.positionDescriptor !== undefined
|
|
122
|
+
) {
|
|
123
|
+
return "emergencyCall";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
body.challenge !== undefined ||
|
|
128
|
+
body.Challenge !== undefined ||
|
|
129
|
+
body.validation !== undefined ||
|
|
130
|
+
body.Validation !== undefined ||
|
|
131
|
+
body.nonce !== undefined ||
|
|
132
|
+
body.Nonce !== undefined
|
|
133
|
+
) {
|
|
134
|
+
return "verification";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return "unknown";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getEventEntries(body = {}) {
|
|
141
|
+
if (Array.isArray(body?.events)) return body.events;
|
|
142
|
+
if (Array.isArray(body?.Events)) return body.Events;
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeEventEntry(entry = {}) {
|
|
147
|
+
const data =
|
|
148
|
+
entry?.data && typeof entry.data === "object"
|
|
149
|
+
? entry.data
|
|
150
|
+
: entry?.Data && typeof entry.Data === "object"
|
|
151
|
+
? entry.Data
|
|
152
|
+
: {};
|
|
153
|
+
const command =
|
|
154
|
+
data.command ??
|
|
155
|
+
data.Command ??
|
|
156
|
+
null;
|
|
157
|
+
const argument =
|
|
158
|
+
data.argument ??
|
|
159
|
+
data.Argument ??
|
|
160
|
+
data.args ??
|
|
161
|
+
data.Args ??
|
|
162
|
+
"";
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
event: entry?.event ?? entry?.Event ?? null,
|
|
166
|
+
origin: entry?.origin ?? entry?.Origin ?? null,
|
|
167
|
+
eventTimestamp: entry?.timestamp ?? entry?.Timestamp ?? null,
|
|
168
|
+
data,
|
|
169
|
+
command: typeof command === "string" ? command : null,
|
|
170
|
+
argument: typeof argument === "string" ? argument : String(argument ?? ""),
|
|
171
|
+
args: typeof argument === "string" ? argument : String(argument ?? ""),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeWebhookPayload(body = {}, meta = {}) {
|
|
176
|
+
const events = getEventEntries(body).map(normalizeEventEntry);
|
|
177
|
+
const first = events[0] || null;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
type: detectWebhookType(body),
|
|
181
|
+
body,
|
|
182
|
+
timestamp: meta.timestamp ?? null,
|
|
183
|
+
signature: meta.signature ?? null,
|
|
184
|
+
rawBody: meta.rawBody ?? null,
|
|
185
|
+
headers: meta.headers ?? {},
|
|
186
|
+
server: body?.server ?? body?.Server ?? null,
|
|
187
|
+
events,
|
|
188
|
+
entry: first,
|
|
189
|
+
event: first?.event ?? null,
|
|
190
|
+
origin: first?.origin ?? null,
|
|
191
|
+
eventTimestamp: first?.eventTimestamp ?? null,
|
|
192
|
+
data: first?.data ?? null,
|
|
193
|
+
command: first?.command ?? null,
|
|
194
|
+
argument: first?.argument ?? "",
|
|
195
|
+
args: first?.args ?? "",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
getHeader,
|
|
201
|
+
verifyWebhookSignature,
|
|
202
|
+
detectWebhookType,
|
|
203
|
+
normalizeWebhookPayload,
|
|
204
|
+
};
|