@trillboards/edge-sdk 0.2.21 → 0.2.24
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/deploy/macos/scripts/setup-litertlm.sh +30 -0
- package/deploy/windows/e2e/test-enterprise-trust-store.ps1 +341 -0
- package/deploy/windows/setup-litertlm.ps1 +49 -1
- package/dist/AudienceAnalyzeClient.d.ts +32 -1
- package/dist/AudienceAnalyzeClient.d.ts.map +1 -1
- package/dist/AudienceAnalyzeClient.js +34 -5
- package/dist/AudienceAnalyzeClient.js.map +1 -1
- package/dist/CloudSensingManager.d.ts +30 -0
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/CloudSensingManager.js +61 -1
- package/dist/CloudSensingManager.js.map +1 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -1
- package/dist/diagnose.d.ts +37 -4
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js +155 -6
- package/dist/diagnose.js.map +1 -1
- package/dist/enterpriseNetwork.d.ts +130 -0
- package/dist/enterpriseNetwork.d.ts.map +1 -0
- package/dist/enterpriseNetwork.js +568 -0
- package/dist/enterpriseNetwork.js.map +1 -0
- package/dist/enterpriseProxy.d.ts +73 -0
- package/dist/enterpriseProxy.d.ts.map +1 -0
- package/dist/enterpriseProxy.js +406 -0
- package/dist/enterpriseProxy.js.map +1 -0
- package/dist/enterpriseTrust.d.ts +188 -0
- package/dist/enterpriseTrust.d.ts.map +1 -0
- package/dist/enterpriseTrust.js +615 -0
- package/dist/enterpriseTrust.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/localNetworkAccess.d.ts +166 -0
- package/dist/localNetworkAccess.d.ts.map +1 -0
- package/dist/localNetworkAccess.js +490 -0
- package/dist/localNetworkAccess.js.map +1 -0
- package/dist/sensingRunMode.d.ts +36 -0
- package/dist/sensingRunMode.d.ts.map +1 -1
- package/dist/sensingRunMode.js +63 -1
- package/dist/sensingRunMode.js.map +1 -1
- package/dist/transportFailure.d.ts +118 -0
- package/dist/transportFailure.d.ts.map +1 -0
- package/dist/transportFailure.js +267 -0
- package/dist/transportFailure.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* macOS **Local Network** (TCC) access detection.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this file exists
|
|
6
|
+
*
|
|
7
|
+
* On macOS 15+ every process that talks to a private-IP LAN host is gated by
|
|
8
|
+
* the *Local Network* privacy grant. The connector's whole camera surface is
|
|
9
|
+
* LAN traffic — ONVIF WS-Discovery multicast, the subnet TCP sweep, and RTSP
|
|
10
|
+
* frame capture — so a missing grant takes every camera offline at once.
|
|
11
|
+
*
|
|
12
|
+
* The failure it produces is not silence, it is **misattribution**: ffmpeg
|
|
13
|
+
* reports "no route to host", `classifyRtspFailureClass` maps that to
|
|
14
|
+
* `rtsp_unreachable`, and the portal tells the operator to check that their
|
|
15
|
+
* camera and connector are on the same network. They are. The fix is a
|
|
16
|
+
* checkbox in System Settings that nobody was told to tick.
|
|
17
|
+
*
|
|
18
|
+
* ## What macOS actually exposes — measured, not assumed
|
|
19
|
+
*
|
|
20
|
+
* Measured on macOS 26.5.2 (arm64) on 2026-08-14:
|
|
21
|
+
*
|
|
22
|
+
* - The grant is **not readable**. `kTCCServiceLocalNetwork` does not exist in
|
|
23
|
+
* `tccd`'s service table (positive control: `kTCCServiceCamera` is present),
|
|
24
|
+
* there is no row in either TCC.db, and `tccutil` cannot address it. The
|
|
25
|
+
* decision is owned by `nehelper` (`com.apple.network.localnetworkdecision`)
|
|
26
|
+
* and is not exposed to a client process by any public or file-level means.
|
|
27
|
+
* **So the state must be inferred from behaviour.**
|
|
28
|
+
*
|
|
29
|
+
* - Denial has a sharp, specific signature. The *same* binary, same machine,
|
|
30
|
+
* same network, same minute, differing only in who launched it:
|
|
31
|
+
*
|
|
32
|
+
* | operation | LAN allowed | LAN denied |
|
|
33
|
+
* |----------------------------------|----------------------|----------------------|
|
|
34
|
+
* | `sendto` 239.255.255.250:3702 | sent | **EHOSTUNREACH, 0 ms** |
|
|
35
|
+
* | `connect` gateway:80 (host up) | connected, 4 ms | **EHOSTUNREACH, 1 ms** |
|
|
36
|
+
* | `connect` gateway:9 (port shut) | ECONNREFUSED, 1005 ms| **EHOSTUNREACH, 0 ms** |
|
|
37
|
+
* | `connect` 192.168.0.222 (no host)| timeout, 3004 ms | timeout, 3193 ms |
|
|
38
|
+
* | `connect` 127.0.0.1 (loopback) | connected | connected |
|
|
39
|
+
*
|
|
40
|
+
* Two things in that table are the entire design:
|
|
41
|
+
*
|
|
42
|
+
* 1. **The absent host times out in BOTH states.** A probe that only tries the
|
|
43
|
+
* camera can never tell "denied" from "the NVR is off", because they look
|
|
44
|
+
* identical. That is precisely the bug, so a probe that reproduces it is
|
|
45
|
+
* worthless.
|
|
46
|
+
*
|
|
47
|
+
* 2. **Multicast `sendto` is the clean discriminator.** An empty network
|
|
48
|
+
* segment still *accepts* a multicast send — nothing answers, but the send
|
|
49
|
+
* succeeds. Only the TCC gate makes `sendto` itself fail. And an immediate
|
|
50
|
+
* `EHOSTUNREACH` toward an address on the machine's own directly-connected
|
|
51
|
+
* subnet cannot come from routing: the kernel has a connected route there by
|
|
52
|
+
* construction, and a missing host shows up as an ARP timeout instead.
|
|
53
|
+
*
|
|
54
|
+
* Loopback is deliberately only a control, never evidence: it is never gated,
|
|
55
|
+
* so it succeeds in both states and separates nothing.
|
|
56
|
+
*
|
|
57
|
+
* ## Why NOT the default gateway
|
|
58
|
+
*
|
|
59
|
+
* The obvious probe — `route -n get default` — is wrong, and measurably so.
|
|
60
|
+
* On the test machine the default route is a Tailscale `utun` interface, and
|
|
61
|
+
* under a denied grant that endpoint answered `ECONNREFUSED` in the same run
|
|
62
|
+
* where the real LAN returned `EHOSTUNREACH`. A VPN default route is not a LAN
|
|
63
|
+
* host and is not gated, so probing it reports "the LAN is fine" every time and
|
|
64
|
+
* sends the operator straight back to debugging the recorder. Probe targets are
|
|
65
|
+
* therefore derived from the machine's **own directly-connected IPv4 subnets**
|
|
66
|
+
* via `os.networkInterfaces()`, never from the default route.
|
|
67
|
+
*
|
|
68
|
+
* ## Failing safe
|
|
69
|
+
*
|
|
70
|
+
* Every ambiguous outcome resolves to `unknown`, never to `denied`. Positive
|
|
71
|
+
* evidence that the LAN is reachable (a completed connect, or an RST) vetoes a
|
|
72
|
+
* denial verdict outright. Claiming a permission problem that isn't there sends
|
|
73
|
+
* the operator to the wrong place just as surely as the bug this replaces.
|
|
74
|
+
*/
|
|
75
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
76
|
+
if (k2 === undefined) k2 = k;
|
|
77
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
78
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
79
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
80
|
+
}
|
|
81
|
+
Object.defineProperty(o, k2, desc);
|
|
82
|
+
}) : (function(o, m, k, k2) {
|
|
83
|
+
if (k2 === undefined) k2 = k;
|
|
84
|
+
o[k2] = m[k];
|
|
85
|
+
}));
|
|
86
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
87
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
88
|
+
}) : function(o, v) {
|
|
89
|
+
o["default"] = v;
|
|
90
|
+
});
|
|
91
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
92
|
+
var ownKeys = function(o) {
|
|
93
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
94
|
+
var ar = [];
|
|
95
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
96
|
+
return ar;
|
|
97
|
+
};
|
|
98
|
+
return ownKeys(o);
|
|
99
|
+
};
|
|
100
|
+
return function (mod) {
|
|
101
|
+
if (mod && mod.__esModule) return mod;
|
|
102
|
+
var result = {};
|
|
103
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
104
|
+
__setModuleDefault(result, mod);
|
|
105
|
+
return result;
|
|
106
|
+
};
|
|
107
|
+
})();
|
|
108
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
109
|
+
exports.LOCAL_NETWORK_DENIED_SUMMARY = exports.LOCAL_NETWORK_DENIED_SUGGESTION = void 0;
|
|
110
|
+
exports.probeLocalNetworkAccess = probeLocalNetworkAccess;
|
|
111
|
+
exports.decideLocalNetworkVerdict = decideLocalNetworkVerdict;
|
|
112
|
+
const dgram = __importStar(require("dgram"));
|
|
113
|
+
const net = __importStar(require("net"));
|
|
114
|
+
const os = __importStar(require("os"));
|
|
115
|
+
/** WS-Discovery group — the exact target `discovery.ts` probes. */
|
|
116
|
+
const WSD_MULTICAST_ADDR = '239.255.255.250';
|
|
117
|
+
const WSD_MULTICAST_PORT = 3702;
|
|
118
|
+
/**
|
|
119
|
+
* A connect that fails this fast never left the host. Real LAN round trips —
|
|
120
|
+
* even an RST from a directly-attached router — were measured at ~1 s; the
|
|
121
|
+
* blocked path returned in 0-1 ms. 250 ms is two orders of magnitude of margin
|
|
122
|
+
* on the blocked side and well under the observed floor on the allowed side.
|
|
123
|
+
*/
|
|
124
|
+
const IMMEDIATE_FAILURE_MS = 250;
|
|
125
|
+
/** Per-target TCP budget. Absent hosts hit this; blocked ones never get near it. */
|
|
126
|
+
const DEFAULT_TCP_TIMEOUT_MS = 1200;
|
|
127
|
+
/** errno names macOS returns for a gated local-network operation. */
|
|
128
|
+
const BLOCKED_ERROR_CODES = new Set(['EHOSTUNREACH', 'ENETUNREACH', 'EPERM', 'EACCES']);
|
|
129
|
+
/** Ports worth trying on a LAN neighbour: a closed one is as good as an open one. */
|
|
130
|
+
const PROBE_PORTS = [80, 443];
|
|
131
|
+
/** IPv4 dotted-quad → uint32. */
|
|
132
|
+
function ipv4ToInt(ip) {
|
|
133
|
+
const parts = ip.split('.');
|
|
134
|
+
if (parts.length !== 4)
|
|
135
|
+
return null;
|
|
136
|
+
let out = 0;
|
|
137
|
+
for (const part of parts) {
|
|
138
|
+
const n = Number(part);
|
|
139
|
+
if (!Number.isInteger(n) || n < 0 || n > 255)
|
|
140
|
+
return null;
|
|
141
|
+
out = (out << 8) | n;
|
|
142
|
+
}
|
|
143
|
+
return out >>> 0;
|
|
144
|
+
}
|
|
145
|
+
function intToIpv4(value) {
|
|
146
|
+
return [(value >>> 24) & 255, (value >>> 16) & 255, (value >>> 8) & 255, value & 255].join('.');
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Directly-connected IPv4 subnets. These are the only place an immediate
|
|
150
|
+
* `EHOSTUNREACH` is diagnostic: the kernel has a route to them by construction,
|
|
151
|
+
* so an instant refusal there came from something above routing.
|
|
152
|
+
*/
|
|
153
|
+
function connectedSubnets(ifaces) {
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const infos of Object.values(ifaces)) {
|
|
156
|
+
if (!infos)
|
|
157
|
+
continue;
|
|
158
|
+
for (const info of infos) {
|
|
159
|
+
// Node <18 reports `family` as 'IPv4'; newer builds may use 4.
|
|
160
|
+
const isV4 = info.family === 'IPv4' || info.family === 4;
|
|
161
|
+
if (!isV4 || info.internal)
|
|
162
|
+
continue;
|
|
163
|
+
const addr = ipv4ToInt(info.address);
|
|
164
|
+
const mask = info.netmask ? ipv4ToInt(info.netmask) : null;
|
|
165
|
+
if (addr === null || mask === null || mask === 0)
|
|
166
|
+
continue;
|
|
167
|
+
// A /32 (point-to-point tunnel: VPN, utun) has no local segment to probe.
|
|
168
|
+
if (mask === 0xffffffff)
|
|
169
|
+
continue;
|
|
170
|
+
out.push({
|
|
171
|
+
address: info.address,
|
|
172
|
+
netmask: info.netmask,
|
|
173
|
+
network: (addr & mask) >>> 0,
|
|
174
|
+
mask,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Every IPv4 address this host owns — loopback and point-to-point tunnels
|
|
182
|
+
* included, which is why this does NOT reuse `connectedSubnets`' filters.
|
|
183
|
+
*
|
|
184
|
+
* A connection to one of our own addresses never leaves the box, so it is
|
|
185
|
+
* never subject to the Local Network gate: it answers (or RSTs) identically
|
|
186
|
+
* whether the grant is allowed or denied. Probing one would therefore produce
|
|
187
|
+
* a `reachableTarget` that is not evidence of anything, and `decideVerdict`
|
|
188
|
+
* treats a reachable target as a VETO over a blocked multicast send — so a
|
|
189
|
+
* self-probe does not merely add noise, it converts a real `denied` into
|
|
190
|
+
* `granted` and puts us straight back to reporting `rtsp_unreachable` while
|
|
191
|
+
* macOS blocks every camera.
|
|
192
|
+
*
|
|
193
|
+
* This is not hypothetical: the `.1` convention in `buildTargets` picks the
|
|
194
|
+
* host's OWN address on any subnet where this Mac is the router — Internet
|
|
195
|
+
* Sharing (192.168.2.1), and the host-only bridges VMware/Parallels/VirtualBox
|
|
196
|
+
* install (172.16.x.1, 10.211.55.1, 192.168.56.1). Those interfaces are
|
|
197
|
+
* non-internal and carry a real netmask, so they survive every other filter.
|
|
198
|
+
*/
|
|
199
|
+
function ownAddresses(ifaces) {
|
|
200
|
+
const own = new Set();
|
|
201
|
+
for (const infos of Object.values(ifaces)) {
|
|
202
|
+
if (!infos)
|
|
203
|
+
continue;
|
|
204
|
+
for (const info of infos) {
|
|
205
|
+
const isV4 = info.family === 'IPv4' || info.family === 4;
|
|
206
|
+
if (isV4 && info.address)
|
|
207
|
+
own.add(info.address);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return own;
|
|
211
|
+
}
|
|
212
|
+
function isOnConnectedSubnet(host, subnets) {
|
|
213
|
+
const addr = ipv4ToInt(host);
|
|
214
|
+
if (addr === null)
|
|
215
|
+
return false;
|
|
216
|
+
return subnets.some((s) => ((addr & s.mask) >>> 0) === s.network);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Probe targets, best first.
|
|
220
|
+
*
|
|
221
|
+
* Configured camera hosts lead because they are known-real LAN devices. The
|
|
222
|
+
* conventional `.1` follows: on a /24 it is the router in the overwhelming
|
|
223
|
+
* majority of deployments, and — critically — a *closed port* on it is just as
|
|
224
|
+
* useful as an open one, because an RST proves the packet made the round trip.
|
|
225
|
+
*
|
|
226
|
+
* `own` removes this host's own addresses from both sources. Only an
|
|
227
|
+
* off-box destination can carry information about a gate that exists to
|
|
228
|
+
* mediate off-box destinations; see `ownAddresses`.
|
|
229
|
+
*/
|
|
230
|
+
function buildTargets(subnets, extraHosts, own) {
|
|
231
|
+
const targets = [];
|
|
232
|
+
const seen = new Set();
|
|
233
|
+
const push = (host) => {
|
|
234
|
+
if (!host || seen.has(host) || own.has(host))
|
|
235
|
+
return;
|
|
236
|
+
seen.add(host);
|
|
237
|
+
targets.push(host);
|
|
238
|
+
};
|
|
239
|
+
for (const host of extraHosts) {
|
|
240
|
+
if (isOnConnectedSubnet(host, subnets))
|
|
241
|
+
push(host);
|
|
242
|
+
}
|
|
243
|
+
for (const subnet of subnets) {
|
|
244
|
+
push(intToIpv4((subnet.network | 1) >>> 0));
|
|
245
|
+
}
|
|
246
|
+
return targets;
|
|
247
|
+
}
|
|
248
|
+
function tcpProbe(host, port, timeoutMs) {
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
const startedAt = Date.now();
|
|
251
|
+
const socket = new net.Socket();
|
|
252
|
+
let settled = false;
|
|
253
|
+
const finish = (outcome, code) => {
|
|
254
|
+
if (settled)
|
|
255
|
+
return;
|
|
256
|
+
settled = true;
|
|
257
|
+
try {
|
|
258
|
+
socket.destroy();
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* already gone */
|
|
262
|
+
}
|
|
263
|
+
resolve({ target: `${host}:${port}`, outcome, code, elapsedMs: Date.now() - startedAt });
|
|
264
|
+
};
|
|
265
|
+
socket.setTimeout(timeoutMs, () => finish('silent'));
|
|
266
|
+
socket.once('connect', () => finish('reachable'));
|
|
267
|
+
socket.once('error', (err) => {
|
|
268
|
+
const code = err.code ?? 'UNKNOWN';
|
|
269
|
+
const elapsed = Date.now() - startedAt;
|
|
270
|
+
// An RST is positive proof the LAN carried our packet and brought an
|
|
271
|
+
// answer back — it means the host is there and declining, not blocked.
|
|
272
|
+
if (code === 'ECONNREFUSED' || code === 'ECONNRESET')
|
|
273
|
+
return finish('reachable', code);
|
|
274
|
+
// Blocked errnos only count when they arrive impossibly fast. The same
|
|
275
|
+
// errno after a real timeout is an ordinary unreachable host.
|
|
276
|
+
if (BLOCKED_ERROR_CODES.has(code) && elapsed <= IMMEDIATE_FAILURE_MS) {
|
|
277
|
+
return finish('blocked', code);
|
|
278
|
+
}
|
|
279
|
+
finish('silent', code);
|
|
280
|
+
});
|
|
281
|
+
try {
|
|
282
|
+
socket.connect(port, host);
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
finish('silent', err?.code);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The WS-Discovery multicast send — the single strongest signal available.
|
|
291
|
+
*
|
|
292
|
+
* An empty LAN still accepts this datagram; only the TCC gate rejects the
|
|
293
|
+
* `sendto` itself. Nothing is expected to answer, so no listener is set up.
|
|
294
|
+
*/
|
|
295
|
+
function multicastProbe(interfaceAddress) {
|
|
296
|
+
return new Promise((resolve) => {
|
|
297
|
+
let socket;
|
|
298
|
+
try {
|
|
299
|
+
socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
return resolve({ result: 'error', code: err?.code });
|
|
303
|
+
}
|
|
304
|
+
let settled = false;
|
|
305
|
+
const finish = (result, code) => {
|
|
306
|
+
if (settled)
|
|
307
|
+
return;
|
|
308
|
+
settled = true;
|
|
309
|
+
try {
|
|
310
|
+
socket.close();
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
/* already closed */
|
|
314
|
+
}
|
|
315
|
+
resolve({ result, code });
|
|
316
|
+
};
|
|
317
|
+
const classify = (code) => code && BLOCKED_ERROR_CODES.has(code) ? finish('blocked', code) : finish('error', code);
|
|
318
|
+
socket.once('error', (err) => classify(err.code));
|
|
319
|
+
// Guard against a socket that never settles for any reason.
|
|
320
|
+
const guard = setTimeout(() => finish('error', 'ETIMEDOUT'), 2000);
|
|
321
|
+
if (typeof guard.unref === 'function')
|
|
322
|
+
guard.unref();
|
|
323
|
+
try {
|
|
324
|
+
socket.bind(0, () => {
|
|
325
|
+
try {
|
|
326
|
+
socket.setMulticastTTL(1);
|
|
327
|
+
// Pin the egress to the LAN interface. Without this the kernel picks
|
|
328
|
+
// one from the route table, and on a host with Tailscale/ZeroTier/a
|
|
329
|
+
// corporate VPN that route is the tunnel — `route to 239.255.255.250
|
|
330
|
+
// -> utun4` was measured on the very machine this was developed on,
|
|
331
|
+
// while the cameras sat on en0. A probe that leaves down the tunnel
|
|
332
|
+
// is not testing the path the cameras are on.
|
|
333
|
+
if (interfaceAddress)
|
|
334
|
+
socket.setMulticastInterface(interfaceAddress);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
/* not fatal — the send is what we are measuring */
|
|
338
|
+
}
|
|
339
|
+
const payload = Buffer.from('<trillboards:probe/>');
|
|
340
|
+
socket.send(payload, 0, payload.length, WSD_MULTICAST_PORT, WSD_MULTICAST_ADDR, (err) => {
|
|
341
|
+
clearTimeout(guard);
|
|
342
|
+
if (err)
|
|
343
|
+
return classify(err.code);
|
|
344
|
+
finish('sent');
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
clearTimeout(guard);
|
|
350
|
+
classify(err?.code);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Decide whether this process can reach the local network.
|
|
356
|
+
*
|
|
357
|
+
* Never throws, never rejects. Ambiguity always resolves toward `unknown`.
|
|
358
|
+
*/
|
|
359
|
+
async function probeLocalNetworkAccess(options = {}) {
|
|
360
|
+
const platform = options.platform ?? process.platform;
|
|
361
|
+
const emptyEvidence = {
|
|
362
|
+
multicastSend: 'skipped',
|
|
363
|
+
silentTargets: [],
|
|
364
|
+
loopbackUsable: false,
|
|
365
|
+
subnets: [],
|
|
366
|
+
};
|
|
367
|
+
// Only macOS gates local-network traffic. Reporting anything else on Linux or
|
|
368
|
+
// Windows would invent a permission problem that cannot exist there.
|
|
369
|
+
if (platform !== 'darwin') {
|
|
370
|
+
return {
|
|
371
|
+
verdict: 'not_applicable',
|
|
372
|
+
reason: `${platform} has no Local Network permission gate`,
|
|
373
|
+
evidence: emptyEvidence,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const ifaces = (options.networkInterfaces ?? os.networkInterfaces)();
|
|
377
|
+
const subnets = connectedSubnets(ifaces);
|
|
378
|
+
const evidence = {
|
|
379
|
+
...emptyEvidence,
|
|
380
|
+
subnets: subnets.map((s) => `${s.address}/${s.netmask}`),
|
|
381
|
+
};
|
|
382
|
+
if (subnets.length === 0) {
|
|
383
|
+
return {
|
|
384
|
+
verdict: 'unknown',
|
|
385
|
+
reason: 'no directly-connected IPv4 network — cannot assess Local Network access',
|
|
386
|
+
evidence,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const timeoutMs = options.tcpTimeoutMs ?? DEFAULT_TCP_TIMEOUT_MS;
|
|
390
|
+
const runTcp = options.tcpProbeImpl ?? tcpProbe;
|
|
391
|
+
const runMulticast = options.multicastProbeImpl ?? multicastProbe;
|
|
392
|
+
// Control: prove this process can open sockets at all. Loopback is never
|
|
393
|
+
// gated, so a failure here means something other than a Local Network denial.
|
|
394
|
+
// ECONNREFUSED on a closed port is the expected, healthy answer.
|
|
395
|
+
const loopback = await runTcp('127.0.0.1', 9, Math.min(timeoutMs, 500));
|
|
396
|
+
evidence.loopbackUsable = loopback.outcome === 'reachable' || loopback.outcome === 'silent';
|
|
397
|
+
const multicast = await runMulticast(subnets[0].address);
|
|
398
|
+
evidence.multicastSend = multicast.result;
|
|
399
|
+
if (multicast.code)
|
|
400
|
+
evidence.multicastErrorCode = multicast.code;
|
|
401
|
+
const targets = buildTargets(subnets, options.extraHosts ?? [], ownAddresses(ifaces));
|
|
402
|
+
for (const host of targets) {
|
|
403
|
+
for (const port of PROBE_PORTS) {
|
|
404
|
+
const outcome = await runTcp(host, port, timeoutMs);
|
|
405
|
+
if (outcome.outcome === 'reachable') {
|
|
406
|
+
evidence.reachableTarget = outcome.target;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
if (outcome.outcome === 'blocked') {
|
|
410
|
+
evidence.blockedTarget = outcome.target;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
evidence.silentTargets.push(outcome.target);
|
|
414
|
+
}
|
|
415
|
+
// Positive reachability is decisive; stop as soon as we have it.
|
|
416
|
+
if (evidence.reachableTarget)
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
const decision = decideLocalNetworkVerdict(evidence);
|
|
420
|
+
return { ...decision, evidence };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The verdict rule, isolated from all I/O.
|
|
424
|
+
*
|
|
425
|
+
* Split out so it can be driven from the socket outcomes *recorded on a Mac
|
|
426
|
+
* that was actually blocked*. The OS-level denial could not be re-induced on
|
|
427
|
+
* demand after it cleared, so replaying the measured evidence through this
|
|
428
|
+
* exact function is the only way the `denied` branch gets tested against real
|
|
429
|
+
* data rather than against an invented signature.
|
|
430
|
+
*/
|
|
431
|
+
function decideLocalNetworkVerdict(evidence) {
|
|
432
|
+
// Anything on the local segment answered us — the grant cannot be missing.
|
|
433
|
+
// This veto runs first on purpose: it is the only unambiguous evidence here.
|
|
434
|
+
if (evidence.reachableTarget) {
|
|
435
|
+
return {
|
|
436
|
+
verdict: 'granted',
|
|
437
|
+
reason: `local network reachable (${evidence.reachableTarget} answered)`,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (!evidence.loopbackUsable) {
|
|
441
|
+
return {
|
|
442
|
+
verdict: 'unknown',
|
|
443
|
+
reason: 'loopback socket unusable — this process cannot open sockets at all',
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
// The multicast send is the discriminator. An empty segment still ACCEPTS the
|
|
447
|
+
// datagram — nothing answers, but `sendto` succeeds — so a refusal here is not
|
|
448
|
+
// something an absent camera, an absent recorder, or an empty VLAN can produce.
|
|
449
|
+
if (evidence.multicastSend === 'blocked') {
|
|
450
|
+
const corroborated = Boolean(evidence.blockedTarget);
|
|
451
|
+
return {
|
|
452
|
+
verdict: 'denied',
|
|
453
|
+
reason: corroborated
|
|
454
|
+
? `Local Network permission denied: multicast send refused (${evidence.multicastErrorCode}) ` +
|
|
455
|
+
`and ${evidence.blockedTarget} refused instantly on a directly-connected subnet`
|
|
456
|
+
: `Local Network permission denied: multicast send to ${WSD_MULTICAST_ADDR} refused ` +
|
|
457
|
+
`(${evidence.multicastErrorCode}) while loopback works`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
// An instant blocked errno toward our own subnet, without the multicast
|
|
461
|
+
// corroboration, is suggestive but not conclusive — say so rather than guess.
|
|
462
|
+
// A reject route produces exactly this shape, and it is NOT a TCC denial.
|
|
463
|
+
if (evidence.blockedTarget) {
|
|
464
|
+
return {
|
|
465
|
+
verdict: 'unknown',
|
|
466
|
+
reason: `${evidence.blockedTarget} refused instantly on a directly-connected subnet, ` +
|
|
467
|
+
'but the multicast probe was not refused — inconclusive',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
verdict: 'unknown',
|
|
472
|
+
reason: evidence.silentTargets.length > 0
|
|
473
|
+
? `no answer from ${evidence.silentTargets.length} local target(s) — ` +
|
|
474
|
+
'the segment may simply be empty'
|
|
475
|
+
: 'no probeable local targets found',
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The operator-facing instruction for a denied grant.
|
|
480
|
+
*
|
|
481
|
+
* Names the pane and says plainly that a human has to be at the machine, because
|
|
482
|
+
* the one thing that cannot be automated here is the only thing that fixes it.
|
|
483
|
+
*/
|
|
484
|
+
exports.LOCAL_NETWORK_DENIED_SUGGESTION = 'Open System Settings › Privacy & Security › Local Network and turn on access for ' +
|
|
485
|
+
'Trillboards. Someone has to do this at the Mac itself — the grant cannot be enabled remotely, ' +
|
|
486
|
+
'and macOS only offers it to an app a person is signed in to.';
|
|
487
|
+
/** Same message, one line, for a log or a health beacon. */
|
|
488
|
+
exports.LOCAL_NETWORK_DENIED_SUMMARY = 'macOS is blocking this connector from reaching the local network ' +
|
|
489
|
+
'(System Settings › Privacy & Security › Local Network)';
|
|
490
|
+
//# sourceMappingURL=localNetworkAccess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localNetworkAccess.js","sourceRoot":"","sources":["../src/localNetworkAccess.ts"],"names":[],"mappings":"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"}
|
package/dist/sensingRunMode.d.ts
CHANGED
|
@@ -73,6 +73,17 @@ export declare const NO_FEEDS_MESSAGE: string;
|
|
|
73
73
|
* 24/7 log, short enough that an operator watching an install sees it.
|
|
74
74
|
*/
|
|
75
75
|
export declare const SENSING_HEALTH_LOG_INTERVAL_MS: number;
|
|
76
|
+
/**
|
|
77
|
+
* How often a CONTINUING transport failure is restated after its first report.
|
|
78
|
+
*
|
|
79
|
+
* A TLS-inspecting proxy fails every upload on every feed, so an unthrottled
|
|
80
|
+
* report is 32 cameras x every cadence tick — which buries the one thing an
|
|
81
|
+
* operator needs to read under the noise it generates, and burns the 20 MB log
|
|
82
|
+
* budget doing it. The first occurrence and every CHANGE of cause are always
|
|
83
|
+
* reported in full; a continuing one restates itself on this interval with a
|
|
84
|
+
* count, so an operator who opens the log an hour later still sees it.
|
|
85
|
+
*/
|
|
86
|
+
export declare const TRANSPORT_FAILURE_RESTATE_INTERVAL_MS: number;
|
|
76
87
|
/**
|
|
77
88
|
* The operator-facing degradation line. A machine that cannot analyze 16
|
|
78
89
|
* cameras every 30 s is not a bug to hide — it is a capacity fact to state, in
|
|
@@ -80,6 +91,31 @@ export declare const SENSING_HEALTH_LOG_INTERVAL_MS: number;
|
|
|
80
91
|
* at, how many analyses/minute this machine sustains, and where the rest went.
|
|
81
92
|
*/
|
|
82
93
|
export declare function formatSensingHealth(health: ReturnType<CloudSensingManager['getSensingHealth']>): string;
|
|
94
|
+
/**
|
|
95
|
+
* Wire the manager's lifecycle events to the IO sink — identical for both modes
|
|
96
|
+
* except the per-frame "analyzed" line (cloud says `analyze →`, local says
|
|
97
|
+
* `local→preAnalyzed →`).
|
|
98
|
+
*
|
|
99
|
+
* The cascade's per-frame outcomes (`motionSkipped`, `admissionDenied`) are
|
|
100
|
+
* deliberately NOT logged per event: on a saturated 16-camera connector they
|
|
101
|
+
* would be tens of thousands of lines a day and would bury the events that
|
|
102
|
+
* need a human. They stay on the EventEmitter for embedders, and reach the
|
|
103
|
+
* operator as counts on the periodic capacity line below.
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* The operator-facing reporter for "I cannot reach the API, and here is why".
|
|
107
|
+
*
|
|
108
|
+
* Returns `true` from `report()` when it handled the error, so the caller can
|
|
109
|
+
* fall through to its ordinary per-feed line for everything else.
|
|
110
|
+
*
|
|
111
|
+
* Exported for tests: the properties worth pinning are that the FIRST failure
|
|
112
|
+
* is stated in full, that a CHANGE of cause is stated in full (otherwise a
|
|
113
|
+
* fixed trust problem followed by a proxy problem would be silent), and that a
|
|
114
|
+
* continuing one is throttled rather than repeated per feed per tick.
|
|
115
|
+
*/
|
|
116
|
+
export declare function createTransportFailureReporter(io: SensingIo, now?: () => number): {
|
|
117
|
+
report(name: string, error: Error): boolean;
|
|
118
|
+
};
|
|
83
119
|
/**
|
|
84
120
|
* Build (but do NOT start) the thin cloud-sensing manager: RTSP frame → POST
|
|
85
121
|
* /v2/earner/audience-analyze (cloud VLM does the inference). Returns `null`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sensingRunMode.d.ts","sourceRoot":"","sources":["../src/sensingRunMode.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EAInB,KAAK,YAAY,EAElB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"sensingRunMode.d.ts","sourceRoot":"","sources":["../src/sensingRunMode.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EAInB,KAAK,YAAY,EAElB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAOhD;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC/C,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,CAClB,IAAI,EAAE,YAAY,EAClB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,KAAK,KACT,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC7B;AA2CD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB;;;;;;;;;;;;;;OAcG;IACH,gBAAgB,CAAC,IAAI;QACnB,eAAe,EAAE,MAAM,CAAC;QACxB;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,eAAe,CAAC,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;KACxC,CAAC;CACH;AAED,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,qEAAqE;AACrE,eAAO,MAAM,SAAS,EAAE,SAIvB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAEgD,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,QAAgB,CAAC;AAE5D;;;;;;;;;GASG;AACH,eAAO,MAAM,qCAAqC,QAAiB,CAAC;AAuNpE;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,CAAC,mBAAmB,CAAC,kBAAkB,CAAC,CAAC,GAC1D,MAAM,CA6CR;AAED;;;;;;;;;;GAUG;AACH;;;;;;;;;;GAUG;AACH,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,SAAS,EACb,GAAG,GAAE,MAAM,MAAiB,GAC3B;IAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAA;CAAE,CA+BjD;AAoED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,eAAe,EACvB,EAAE,GAAE,SAAqB,EACzB,IAAI,CAAC,EAAE,kBAAkB,GACxB,mBAAmB,GAAG,IAAI,CAiE5B;AAqCD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,eAAe,EACvB,EAAE,GAAE,SAAqB,EACzB,IAAI,CAAC,EAAE,kBAAkB,GACxB,mBAAmB,GAAG,IAAI,CAyF5B"}
|
package/dist/sensingRunMode.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SENSING_HEALTH_LOG_INTERVAL_MS = exports.NO_FEEDS_MESSAGE = exports.consoleIo = void 0;
|
|
3
|
+
exports.TRANSPORT_FAILURE_RESTATE_INTERVAL_MS = exports.SENSING_HEALTH_LOG_INTERVAL_MS = exports.NO_FEEDS_MESSAGE = exports.consoleIo = void 0;
|
|
4
4
|
exports.formatSensingHealth = formatSensingHealth;
|
|
5
|
+
exports.createTransportFailureReporter = createTransportFailureReporter;
|
|
5
6
|
exports.buildCloudSensingManager = buildCloudSensingManager;
|
|
6
7
|
exports.buildLocalSensingManager = buildLocalSensingManager;
|
|
7
8
|
const CloudSensingManager_1 = require("./CloudSensingManager");
|
|
@@ -9,6 +10,7 @@ const localSensing_1 = require("./localSensing");
|
|
|
9
10
|
const SensingProfileClient_1 = require("./SensingProfileClient");
|
|
10
11
|
const deployedCadenceSource_1 = require("./deployedCadenceSource");
|
|
11
12
|
const RecorderAddressRecovery_1 = require("./RecorderAddressRecovery");
|
|
13
|
+
const transportFailure_1 = require("./transportFailure");
|
|
12
14
|
/**
|
|
13
15
|
* Shared construction of the cloud- and local-sensing managers.
|
|
14
16
|
*
|
|
@@ -62,6 +64,17 @@ exports.NO_FEEDS_MESSAGE = 'No camera feeds configured. Set `cameras: [{ name, s
|
|
|
62
64
|
* 24/7 log, short enough that an operator watching an install sees it.
|
|
63
65
|
*/
|
|
64
66
|
exports.SENSING_HEALTH_LOG_INTERVAL_MS = 5 * 60 * 1000;
|
|
67
|
+
/**
|
|
68
|
+
* How often a CONTINUING transport failure is restated after its first report.
|
|
69
|
+
*
|
|
70
|
+
* A TLS-inspecting proxy fails every upload on every feed, so an unthrottled
|
|
71
|
+
* report is 32 cameras x every cadence tick — which buries the one thing an
|
|
72
|
+
* operator needs to read under the noise it generates, and burns the 20 MB log
|
|
73
|
+
* budget doing it. The first occurrence and every CHANGE of cause are always
|
|
74
|
+
* reported in full; a continuing one restates itself on this interval with a
|
|
75
|
+
* count, so an operator who opens the log an hour later still sees it.
|
|
76
|
+
*/
|
|
77
|
+
exports.TRANSPORT_FAILURE_RESTATE_INTERVAL_MS = 10 * 60 * 1000;
|
|
65
78
|
const CADENCE_CLAMP_WORDING = {
|
|
66
79
|
no_request: '',
|
|
67
80
|
operator_request: '',
|
|
@@ -319,6 +332,47 @@ function formatSensingHealth(health) {
|
|
|
319
332
|
* need a human. They stay on the EventEmitter for embedders, and reach the
|
|
320
333
|
* operator as counts on the periodic capacity line below.
|
|
321
334
|
*/
|
|
335
|
+
/**
|
|
336
|
+
* The operator-facing reporter for "I cannot reach the API, and here is why".
|
|
337
|
+
*
|
|
338
|
+
* Returns `true` from `report()` when it handled the error, so the caller can
|
|
339
|
+
* fall through to its ordinary per-feed line for everything else.
|
|
340
|
+
*
|
|
341
|
+
* Exported for tests: the properties worth pinning are that the FIRST failure
|
|
342
|
+
* is stated in full, that a CHANGE of cause is stated in full (otherwise a
|
|
343
|
+
* fixed trust problem followed by a proxy problem would be silent), and that a
|
|
344
|
+
* continuing one is throttled rather than repeated per feed per tick.
|
|
345
|
+
*/
|
|
346
|
+
function createTransportFailureReporter(io, now = Date.now) {
|
|
347
|
+
let lastClass = null;
|
|
348
|
+
let lastFullReportAtMs = 0;
|
|
349
|
+
let sinceLastReport = 0;
|
|
350
|
+
const feeds = new Set();
|
|
351
|
+
return {
|
|
352
|
+
report(name, error) {
|
|
353
|
+
if (!(error instanceof transportFailure_1.TransportFailure))
|
|
354
|
+
return false;
|
|
355
|
+
feeds.add(name);
|
|
356
|
+
sinceLastReport++;
|
|
357
|
+
const at = now();
|
|
358
|
+
const isNewCause = error.failureClass !== lastClass;
|
|
359
|
+
const isDue = at - lastFullReportAtMs >= exports.TRANSPORT_FAILURE_RESTATE_INTERVAL_MS;
|
|
360
|
+
if (!isNewCause && !isDue)
|
|
361
|
+
return true;
|
|
362
|
+
const suppressed = sinceLastReport - 1;
|
|
363
|
+
for (const line of (0, transportFailure_1.formatTransportFailure)(error))
|
|
364
|
+
io.error(line);
|
|
365
|
+
io.error(` Affected feeds: ${[...feeds].join(', ')}` +
|
|
366
|
+
(suppressed > 0 ? ` (${suppressed} further attempt(s) since the last report)` : ''));
|
|
367
|
+
lastClass = error.failureClass;
|
|
368
|
+
lastFullReportAtMs = at;
|
|
369
|
+
sinceLastReport = 0;
|
|
370
|
+
feeds.clear();
|
|
371
|
+
feeds.add(name);
|
|
372
|
+
return true;
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
322
376
|
function wireSensingLogs(manager, io, analyzedLine) {
|
|
323
377
|
manager.on('analyzed', ({ name, response }) => {
|
|
324
378
|
io.log(analyzedLine(name, response));
|
|
@@ -344,7 +398,15 @@ function wireSensingLogs(manager, io, analyzedLine) {
|
|
|
344
398
|
manager.on('reconnected', ({ name }) => {
|
|
345
399
|
io.log(`[${name}] RTSP reconnected — sensing resumed`);
|
|
346
400
|
});
|
|
401
|
+
const transportReporter = createTransportFailureReporter(io);
|
|
347
402
|
manager.on('error', ({ name, error }) => {
|
|
403
|
+
// A TRANSPORT failure is not a feed error and must not be filed as one:
|
|
404
|
+
// nothing reached the server, so there is no status code, the health beacon
|
|
405
|
+
// cannot get through either, and the fix is on the customer's network. It
|
|
406
|
+
// gets its own block naming the cause and the action. A REJECTION — the
|
|
407
|
+
// server answering non-2xx — stays on `analyzeRejected` with its status.
|
|
408
|
+
if (transportReporter.report(name, error))
|
|
409
|
+
return;
|
|
348
410
|
io.error(`[${name}] error: ${error.message}`);
|
|
349
411
|
});
|
|
350
412
|
// Periodic capacity line. `unref()` so it can never hold the process open.
|