@trillboards/edge-sdk 0.2.24 → 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.
@@ -202,6 +202,7 @@ Copy-Item -Force "$Here\trillboards-hidden-run.vbs" "$Payload\bin\trillboards-hi
202
202
  Copy-Item -Force "$Here\ensure-sensing.ps1" "$Payload\bin\ensure-sensing.ps1"
203
203
  Copy-Item -Force "$Here\run-connector.ps1" "$Payload\bin\run-connector.ps1"
204
204
  Copy-Item -Force "$Here\register-task.ps1" "$Payload\bin\register-task.ps1"
205
+ Copy-Item -Force "$Here\enroll-connector.ps1" "$Payload\bin\enroll-connector.ps1"
205
206
  Copy-Item -Force "$Here\stop-existing.ps1" "$Payload\bin\stop-existing.ps1"
206
207
  Copy-Item -Force "$Here\install-update.ps1" "$Payload\bin\install-update.ps1"
207
208
  # The in-field updater. Ships in bin\ so an INSTALLED connector can drive its own
@@ -0,0 +1,169 @@
1
+ <#
2
+ enroll-connector.ps1 - UNATTENDED provisioning step for an MDM/Intune/SCCM
3
+ deployment. Invoked by installer.nsi when /ENROLLMENT_TOKEN= was supplied.
4
+
5
+ WHAT IT DOES
6
+ 1. Writes the fleet enrollment credential to $InstallDir\trillboards.enrollment
7
+ with an ACL that grants the installing user ONLY (inheritance disabled),
8
+ because a plain %LOCALAPPDATA% file inherits whatever the parent grants
9
+ and this is the one credential in the package that can enroll machines.
10
+ 2. Runs `trillboards-edge enroll`, which exchanges it for THIS machine's own
11
+ credential, writes that into trillboards.config.json, and DELETES the
12
+ enrollment file.
13
+ 3. Deletes the file itself if the CLI could not (belt and braces - a fleet
14
+ credential left on 200 store PCs is the failure this whole design exists
15
+ to prevent).
16
+
17
+ EXIT CODES - these are the installer's, and they are deliberately NOT fatal to
18
+ the install:
19
+ 0 enrolled, or already enrolled (a re-push is idempotent and free)
20
+ 1 the credential was rejected: expired, revoked, or every enrollment it was
21
+ issued for is already used. A human must issue a new one.
22
+ 6 network/server failure. The credential was NOT spent; the scheduled task
23
+ retries on the next logon.
24
+
25
+ A failed enrollment leaves a correctly INSTALLED connector that is simply not
26
+ yet provisioned, which is recoverable by re-running this script or by the
27
+ ordinary interactive setup. Aborting the whole install instead would leave the
28
+ machine with nothing and no way to retry.
29
+ #>
30
+ [CmdletBinding()]
31
+ param(
32
+ [Parameter(Mandatory = $true)][string]$InstallDir,
33
+ [Parameter(Mandatory = $true)][string]$EnrollmentToken,
34
+ [string]$SiteLabel = "",
35
+ [string]$ApiBase = "",
36
+ # Optional recorder credentials. Passed straight through to the CLI and never
37
+ # written to disk or to the log - only their PRESENCE is logged.
38
+ [string]$NvrUser = "",
39
+ [string]$NvrPass = ""
40
+ )
41
+
42
+ $ErrorActionPreference = "Stop"
43
+
44
+ $logDir = Join-Path $env:LOCALAPPDATA "Trillboards\logs"
45
+ New-Item -ItemType Directory -Force -Path $logDir | Out-Null
46
+ $logPath = Join-Path $logDir "enroll.log"
47
+
48
+ function Write-Log([string]$Message) {
49
+ # NEVER log the token. Only its shape, so support can match it to a portal row
50
+ # without the log becoming a credential store.
51
+ $stamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
52
+ Add-Content -Path $logPath -Value "[$stamp] $Message"
53
+ }
54
+
55
+ $enrollmentFile = Join-Path $InstallDir "trillboards.enrollment"
56
+ $configPath = Join-Path $InstallDir "trillboards.config.json"
57
+
58
+ function Remove-EnrollmentFile {
59
+ if (Test-Path $enrollmentFile) {
60
+ try {
61
+ Remove-Item -Force $enrollmentFile
62
+ Write-Log "enrollment file removed"
63
+ } catch {
64
+ Write-Log "WARNING: could not remove enrollment file: $($_.Exception.Message)"
65
+ }
66
+ }
67
+ }
68
+
69
+ try {
70
+ if ([string]::IsNullOrWhiteSpace($EnrollmentToken)) {
71
+ Write-Log "no enrollment token supplied; nothing to do"
72
+ exit 0
73
+ }
74
+
75
+ $tokenShape = if ($EnrollmentToken.Length -gt 11) {
76
+ "$($EnrollmentToken.Substring(0,7))...$($EnrollmentToken.Substring($EnrollmentToken.Length-4))"
77
+ } else { "(short)" }
78
+ $credState = if ([string]::IsNullOrWhiteSpace($NvrUser)) { "absent" } else { "supplied" }
79
+ Write-Log "starting unattended enrollment (credential $tokenShape, site '$SiteLabel', recorder credentials $credState)"
80
+
81
+ New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
82
+ # -NoNewline so the CLI's trim() sees exactly the token; ASCII because the key
83
+ # alphabet is hex and a BOM would corrupt the first character.
84
+ [System.IO.File]::WriteAllText($enrollmentFile, $EnrollmentToken, [System.Text.UTF8Encoding]::new($false))
85
+
86
+ # Owner-only ACL with inheritance broken. %LOCALAPPDATA% is already per-user,
87
+ # but this file is the one credential that can enroll further machines, so it
88
+ # does not ride on a directory default that a group policy may widen.
89
+ try {
90
+ $acl = Get-Acl $enrollmentFile
91
+ $acl.SetAccessRuleProtection($true, $false) # break inheritance, drop inherited ACEs
92
+ $me = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
93
+ $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
94
+ $me, "FullControl", "None", "None", "Allow")
95
+ $acl.SetAccessRule($rule)
96
+ Set-Acl -Path $enrollmentFile -AclObject $acl
97
+ Write-Log "enrollment file ACL set to owner-only"
98
+ } catch {
99
+ # Non-fatal: the file is deleted seconds from now. Losing the ACL is worth a
100
+ # log line, not a failed rollout.
101
+ Write-Log "WARNING: could not tighten enrollment file ACL: $($_.Exception.Message)"
102
+ }
103
+
104
+ $node = Join-Path $InstallDir "bin\node.exe"
105
+ $cli = Join-Path $InstallDir "lib\edge-sdk\dist\cli.js"
106
+ if (-not (Test-Path $node) -or -not (Test-Path $cli)) {
107
+ # Retryable and the credential is UNSPENT - keep it so the next-logon retry
108
+ # can use it once the install finishes laying the tree down.
109
+ Write-Log "ERROR: connector is not installed at $InstallDir (retryable, credential preserved)"
110
+ exit 6
111
+ }
112
+
113
+ $cliArgs = @($cli, "enroll", "--config", $configPath, "--enrollment-file", $enrollmentFile)
114
+ if (-not [string]::IsNullOrWhiteSpace($SiteLabel)) { $cliArgs += @("--site", $SiteLabel) }
115
+ if (-not [string]::IsNullOrWhiteSpace($ApiBase)) { $cliArgs += @("--api-base", $ApiBase) }
116
+ if (-not [string]::IsNullOrWhiteSpace($NvrUser)) { $cliArgs += @("--nvr-user", $NvrUser) }
117
+ if (-not [string]::IsNullOrWhiteSpace($NvrPass)) { $cliArgs += @("--nvr-pass", $NvrPass) }
118
+
119
+ $oldPath = $env:PATH
120
+ $oldNodePath = $env:NODE_PATH
121
+ try {
122
+ $env:PATH = "$(Join-Path $InstallDir 'bin');$oldPath"
123
+ $env:NODE_PATH = Join-Path $InstallDir "lib\node_modules"
124
+ $output = (& $node @cliArgs 2>&1 | Out-String)
125
+ $code = $LASTEXITCODE
126
+ } finally {
127
+ $env:PATH = $oldPath
128
+ $env:NODE_PATH = $oldNodePath
129
+ }
130
+
131
+ # The CLI never prints the credential (see maskConnectorKey), so its output is
132
+ # safe to log verbatim and is the only diagnostic a field failure leaves.
133
+ Write-Log "enroll exit=$code output=$($output.Trim())"
134
+
135
+ # DELETE ONLY ON A TERMINAL OUTCOME.
136
+ #
137
+ # The CLI already deletes the file itself on success. The dangerous case is a
138
+ # RETRYABLE failure: an unconditional delete here would destroy the only
139
+ # persisted copy of the fleet credential on a transient outage during install,
140
+ # leaving that store permanently unprovisioned with nothing to retry from - the
141
+ # opposite of what the exit code says. So the file survives exit 6 and the
142
+ # next-logon retry in run-connector.ps1 picks it up.
143
+ #
144
+ # It IS deleted on a credential REJECTION (exit 1), because that token can
145
+ # never enroll anything again and leaving a dead credential on disk is pure
146
+ # exposure.
147
+ switch ($code) {
148
+ 0 { Remove-EnrollmentFile; Write-Log "enrollment complete, cameras registered"; exit 0 }
149
+ 5 {
150
+ # Provisioned, but no feeds. The credential is SPENT, so the enrollment
151
+ # file is done - what is missing is recorder credentials, not a retry.
152
+ Remove-EnrollmentFile
153
+ Write-Log "enrolled, but NO camera feeds registered (recorder credentials absent or no recorder found)"
154
+ exit 5
155
+ }
156
+ 1 { Remove-EnrollmentFile; Write-Log "enrollment credential REJECTED - a new one must be issued"; exit 1 }
157
+ 4 { Remove-EnrollmentFile; Write-Log "no enrollment token was readable"; exit 1 }
158
+ default {
159
+ Write-Log "enrollment deferred (retryable) - credential PRESERVED for the next-logon retry"
160
+ exit 6
161
+ }
162
+ }
163
+ } catch {
164
+ # An exception here is a local/environmental failure, not a rejected
165
+ # credential, so it is retryable and the token is preserved for the same
166
+ # reason as exit 6 above.
167
+ Write-Log "ERROR (retryable, credential preserved): $($_.Exception.Message)"
168
+ exit 6
169
+ }
@@ -1,4 +1,4 @@
1
- ; installer.nsi — Trillboards Edge on-device (local-sense) CCTV connector (Windows).
1
+ ; installer.nsi - Trillboards Edge on-device (local-sense) CCTV connector (Windows).
2
2
  ; Per-user, no-admin install (RequestExecutionLevel user) that mirrors the
