@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.
Files changed (65) hide show
  1. package/deploy/macos/scripts/setup-litertlm.sh +30 -0
  2. package/deploy/windows/build-windows.ps1 +1 -0
  3. package/deploy/windows/e2e/test-enterprise-trust-store.ps1 +341 -0
  4. package/deploy/windows/enroll-connector.ps1 +169 -0
  5. package/deploy/windows/installer.nsi +102 -6
  6. package/deploy/windows/run-connector.ps1 +30 -0
  7. package/deploy/windows/setup-litertlm.ps1 +49 -1
  8. package/dist/AudienceAnalyzeClient.d.ts +32 -1
  9. package/dist/AudienceAnalyzeClient.d.ts.map +1 -1
  10. package/dist/AudienceAnalyzeClient.js +34 -5
  11. package/dist/AudienceAnalyzeClient.js.map +1 -1
  12. package/dist/CloudSensingManager.d.ts +30 -0
  13. package/dist/CloudSensingManager.d.ts.map +1 -1
  14. package/dist/CloudSensingManager.js +61 -1
  15. package/dist/CloudSensingManager.js.map +1 -1
  16. package/dist/ConnectorCameraClient.d.ts +79 -0
  17. package/dist/ConnectorCameraClient.d.ts.map +1 -0
  18. package/dist/ConnectorCameraClient.js +114 -0
  19. package/dist/ConnectorCameraClient.js.map +1 -0
  20. package/dist/EdgeAgent.js +1 -1
  21. package/dist/EnrollClient.d.ts +81 -0
  22. package/dist/EnrollClient.d.ts.map +1 -0
  23. package/dist/EnrollClient.js +97 -0
  24. package/dist/EnrollClient.js.map +1 -0
  25. package/dist/cli.d.ts +49 -0
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +392 -0
  28. package/dist/cli.js.map +1 -1
  29. package/dist/config.d.ts +15 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +4 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/diagnose.d.ts +37 -4
  34. package/dist/diagnose.d.ts.map +1 -1
  35. package/dist/diagnose.js +155 -6
  36. package/dist/diagnose.js.map +1 -1
  37. package/dist/enterpriseNetwork.d.ts +130 -0
  38. package/dist/enterpriseNetwork.d.ts.map +1 -0
  39. package/dist/enterpriseNetwork.js +568 -0
  40. package/dist/enterpriseNetwork.js.map +1 -0
  41. package/dist/enterpriseProxy.d.ts +73 -0
  42. package/dist/enterpriseProxy.d.ts.map +1 -0
  43. package/dist/enterpriseProxy.js +406 -0
  44. package/dist/enterpriseProxy.js.map +1 -0
  45. package/dist/enterpriseTrust.d.ts +188 -0
  46. package/dist/enterpriseTrust.d.ts.map +1 -0
  47. package/dist/enterpriseTrust.js +615 -0
  48. package/dist/enterpriseTrust.js.map +1 -0
  49. package/dist/index.d.ts +10 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +29 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/localNetworkAccess.d.ts +166 -0
  54. package/dist/localNetworkAccess.d.ts.map +1 -0
  55. package/dist/localNetworkAccess.js +490 -0
  56. package/dist/localNetworkAccess.js.map +1 -0
  57. package/dist/sensingRunMode.d.ts +36 -0
  58. package/dist/sensingRunMode.d.ts.map +1 -1
  59. package/dist/sensingRunMode.js +63 -1
  60. package/dist/sensingRunMode.js.map +1 -1
  61. package/dist/transportFailure.d.ts +118 -0
  62. package/dist/transportFailure.d.ts.map +1 -0
  63. package/dist/transportFailure.js +267 -0
  64. package/dist/transportFailure.js.map +1 -0
  65. 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":";AA+XA;;;;;;;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"}
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