Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Dependabot configuration.
#
# This repo had NO dependency watching until 2026-09-09, and it is the
# one where that matters most: CameraNode ships as a BINARY onto
# customers' own hardware. A vulnerable dependency here is not something
# we can fix by redeploying — it sits on machines we do not control
# until each operator updates, so the lag between "advisory published"
# and "our build is clean" is the part that actually costs something.
#
# The gap was concrete, not theoretical. RUSTSEC advisories in this
# tree were cleared BY HAND earlier on 2026-09-09 (reqwest, tokio-
# tungstenite and warp were all moved), and nothing has watched them
# since. `cargo audit` in CI catches a known advisory against what is
# already pinned; it does nothing about a lockfile ageing out of
# support. That is what this is for.
#
# NO AUTO-MERGE WORKFLOW HERE, DELIBERATELY — a Cargo bump changes a
# binary that customers install. These get reviewed. (The sibling
# services skip auto-merge for a different reason: GITHUB_TOKEN merges
# do not trigger their deploy workflows.)
version: 2
updates:
# Rust — Cargo.toml + Cargo.lock at the repo root. This is the one
# that ends up in the shipped binary.
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
commit-message:
prefix: "build(deps)"
labels:
- "dependencies"
- "rust"

# The embedded web UI, built into the binary at compile time — so a
# vulnerable frontend dependency ships to customer hardware too,
# rather than being served from somewhere we can patch centrally.
- package-ecosystem: "npm"
directory: "/web"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
commit-message:
prefix: "build(deps-dev)"
labels:
- "dependencies"
- "frontend"

# GitHub Actions — this repo's Release workflow builds the artifacts
# customers download, so a stale action here is on the supply path.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "ci"
22 changes: 11 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,15 @@ jobs:
if: matrix.platform != 'windows'
run: |
cd target/${{ matrix.target }}/release
tar czf ../../../sourcebox-sentry-cloudnode-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz sourcebox-sentry-cloudnode
tar czf ../../../sourcebox-sentry-cameranode-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz sourcebox-sentry-cameranode
cd ../../..

- name: Package (Windows)
if: matrix.platform == 'windows'
shell: pwsh
run: |
cd target/${{ matrix.target }}/release
Compress-Archive -Path sourcebox-sentry-cloudnode.exe -DestinationPath ../../../sourcebox-sentry-cloudnode-${{ matrix.platform }}-${{ matrix.arch }}.zip
Compress-Archive -Path sourcebox-sentry-cameranode.exe -DestinationPath ../../../sourcebox-sentry-cameranode-${{ matrix.platform }}-${{ matrix.arch }}.zip
cd ../../..

# ── Build the Windows MSI ──────────────────────────────────────
Expand Down Expand Up @@ -187,7 +187,7 @@ jobs:
shell: pwsh
run: |
# --no-build because the cargo build step above already
# produced target\release\sourcebox-sentry-cloudnode.exe with the
# produced target\release\sourcebox-sentry-cameranode.exe with the
# right release-profile flags (LTO, strip). Letting cargo-wix
# rebuild would also use a stripped exe but pays the cost twice.
cargo wix --no-build --nocapture --output target/wix/
Expand All @@ -196,21 +196,21 @@ jobs:
if: matrix.platform == 'windows'
shell: pwsh
run: |
# cargo-wix names the MSI `sourcebox-sentry-cloudnode-<version>-<arch>.msi`
# cargo-wix names the MSI `sourcebox-sentry-cameranode-<version>-<arch>.msi`
# by default. Move it to the predictable name the install
# script + landing page link to. We keep the version-stamped
# name in the GitHub release URL by also uploading the
# original; this rename produces the "always-latest" copy
# the install scripts pull.
$generated = Get-ChildItem target/wix -Filter '*.msi' | Select-Object -First 1
if (-not $generated) { throw "No MSI generated under target/wix/" }
Copy-Item $generated.FullName -Destination "sourcebox-sentry-cloudnode-${{ matrix.platform }}-${{ matrix.arch }}.msi"
Copy-Item $generated.FullName -Destination "sourcebox-sentry-cameranode-${{ matrix.platform }}-${{ matrix.arch }}.msi"

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: sourcebox-sentry-cloudnode-${{ matrix.platform }}-${{ matrix.arch }}
path: sourcebox-sentry-cloudnode-${{ matrix.platform }}-${{ matrix.arch }}.${{ matrix.ext }}
name: sourcebox-sentry-cameranode-${{ matrix.platform }}-${{ matrix.arch }}
path: sourcebox-sentry-cameranode-${{ matrix.platform }}-${{ matrix.arch }}.${{ matrix.ext }}

