fctl

package
v3.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 14, 2026 License: MIT Imports: 40 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultMembershipURI = "https://app.formance.cloud/api"
	DefaultConsoleURL    = "https://portal.formance.cloud"
)
View Source
const (
	MembershipURIFlag    = "membership-uri"
	ConfigDir            = "config-dir"
	ProfileFlag          = "profile"
	OutputFlag           = "output"
	DebugFlag            = "debug"
	InsecureTlsFlag      = "insecure-tls"
	HTTPCloseOnErrorFlag = "http-close-on-error"
	TelemetryFlag        = "telemetry"
	StackFlag            = "stack"
	OrganizationFlag     = "organization"
	FrameworkURIFlag     = "framework-uri"
)
View Source
const AuthClient = "fctl"

Variables

View Source
var (
	OrganizationScopes = []string{
		"organization:Read",
		"organization:Create",
		"organization:Update",
		"organization:Delete",
		"organization:ListUsers",
		"organization:ReadUser",
		"organization:CreateUser",
		"organization:UpdateUser",
		"organization:DeleteUser",
		"organization:ListPolicies",
		"organization:ReadPolicy",
		"organization:CreatePolicy",
		"organization:UpdatePolicy",
		"organization:DeletePolicy",
		"organization:ListInvitations",
		"organization:ReadInvitation",
		"organization:CreateInvitation",
		"organization:UpdateInvitation",
		"organization:AcceptInvitation",
		"organization:RejectInvitation",
		"organization:DeleteInvitation",
		"organization:ListRegions",
		"organization:ReadRegion",
		"organization:CreateRegion",
		"organization:UpdateRegion",
		"organization:DeleteRegion",
		"organization:ListStacks",
		"organization:ReadStack",
		"organization:CreateStack",
		"organization:UpdateStack",
		"organization:DeleteStack",
		"organization:EnableStack",
		"organization:DisableStack",
		"organization:RestoreStack",
		"organization:UpgradeStack",
		"organization:ListStackUsers",
		"organization:ReadStackUser",
		"organization:CreateStackUser",
		"organization:UpdateStackUser",
		"organization:DeleteStackUser",
		"organization:ListStackModules",
		"organization:EnableStackModule",
		"organization:DisableStackModule",
		"organization:ListClients",
		"organization:ReadClient",
		"organization:CreateClient",
		"organization:UpdateClient",
		"organization:DeleteClient",
		"organization:ReadAuthProvider",
		"organization:UpdateAuthProvider",
		"organization:DeleteAuthProvider",
		"organization:ReadLogs",
		"organization:ListFeatures",
		"organization:ReadFeature",
	}
	StackScopes = []string{
		"stack:Read",
		"stack:Write",
	}
)
View Source
var (
	ErrOrganizationNotSpecified   = errors.New("organization not specified")
	ErrMultipleOrganizationsFound = errors.New("found more than one organization and no organization specified")
	ErrNoStackSpecified           = errors.New("no stack specified: use --stack=<stack-id>")
)
View Source
var (
	StyleGreen = pterm.NewStyle(pterm.FgLightGreen)
	StyleRed   = pterm.NewStyle(pterm.FgLightRed)
	StyleCyan  = pterm.NewStyle(pterm.FgLightCyan)

	BasicText     = pterm.DefaultBasicText
	BasicTextRed  = pterm.DefaultBasicText.WithStyle(StyleRed)
	BasicTextCyan = pterm.DefaultBasicText.WithStyle(StyleCyan)
	Section       = pterm.SectionPrinter{
		Style:           &pterm.ThemeDefault.SectionStyle,
		Level:           1,
		TopPadding:      0,
		BottomPadding:   0,
		IndentCharacter: "#",
	}
)
View Source
var ErrMissingApproval = errors.New("Missing approval.")
View Source
var (
	ErrOpeningBrowser = errors.New("opening browser")
)

Functions

func BoolPointerToString

func BoolPointerToString(v *bool) string

func BoolToString

func BoolToString(v bool) string