3
3
  ; home-domain macOS .pkg: lays down the self-contained tree under
4
4
  ; %LOCALAPPDATA%\Trillboards\edge, registers a per-user Scheduled Task that
@@ -26,6 +26,16 @@
26
26
  !define LITERT_TASK_NAME "TrillboardsLiteRtLm"
27
27
 
28
28
  Var ModelSource
29
+ ; -- Unattended (MDM/Intune/SCCM) parameters --------------------------------
30
+ ; Before these, the installer parsed exactly ONE switch (/MODEL_SOURCE=) and a
31
+ ; perfect silent install still left the machine UNPAIRED, because deviceToken is
32
+ ; per-camera-feed and the only writers were the interactive pair/setup paths. A
33
+ ; silent install that produces a non-working install is not a deployment story.
34
+ Var EnrollmentToken
35
+ Var SiteLabel
36
+ Var NvrUser
37
+ Var NvrPass
38
+ Var IsUnattended
29
39
 
30
40
  Name "${APP_NAME}"
31
41
  OutFile "${OUT_FILE}"
@@ -47,16 +57,61 @@ BrandingText "Trillboards"
47
57
 
48
58
  ; Finish page: launch the native Windows Forms setup app through wscript. The
49
59
  ; setup engine and its cmd wrapper run with CreateNoWindow, so no console flashes.