# Separate upload for the MSI so the `release` job's
# `download-artifact` step picks it up alongside the .zip.
Expand All @@ -220,8 +220,8 @@ jobs:
if: matrix.platform == 'windows'
uses: actions/upload-artifact@v4
with:
name: sourcebox-sentry-cloudnode-${{ matrix.platform }}-${{ matrix.arch }}-msi
path: sourcebox-sentry-cloudnode-${{ matrix.platform }}-${{ matrix.arch }}.msi
name: sourcebox-sentry-cameranode-${{ matrix.platform }}-${{ matrix.arch }}-msi
path: sourcebox-sentry-cameranode-${{ matrix.platform }}-${{ matrix.arch }}.msi

release:
name: Create Release
Expand Down Expand Up @@ -300,10 +300,10 @@ jobs:
# `SourceBox-LLC/Sentinel-CameraNode` → `sourcebox-llc/sentinel-cameranode`.
# The GitHub repo was renamed `opensentry-cloud-node` → `Sentinel-CameraNode`
# in May 2026; ghcr.io image name follows the repo, so existing pulls of
# `ghcr.io/sourcebox-llc/opensentry-cloudnode:<tag>` will keep working
# `ghcr.io/sourcebox-llc/opensentry-cameranode:<tag>` will keep working
# only via GitHub's package-redirect window — re-pull against the new
# image name once you next update your compose/k8s manifests. The
# binary inside is still `sourcebox-sentry-cloudnode` per Cargo.toml.
# binary inside is still `sourcebox-sentry-cameranode` per Cargo.toml.
# `latest=auto` tags :latest only on stable semver tag pushes.
- name: Extract metadata
id: meta
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 23 additions & 17 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
[package]
# Package name = binary name. Brand history (display copy + repo URL
# have been updated to match the latest line; the BINARY name below
# has NOT, on purpose, because changing it breaks existing installs):
# v0 : OpenSentry → original brand
# v0 : SourceBox Sentry → mid-life rename; binary name was minted
# here as `sourcebox-sentry-cloudnode`
# v0+: Sentinel by SourceBox (current — repo renamed
# `opensentry-cloud-node` → `Sentinel-CameraNode` in May 2026,
# GitHub auto-redirects the old URL; binary name preserved
# verbatim so existing systemd units / Windows services /
# install scripts / Docker volumes keep working).
# Renaming the binary would orphan existing systemd units, Windows
# service registrations, install scripts, and Docker volumes. When we
# eventually do the breaking rename (probably alongside a v1.0.0
# release), this is the line that changes.
name = "sourcebox-sentry-cloudnode"
# Package name = binary name. Brand history:
# v0 : OpenSentry → original brand; repo was
# `opensentry-cloud-node`
# v0 : SourceBox Sentry → mid-life rename; binary was minted here as
# `sourcebox-sentry-cloudnode`
# v0+: Sentinel by SourceBox (current — repo renamed to
# `Sentinel-CameraNode` in May 2026; GitHub auto-redirects the
# old URL)
#
# The binary lagged the repo by one brand for years. This comment used
# to say the rename was deferred until "probably alongside a v1.0.0
# release" because renaming would orphan existing systemd units, Windows
# service registrations, and Docker volumes.
#
# Done 2026-09-09 instead, while the product had **zero installs** —
# there was nothing to orphan, and that is the only moment this costs
# nothing. The deferral was protecting against a cost that did not
# exist yet and would only have grown.
#
# `cameranode` is also simply the more accurate word: this binary drives
# cameras. It was never a cloud node.
name = "sourcebox-sentry-cameranode"
version = "0.1.76"
edition = "2021"
authors = ["SourceBox LLC"]
description = "Sentinel CloudNode — turns a USB webcam into a cloud-connected security camera."
description = "Sentinel CameraNode — turns a USB webcam into a cloud-connected security camera."
license = "GPL-3.0"
repository = "https://github.com/SourceBox-LLC/Sentinel-CameraNode"
homepage = "https://sentinel-command.com"
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ WORKDIR /app
COPY Cargo.toml Cargo.lock build.rs ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
cargo build --release && \
rm -rf src target/release/sourcebox-sentry-cloudnode*
rm -rf src target/release/sourcebox-sentry-cameranode*

# Pull the freshly-built SPA bundle from the web-builder stage so
# rust-embed picks it up at compile time. build.rs would otherwise
Expand Down Expand Up @@ -59,7 +59,7 @@ RUN adduser -D -s /bin/sh sentry
WORKDIR /app