func CheckOrganizationApprobation

func CheckOrganizationApprobation(cmd *cobra.Command, disclaimer string, args ...any) bool

func CheckStackApprobation

func CheckStackApprobation(cmd *cobra.Command, disclaimer string, args ...any) bool

func ContainValue

func ContainValue[V comparable](array []V, value V) bool

func DeleteProfile

func DeleteProfile(cmd *cobra.Command, name string) error

func EnsureStackAccess

func EnsureStackAccess(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
	organizationID, stackID string,
) (*AccessToken, *StackAccess, error)

func FetchStackToken

func FetchStackToken(ctx context.Context, httpClient *http.Client, stackURI, token string) (*oauth2.Token, error)

func GetAuthRelyingParty

func GetAuthRelyingParty(ctx context.Context, httpClient *http.Client, membershipURI string) (client.RelyingParty, error)

func GetBool

func GetBool(cmd *cobra.Command, flagName string) bool

func GetCurrentProfileName

func GetCurrentProfileName(cmd *cobra.Command, config Config) string

func GetCursor

func GetCursor(cmd *cobra.Command) (string, error)

func GetDateTime

func GetDateTime(cmd *cobra.Command, flagName string) (*time.Time, error)

func GetFilePath

func GetFilePath(cmd *cobra.Command, filename string) string

func GetHttpClient

func GetHttpClient(cmd *cobra.Command) *http.Client

func GetInt

func GetInt(cmd *cobra.Command, flagName string) int

func GetPageSize

func GetPageSize(cmd *cobra.Command) (int32, error)

func GetSelectedOrganizationID

func GetSelectedOrganizationID(cmd *cobra.Command) string

func GetSelectedStackID

func GetSelectedStackID(cmd *cobra.Command, profile Profile) (string, error)

func GetString

func GetString(cmd *cobra.Command, flagName string) string

func GetStringSlice

func GetStringSlice(cmd *cobra.Command, flagName string) []string

func IsInvalidAuthentication

func IsInvalidAuthentication(err error) bool

func ListProfiles

func ListProfiles(cmd *cobra.Command, filters ...func(string) bool) ([]string, error)

func LoadAndAuthenticateCurrentProfile

func LoadAndAuthenticateCurrentProfile(cmd *cobra.Command) (*Config, *Profile, string, client.RelyingParty, error)

func LoadConfigDir

func LoadConfigDir(cmd *cobra.Command) string

func LoadConfigFilePath

func LoadConfigFilePath(cmd *cobra.Command) string

func Map

func Map[SRC any, DST any](srcs []SRC, mapper func(SRC) DST) []DST

func MapMap

func MapMap[KEY comparable, VALUE any, DST any](srcs map[KEY]VALUE, mapper func(KEY, VALUE) DST) []DST

func MembershipServerInfo

func MembershipServerInfo(ctx context.Context, apiClient *membershipclient.SDK) (*components.ServerInfo, error)

func MetadataAsShortString

func MetadataAsShortString[V any](metadata map[string]V) string

func NeedConfirm

func NeedConfirm(cmd *cobra.Command) bool

func NewAppDeployClient

func NewAppDeployClient(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
	organizationID string,
) (*deployserverclient.DeployServer, error)

func NewAppDeployClientFromFlags

func NewAppDeployClientFromFlags(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
) (string, *deployserverclient.DeployServer, error)

todo: deploy use membership token, we have to rely on membership applications

func NewCommand

func NewCommand(use string, opts ...CommandOption) *cobra.Command

func NewHTTPTransport

func NewHTTPTransport(cmd *cobra.Command) http.RoundTripper

func NewLazyRelyingParty

func NewLazyRelyingParty(ctx context.Context, httpClient *http.Client, membershipURI string) client.RelyingParty

func NewMembershipClient

func NewMembershipClient(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
) (*membershipclient.SDK, error)

func NewMembershipClientForOrganization

func NewMembershipClientForOrganization(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
	organizationID string,
) (*membershipclient.SDK, error)

