package pusher import ( "context" "errors" "strings" "github.com/flowy-live/llink/internal/network" ) var ErrUnauthorized = errors.New("unauthorized") type Authorizer struct { networkReader network.Reader } func NewAuthorizer(networkReader network.Reader) *Authorizer { return &Authorizer{networkReader: networkReader} } // Authorize accepts channel IDs of the form: // // network:{networkId} // stream:{networkId}:{streamId} // _presence:{humanId} func (a *Authorizer) Authorize(ctx context.Context, channelID, humanID string) error { parts := strings.SplitN(channelID, ":", 2) if len(parts) < 2 { return ErrUnauthorized } channelType := parts[0] rest := parts[1] switch channelType { case "network": return a.authorizeNetwork(ctx, rest, humanID) case "stream": return a.authorizeStream(ctx, rest, humanID) case "_presence": // Only the owning human may subscribe to their presence channel. if rest != humanID { return ErrUnauthorized } return nil default: return ErrUnauthorized } } func (a *Authorizer) authorizeNetwork(ctx context.Context, networkID, humanID string) error { isMember, err := a.networkReader.IsMember(ctx, networkID, humanID) if err != nil { return err } if !isMember { return ErrUnauthorized } return nil } // rest is "{networkId}:{streamId}"; stream-level visibility is enforced by network access. func (a *Authorizer) authorizeStream(ctx context.Context, rest, humanID string) error { parts := strings.SplitN(rest, ":", 2) if len(parts) < 2 { return ErrUnauthorized } networkID := parts[0] return a.authorizeNetwork(ctx, networkID, humanID) }