Files
silo-server/internal/streamtoken/token.go
T
QuickandGitHub 47e45f4d67 feat(downloads): relay node-local artifacts through proxies (#608)
* feat(downloads): relay node-local artifacts through proxies

* fix(downloads): harden remote artifact relay

* fix(downloads): close distributed artifact review gaps

* fix(downloads): bound relays and refresh origins

* fix(downloads): recover stalled remote artifacts

* fix(transcode): report session cleanup failures

* fix(downloads): harden artifact cleanup recovery

* fix(downloads): close remote artifact lifecycle races

* fix(downloads): honor configured artifact storage

* fix(downloads): recover proxy-observed artifact misses

* test(downloads): isolate remote cleanup assertions

* fix(downloads): bound remote artifact recovery

* fix(downloads): allow concurrent artifact relays

* fix(downloads): harden distributed artifact recovery
2026-08-12 15:14:08 -04:00

122 lines
5.3 KiB
Go

package streamtoken
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
const (
// PlayMethodDownload identifies a token minted only after the API has
// authorized a file download. Proxy download routes reject playback tokens.
PlayMethodDownload = "download"
)
// 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"`
AudioOnly bool `json:"ao,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"`
// DownloadArtifactID is an opaque transcode-node artifact handle. For
// download tokens TranscodeNode is its authenticated origin; MediaPath stays
// empty so node-local filesystem paths never leave the owning node.
DownloadArtifactID string `json:"daid,omitempty"`
// DownloadArtifactRowID identifies the authoritative database row so a
// proxy can fence and requeue a signed remote locator that returns 404.
DownloadArtifactRowID string `json:"darid,omitempty"`
// DownloadFilename is the client-facing attachment name. Remote artifact
// ids are internal attempt handles and must never become saved filenames.
DownloadFilename string `json:"dfn,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"`
SourceVideoProfile string `json:"svp,omitempty"`
SourceVideoBitDepth int `json:"svb,omitempty"`
SoftwareVideoDecode bool `json:"svd,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"`
TargetAudioChannels int `json:"tac,omitempty"`
TargetAudioBitrateKbps int `json:"tabr,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
}