50
- !define MUI_FINISHPAGE_RUN "$INSTDIR\bin\trillboards-setup.vbs"
60
+ ; MUI_FINISHPAGE_RUN_FUNCTION rather than a bare RUN: an admin who passes
61
+ ; /ENROLLMENT_TOKEN= without /S still gets the pages, and launching the
62
+ ; interactive camera wizard on a machine that just provisioned itself is exactly
63
+ ; the human step this feature removes. (Under /S the finish page never renders
64
+ ; at all, so this only matters for the visible-but-unattended case.)
65
+ !define MUI_FINISHPAGE_RUN
66
+ !define MUI_FINISHPAGE_RUN_FUNCTION LaunchSetupAppUnlessUnattended
51
67
  !define MUI_FINISHPAGE_RUN_TEXT "Set up my CCTV cameras now"
52
68
  !define MUI_FINISHPAGE_TEXT "The connector and private local AI are installed. Continue in the Trillboards CCTV Setup app to pair your cameras."
53
69
  !insertmacro MUI_PAGE_FINISH
54
70
 
71
+ Function LaunchSetupAppUnlessUnattended
72
+ ${If} $IsUnattended == 1
73
+ Return
74
+ ${EndIf}
75
+ Exec '"$WINDIR\System32\wscript.exe" "$INSTDIR\bin\trillboards-setup.vbs"'
76
+ FunctionEnd
77
+
55
78
  !insertmacro MUI_UNPAGE_CONFIRM
