buzz-me-in 2.2.0-beta.4 → 2.2.0-beta.5
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/.env.example +12 -0
- package/package.json +1 -1
- package/server.js +37 -0
- package/src/core/config.js +45 -0
package/.env.example
CHANGED
|
@@ -66,6 +66,18 @@ SHUTDOWN_GRACE_MS=10000
|
|
|
66
66
|
# unlock bothers you.
|
|
67
67
|
HOMEKIT_HANGUP_GRACE_MS=8000
|
|
68
68
|
|
|
69
|
+
# Optional labels for numbers you expect to call in, so the journal says who
|
|
70
|
+
# rang rather than only a number. The intercom panel is normally the only
|
|
71
|
+
# caller; anything else is a wrong number or a dialler that found the number.
|
|
72
|
+
# Comma-separated <E.164>=<label> pairs. Labels may contain spaces, up to 64
|
|
73
|
+
# characters, and each number may appear once.
|
|
74
|
+
# Purely descriptive: an unlabelled caller is logged as "unknown" and still
|
|
75
|
+
# rings the doorbell. Nothing is ever rejected on the strength of this, because
|
|
76
|
+
# the building's dialler could change without warning and a silently blocked
|
|
77
|
+
# visitor is much worse than a labelled unknown one.
|
|
78
|
+
# Leave unset to omit the caller field entirely.
|
|
79
|
+
#KNOWN_CALLERS=+15551234567=Intercom,+15557654321=Building management
|
|
80
|
+
|
|
69
81
|
# Logging
|
|
70
82
|
# LOG_LEVEL controls minimum emitted level: debug | info | warn | error
|
|
71
83
|
# Default is info when unset/invalid.
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -165,6 +165,33 @@ function buildTwiml(callSid) {
|
|
|
165
165
|
</Response>`;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* A repeated form field parses to an array, so take the first value. A sender
|
|
170
|
+
* can repeat From freely; the signature covers the whole body, not its shape.
|
|
171
|
+
*
|
|
172
|
+
* @param {string | string[] | undefined} value
|
|
173
|
+
* @returns {string | undefined}
|
|
174
|
+
*/
|
|
175
|
+
function firstFormValue(value) {
|
|
176
|
+
if (Array.isArray(value)) return value.length ? value[0] : undefined;
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Label for a calling number, from KNOWN_CALLERS. Returns 'unknown' for a
|
|
182
|
+
* number we have no label for, and null when nothing was configured, so the
|
|
183
|
+
* field is absent rather than misleadingly saying 'unknown' on every call.
|
|
184
|
+
*
|
|
185
|
+
* @param {string | string[] | undefined} from
|
|
186
|
+
* @returns {string | null}
|
|
187
|
+
*/
|
|
188
|
+
function describeCaller(from) {
|
|
189
|
+
if (config.knownCallers.size === 0) return null;
|
|
190
|
+
const number = firstFormValue(from);
|
|
191
|
+
if (!number) return 'unknown';
|
|
192
|
+
return config.knownCallers.get(number.trim()) || 'unknown';
|
|
193
|
+
}
|
|
194
|
+
|
|
168
195
|
async function handleTwimlRequest(req, res) {
|
|
169
196
|
twimlLogger.info('Incoming TwiML request', {
|
|
170
197
|
event: 'twiml-request',
|
|
@@ -200,9 +227,18 @@ async function handleTwimlRequest(req, res) {
|
|
|
200
227
|
const formData = parseFormUrlEncoded(rawBody);
|
|
201
228
|
const callSid = normalizeCallSid(formData.CallSid);
|
|
202
229
|
const body = buildTwiml(callSid);
|
|
230
|
+
// Who rang. Logged here rather than on twiml-request, which fires before the
|
|
231
|
+
// body is read and before the signature is checked: an unverified From is
|
|
232
|
+
// whatever the sender typed. forwardedFrom is the number that diverted the
|
|
233
|
+
// call, which is the intercom panel's signature when the building's line
|
|
234
|
+
// forwards to us.
|
|
235
|
+
const caller = describeCaller(formData.From);
|
|
203
236
|
twimlLogger.info('TwiML response generated', {
|
|
204
237
|
event: 'twiml-response',
|
|
205
238
|
callSid: callSid || undefined,
|
|
239
|
+
from: firstFormValue(formData.From) || undefined,
|
|
240
|
+
forwardedFrom: firstFormValue(formData.ForwardedFrom) || undefined,
|
|
241
|
+
caller: caller || undefined,
|
|
206
242
|
});
|
|
207
243
|
res.writeHead(200, {
|
|
208
244
|
'Content-Type': 'text/xml',
|
|
@@ -506,6 +542,7 @@ module.exports = {
|
|
|
506
542
|
main,
|
|
507
543
|
beginShutdown,
|
|
508
544
|
buildTwiml,
|
|
545
|
+
describeCaller,
|
|
509
546
|
handleRequest,
|
|
510
547
|
isAuthorizedForStatus,
|
|
511
548
|
isValidTwilioRequest,
|
package/src/core/config.js
CHANGED
|
@@ -96,6 +96,50 @@ if (!MEDIA_SIGNATURE_MODES.includes(twilioMediaSignatureMode)) {
|
|
|
96
96
|
fail(`TWILIO_MEDIA_SIGNATURE_MODE must be one of: ${MEDIA_SIGNATURE_MODES.join(', ')}`);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Optional labels for numbers we expect to see calling in, so the journal says
|
|
100
|
+
// who rang rather than only a number. The intercom panel is the only caller
|
|
101
|
+
// that should normally appear; anything else is a wrong number or a dialler
|
|
102
|
+
// finding an open PSTN number, and being able to tell them apart at a glance is
|
|
103
|
+
// the point. Purely descriptive: nothing is rejected on the strength of it, and
|
|
104
|
+
// an unknown caller still rings the doorbell, because the building's dialler
|
|
105
|
+
// could change without warning and a silently blocked visitor is far worse
|
|
106
|
+
// than a labelled unknown one.
|
|
107
|
+
//
|
|
108
|
+
// Format: comma-separated E.164=Label pairs, e.g.
|
|
109
|
+
// KNOWN_CALLERS=+15551234567=Intercom,+15557654321=Building management
|
|
110
|
+
const E164 = /^\+[1-9]\d{1,14}$/;
|
|
111
|
+
|
|
112
|
+
function parseKnownCallers(name) {
|
|
113
|
+
const raw = optional(name);
|
|
114
|
+
const callers = new Map();
|
|
115
|
+
if (!raw) return callers;
|
|
116
|
+
|
|
117
|
+
for (const entry of raw.split(',')) {
|
|
118
|
+
const trimmed = entry.trim();
|
|
119
|
+
if (!trimmed) continue;
|
|
120
|
+
const separator = trimmed.indexOf('=');
|
|
121
|
+
if (separator === -1) {
|
|
122
|
+
fail(`${name} entries must be <E.164>=<label>, got "${trimmed}"`);
|
|
123
|
+
}
|
|
124
|
+
const number = trimmed.slice(0, separator).trim();
|
|
125
|
+
const label = trimmed.slice(separator + 1).trim();
|
|
126
|
+
if (!E164.test(number)) {
|
|
127
|
+
fail(`${name} entry "${trimmed}" does not start with an E.164 number`);
|
|
128
|
+
}
|
|
129
|
+
if (!label) {
|
|
130
|
+
fail(`${name} entry "${trimmed}" has an empty label`);
|
|
131
|
+
}
|
|
132
|
+
if (label.length > 64) {
|
|
133
|
+
fail(`${name} label for ${number} is longer than 64 characters`);
|
|
134
|
+
}
|
|
135
|
+
if (callers.has(number)) {
|
|
136
|
+
fail(`${name} lists ${number} more than once`);
|
|
137
|
+
}
|
|
138
|
+
callers.set(number, label);
|
|
139
|
+
}
|
|
140
|
+
return callers;
|
|
141
|
+
}
|
|
142
|
+
|
|
99
143
|
const twilioWebhookBaseUrl = sanitizeBaseUrl(
|
|
100
144
|
optional('TWILIO_WEBHOOK_BASE_URL') || `https://${tunnelHostname}`
|
|
101
145
|
);
|
|
@@ -139,6 +183,7 @@ module.exports = {
|
|
|
139
183
|
twilioMediaPayloadMaxBytes: parsePositiveInt('TWILIO_MEDIA_PAYLOAD_MAX_BYTES', '512'),
|
|
140
184
|
shutdownGraceMs: parsePositiveInt('SHUTDOWN_GRACE_MS', '10000'),
|
|
141
185
|
homekitHangupGraceMs: parsePositiveInt('HOMEKIT_HANGUP_GRACE_MS', '8000'),
|
|
186
|
+
knownCallers: parseKnownCallers('KNOWN_CALLERS'),
|
|
142
187
|
streamAuthSecret,
|
|
143
188
|
streamAuthTtlSec: 90,
|
|
144
189
|
statusApiToken,
|