* 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
122 lines
5.3 KiB
Go
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
|
|
}
|