56
79
  !insertmacro MUI_UNPAGE_INSTFILES
57
80
  !insertmacro MUI_LANGUAGE "English"
58
81
 
59
82
  Function .onInit
83
+ ; -- Unattended parameters ------------------------------------------------
84
+ ; /S NSIS silent install (no pages, no prompts)
85
+ ; /ENROLLMENT_TOKEN=<tok> the FLEET enrollment credential from the portal
86
+ ; /SITE="Store 0417" free-text install label carried into the rollout
87
+ ; view and prefixed onto camera names
88
+ ;
89
+ ; The token is written to $INSTDIR\trillboards.enrollment and the connector
90
+ ; DELETES that file the moment it has exchanged it for this machine's own
91
+ ; credential - a bootstrap credential must not outlive its bootstrap, or every
92
+ ; store PC keeps something that can enroll more machines.
93
+ ${GetParameters} $R0
94
+ ${GetOptions} $R0 "/ENROLLMENT_TOKEN=" $EnrollmentToken
95
+ ${GetOptions} $R0 "/SITE=" $SiteLabel
96
+ ; Recorder credentials are OPTIONAL and the deployment is useful without them:
97
+ ; with none, the machine still provisions its own identity and simply registers
98
+ ; no feeds. With them the install is genuinely zero-touch, because the channels
99
+ ; on the cheap DVRs this product targets cannot be opened otherwise - and
100
+ ; registering a channel we cannot open would mint a screen that is permanently
101
+ ; dark. Whether the recorder password belongs in an MDM package is the
102
+ ; customer's call, which is why this is a switch and not a requirement.
103
+ ${GetOptions} $R0 "/NVR_USER=" $NvrUser
104
+ ${GetOptions} $R0 "/NVR_PASS=" $NvrPass
105
+
106
+ ; "Unattended" is silent OR token-driven: an admin scripting a visible install
107
+ ; still wants no prompts to block the run.
108
+ StrCpy $IsUnattended 0
109
+ ${If} ${Silent}
110
+ StrCpy $IsUnattended 1
111
+ ${ElseIf} $EnrollmentToken != ""
112
+ StrCpy $IsUnattended 1
113
+ ${EndIf}
114
+
60
115
  ; The SFX launcher passes its absolute extraction path. Keep the environment
61
116
  ; value as an engineering and recovery fallback for the signed inner package.
62
117
  ReadEnvStr $ModelSource "TRILLBOARDS_BUNDLED_MODEL_PATH"
@@ -70,7 +125,12 @@ Function .onInit
70
125
  SetErrorLevel 2
71
126
  Quit
72
127
  ${EndIf}
73
- MessageBox MB_ICONSTOP|MB_OK "This installer is missing its included local AI model. Download the complete Trillboards CCTV installer and try again."
128
+ ; /SD IDOK: under /S a modal has nobody to click it and the install hangs
129
+ ; forever. The default answer dismisses the box; the Abort on the next line
130
+ ; is what actually decides the outcome, so a silent run FAILS here rather
131
+ ; than proceeding without a model.
132
+ MessageBox MB_ICONSTOP|MB_OK "This installer is missing its included local AI model. Download the complete Trillboards CCTV installer and try again." /SD IDOK
133
+ SetErrorLevel 2
74
134
  Abort
75
135
  ${EndIf}
76
136
 
@@ -111,7 +171,8 @@ Section "Install"
111
171
  nsExec::ExecToLog 'powershell -NoProfile -ExecutionPolicy Bypass -File "$PLUGINSDIR\install-update.ps1" -InstallDir "$INSTDIR" -StagingDir "$INSTDIR\.update-staging"'
112
172
  Pop $0
113
173
  ${If} $0 != 0
