Five components that ship as one product:
- behavision/ the recognition engine. RTSP ingest, YuNet detection, IoU
tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
FastAPI dashboard. Identity is decided once per TRACK from an
average of at least three embeddings, never per frame.
- agent/ the Go edge agent: supervises the engine, holds a durable
spool, and drains it to MQTT. Nothing is acked before the
broker confirms.
- desktop/ the shop PC application (Wails + React + tray).
- server/ the cloud API, MQTT consumer, reports and assistant.
- web/ platform.loyaly.ai, the head-office app, embedded in the
server binary.
The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.
CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
117 lines
3.7 KiB
Go
117 lines
3.7 KiB
Go
package blob
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func testStore(t *testing.T) *Store {
|
|
t.Helper()
|
|
s, err := New(Config{
|
|
Region: "sgp1", Endpoint: "sgp1.example.com", Bucket: "b",
|
|
AccessKey: "AK", SecretKey: "SK", Prefix: "behavision",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestNewNamesTheMissingSetting(t *testing.T) {
|
|
_, err := New(Config{Region: "sgp1"})
|
|
if err == nil {
|
|
t.Fatal("an empty config was accepted")
|
|
}
|
|
// A deploy fails at 9am in a shop, not at a keyboard, so the error has to
|
|
// say which variable is missing.
|
|
if !strings.Contains(err.Error(), "is missing") {
|
|
t.Fatalf("unhelpful error: %v", err)
|
|
}
|
|
}
|
|
|
|
// The server builds keys from the credential the request authenticated with,
|
|
// so a site cannot write into another site's prefix. This is the property that
|
|
// lets uploads be presigned instead of handing shop PCs a bucket key.
|
|
func TestKeyIsNamespacedAndCannotEscapeItsPrefix(t *testing.T) {
|
|
s := testStore(t)
|
|
at := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
|
|
|
|
got := s.Key("acme", "store1", "aaaa-bbbb", at)
|
|
want := "behavision/acme/store1/2026/08/31/aaaa-bbbb.jpg"
|
|
if got != want {
|
|
t.Fatalf("got %q, want %q", got, want)
|
|
}
|
|
// Slugs are constrained by a CHECK and visit ids are uuids, so this should
|
|
// never fire - which is why it is cheap to keep. One "../" reaching a key
|
|
// builder is a cross-tenant overwrite.
|
|
evil := s.Key("../../rival", "../store9", "../../../etc/passwd", at)
|
|
if strings.Contains(evil, "..") || strings.Contains(evil, "/etc/") {
|
|
t.Fatalf("a key escaped its prefix: %q", evil)
|
|
}
|
|
if !strings.HasPrefix(evil, "behavision/") {
|
|
t.Fatalf("key left the deployment prefix: %q", evil)
|
|
}
|
|
}
|
|
|
|
// The agent sends back the key it was given and a buggy or compromised one
|
|
// could send any string. Without this the server would presign reads for
|
|
// arbitrary objects in a bucket it shares with another application.
|
|
func TestOwnsKeyRejectsAnythingOutsideThePrefix(t *testing.T) {
|
|
s := testStore(t)
|
|
for _, key := range []string{
|
|
"", "Profile/93/user_profile-28.jpg", "behavision/../Profile/x.jpg",
|
|
"behavision//x.jpg", "other/behavision/x.jpg",
|
|
} {
|
|
if s.OwnsKey(key) {
|
|
t.Errorf("OwnsKey(%q) = true", key)
|
|
}
|
|
}
|
|
if !s.OwnsKey("behavision/acme/store1/2026/08/31/x.jpg") {
|
|
t.Error("a legitimate key was rejected")
|
|
}
|
|
}
|
|
|
|
func TestPresignRefusesForeignKeys(t *testing.T) {
|
|
s := testStore(t)
|
|
if _, err := s.PresignGet("Profile/93/user_profile-28.jpg", time.Minute); err == nil {
|
|
t.Fatal("presigned a read for another application's object")
|
|
}
|
|
if _, _, err := s.PresignPut("Profile/93/x.jpg", time.Minute); err == nil {
|
|
t.Fatal("presigned a write outside our prefix")
|
|
}
|
|
}
|
|
|
|
// A presigned URL is a bearer token for one object. A day-long one forwarded
|
|
// in an email outlives every reason it was issued for.
|
|
func TestPresignClampsAbsurdLifetimes(t *testing.T) {
|
|
s := testStore(t)
|
|
for _, ttl := range []time.Duration{0, -time.Hour, 72 * time.Hour} {
|
|
u, err := s.PresignGet("behavision/a/b/2026/08/31/x.jpg", ttl)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(u, "X-Amz-Expires=900") {
|
|
t.Errorf("ttl %s was not clamped to 15 minutes: %s", ttl, u)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPresignedPutSignsThePrivateACL(t *testing.T) {
|
|
s := testStore(t)
|
|
u, hdr, err := s.PresignPut("behavision/a/b/2026/08/31/x.jpg", time.Minute)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Signed, so the agent must send it and cannot choose to publish instead.
|
|
if !strings.Contains(u, "x-amz-acl") {
|
|
t.Fatalf("the ACL is not part of the signature: %s", u)
|
|
}
|
|
if hdr.Get("x-amz-acl") != "private" {
|
|
t.Fatalf("caller is not told to send a private ACL: %v", hdr)
|
|
}
|
|
if strings.Contains(u, s.cfg.SecretKey) {
|
|
t.Fatal("the secret key is in the URL")
|
|
}
|
|
}
|