func NewMembershipClientForOrganizationFromFlags

func NewMembershipClientForOrganizationFromFlags(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
) (string, *membershipclient.SDK, error)

func NewMembershipCommand

func NewMembershipCommand(use string, opts ...CommandOption) *cobra.Command

func NewStackClient

func NewStackClient(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
	organizationID, stackID string,
) (*formance.Formance, error)

func NewStackClientFromFlags

func NewStackClientFromFlags(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
) (*formance.Formance, error)

func NewStackCommand

func NewStackCommand(use string, opts ...CommandOption) *cobra.Command

func NewStackTokenSource

func NewStackTokenSource(
	stackToken AccessToken,
	stackAccess *StackAccess,
	relyingParty client.RelyingParty,
	onRefresh func(newToken AccessToken) error,
	cmd *cobra.Command,
	profileName string,
	organizationID string,
	stackID string,
) oauth2.TokenSource

func OpenURL

func OpenURL(urlString string) error

func OrganizationCompletion

func OrganizationCompletion(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective)

func ParseMetadata

func ParseMetadata(array []string) (metadata.Metadata, error)

func Prepend

func Prepend[V any](array []V, items ...V) []V

func PrintMetadata

func PrintMetadata(out io.Writer, metadata metadata.Metadata) error

func Printfln

func Printfln(fmt string, args ...any)

func Println

func Println(args ...any)

func Ptr

func Ptr[T any](t T) *T

func ReadFile

func ReadFile(cmd *cobra.Command, where string) (string, error)

func ReadJSONFile

func ReadJSONFile[V any](cmd *cobra.Command, filePath string) (*V, error)

func RenameProfile

func RenameProfile(cmd *cobra.Command, oldName, newName string) error

func RenderCursor

func RenderCursor(writer io.Writer, cursor Cursor) error

func ResetProfile

func ResetProfile(cmd *cobra.Command, name string) error

func ResolveOrganizationID

func ResolveOrganizationID(cmd *cobra.Command, profile Profile) (string, error)

func ResolveStackID

func ResolveStackID(cmd *cobra.Command, profile Profile) (string, string, error)

func StackCompletion

func StackCompletion(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective)

func StringPointerToString

func StringPointerToString(v *string) string

func StructToMap

func StructToMap(obj interface{}) (newMap map[string]interface{}, err error)

func UpsertConfigDir

func UpsertConfigDir(cmd *cobra.Command) error

func WithRender

func WithRender[T any](cmd *cobra.Command, args []string, c Controller[T], r Renderable) error

func WriteAppToken

func WriteAppToken(cmd *cobra.Command, profileName, appAlias string, token AccessToken) error

func WriteCachedStackAPIToken

func WriteCachedStackAPIToken(cmd *cobra.Command, profileName, organizationID, stackID string, token CachedStackAPIToken) error

func WriteConfig

func WriteConfig(cmd *cobra.Command, config Config) error

func WriteJSONFile

func WriteJSONFile(filePath string, data any) error

func WriteOrganizationToken

func WriteOrganizationToken(cmd *cobra.Command, profileName string, token AccessToken) error

func WriteProfile

func WriteProfile(cmd *cobra.Command, name string, profile Profile) error

func WriteStackToken

func WriteStackToken(cmd *cobra.Command, profileName, stackID string, token AccessToken) error

Types

type AccessToken

type AccessToken struct {
	TokenWithClaims[AccessTokenClaims]
	Refresh string `json:"refreshToken"`
}

func EnsureAppAccess

func EnsureAppAccess(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
	organizationID string,
	appAlias string,
	appScopes []string,
) (*AccessToken, error)

func EnsureMembershipAccess

func EnsureMembershipAccess(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
) (*AccessToken, error)

func EnsureOrganizationAccess

func EnsureOrganizationAccess(
	cmd *cobra.Command,
	relyingParty client.RelyingParty,
	dialog Dialog,
	profileName string,
	profile Profile,
	organizationID string,
) (*AccessToken, error)

func ReadAppToken

