The first demo sealed the office cameras into the package and ran the PC standalone - a copy of the product with no head office. The bundle can now carry an installation code instead: setup redeems it exactly as the app's Setup screen does, the PC joins the shop, and its cameras arrive from head office on the first sync. The demo then IS the product - login, Loya, head office - not a local imitation of it. The code is single-use, so one bundle is one install. release.sh ships the bundle with DEMO_PACK=. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
94 lines
2.8 KiB
Go
94 lines
2.8 KiB
Go
// Command behavision-demo-pack seals a camera list into demo-cameras.enc for a
|
|
// demo release. It runs on the machine that builds the release and is never
|
|
// shipped.
|
|
//
|
|
// behavision-demo-pack -cameras cameras.json -out demo-cameras.enc
|
|
//
|
|
// Prints the unlock code exactly once. It is not stored anywhere; a code you
|
|
// can look up later is a code anyone with access to the build machine holds.
|
|
// Lose it and seal again.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/loyaly/behavision-agent/pkg/demo"
|
|
)
|
|
|
|
func main() {
|
|
in := flag.String("cameras", "", "JSON array of cameras (id, host, port, path, username, password) - the PC then runs on its own")
|
|
enrolCode := flag.String("enrol-code", "", "an installation code from head office - the PC then claims that shop and gets its cameras from there")
|
|
cloud := flag.String("cloud", "https://mcp.loyaly.ai", "head office, with -enrol-code")
|
|
out := flag.String("out", "demo-cameras.enc", "sealed bundle to write")
|
|
flag.Parse()
|
|
if *in == "" && *enrolCode == "" {
|
|
fmt.Fprintln(os.Stderr, "usage: behavision-demo-pack (-cameras cameras.json | -enrol-code CODE [-cloud URL]) [-out demo-cameras.enc]")
|
|
os.Exit(2)
|
|
}
|
|
|
|
var payload demo.Payload
|
|
if *in != "" {
|
|
raw, err := os.ReadFile(*in)
|
|
if err != nil {
|
|
die("read cameras: %v", err)
|
|
}
|
|
if err := json.Unmarshal(raw, &payload.Cameras); err != nil {
|
|
die("cameras.json: %v", err)
|
|
}
|
|
if len(payload.Cameras) == 0 {
|
|
die("no cameras in %s", *in)
|
|
}
|
|
}
|
|
payload.EnrolCode = *enrolCode
|
|
if *enrolCode != "" {
|
|
payload.CloudBase = *cloud
|
|
}
|
|
cams := payload.Cameras
|
|
for i, c := range cams {
|
|
switch {
|
|
case c.ID == "":
|
|
die("camera %d has no id", i)
|
|
case c.Host == "":
|
|
die("camera %q has no host", c.ID)
|
|
case c.Path == "":
|
|
die("camera %q has no path - the stream path is the field nobody can guess", c.ID)
|
|
}
|
|
}
|
|
// Re-marshal so only the fields the engine accepts travel, in a stable
|
|
// shape, whatever extra keys the input happened to carry.
|
|
plain, err := json.Marshal(payload)
|
|
if err != nil {
|
|
die("marshal: %v", err)
|
|
}
|
|
|
|
code, err := demo.NewCode()
|
|
if err != nil {
|
|
die("code: %v", err)
|
|
}
|
|
sealed, err := demo.Seal(code, plain)
|
|
if err != nil {
|
|
die("seal: %v", err)
|
|
}
|
|
if err := os.WriteFile(*out, sealed, 0o644); err != nil {
|
|
die("write: %v", err)
|
|
}
|
|
|
|
if payload.EnrolCode != "" {
|
|
fmt.Printf("\n sealed an installation code for %s into %s (%d bytes)\n\n", payload.CloudBase, *out, len(sealed))
|
|
} else {
|
|
fmt.Printf("\n sealed %d camera(s) into %s (%d bytes)\n\n", len(cams), *out, len(sealed))
|
|
}
|
|
fmt.Printf(" unlock code: %s\n\n", code)
|
|
fmt.Println(" Shown once. Give it to whoever runs behavision-setup, by voice")
|
|
fmt.Println(" or message - not in the same place as the zip.")
|
|
fmt.Println()
|
|
}
|
|
|
|
func die(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, " "+format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|