114
- MessageBox MB_ICONSTOP|MB_OK "Trillboards CCTV setup could not be updated safely. Your previous connector was restored. Please retry the installer."
174
+ MessageBox MB_ICONSTOP|MB_OK "Trillboards CCTV setup could not be updated safely. Your previous connector was restored. Please retry the installer." /SD IDOK
175
+ SetErrorLevel 3
115
176
  Abort
116
177
  ${EndIf}
117
178
  SetOutPath "$INSTDIR"
@@ -120,18 +181,49 @@ Section "Install"
120
181
  ; is synchronous and required: pairing is never offered while AI is absent or
121
182
  ; still downloading in the background.
122
183
  ${IfNot} ${FileExists} "$ModelSource"
123
- MessageBox MB_ICONSTOP|MB_OK "The local AI model included with this installer could not be found. Download a fresh Trillboards CCTV installer."
184
+ MessageBox MB_ICONSTOP|MB_OK "The local AI model included with this installer could not be found. Download a fresh Trillboards CCTV installer." /SD IDOK
185
+ SetErrorLevel 2
124
186
  Abort
125
187
  ${EndIf}
126
188
  DetailPrint "Installing and verifying private local AI..."
127
189
  nsExec::ExecToLog '"$WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\bin\setup-litertlm.ps1" -BundledModelPath "$ModelSource" -RequireBundledModel'
128
190
  Pop $0
129
191
  ${If} $0 != 0
130
- MessageBox MB_ICONSTOP|MB_OK "Private local AI could not be installed. Camera pairing has not started. Download a fresh installer or contact Trillboards support."
192
+ MessageBox MB_ICONSTOP|MB_OK "Private local AI could not be installed. Camera pairing has not started. Download a fresh installer or contact Trillboards support." /SD IDOK
193
+ SetErrorLevel 4
131
194
  Abort
132
195
  ${EndIf}
133
196
  DetailPrint "Private local AI is installed and healthy."
134
197
 
198
+ ; -- Unattended provisioning ----------------------------------------------
199
+ ; With /ENROLLMENT_TOKEN=, the machine provisions ITSELF here - no code typed
200
+ ; at the machine, no browser, no human. enroll-connector.ps1 writes the
201
+ ; credential owner-only, exchanges it for this machine's own credential, and
202
+ ; deletes it.
203
+ ;
204
+ ; A failed enrollment does NOT abort the install. The connector is correctly
205
+ ; installed either way; what is missing is provisioning, which is retryable
206
+ ; (re-run the package, or use the ordinary interactive setup). Aborting would
207
+ ; leave the store with nothing and no path forward - strictly worse than an
208
+ ; installed-but-unprovisioned connector that a support call can finish.
209
+ ${If} $EnrollmentToken != ""
210
+ DetailPrint "Provisioning this connector..."
211
+ nsExec::ExecToLog '"$WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\bin\enroll-connector.ps1" -InstallDir "$INSTDIR" -EnrollmentToken "$EnrollmentToken" -SiteLabel "$SiteLabel" -NvrUser "$NvrUser" -NvrPass "$NvrPass"'
212
+ Pop $0
213
+ ${If} $0 == 0
214
+ DetailPrint "Connector provisioned and cameras registered."
215
+ ${ElseIf} $0 == 5
216
+ DetailPrint "Connector provisioned, but no camera feeds were registered. Supply /NVR_USER= and /NVR_PASS=, or finish in the portal."
217
+ SetErrorLevel 5
218
+ ${ElseIf} $0 == 1
219
+ DetailPrint "Enrollment credential was rejected (expired, revoked, or fully used). Issue a new one in the portal."
220
+ SetErrorLevel 5
221
+ ${Else}
222
+ DetailPrint "Enrollment could not reach Trillboards; it will be retried. The credential was not used."
223
+ SetErrorLevel 6
224
+ ${EndIf}
225
+ ${EndIf}
226
+
135
227
  ; Start Menu shortcuts.
136
228
  CreateDirectory "$SMPROGRAMS\Trillboards"
137
229
  CreateShortcut "$SMPROGRAMS\Trillboards\Trillboards CCTV Setup.lnk" "$WINDIR\System32\wscript.exe" '$\"$INSTDIR\bin\trillboards-setup.vbs$\"'
@@ -154,6 +246,10 @@ Section "Uninstall"
154
246
  nsExec::ExecToLog 'schtasks /Delete /F /TN "${TASK_NAME}"'
155
247
  nsExec::ExecToLog 'schtasks /End /TN "${LITERT_TASK_NAME}"'