func ReadAppToken(cmd *cobra.Command, profileName, organizationID, appAlias string) (*AccessToken, error)

func ReadOrganizationToken

func ReadOrganizationToken(cmd *cobra.Command, profileName, organizationID string) (*AccessToken, error)

func ReadStackToken

func ReadStackToken(cmd *cobra.Command, profileName, organizationID, stackID string) (*AccessToken, error)

func Refresh

func Refresh(ctx context.Context, relyingParty client.RelyingParty, token AccessToken) (*AccessToken, error)

func (AccessToken) Expired

func (t AccessToken) Expired() bool

func (AccessToken) ToOAuth2

func (t AccessToken) ToOAuth2() *oauth2.Token

type AccessTokenClaims

type AccessTokenClaims struct {
	oidc.TokenClaims
	Scopes         oidc.SpaceDelimitedArray `json:"scope,omitempty"`
	OrganizationID string                   `json:"organization_id"`
}

type ApplicationAccess

type ApplicationAccess struct {
	Alias string `json:"alias"`
	ID    string `json:"id"`
	Name  string `json:"name"`
}

type AuthenticationOption

type AuthenticationOption func(url.Values)

func AuthenticateWithIDTokenHint

func AuthenticateWithIDTokenHint(idToken string) AuthenticationOption

func AuthenticateWithOrganizationID

func AuthenticateWithOrganizationID(organization string) AuthenticationOption

func AuthenticateWithPrompt

func AuthenticateWithPrompt(prompt ...string) AuthenticationOption

func AuthenticateWithResource

func AuthenticateWithResource(resource string) AuthenticationOption

func AuthenticateWithScopes

func AuthenticateWithScopes(scopes ...string) AuthenticationOption

type CachedStackAPIToken

type CachedStackAPIToken struct {
	AccessToken string    `json:"accessToken"`
	TokenType   string    `json:"tokenType"`
	Expiry      time.Time `json:"expiry"`
}

func ReadCachedStackAPIToken

func ReadCachedStackAPIToken(cmd *cobra.Command, profileName, organizationID, stackID string) (*CachedStackAPIToken, error)

type CommandOption

type CommandOption interface {
	// contains filtered or unexported methods
}

type CommandOptionFn

type CommandOptionFn func(cmd *cobra.Command)

func WithAliases

func WithAliases(aliases ...string) CommandOptionFn

func WithBoolFlag

func WithBoolFlag(name string, defaultValue bool, help string) CommandOptionFn

func WithChildCommands

func WithChildCommands(cmds ...*cobra.Command) CommandOptionFn

func WithConfirmFlag

func WithConfirmFlag() CommandOptionFn

func WithController

func WithController[T any](c Controller[T]) CommandOptionFn

func WithCursorFlag

func WithCursorFlag() CommandOptionFn

func WithDeprecated

func WithDeprecated(message string) CommandOptionFn

func WithDeprecatedFlag

func WithDeprecatedFlag(name, message string) CommandOptionFn

func WithDescription

func WithDescription(v string) CommandOptionFn

func WithHidden

func WithHidden() CommandOptionFn

func WithHiddenFlag

func WithHiddenFlag(name string) CommandOptionFn

func WithIntFlag

func WithIntFlag(name string, defaultValue int, help string) CommandOptionFn

func WithPageSizeFlag

func WithPageSizeFlag() CommandOptionFn

func WithPersistentBoolFlag

func WithPersistentBoolFlag(name string, defaultValue bool, help string) CommandOptionFn

func WithPersistentBoolPFlag

func WithPersistentBoolPFlag(name, short string, defaultValue bool, help string) CommandOptionFn

func WithPersistentPreRunE

func WithPersistentPreRunE(fn func(cmd *cobra.Command, args []string) error) CommandOptionFn

func WithPersistentStringFlag

func WithPersistentStringFlag(name, defaultValue, help string) CommandOptionFn

func WithPersistentStringPFlag

func WithPersistentStringPFlag(name, short, defaultValue, help string) CommandOptionFn

