Files
silo-server/internal/streamtoken/token.go
T
CoffeeKnyte 22ffaad911 feat(playback): server-observed stream monitoring (async, no client trust)
Introduce a first-class, authoritative view of what is actually streaming,
observed server-side and never trusting client progress reports. This is the
base observation layer the kill switch and async over-cap enforcer build on.

- internal/streammonitor: live-stream snapshot model plus pluggable Sources
  (local func source, Redis source, multi-source fan-in) so a single node and a
  multi-node deployment expose the same picture.
- internal/nodesessions/tracker: serve-activity attribution — LastServedAt and
  served-byte counters advance from real serving, not client pings, giving an
  authoritative liveness signal.
- Client identity as monitoring attribution: Origin ("native" | "jellycompat")
  and ClientName ride the server-signed stream token (streamtoken.Claims) and
  the transcode-start request so an edge/transcode node — which never sees the
  originating API path — can stamp them onto its live-session record. These are
  attribution only: not byte-affecting and not a trust assertion.
- Serve-activity marks on the transcode node (MarkServed) so a node's own record
  reflects real serving instead of a LastServedAt frozen at start time.
- Admin observation surfaces: node/session listing carries owner + client
  identity and dedupes multi-record sessions.

Part of the stream monitoring & kill-switch epic.
2026-07-29 12:23:48 +00:00

107 lines
4.5 KiB
Go

package streamtoken
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
// Claims holds everything a stateless proxy or transcode node needs
// to serve a streaming session without database access.
//
// Under token-carried reconstruction (TR-lease) the token is also the durable
// reconstruction descriptor: its claims carry the full set of byte-affecting
// encode parameters (the former Postgres "recipe card"), so a front-end that has
// lost its in-memory session can rebuild ffmpeg from the token the client
// re-presents — no shared per-session store. The ownership claims (uid/pid/mfid)
// are lookup keys re-resolved against the authority on reconstruct; they are
// never trusted on their own.
type Claims struct {
SessionID string `json:"sid"`
MediaPath string `json:"path"`
PlayMethod string `json:"method"`
TranscodeAudio bool `json:"ta,omitempty"`
TranscodeNode string `json:"tnode,omitempty"`
TranscodeTransportID string `json:"tid,omitempty"`
TargetCodec string `json:"tc,omitempty"`
TargetRes string `json:"tres,omitempty"`
AudioCodec string `json:"ac,omitempty"`
AudioChannels int `json:"ach,omitempty"`
AudioTrackIndex int `json:"ati,omitempty"`
// DVProfile is the file's Dolby Vision profile (0 = none); remux nodes
// use it to strip dangling profile 7 RPUs. Absent in older tokens, which
// decodes as 0 (no strip — the pre-existing behavior).
DVProfile int `json:"dvp,omitempty"`
// RemuxDVMode freezes whether a Profile 7 remux preserves or strips DV
// metadata. Empty is the legacy auto behavior for old tokens.
RemuxDVMode string `json:"dvm,omitempty"`
// Ownership / authorization lookup keys (re-resolved at reconstruct).
// Not trust assertions.
UserID int `json:"uid,omitempty"`
ProfileID string `json:"pid,omitempty"`
MediaFileID int `json:"mfid,omitempty"`
// Monitoring attribution (not byte-affecting, not a trust assertion): the
// origin protocol ("native" | "jellycompat") and the reported client/app
// name, carried so an edge node — which never sees the originating API path —
// can stamp them onto its live-session record for the first-class monitor view.
Origin string `json:"org,omitempty"`
ClientName string `json:"cn,omitempty"`
// Reconstruction recipe — the byte-affecting encode parameters, mirroring the
// former playback.RecipeCard. Zero for direct/remux tokens, which reconstruct
// from identity alone plus the client-supplied position.
SourceVideoCodec string `json:"svc,omitempty"`
VideoBitstreamFilter string `json:"vbsf,omitempty"`
OutputSubdir string `json:"osd,omitempty"`
SeekSeconds float64 `json:"seek,omitempty"`
StreamOriginSeconds float64 `json:"origin,omitempty"`
CopySeekAnchorResolved bool `json:"origin_ok,omitempty"`
SegmentDuration int `json:"segd,omitempty"`
StartSegmentNumber int `json:"ssn,omitempty"`
SubtitleTrackIndex int `json:"sti,omitempty"`
SubtitleBurnIn bool `json:"sbi,omitempty"`
SubtitleCodec string `json:"sbc,omitempty"`
TargetBitrateKbps int `json:"tbr,omitempty"`
TotalDuration float64 `json:"dur,omitempty"`
FastStart bool `json:"fs,omitempty"`
TargetCodecAudio string `json:"tca,omitempty"`
// Recipe staleness hint, bumped on each re-mint after a recipe mutation
// (audio/quality/seek switch). An optional client-side hint only.
Version int `json:"ver,omitempty"`
jwt.RegisteredClaims
}
// Sign creates a signed JWT string from the given claims.
func Sign(c Claims, secret string, ttl time.Duration) (string, error) {
now := time.Now()
c.RegisteredClaims = jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
IssuedAt: jwt.NewNumericDate(now),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
return token.SignedString([]byte(secret))
}
// Verify parses and validates a stream token JWT string.
func Verify(tokenString, secret string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return nil, fmt.Errorf("invalid stream token: %w", err)
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid stream token claims")
}
return claims, nil
}