156
248
  nsExec::ExecToLog 'schtasks /Delete /F /TN "${LITERT_TASK_NAME}"'
249
+ ; Explicit, ahead of RMDir /r: a fleet enrollment credential must never
250
+ ; survive an uninstall, and relying on the recursive delete makes that a
251
+ ; property of statement ORDER rather than of intent.
252
+ Delete "$INSTDIR\trillboards.enrollment"
157
253
  Delete "$SMPROGRAMS\Trillboards\Pair a camera.lnk"
158
254
  Delete "$SMPROGRAMS\Trillboards\Trillboards CCTV Setup.lnk"
159
255
  Delete "$SMPROGRAMS\Trillboards\Uninstall Trillboards Edge.lnk"
@@ -30,6 +30,36 @@ if ((Test-Path $logPath) -and (Get-Item $logPath).Length -ge $maxLogBytes) {
30
30
  Move-ConnectorLogs
31
31
  }
32
32
 
33
+ # ---------------------------------------------------------------------------
34
+ # Unattended-provisioning retry, at every logon.
35
+ #
36
+ # THE FAILURE THIS EXISTS FOR: enrollment runs once, inside the installer. If the
37
+ # store's link is down at that moment - a DSL blip, a captive portal, a firewall
38
+ # rule not yet pushed - the credential is unspent and the machine is installed
39
+ # but unprovisioned, with nothing scheduled to try again. Across a 200-store push
40
+ # that is a handful of stores permanently dark for a reason nobody can see.
41
+ #
42
+ # The scheduled task already runs this supervisor at every logon, so the retry
43
+ # belongs here rather than in a second task nobody would remember to register.
44
+ #
45
+ # It fires ONLY when the enrollment file still exists, and enroll-connector.ps1
46
+ # leaves it there only for retryable outcomes - a spent or rejected credential is
47
+ # deleted, so this cannot become a loop hammering a dead token.
48
+ $enrollmentFile = Join-Path $InstallDir "trillboards.enrollment"
49
+ $enrollScript = Join-Path $InstallDir "bin\enroll-connector.ps1"
50
+ if ((Test-Path $enrollmentFile) -and (Test-Path $enrollScript)) {
51
+ try {
52
+ $pendingToken = (Get-Content -Raw -Path $enrollmentFile).Trim()
53
+ if ($pendingToken) {
54
+ & $enrollScript -InstallDir $InstallDir -EnrollmentToken $pendingToken | Out-Null
55
+ }
56
+ } catch {
57
+ # Never let a provisioning retry stop the connector from starting: a machine
58
+ # that is already provisioned and simply has a stale file must still sense.
59
+ # The detail lands in enroll.log, which is where a support call looks.
60
+ }
61
+ }
62
+
33
63
  $writer = $null
34
64
  $process = $null