func WithPreRunE

func WithPreRunE(fn func(cmd *cobra.Command, args []string) error) CommandOptionFn

func WithRunE

func WithRunE(fn func(cmd *cobra.Command, args []string) error) CommandOptionFn

func WithShortDescription

func WithShortDescription(v string) CommandOptionFn

func WithSilenceError

func WithSilenceError() CommandOptionFn

func WithStringArrayFlag

func WithStringArrayFlag(name string, defaultValue []string, help string) CommandOptionFn

func WithStringFlag

func WithStringFlag(name, defaultValue, help string) CommandOptionFn

func WithStringSliceFlag

func WithStringSliceFlag(name string, defaultValue []string, help string) CommandOptionFn

func WithValidArgs

func WithValidArgs(validArgs ...string) CommandOptionFn

func WithValidArgsFunction

func WithValidArgsFunction(fn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)) CommandOptionFn

type Config

type Config struct {
	CurrentProfile string `json:"currentProfile"`
	UniqueID       string `json:"uniqueID,omitempty"`
}

func LoadConfig

func LoadConfig(cmd *cobra.Command) (*Config, error)

type Controller

type Controller[T any] interface {
	GetStore() T
	Run(cmd *cobra.Command, args []string) (Renderable, error)
}

type CurrentProfile

type CurrentProfile Profile

type Cursor

type Cursor struct {
	HasMore  bool
	PageSize int64
	Next     *string
	Previous *string
}

type Dialog

type Dialog interface {
	Info(msg string, args ...any)
}

func NewPTermDialog

func NewPTermDialog() Dialog

type ErrForbidden

type ErrForbidden struct {
}

func (*ErrForbidden) Error

func (e *ErrForbidden) Error() string

func (ErrForbidden) Is

func (e ErrForbidden) Is(target error) bool

type ErrInvalidAuthentication

type ErrInvalidAuthentication struct {
	// contains filtered or unexported fields
}

func (ErrInvalidAuthentication) Error

func (e ErrInvalidAuthentication) Error() string

func (ErrInvalidAuthentication) Is

func (ErrInvalidAuthentication) Unwrap

func (e ErrInvalidAuthentication) Unwrap() error

type ErrUnauthorized

type ErrUnauthorized struct {
}

func (*ErrUnauthorized) Error

func (e *ErrUnauthorized) Error() string

func (ErrUnauthorized) Is

func (e ErrUnauthorized) Is(target error) bool

type ExportedData

type ExportedData struct {
	Data interface{} `json:"data"`
}

type IDToken

type IDToken = TokenWithClaims[IDTokenClaims]

type IDTokenClaims

type IDTokenClaims struct {
	oidc.TokenClaims
	NotBefore       oidc.Time `json:"nbf,omitempty"`
	AccessTokenHash string    `json:"at_hash,omitempty"`
	CodeHash        string    `json:"c_hash,omitempty"`
	SessionID       string    `json:"sid,omitempty"`
	oidc.UserInfoProfile
	oidc.UserInfoEmail
	oidc.UserInfoPhone
	Address       *oidc.UserInfoAddress `json:"address,omitempty"`
	Organizations []OrganizationAccess  `json:"org"`
}

func (IDTokenClaims) GetAccessTokenHash

func (i IDTokenClaims) GetAccessTokenHash() string

func (IDTokenClaims) GetAuthTime

func (i IDTokenClaims) GetAuthTime() time.Time

func (IDTokenClaims) GetExpiration

func (i IDTokenClaims) GetExpiration() time.Time

func (IDTokenClaims) GetIssuedAt

func (i IDTokenClaims) GetIssuedAt() time.Time

func (IDTokenClaims) GetOrganizationAccess

func (i IDTokenClaims) GetOrganizationAccess(id string) *OrganizationAccess

func (IDTokenClaims) HasApplicationsAccess

func (i IDTokenClaims) HasApplicationsAccess(organizationID string, alias string) bool

func (IDTokenClaims) HasOrganizationAccess