# Copy binary
COPY --from=builder /app/target/release/sourcebox-sentry-cloudnode /usr/local/bin/
COPY --from=builder /app/target/release/sourcebox-sentry-cameranode /usr/local/bin/

# Create storage directories
RUN mkdir -p /app/data/hls && \
Expand All @@ -79,10 +79,10 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
# Volume for persistence
VOLUME ["/app/data"]

# Tell CloudNode where to persist its fallback machine-ID when /etc/machine-id
# Tell CameraNode where to persist its fallback machine-ID when /etc/machine-id
# isn't bind-mounted from the host. The ID lives in the volume so it survives
# container rebuilds. For stronger encryption (key tied to host, not data
# volume), run with `-v /etc/machine-id:/etc/machine-id:ro`.
ENV SOURCEBOX_SENTRY_DATA_DIR=/app/data

ENTRYPOINT ["sourcebox-sentry-cloudnode"]
ENTRYPOINT ["sourcebox-sentry-cameranode"]
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ curl -fsSL https://app.sentinel-command.com/install.sh | bash

1. Download `sourcebox-sentry-cameranode-windows-x86_64.msi` from the [latest release](https://github.com/SourceBox-LLC/Sentinel-CameraNode/releases/latest).
2. Run the MSI (UAC prompt). SmartScreen will warn "Windows protected your PC" because the installer is unsigned — click **More info → Run anyway**. (Code signing is on the roadmap.)
3. From the Start menu, click **Sentinel Camera Node**. First launch runs the setup wizard interactively, then drops into the foreground TUI dashboard with cameras streaming. Every launch after just streams.
3. From the Start menu, click **Sentinel CameraNode**. First launch runs the setup wizard interactively, then drops into the foreground TUI dashboard with cameras streaming. Every launch after just streams.

Config + recordings live under `C:\ProgramData\SourceBoxSentry\`. The setup wizard checks for FFmpeg and offers to install it via `winget install Gyan.FFmpeg` if it isn't already on PATH.

Expand All @@ -88,7 +88,7 @@ cd Sentinel-CameraNode
cargo build --release

# Run the interactive setup wizard
./target/release/sourcebox-sentry-cloudnode setup
./target/release/sourcebox-sentry-cameranode setup
```
</details>

Expand All @@ -104,7 +104,7 @@ The setup wizard handles everything automatically:
After setup, start the node:

```bash
./target/release/sourcebox-sentry-cloudnode
./target/release/sourcebox-sentry-cameranode
```

The TUI status bar prints the local browser-dashboard URL (e.g.
Expand Down Expand Up @@ -270,7 +270,7 @@ This is the everyday-use path: you can see what's happening, hit a slash command

| Path | Purpose |
|------|---------|
| `C:\Program Files\Sentinel Camera Node\sourcebox-sentry-cameranode.exe` | Binary (read-only after install) |
| `C:\Program Files\Sentinel CameraNode\sourcebox-sentry-cameranode.exe` | Binary (read-only after install) |
| `C:\ProgramData\SourceBoxSentry\node.db` | Encrypted SQLite — config + (optionally) recordings |
| `C:\ProgramData\SourceBoxSentry\logs\` | Log files (only written when running unattended; the foreground TUI logs to the console) |

Expand All @@ -285,7 +285,7 @@ sourcebox-sentry-cameranode setup

### Uninstalling

Use **Settings → Apps → Sentinel Camera Node → Uninstall**. The MSI uninstaller removes the binary and wipes `C:\ProgramData\SourceBoxSentry\` — including your encrypted config and recordings. FFmpeg installed via `winget` stays put because it's a separately-managed package.
Use **Settings → Apps → Sentinel CameraNode → Uninstall**. The MSI uninstaller removes the binary and wipes `C:\ProgramData\SourceBoxSentry\` — including your encrypted config and recordings. FFmpeg installed via `winget` stays put because it's a separately-managed package.

> **Heads up:** the MSI is **unsigned** today. SmartScreen flags unsigned installers with "Windows protected your PC" — click **More info → Run anyway** to proceed. Code signing is deferred until we have a sustained release cadence worth the EV-cert fee; the binary itself is open source and reproducibly buildable from this repository if you want to verify.

Expand Down Expand Up @@ -656,7 +656,7 @@ source "$HOME/.cargo/env"
git clone https://github.com/SourceBox-LLC/Sentinel-CameraNode.git
cd Sentinel-CameraNode
cargo build --release
./target/release/sourcebox-sentry-cloudnode setup
./target/release/sourcebox-sentry-cameranode setup
```

The first `cargo build --release` on a Pi 4 takes 15–20 minutes. Subsequent incremental builds after `git pull` are 1–3 minutes.
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
version: "3.8"

services:
sourcebox-sentry-cloudnode:
sourcebox-sentry-cameranode:
build: .
image: sourcebox-sentry-cloudnode:latest
container_name: sourcebox-sentry-cloudnode
image: sourcebox-sentry-cameranode:latest
container_name: sourcebox-sentry-cameranode

# Required for USB camera access
privileged: true
Expand Down
2 changes: 1 addition & 1 deletion examples/wsl_preflight_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//
// Useful for eyeballing the output when developing the preflight UI.

use sourcebox_sentry_cloudnode::setup::wsl_preflight::{
use sourcebox_sentry_cameranode::setup::wsl_preflight::{
is_internal_distro, is_likely_camera, probe_distro, probe_usbipd, probe_wsl,
};

Expand Down
4 changes: 2 additions & 2 deletions src/api/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Sentinel CloudNode - Camera streaming node for Sentinel Command Center
// Sentinel CameraNode - Camera streaming node for Sentinel Command Center
// Copyright (C) 2026 SourceBox LLC
//
// This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -179,7 +179,7 @@ impl ApiClient {
lan_streaming,
cameras,
// Sent on every heartbeat (not just register) so the backend
// sees in-place CloudNode upgrades without us needing to
// sees in-place CameraNode upgrades without us needing to
// re-register. Read from the build at compile time so a
// tampered runtime config can't claim a fake version.
version: env!("CARGO_PKG_VERSION").to_string(),
Expand Down
2 changes: 1 addition & 1 deletion src/api/commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Sentinel CloudNode - Camera streaming node for Sentinel Command Center
// Sentinel CameraNode - Camera streaming node for Sentinel Command Center
// Copyright (C) 2026 SourceBox LLC
//
// This program is free software: you can redistribute it and/or modify
Expand Down
2 changes: 1 addition & 1 deletion src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Sentinel CloudNode - Camera streaming node for Sentinel Command Center
// Sentinel CameraNode - Camera streaming node for Sentinel Command Center
// Copyright (C) 2026 SourceBox LLC
//
// This program is free software: you can redistribute it and/or modify
Expand Down
12 changes: 6 additions & 6 deletions src/api/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Sentinel CloudNode - Camera streaming node for Sentinel Command Center
// Sentinel CameraNode - Camera streaming node for Sentinel Command Center
// Copyright (C) 2026 SourceBox LLC
//
// This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -192,7 +192,7 @@ pub struct HeartbeatRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub cameras: Option<Vec<CameraStatus>>,

/// CloudNode build version (`env!("CARGO_PKG_VERSION")`).
/// CameraNode build version (`env!("CARGO_PKG_VERSION")`).
///
/// The backend uses this to gate too-old nodes (HTTP 426) and to flag
/// "update available" when we ship a newer release. Always sent — old
Expand Down Expand Up @@ -248,10 +248,10 @@ pub struct HeartbeatResponse {
// the fresh key" (what CC's rotation modal instructs); the 403
// error path in client.rs carries that hint.

/// Newer CloudNode release available (e.g. "0.2.0").
/// Newer CameraNode release available (e.g. "0.2.0").
///
/// Set when the backend's `LATEST_NODE_VERSION` is ahead of what we
/// reported. CloudNode logs a one-line "update available" warning when
/// reported. CameraNode logs a one-line "update available" warning when
/// this changes; we deliberately do NOT auto-update because operators
/// are running this on their own hardware. `None` means we're current.
#[serde(default)]
Expand Down Expand Up @@ -280,7 +280,7 @@ pub struct HeartbeatResponse {
/// Per-camera recording state, authoritative. `{camera_id: bool}`.
/// Computed server-side from each camera's `continuous_24_7` /
/// `scheduled_recording` policy + the current wall-clock time, so
/// the answer is fresh as of THIS heartbeat tick. CloudNode
/// the answer is fresh as of THIS heartbeat tick. CameraNode
/// reconciles its in-memory `recording_state: HashSet<camera_id>`
/// to exactly match this map: cameras with `true` get inserted,
/// cameras with `false` (or omitted) get removed.
Expand Down Expand Up @@ -362,7 +362,7 @@ mod tests {
fn register_request_includes_version() {
// Wire key MUST be `node_version` so the backend's Pydantic schema
// picks it up. The historical `version` key was silently dropped
// by Pydantic's default extra="ignore", so every CloudNode looked
// by Pydantic's default extra="ignore", so every CameraNode looked
// legacy at register time and the 426 gate never fired. Pin the
// correct name here so the bug can't come back.
let req = RegisterRequest {
Expand Down
Loading
Loading