35
65
  try {
@@ -0,0 +1,79 @@
1
+ import { type HttpPoster } from './AudienceAnalyzeClient';
2
+ import type { CameraFeedConfig } from './config';
3
+ import type { DiscoveredCamera } from './discovery';
4
+ /**
5
+ * ConnectorCameraClient — registers the feeds this machine found, using THIS
6
+ * machine's own connector credential, and turns the answer into `cameras[]`.
7
+ *
8
+ * This is the consumer of `connector.key` and the producer for
9
+ * `POST /v2/earner/connectors/cameras`. Without it the unattended path stops one
10
+ * step short of useful: the machine has an identity, and no feeds, and no device
11
+ * tokens — so `auto-sense` finds no `cameras[]` and exits through its no-feeds
12
+ * path while the installer reports success. A human would still have to run the
13
+ * interactive camera setup, which is the exact step this whole feature removes.
14
+ *
15
+ * WHAT IT SENDS, AND WHAT IT DOES NOT. The body carries only a stable
16
+ * `cameraKey` per feed plus a display name — never the RTSP URL and never the
17
+ * recorder credentials. Those stay on this machine: the server mints one screen
18
+ * + device token per `cameraKey`, and the URLs are matched back to the returned
19
+ * tokens locally, below. That preserves the property the interactive setup
20
+ * already has and is worth selling — the connection details never leave the
21
+ * venue on this path.
22
+ *
23
+ * The owner is NOT in the body. It comes from the credential server-side.
24
+ */
25
+ export interface ConnectorCameraClientOptions {
26
+ apiBaseUrl: string;
27
+ /** This machine's connector credential (`connector.key`). */
28
+ connectorKey: string;
29
+ timeoutMs?: number;
30
+ }
31
+ /** One feed as the server needs to see it — no URL, no credentials. */
32
+ export interface RegisterCameraInput {
33
+ cameraKey: string;
34
+ name?: string;
35
+ brand?: string;
36
+ latitude?: number;
37
+ longitude?: number;
38
+ }
39
+ export interface RegisteredCamera {
40
+ screenId: string;
41
+ deviceId: string;
42
+ deviceToken: string;
43
+ cameraKey: string;
44
+ name: string;
45
+ isReRegistration?: boolean;
46
+ }
47
+ export interface RegisterResult {
48
+ ok: boolean;
49
+ cameras?: RegisteredCamera[];
50
+ error?: string;
51
+ /** Same taxonomy as EnrollClient — see there for why it matters. */
52
+ kind?: 'auth' | 'network' | 'server' | 'invalid';
53
+ }
54
+ /** The server caps a single registration call; mirrors MAX_CAMERAS_PER_CALL. */
55
+ export declare const MAX_CAMERAS_PER_CALL = 64;
56
+ export declare class ConnectorCameraClient {
57
+ private readonly endpoint;
58
+ private readonly connectorKey;
59
+ private readonly timeoutMs;
60
+ private readonly poster;
61
+ constructor(options: ConnectorCameraClientOptions, poster?: HttpPoster);
62
+ registerCameras(cameras: RegisterCameraInput[]): Promise<RegisterResult>;
63
+ }
64
+ /**
65
+ * Join the server's answer back to the LOCAL stream URLs, producing the
66
+ * `cameras[]` the connector runs on.
67
+ *
68
+ * Matching is by `cameraKey` and ONLY by `cameraKey` — never by name and never
69
+ * by array position. Names are truncated server-side (200 chars) so two feeds
70
+ * differing only past that point would collapse onto one screen, and the server
71
+ * is free to return fewer rows than were sent. `cameraKey` is the deterministic
72
+ * idempotency key both sides already agree on.
73
+ *
74
+ * A returned camera with no local match is DROPPED rather than guessed at: a
75
+ * feed configured with the wrong URL would sense the wrong room, which is worse
76
+ * than a feed that is absent and visibly missing from the camera board.
77
+ */
78
+ export declare function buildCameraFeeds(registered: RegisteredCamera[], discovered: DiscoveredCamera[]): CameraFeedConfig[];
79
+ //# sourceMappingURL=ConnectorCameraClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConnectorCameraClient.d.ts","sourceRoot":"","sources":["../src/ConnectorCameraClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,4BAA4B;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;CAClD;AAED,gFAAgF;AAChF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;gBAExB,OAAO,EAAE,4BAA4B,EAAE,MAAM,GAAE,UAA8B;IAUnF,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC;CAmE/E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,gBAAgB,EAAE,EAC9B,UAAU,EAAE,gBAAgB,EAAE,GAC7B,gBAAgB,EAAE,CAiBpB"}
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectorCameraClient = exports.MAX_CAMERAS_PER_CALL = void 0;
4
+ exports.buildCameraFeeds = buildCameraFeeds;
5
+ const AudienceAnalyzeClient_1 = require("./AudienceAnalyzeClient");
6
+ /** The server caps a single registration call; mirrors MAX_CAMERAS_PER_CALL. */
7
+ exports.MAX_CAMERAS_PER_CALL = 64;
8
+ class ConnectorCameraClient {
9
+ constructor(options, poster = AudienceAnalyzeClient_1.defaultHttpPoster) {
10
+ if (!options?.apiBaseUrl)
11
+ throw new Error('ConnectorCameraClient requires an apiBaseUrl');
12
+ if (!options?.connectorKey)
13
+ throw new Error('ConnectorCameraClient requires a connectorKey');
14
+ const base = options.apiBaseUrl.replace(/\/+$/, '');
15
+ this.endpoint = `${base}/v2/earner/connectors/cameras`;
16
+ this.connectorKey = options.connectorKey;
17
+ this.timeoutMs = options.timeoutMs ?? 60000;
18
+ this.poster = poster;
19
+ }
20
+ async registerCameras(cameras) {
21
+ if (!Array.isArray(cameras) || cameras.length === 0) {
22
+ return { ok: false, error: 'registerCameras() requires at least one camera', kind: 'invalid' };
23
+ }
24
+ if (cameras.length > exports.MAX_CAMERAS_PER_CALL) {
25
+ return {
26
+ ok: false,
27
+ error: `registerCameras() accepts at most ${exports.MAX_CAMERAS_PER_CALL} cameras per call`,
28
+ kind: 'invalid',
29
+ };
30
+ }
31
+ let response;
32
+ try {
33
+ response = await this.poster(this.endpoint, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ Accept: 'application/json',
38
+ Authorization: `Bearer ${this.connectorKey}`,
39
+ },
40
+ timeoutMs: this.timeoutMs,
41
+ }, JSON.stringify({ cameras }));
42
+ }
43
+ catch (err) {
44
+ return {
45
+ ok: false,
46
+ error: `camera registration failed: ${err?.message ?? 'unknown error'}`,
47
+ kind: 'network',
48
+ };
49
+ }
50
+ let parsed = null;
51
+ try {
52
+ parsed = JSON.parse(response.body);
53
+ }
54
+ catch {
55
+ parsed = null;
56
+ }
57
+ const code = response.statusCode;
58
+ if (code < 200 || code >= 300) {
59
+ const kind = code === 401 || code === 403 ? 'auth' : code === 400 ? 'invalid' : 'server';
60
+ return {
61
+ ok: false,
62
+ error: parsed?.error || `camera registration failed (HTTP ${code})`,
63
+ kind,
64
+ };
65
+ }
66
+ const list = Array.isArray(parsed?.cameras) ? parsed.cameras : [];
67
+ // A 2xx that carries no tokens is a failure, not an empty success — treating
68
+ // it as success would write a `cameras[]` with no credentials and the defect
69
+ // would surface much later as a feed that never posts.
70
+ if (list.length === 0 || list.some((c) => !c?.deviceToken || !c?.screenId)) {
71
+ return {
72
+ ok: false,
73
+ error: 'registration response did not contain device tokens for every camera',
74
+ kind: 'server',
75
+ };
76
+ }
77
+ return { ok: true, cameras: list };
78
+ }
79
+ }
80
+ exports.ConnectorCameraClient = ConnectorCameraClient;
81
+ /**
82
+ * Join the server's answer back to the LOCAL stream URLs, producing the
83
+ * `cameras[]` the connector runs on.
84
+ *
85
+ * Matching is by `cameraKey` and ONLY by `cameraKey` — never by name and never
86
+ * by array position. Names are truncated server-side (200 chars) so two feeds
87
+ * differing only past that point would collapse onto one screen, and the server
88
+ * is free to return fewer rows than were sent. `cameraKey` is the deterministic
89
+ * idempotency key both sides already agree on.
90
+ *
91
+ * A returned camera with no local match is DROPPED rather than guessed at: a
92
+ * feed configured with the wrong URL would sense the wrong room, which is worse
93
+ * than a feed that is absent and visibly missing from the camera board.
94
+ */
95
+ function buildCameraFeeds(registered, discovered) {
96
+ const byKey = new Map(discovered.map((d) => [d.cameraKey, d]));
97
+ const feeds = [];
98
+ for (const cam of registered) {
99
+ const local = byKey.get(cam.cameraKey);
100
+ if (!local?.rtspUrl)
101
+ continue;
102
+ feeds.push({
103
+ name: cam.name || local.name,
104
+ screenId: cam.screenId,
105
+ deviceToken: cam.deviceToken,
106
+ rtspUrl: local.rtspUrl,
107
+ sourceType: local.source === 'channel-range' ? 'recorder' : 'direct_url',
108
+ channelKey: cam.cameraKey,
109
+ transport: 'tcp',
110
+ });
111
+ }
112
+ return feeds;
113
+ }
114
+ //# sourceMappingURL=ConnectorCameraClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConnectorCameraClient.js","sourceRoot":"","sources":["../src/ConnectorCameraClient.ts"],"names":[],"mappings":"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"}
package/dist/EdgeAgent.js CHANGED
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
44
44
  const CommandRouter_1 = require("./CommandRouter");
45
45
  const config_1 = require("./config");
46
46
  const dataDir_1 = require("./dataDir");
47
- const SDK_VERSION = '0.2.24';
47
+ const SDK_VERSION = '0.2.25';
48
48
  class EdgeAgent extends events_1.EventEmitter {
49
49
  constructor(config, deps) {
50
50
  super();