@trillboards/edge-sdk 0.2.21 → 0.2.25
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/build-windows.ps1 +1 -0
- package/deploy/windows/e2e/test-enterprise-trust-store.ps1 +341 -0
- package/deploy/windows/enroll-connector.ps1 +169 -0
- package/deploy/windows/installer.nsi +102 -6
- package/deploy/windows/run-connector.ps1 +30 -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/ConnectorCameraClient.d.ts +79 -0
- package/dist/ConnectorCameraClient.d.ts.map +1 -0
- package/dist/ConnectorCameraClient.js +114 -0
- package/dist/ConnectorCameraClient.js.map +1 -0
- package/dist/EdgeAgent.js +1 -1
- package/dist/EnrollClient.d.ts +81 -0
- package/dist/EnrollClient.d.ts.map +1 -0
- package/dist/EnrollClient.js +97 -0
- package/dist/EnrollClient.js.map +1 -0
- package/dist/cli.d.ts +49 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +392 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.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 @@
|
|
|
1
|
+
{"version":3,"file":"EnrollClient.d.ts","sourceRoot":"","sources":["../src/EnrollClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,mBAAmB;IAClC,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,eAAe,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,4BAA4B;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;CAClD;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;gBAExB,OAAO,EAAE,mBAAmB,EAAE,MAAM,GAAE,UAA8B;IAU1E,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;CAqFxD"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EnrollClient = void 0;
|
|
4
|
+
const AudienceAnalyzeClient_1 = require("./AudienceAnalyzeClient");
|
|
5
|
+
class EnrollClient {
|
|
6
|
+
constructor(options, poster = AudienceAnalyzeClient_1.defaultHttpPoster) {
|
|
7
|
+
if (!options?.apiBaseUrl) {
|
|
8
|
+
throw new Error('EnrollClient requires an apiBaseUrl');
|
|
9
|
+
}
|
|
10
|
+
const base = options.apiBaseUrl.replace(/\/+$/, '');
|
|
11
|
+
this.endpoint = `${base}/v2/earner/connectors/enroll`;
|
|
12
|
+
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
13
|
+
this.poster = poster;
|
|
14
|
+
}
|
|
15
|
+
async enroll(req) {
|
|
16
|
+
const token = (req?.enrollmentToken ?? '').trim();
|
|
17
|
+
if (!token) {
|
|
18
|
+
return { ok: false, error: 'enroll() requires a non-empty enrollmentToken', kind: 'invalid' };
|
|
19
|
+
}
|
|
20
|
+
if (!req?.machineId) {
|
|
21
|
+
return { ok: false, error: 'enroll() requires a machineId', kind: 'invalid' };
|
|
22
|
+
}
|
|
23
|
+
const body = { machineId: req.machineId };
|
|
24
|
+
if (req.siteLabel)
|
|
25
|
+
body.siteLabel = req.siteLabel;
|
|
26
|
+
if (req.hostname)
|
|
27
|
+
body.hostname = req.hostname;
|
|
28
|
+
if (req.platform)
|
|
29
|
+
body.platform = req.platform;
|
|
30
|
+
if (req.connectorVersion)
|
|
31
|
+
body.connectorVersion = req.connectorVersion;
|
|
32
|
+
let response;
|
|
33
|
+
try {
|
|
34
|
+
response = await this.poster(this.endpoint, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
Accept: 'application/json',
|
|
39
|
+
Authorization: `Bearer ${token}`,
|
|
40
|
+
},
|
|
41
|
+
timeoutMs: this.timeoutMs,
|
|
42
|
+
}, JSON.stringify(body));
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: `enrollment request failed: ${err?.message ?? 'unknown error'}`,
|
|
48
|
+
kind: 'network',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let parsed = null;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(response.body);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
parsed = null;
|
|
57
|
+
}
|
|
58
|
+
const code = response.statusCode;
|
|
59
|
+
if (code < 200 || code >= 300) {
|
|
60
|
+
// 401/403 is terminal: the credential is wrong, spent, expired or revoked.
|
|
61
|
+
// The server deliberately does not say WHICH — see consumeKeyUse — so the
|
|
62
|
+
// connector cannot tell either, and must not retry as if it could.
|
|
63
|
+
const kind = code === 401 || code === 403
|
|
64
|
+
? 'auth'
|
|
65
|
+
: code === 400
|
|
66
|
+
? 'invalid'
|
|
67
|
+
: 'server';
|
|
68
|
+
const message = parsed?.error
|
|
69
|
+
|| parsed?.message
|
|
70
|
+
|| `enrollment failed (HTTP ${code})`;
|
|
71
|
+
return { ok: false, error: message, kind };
|
|
72
|
+
}
|
|
73
|
+
const connectorKey = typeof parsed?.connectorKey === 'string' ? parsed.connectorKey : '';
|
|
74
|
+
const connectorId = typeof parsed?.connectorId === 'string' ? parsed.connectorId : '';
|
|
75
|
+
if (!connectorKey || !connectorId) {
|
|
76
|
+
// A 2xx that carries no credential is the ingestion-endpoint trap in
|
|
77
|
+
// reverse: treating it as success would leave a "provisioned" machine with
|
|
78
|
+
// nothing to authenticate with, and the failure would only surface much
|
|
79
|
+
// later as a camera that never registers.
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: 'enrollment response did not contain a connector credential',
|
|
83
|
+
kind: 'server',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
connectorId,
|
|
89
|
+
connectorKey,
|
|
90
|
+
siteLabel: parsed?.siteLabel ?? null,
|
|
91
|
+
reEnrollment: parsed?.reEnrollment === true,
|
|
92
|
+
remainingEnrollments: typeof parsed?.remainingEnrollments === 'number' ? parsed.remainingEnrollments : null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.EnrollClient = EnrollClient;
|
|
97
|
+
//# sourceMappingURL=EnrollClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EnrollClient.js","sourceRoot":"","sources":["../src/EnrollClient.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -26,4 +26,53 @@ export declare function configWritePermissionMessage(resolvedConfigPath: string)
|
|
|
26
26
|
export declare function writePairedConfig(resolvedConfigPath: string, serialized: string, writeFile?: (p: string, data: string, opts: {
|
|
27
27
|
mode: number;
|
|
28
28
|
}) => void): void;
|
|
29
|
+
/** Show a credential's SHAPE without showing the credential. */
|
|
30
|
+
export declare function maskConnectorKey(key: string | undefined | null): string;
|
|
31
|
+
/**
|
|
32
|
+
* Read the enrollment token for an unattended install, in precedence order.
|
|
33
|
+
*
|
|
34
|
+
* 1. `--enrollment-token <value>` — for a scripted/manual run.
|
|
35
|
+
* 2. `TRILLBOARDS_ENROLLMENT_TOKEN` — the Docker/systemd path.
|
|
36
|
+
* 3. `--enrollment-file <path>` / the default bootstrap file next to the config
|
|
37
|
+
* — what the Windows installer writes from `/ENROLLMENT_TOKEN=`.
|
|
38
|
+
*
|
|
39
|
+
* The FILE is the preferred form for MDM and the reason this function exists:
|
|
40
|
+
* an argument shows up in the process table and an env var is inherited by every
|
|
41
|
+
* child process, while a file can be read once and DELETED. That deletion is not
|
|
42
|
+
* hygiene, it is the design — the fleet credential must not outlive the
|
|
43
|
+
* bootstrap, or every store PC keeps something that can enroll more machines.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readEnrollmentToken(args: string[], configPath: string, env?: NodeJS.ProcessEnv, readFile?: (p: string) => string, exists?: (p: string) => boolean): {
|
|
46
|
+
token: string | null;
|
|
47
|
+
source: 'flag' | 'env' | 'file' | null;
|
|
48
|
+
filePath: string | null;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* UNATTENDED first-boot provisioning — the MDM/Intune/SCCM path.
|
|
52
|
+
*
|
|
53
|
+
* trillboards-edge enroll [--enrollment-token T | --enrollment-file P]
|
|
54
|
+
* [--site "Store 0417"] [--config PATH] [--api-base URL]
|
|
55
|
+
*
|
|
56
|
+
* Exchanges the FLEET enrollment credential shipped in the deployment package
|
|
57
|
+
* for THIS MACHINE's own connector credential and writes it to the config. No
|
|
58
|
+
* human touches the machine, which is the entire point: `pair` needs a code
|
|
59
|
+
* typed in and `setup` needs a code read off the screen, so neither can be
|
|
60
|
+
* automated across N stores.
|
|
61
|
+
*
|
|
62
|
+
* IDEMPOTENT BY DESIGN. Re-running on an already-enrolled machine is safe and
|
|
63
|
+
* FREE — the server keys on `machineId` and does not spend another use of the
|
|
64
|
+
* bounded fleet credential. MDM re-pushes packages; if a re-push cost a use, a
|
|
65
|
+
* 200-use credential would die partway through a 200-store rollout.
|
|
66
|
+
*
|
|
67
|
+
* ON SUCCESS THE ENROLLMENT FILE IS DELETED. See readEnrollmentToken.
|
|
68
|
+
*
|
|
69
|
+
* Exit codes (the install wrappers branch on these — keep stable, and note they
|
|
70
|
+
* mirror `pair`'s so a wrapper can treat the two paths alike):
|
|
71
|
+
* 0 enrolled (or already enrolled), config written
|
|
72
|
+
* 1 the credential was rejected — wrong, expired, revoked or exhausted. A
|
|
73
|
+
* human must issue a new one; retrying cannot help.
|
|
74
|
+
* 4 no enrollment token was supplied at all (not an unattended install)
|
|
75
|
+
* 6 network/server failure — the credential is UNSPENT. Retry later.
|
|
76
|
+
*/
|
|
77
|
+
export declare function runEnroll(args: string[]): Promise<void>;
|
|
29
78
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAscA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAKvD;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAW/E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,MAAM,EAC1B,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,KAAK,IAC/B,GAChC,IAAI,CAWN;AAUD,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CASvE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,QAAQ,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAA2C,EACpE,MAAM,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAiC,GACvD;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAyB3F;AAgGD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+K7D"}
|
package/dist/cli.js
CHANGED
|
@@ -37,6 +37,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
37
37
|
exports.isPermissionError = isPermissionError;
|
|
38
38
|
exports.configWritePermissionMessage = configWritePermissionMessage;
|
|
39
39
|
exports.writePairedConfig = writePairedConfig;
|
|
40
|
+
exports.maskConnectorKey = maskConnectorKey;
|
|
41
|
+
exports.readEnrollmentToken = readEnrollmentToken;
|
|
42
|
+
exports.runEnroll = runEnroll;
|
|
40
43
|
const config_1 = require("./config");
|
|
41
44
|
const autoSense_1 = require("./autoSense");
|
|
42
45
|
const sensingRunMode_1 = require("./sensingRunMode");
|
|
@@ -69,9 +72,69 @@ const crypto_1 = require("crypto");
|
|
|
69
72
|
// package.json. Required at runtime (commonjs), like the lazy command handlers.
|
|
70
73
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
71
74
|
const VERSION = require('../package.json').version;
|
|
75
|
+
/** The API the connector talks to, resolved the same way every command does. */
|
|
76
|
+
function resolveApiBaseUrl(args) {
|
|
77
|
+
return (getArg(args, '--api-base') || process.env.TRILLBOARDS_API_BASE || 'https://api.trillboards.com');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Read the machine's trust store and proxy, install them on `https.globalAgent`,
|
|
81
|
+
* and PRINT what was found.
|
|
82
|
+
*
|
|
83
|
+
* The printed line is the whole point. A TLS-inspecting proxy used to leave the
|
|
84
|
+
* connector unable to reach the API in a background process with no window and
|
|
85
|
+
* no log line naming a cause — the install had completed, so the customer
|
|
86
|
+
* believed the machine was working. Now the first two lines of `connector.log`
|
|
87
|
+
* on every start say which trust anchors and which proxy are in effect, so the
|
|
88
|
+
* difference between "no proxy configured" and "a proxy is configured and we
|
|
89
|
+
* are using it" is legible before anything fails.
|
|
90
|
+
*
|
|
91
|
+
* Never throws: a machine whose trust store cannot be read must still start,
|
|
92
|
+
* with Node's bundled roots, exactly as it did before this existed.
|
|
93
|
+
*/
|
|
94
|
+
function initEnterpriseNetworking(args) {
|
|
95
|
+
try {
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
97
|
+
const { installEnterpriseNetworking } = require('./enterpriseNetwork');
|
|
98
|
+
const posture = installEnterpriseNetworking(resolveApiBaseUrl(args));
|
|
99
|
+
for (const line of posture.lines)
|
|
100
|
+
console.log(`[network] ${line}`);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.warn(`[network] could not resolve this machine's trust store or proxy: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Commands that talk to the cloud, and therefore need the enterprise trust
|
|
108
|
+
* store and proxy installed before their first request.
|
|
109
|
+
*
|
|
110
|
+
* An explicit list rather than "always": `version`, `help` and `demo` make no
|
|
111
|
+
* network call, and reading a certificate store costs a subprocess.
|
|
112
|
+
*/
|
|
113
|
+
const NETWORKED_COMMANDS = new Set([
|
|
114
|
+
'start',
|
|
115
|
+
'cloud-sense',
|
|
116
|
+
'local-sense',
|
|
117
|
+
'auto-sense',
|
|
118
|
+
'sense',
|
|
119
|
+
'combined-sense',
|
|
120
|
+
'pair',
|
|
121
|
+
'setup',
|
|
122
|
+
'verify-connection',
|
|
123
|
+
'discover',
|
|
124
|
+
'diagnose',
|
|
125
|
+
'status',
|
|
126
|
+
'mcp',
|
|
127
|
+
// `download-models --from <url>` fetches over a bare `https.get` (:660). It
|
|
128
|
+
// is the one command whose entire job is a download, so leaving it out would
|
|
129
|
+
// have meant the fix for TLS inspection did not cover the case an operator
|
|
130
|
+
// most obviously runs into it.
|
|
131
|
+
'download-models',
|
|
132
|
+
]);
|
|
72
133
|
async function main() {
|
|
73
134
|
const args = process.argv.slice(2);
|
|
74
135
|
const command = args[0];
|
|
136
|
+
if (command && NETWORKED_COMMANDS.has(command))
|
|
137
|
+
initEnterpriseNetworking(args.slice(1));
|
|
75
138
|
switch (command) {
|
|
76
139
|
case 'demo': {
|
|
77
140
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
@@ -120,6 +183,9 @@ async function main() {
|
|
|
120
183
|
case 'pair':
|
|
121
184
|
await runPair(args.slice(1));
|
|
122
185
|
break;
|
|
186
|
+
case 'enroll':
|
|
187
|
+
await runEnroll(args.slice(1));
|
|
188
|
+
break;
|
|
123
189
|
case 'setup': {
|
|
124
190
|
// Validate-first onboarding (direction-inverted pairing): WE display a
|
|
125
191
|
// short code, the owner claims it in the portal, the portal drives
|
|
@@ -430,6 +496,318 @@ function writePairedConfig(resolvedConfigPath, serialized, writeFile = (p, data,
|
|
|
430
496
|
throw err;
|
|
431
497
|
}
|
|
432
498
|
}
|
|
499
|
+
/** Show a credential's SHAPE without showing the credential. */
|
|
500
|
+
function maskConnectorKey(key) {
|
|
501
|
+
if (!key)
|
|
502
|
+
return '(none)';
|
|
503
|
+
const s = String(key);
|
|
504
|
+
// Prefix identifies the key family; last 4 lets an admin match it to the
|
|
505
|
+
// portal row. Everything between is what must never reach a log, a support
|
|
506
|
+
// bundle, or a screenshot of this terminal.
|
|
507
|
+
const head = s.slice(0, 7);
|
|
508
|
+
const tail = s.length > 4 ? s.slice(-4) : '';
|
|
509
|
+
return `${head}…${tail}`;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Read the enrollment token for an unattended install, in precedence order.
|
|
513
|
+
*
|
|
514
|
+
* 1. `--enrollment-token <value>` — for a scripted/manual run.
|
|
515
|
+
* 2. `TRILLBOARDS_ENROLLMENT_TOKEN` — the Docker/systemd path.
|
|
516
|
+
* 3. `--enrollment-file <path>` / the default bootstrap file next to the config
|
|
517
|
+
* — what the Windows installer writes from `/ENROLLMENT_TOKEN=`.
|
|
518
|
+
*
|
|
519
|
+
* The FILE is the preferred form for MDM and the reason this function exists:
|
|
520
|
+
* an argument shows up in the process table and an env var is inherited by every
|
|
521
|
+
* child process, while a file can be read once and DELETED. That deletion is not
|
|
522
|
+
* hygiene, it is the design — the fleet credential must not outlive the
|
|
523
|
+
* bootstrap, or every store PC keeps something that can enroll more machines.
|
|
524
|
+
*/
|
|
525
|
+
function readEnrollmentToken(args, configPath, env = process.env, readFile = (p) => fs.readFileSync(p, 'utf-8'), exists = (p) => fs.existsSync(p)) {
|
|
526
|
+
const fromFlag = getArg(args, '--enrollment-token');
|
|
527
|
+
if (fromFlag && fromFlag.trim())
|
|
528
|
+
return { token: fromFlag.trim(), source: 'flag', filePath: null };
|
|
529
|
+
const fromEnv = env.TRILLBOARDS_ENROLLMENT_TOKEN;
|
|
530
|
+
if (fromEnv && fromEnv.trim())
|
|
531
|
+
return { token: fromEnv.trim(), source: 'env', filePath: null };
|
|
532
|
+
const filePath = path.resolve(getArg(args, '--enrollment-file')
|
|
533
|
+
|| path.join(path.dirname(path.resolve(configPath)), 'trillboards.enrollment'));
|
|
534
|
+
if (exists(filePath)) {
|
|
535
|
+
try {
|
|
536
|
+
// The installer writes the bare token, but tolerate a trailing newline and
|
|
537
|
+
// a `TOKEN=` prefix — an admin editing this file by hand should not have
|
|
538
|
+
// their rollout fail on a shell habit.
|
|
539
|
+
const raw = readFile(filePath).trim();
|
|
540
|
+
const token = raw.replace(/^TRILLBOARDS_ENROLLMENT_TOKEN\s*=\s*/i, '').replace(/^["']|["']$/g, '').trim();
|
|
541
|
+
if (token)
|
|
542
|
+
return { token, source: 'file', filePath };
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// Unreadable is the same as absent for the caller; the missing-token
|
|
546
|
+
// message below is the right thing to print either way.
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return { token: null, source: null, filePath };
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Find the feeds on this machine's LAN and hand them to the server, returning
|
|
553
|
+
* the `cameras[]` this connector will run on.
|
|
554
|
+
*
|
|
555
|
+
* WHY CREDENTIALS ARE REQUIRED HERE, and why "no credentials" is not an error:
|
|
556
|
+
* this product targets cheap analog DVRs whose channels are behind a recorder
|
|
557
|
+
* password. Registering a channel we cannot open would mint a screen that never
|
|
558
|
+
* sees a frame — a camera board full of feeds that are permanently dark, which
|
|
559
|
+
* is worse than an empty one. So with no credentials we register NOTHING and say
|
|
560
|
+
* so plainly; the operator finishes in the portal exactly as before.
|
|
561
|
+
*
|
|
562
|
+
* The recorder credentials never leave this machine: only a stable `cameraKey`
|
|
563
|
+
* and a display name go to the server, and the credentialed URLs are matched
|
|
564
|
+
* back to the returned device tokens locally.
|
|
565
|
+
*/
|
|
566
|
+
async function discoverAndRegisterFeeds(opts) {
|
|
567
|
+
if (!opts.nvrUser || !opts.nvrPass) {
|
|
568
|
+
return {
|
|
569
|
+
feeds: [],
|
|
570
|
+
discovered: 0,
|
|
571
|
+
reason: 'no recorder credentials were supplied (--nvr-user / --nvr-pass), so no feeds were registered',
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
575
|
+
const discovery = require('./discovery');
|
|
576
|
+
const recorders = await discovery.sweepLanForRecorders({});
|
|
577
|
+
if (!recorders.length) {
|
|
578
|
+
return { feeds: [], discovered: 0, reason: 'no recorders answered on this network' };
|
|
579
|
+
}
|
|
580
|
+
// The XM/DVRIP handshake makes a recorder state its OWN channel count, which
|
|
581
|
+
// is the difference between "a socket accepted a connection" and "a 16-channel
|
|
582
|
+
// recorder, expand it". Hosts that do not answer keep their port-open shape and
|
|
583
|
+
// fall back to a single channel below.
|
|
584
|
+
const annotated = await discovery.annotateXmRecorders(recorders);
|
|
585
|
+
const brand = (opts.brand || 'generic');
|
|
586
|
+
const discovered = [];
|
|
587
|
+
for (const rec of annotated) {
|
|
588
|
+
const xm = rec.xm;
|
|
589
|
+
const channels = Number(xm?.channelNum) > 0 ? Number(xm.channelNum) : 1;
|
|
590
|
+
const expanded = discovery.expandChannelRange({
|
|
591
|
+
brand,
|
|
592
|
+
host: rec.host,
|
|
593
|
+
count: channels,
|
|
594
|
+
username: opts.nvrUser,
|
|
595
|
+
password: opts.nvrPass,
|
|
596
|
+
namePrefix: opts.siteLabel ? `${opts.siteLabel} channel` : 'Channel',
|
|
597
|
+
});
|
|
598
|
+
for (const cam of expanded)
|
|
599
|
+
discovered.push(cam);
|
|
600
|
+
}
|
|
601
|
+
if (!discovered.length) {
|
|
602
|
+
return { feeds: [], discovered: 0, reason: 'recorders answered but no channels could be expanded' };
|
|
603
|
+
}
|
|
604
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
605
|
+
const { ConnectorCameraClient, buildCameraFeeds, MAX_CAMERAS_PER_CALL } = require('./ConnectorCameraClient');
|
|
606
|
+
const client = new ConnectorCameraClient({ apiBaseUrl: opts.apiBaseUrl, connectorKey: opts.connectorKey });
|
|
607
|
+
// The server caps one call; a 64-channel recorder is a real configuration, so
|
|
608
|
+
// page rather than silently truncating (the claim-code path's `.slice(0, 64)`
|
|
609
|
+
// is exactly the bug this avoids).
|
|
610
|
+
const feeds = [];
|
|
611
|
+
for (let i = 0; i < discovered.length; i += MAX_CAMERAS_PER_CALL) {
|
|
612
|
+
const page = discovered.slice(i, i + MAX_CAMERAS_PER_CALL);
|
|
613
|
+
// eslint-disable-next-line no-await-in-loop
|
|
614
|
+
const result = await client.registerCameras(page.map((c) => ({ cameraKey: c.cameraKey, name: c.name })));
|
|
615
|
+
if (!result.ok) {
|
|
616
|
+
// Partial success is kept: the feeds already registered are real and their
|
|
617
|
+
// tokens work. Reporting zero would throw away provisioning that happened.
|
|
618
|
+
return {
|
|
619
|
+
feeds,
|
|
620
|
+
discovered: discovered.length,
|
|
621
|
+
reason: `camera registration failed after ${feeds.length} feed(s): ${result.error}`,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
for (const feed of buildCameraFeeds(result.cameras, discovered))
|
|
625
|
+
feeds.push(feed);
|
|
626
|
+
}
|
|
627
|
+
return { feeds, discovered: discovered.length };
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* UNATTENDED first-boot provisioning — the MDM/Intune/SCCM path.
|
|
631
|
+
*
|
|
632
|
+
* trillboards-edge enroll [--enrollment-token T | --enrollment-file P]
|
|
633
|
+
* [--site "Store 0417"] [--config PATH] [--api-base URL]
|
|
634
|
+
*
|
|
635
|
+
* Exchanges the FLEET enrollment credential shipped in the deployment package
|
|
636
|
+
* for THIS MACHINE's own connector credential and writes it to the config. No
|
|
637
|
+
* human touches the machine, which is the entire point: `pair` needs a code
|
|
638
|
+
* typed in and `setup` needs a code read off the screen, so neither can be
|
|
639
|
+
* automated across N stores.
|
|
640
|
+
*
|
|
641
|
+
* IDEMPOTENT BY DESIGN. Re-running on an already-enrolled machine is safe and
|
|
642
|
+
* FREE — the server keys on `machineId` and does not spend another use of the
|
|
643
|
+
* bounded fleet credential. MDM re-pushes packages; if a re-push cost a use, a
|
|
644
|
+
* 200-use credential would die partway through a 200-store rollout.
|
|
645
|
+
*
|
|
646
|
+
* ON SUCCESS THE ENROLLMENT FILE IS DELETED. See readEnrollmentToken.
|
|
647
|
+
*
|
|
648
|
+
* Exit codes (the install wrappers branch on these — keep stable, and note they
|
|
649
|
+
* mirror `pair`'s so a wrapper can treat the two paths alike):
|
|
650
|
+
* 0 enrolled (or already enrolled), config written
|
|
651
|
+
* 1 the credential was rejected — wrong, expired, revoked or exhausted. A
|
|
652
|
+
* human must issue a new one; retrying cannot help.
|
|
653
|
+
* 4 no enrollment token was supplied at all (not an unattended install)
|
|
654
|
+
* 6 network/server failure — the credential is UNSPENT. Retry later.
|
|
655
|
+
*/
|
|
656
|
+
async function runEnroll(args) {
|
|
657
|
+
const configPath = getArg(args, '--config') || 'trillboards.config.json';
|
|
658
|
+
const resolvedConfigPath = path.resolve(configPath);
|
|
659
|
+
const apiBaseUrl = getArg(args, '--api-base')
|
|
660
|
+
|| process.env.TRILLBOARDS_API_BASE
|
|
661
|
+
|| 'https://api.trillboards.com';
|
|
662
|
+
const siteLabel = getArg(args, '--site') || process.env.TRILLBOARDS_SITE || undefined;
|
|
663
|
+
const { token, source, filePath } = readEnrollmentToken(args, resolvedConfigPath);
|
|
664
|
+
if (!token) {
|
|
665
|
+
console.error('No enrollment credential found.\n' +
|
|
666
|
+
' Pass --enrollment-token <token>, set TRILLBOARDS_ENROLLMENT_TOKEN,\n' +
|
|
667
|
+
` or place the token in ${filePath ?? 'trillboards.enrollment'}.\n` +
|
|
668
|
+
' Create one in the portal under Cameras → Deploy connectors.');
|
|
669
|
+
process.exit(4);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const machineId = hostFingerprint();
|
|
673
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
674
|
+
const { EnrollClient } = require('./EnrollClient');
|
|
675
|
+
const client = new EnrollClient({ apiBaseUrl });
|
|
676
|
+
const result = await client.enroll({
|
|
677
|
+
enrollmentToken: token,
|
|
678
|
+
machineId,
|
|
679
|
+
siteLabel,
|
|
680
|
+
hostname: os.hostname(),
|
|
681
|
+
platform: process.platform,
|
|
682
|
+
connectorVersion: VERSION,
|
|
683
|
+
});
|
|
684
|
+
if (!result.ok) {
|
|
685
|
+
console.error(`Enrollment failed: ${result.error}`);
|
|
686
|
+
if (result.kind === 'auth') {
|
|
687
|
+
console.error('The enrollment credential was rejected. It may be expired, revoked, or it may\n' +
|
|
688
|
+
'have already enrolled every machine it was issued for. Issue a new one in the\n' +
|
|
689
|
+
'portal — retrying with this one cannot succeed.');
|
|
690
|
+
process.exit(1);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (result.kind === 'invalid') {
|
|
694
|
+
process.exit(1);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
console.error('This is a network or server failure — the credential was NOT spent. Retry later.');
|
|
698
|
+
process.exit(6);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
// Merge into any existing config rather than replacing it. A machine may be
|
|
702
|
+
// re-enrolled after cameras are already registered, and clobbering
|
|
703
|
+
// `cameras[]` would drop every device token on the box.
|
|
704
|
+
let existing = {};
|
|
705
|
+
if (fs.existsSync(resolvedConfigPath)) {
|
|
706
|
+
try {
|
|
707
|
+
existing = JSON.parse(fs.readFileSync(resolvedConfigPath, 'utf-8'));
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
console.warn(`Existing config at ${resolvedConfigPath} is not valid JSON — writing a fresh one.`);
|
|
711
|
+
existing = {};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// Now that this machine has a credential, find its feeds and register them.
|
|
715
|
+
// Enrollment without this leaves the box with an identity and no cameras, and
|
|
716
|
+
// `auto-sense` exits through its no-feeds path — an install that reports
|
|
717
|
+
// success while a human still has to run the interactive camera setup.
|
|
718
|
+
let registration = {
|
|
719
|
+
feeds: [],
|
|
720
|
+
discovered: 0,
|
|
721
|
+
reason: 'camera registration was skipped',
|
|
722
|
+
};
|
|
723
|
+
try {
|
|
724
|
+
registration = await discoverAndRegisterFeeds({
|
|
725
|
+
apiBaseUrl,
|
|
726
|
+
connectorKey: result.connectorKey,
|
|
727
|
+
nvrUser: getArg(args, '--nvr-user') || process.env.TRILLBOARDS_NVR_USER,
|
|
728
|
+
nvrPass: getArg(args, '--nvr-pass') || process.env.TRILLBOARDS_NVR_PASS,
|
|
729
|
+
brand: getArg(args, '--brand'),
|
|
730
|
+
siteLabel: result.siteLabel || siteLabel,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
// A discovery failure must never lose the credential we just obtained: the
|
|
735
|
+
// enrollment itself SUCCEEDED and its use is spent, so write the config and
|
|
736
|
+
// let the operator (or the next-logon retry) finish the camera half.
|
|
737
|
+
registration = {
|
|
738
|
+
feeds: [],
|
|
739
|
+
discovered: 0,
|
|
740
|
+
reason: `camera discovery failed: ${err?.message ?? 'unknown error'}`,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
const existingCameras = Array.isArray(existing.cameras)
|
|
744
|
+
? (existing.cameras)
|
|
745
|
+
: [];
|
|
746
|
+
// Merge by screenId so a re-run refreshes a feed's token rather than
|
|
747
|
+
// duplicating it, and never drops a feed the operator added by hand.
|
|
748
|
+
const mergedCameras = registration.feeds.length > 0
|
|
749
|
+
? [
|
|
750
|
+
...registration.feeds,
|
|
751
|
+
...existingCameras.filter((c) => !registration.feeds.some((f) => f.screenId && f.screenId === c.screenId)),
|
|
752
|
+
]
|
|
753
|
+
: existingCameras;
|
|
754
|
+
const merged = {
|
|
755
|
+
...existing,
|
|
756
|
+
apiBaseUrl,
|
|
757
|
+
...(mergedCameras.length > 0 ? { cameras: mergedCameras } : {}),
|
|
758
|
+
connector: {
|
|
759
|
+
id: result.connectorId,
|
|
760
|
+
key: result.connectorKey,
|
|
761
|
+
machineId,
|
|
762
|
+
...(result.siteLabel ? { siteLabel: result.siteLabel } : {}),
|
|
763
|
+
enrolledAt: new Date().toISOString(),
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
fs.mkdirSync(path.dirname(resolvedConfigPath), { recursive: true });
|
|
767
|
+
writePairedConfig(resolvedConfigPath, JSON.stringify(merged, null, 2) + '\n');
|
|
768
|
+
// `mode` on writeFileSync only applies on CREATE, so an existing 0644 config —
|
|
769
|
+
// exactly what `trillboards-edge init` leaves behind — would keep its
|
|
770
|
+
// permissions and this credential would land world-readable. Tighten
|
|
771
|
+
// explicitly, as setupMode.ts does for the same reason.
|
|
772
|
+
try {
|
|
773
|
+
fs.chmodSync(resolvedConfigPath, 0o600);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
// Windows has no POSIX modes; %LOCALAPPDATA% is the per-user boundary there.
|
|
777
|
+
}
|
|
778
|
+
// The fleet credential has done its one job. Anything left on disk that can
|
|
779
|
+
// enroll another machine is a skeleton key sitting in a store.
|
|
780
|
+
if (source === 'file' && filePath) {
|
|
781
|
+
try {
|
|
782
|
+
fs.unlinkSync(filePath);
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
console.warn(`WARNING: enrolled successfully but could not delete the enrollment file at ${filePath}: ` +
|
|
786
|
+
`${err?.message}. Delete it manually — it can enroll further machines.`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
console.log(`${result.reEnrollment ? 'Re-enrolled' : 'Enrolled'} connector ${result.connectorId}` +
|
|
790
|
+
(result.siteLabel ? ` (${result.siteLabel})` : '') +
|
|
791
|
+
`\n machine: ${machineId}` +
|
|
792
|
+
`\n credential: ${maskConnectorKey(result.connectorKey)}` +
|
|
793
|
+
(result.remainingEnrollments != null
|
|
794
|
+
? `\n remaining enrollments on this credential: ${result.remainingEnrollments}`
|
|
795
|
+
: '') +
|
|
796
|
+
`\n cameras: ${mergedCameras.length} registered` +
|
|
797
|
+
(registration.reason ? ` (${registration.reason})` : ''));
|
|
798
|
+
// Exit 5 = provisioned but NOT sensing. A distinct code, because "enrolled"
|
|
799
|
+
// and "enrolled and actually watching something" are different outcomes and
|
|
800
|
+
// collapsing them into 0 is how an installer reports success on a machine that
|
|
801
|
+
// still needs a human.
|
|
802
|
+
if (mergedCameras.length === 0) {
|
|
803
|
+
console.log('This connector is provisioned but has no camera feeds yet. Supply recorder\n' +
|
|
804
|
+
'credentials (--nvr-user / --nvr-pass, or /NVR_USER= /NVR_PASS= at install)\n' +
|
|
805
|
+
'and re-run, or finish in the portal.');
|
|
806
|
+
process.exit(5);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
process.exit(0);
|
|
810
|
+
}
|
|
433
811
|
/**
|
|
434
812
|
* First-run pairing: exchange a portal claim code for the connector config and
|
|
435
813
|
* write trillboards.config.json. The device token is delivered ONLY here (never
|
|
@@ -689,6 +1067,20 @@ Usage:
|
|
|
689
1067
|
trillboards-edge setup [--config <path>] Validate-first onboarding: opens the portal and keeps a code fallback
|
|
690
1068
|
trillboards-edge pair --code <XXXX-XXXX> Pair with a portal claim code (writes config)
|
|
691
1069
|
trillboards-edge pair --qr <image.png> Pair by scanning a screenshot/photo of the portal QR
|
|
1070
|
+
trillboards-edge enroll [--site "Store 0417"] [--nvr-user U --nvr-pass P]
|
|
1071
|
+
Unattended provisioning for MDM/Intune/SCCM — no
|
|
1072
|
+
human at the machine. Reads the fleet enrollment
|
|
1073
|
+
credential from --enrollment-token, the
|
|
1074
|
+
TRILLBOARDS_ENROLLMENT_TOKEN env var, or the
|
|
1075
|
+
trillboards.enrollment file, exchanges it for THIS
|
|
1076
|
+
machine's own credential, deletes the file, then
|
|
1077
|
+
discovers recorders on the LAN and registers their
|
|
1078
|
+
channels (recorder credentials required for that
|
|
1079
|
+
half — without them the machine provisions its
|
|
1080
|
+
identity and registers no feeds).
|
|
1081
|
+
Safe to re-run: a re-push does not consume another
|
|
1082
|
+
enrollment. (exit 0=ok 1=credential rejected
|
|
1083
|
+
4=no token 5=enrolled but no feeds 6=network, retry)
|
|
692
1084
|
trillboards-edge verify-connection [--config p] Prove the saved pairing works (exit 0=ok 2=network 3=auth 4=no config)
|
|
693
1085
|
trillboards-edge discover Find cameras on your network (ONVIF) or expand an NVR's channels
|
|
694
1086
|
trillboards-edge status [--host h] [--port p] Show agent status dashboard
|