func (i IDTokenClaims) HasOrganizationAccess(id string) bool

func (IDTokenClaims) HasStackAccess

func (i IDTokenClaims) HasStackAccess(organizationID string, stackID string) bool

type OrganizationAccess

type OrganizationAccess struct {
	ID           string              `json:"id"`
	DisplayName  string              `json:"displayName"`
	Stacks       []StackAccess       `json:"stacks"`
	Applications []ApplicationAccess `json:"applications"`
}

func (*OrganizationAccess) GetStackAccess

func (o *OrganizationAccess) GetStackAccess(stackID string) *StackAccess

type OrganizationsClaim

type OrganizationsClaim []OrganizationAccess

type Profile

type Profile struct {
	MembershipURI string  `json:"membershipURI"`
	RootTokens    *Tokens `json:"rootTokens"`

	DefaultOrganization string `json:"defaultOrganization"`
	DefaultStack        string `json:"defaultStack"`
}

func LoadAndAuthenticateCurrentProfileWithConfig

func LoadAndAuthenticateCurrentProfileWithConfig(cmd *cobra.Command, cfg Config) (*Profile, string, client.RelyingParty, error)

func LoadCurrentProfile

func LoadCurrentProfile(cmd *cobra.Command, cfg Config) (*Profile, string, error)

func LoadProfile

func LoadProfile(cmd *cobra.Command, name string) (*Profile, error)

func (*Profile) GetClaims

func (p *Profile) GetClaims() (AccessTokenClaims, error)

func (*Profile) GetDefaultOrganization

func (p *Profile) GetDefaultOrganization() string

func (*Profile) GetDefaultStack

func (p *Profile) GetDefaultStack() string

func (*Profile) GetMembershipURI

func (p *Profile) GetMembershipURI() string

func (*Profile) GetRootToken

func (p *Profile) GetRootToken() (*AccessToken, error)

func (*Profile) IsConnected

func (p *Profile) IsConnected() bool

func (*Profile) SetDefaultOrganization

func (p *Profile) SetDefaultOrganization(o string)

func (*Profile) UpdateRootToken

func (p *Profile) UpdateRootToken(tokens *Tokens)

type Renderable

type Renderable interface {
	Render(cmd *cobra.Command, args []string) error
}

type RoundTripperFn

type RoundTripperFn func(req *http.Request) (*http.Response, error)

func (RoundTripperFn) RoundTrip

func (fn RoundTripperFn) RoundTrip(req *http.Request) (*http.Response, error)

type StackAccess

type StackAccess struct {
	ID          string   `json:"id"`
	DisplayName string   `json:"displayName"`
	URI         string   `json:"uri"`
	Scopes      []string `json:"scopes"`
}

type TokenOption

type TokenOption func(url.Values)

func RequestResource

func RequestResource(resource string) TokenOption

type TokenWithClaims

type TokenWithClaims[T any] struct {
	Token  string `json:"token"`
	Claims T      `json:"claims"`
}

type Tokens

type Tokens struct {
	Access AccessToken `json:"accessToken"`
	ID     IDToken     `json:"idToken"`
}

func Authenticate

func Authenticate(
	ctx context.Context,
	relyingParty client.RelyingParty,
	dialog Dialog,
	authenticationOptions []AuthenticationOption,
	tokenOptions []TokenOption,
) (*Tokens, error)

type UnexpectedStatusCodeError

type UnexpectedStatusCodeError struct {
	StatusCode int
}

func (*UnexpectedStatusCodeError) Error

func (e *UnexpectedStatusCodeError) Error() string

func (UnexpectedStatusCodeError) Is

func (e UnexpectedStatusCodeError) Is(target error) bool

type UserClaims

type UserClaims struct {
	Email   string             `json:"email"`
	Subject string             `json:"sub"`
	Org     OrganizationsClaim `json:"org"`
}

func UserInfo

func UserInfo(cmd *cobra.Command, relyingParty client.RelyingParty, token AccessToken) (*UserClaims, error)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL