Documentation
¶
Index ¶
- func GetUserIDFromContext(ctx context.Context) (string, bool)
- func GetUserIDFromRequest(req *http.Request) (string, bool)
- func NewContextWithRequestContext(ctx context.Context, rc *RequestContext) context.Context
- func SetRequestContext(ctx context.Context, rc *RequestContext) context.Context
- type Account
- type AccountDatabaseHooksConfig
- type AuthMethodProvider
- type AuthProviderID
- type CORSConfig
- type Config
- type ConfigManager
- type ContextKey
- type CoreDatabaseHooksConfig
- type DatabaseConfig
- type Event
- type EventBus
- type EventBusConfig
- type EventHandler
- type EventPublisher
- type EventSubscriber
- type GoChannelConfig
- type Hook
- type HookHandler
- type HookMatcher
- type HookStage
- type KafkaConfig
- type Logger
- type LoggerConfig
- type Message
- type MiddlewareProvider
- type NatsConfig
- type Plugin
- type PluginContext
- type PluginID
- type PluginMetadata
- type PluginOption
- type PluginRegistry
- type PluginWithConfigWatcher
- type PluginWithHooks
- type PluginWithMiddleware
- type PluginWithMigrations
- type PluginWithRoutes
- type PluginsConfig
- type PostgreSQLConfig
- type PubSub
- type RabbitMQConfig
- type RedisConfig
- type RequestContext
- type Route
- type RouteMapping
- type SQLiteConfig
- type SecondaryStorage
- type SecondaryStorageType
- type SecurityConfig
- type ServiceID
- type ServiceRegistry
- type Session
- type SessionConfig
- type SessionDatabaseHooksConfig
- type SocialProviderConfig
- type SubscriptionID
- type User
- type UserDatabaseHooksConfig
- type Verification
- type VerificationDatabaseHooksConfig
- type VerificationType
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GetUserIDFromRequest ¶
GetUserIDFromRequest extracts the user ID from an HTTP request's context. It returns the user ID and a boolean indicating whether it was found.
func NewContextWithRequestContext ¶
func NewContextWithRequestContext(ctx context.Context, rc *RequestContext) context.Context
NewContextWithRequestContext returns a new context with the RequestContext attached
func SetRequestContext ¶
func SetRequestContext(ctx context.Context, rc *RequestContext) context.Context
SetRequestContext is an alias for NewContextWithRequestContext for convenience
Types ¶
type Account ¶
type Account struct {
bun.BaseModel `bun:"table:accounts"`
ID string `json:"id" bun:"column:id,pk"`
UserID string `json:"user_id" bun:"column:user_id"`
AccountID string `json:"account_id" bun:"column:account_id"`
ProviderID string `json:"provider_id" bun:"column:provider_id"`
AccessToken *string `json:"access_token" bun:"column:access_token"`
RefreshToken *string `json:"refresh_token" bun:"column:refresh_token"`
IDToken *string `json:"id_token" bun:"column:id_token"`
AccessTokenExpiresAt *time.Time `json:"access_token_expires_at" bun:"column:access_token_expires_at"`
RefreshTokenExpiresAt *time.Time `json:"refresh_token_expires_at" bun:"column:refresh_token_expires_at"`
Scope *string `json:"scope" bun:"column:scope"`
Password *string `json:"password" bun:"column:password"` // for email/password auth
CreatedAt time.Time `json:"created_at" bun:"column:created_at,default:current_timestamp"`
UpdatedAt time.Time `json:"updated_at" bun:"column:updated_at,default:current_timestamp"`
User User `json:"-" bun:"rel:belongs-to,join:user_id=id"`
}
type AuthMethodProvider ¶
type AuthMethodProvider interface {
AuthMiddleware() func(http.Handler) http.Handler
OptionalAuthMiddleware() func(http.Handler) http.Handler
}
AuthMethodProvider is an interface for plugins that provide authentication mechanisms
type AuthProviderID ¶
type AuthProviderID string
const ( AuthProviderEmail AuthProviderID = "email" AuthProviderDiscord AuthProviderID = "discord" AuthProviderGitHub AuthProviderID = "github" AuthProviderGoogle AuthProviderID = "google" )
func (AuthProviderID) String ¶
func (id AuthProviderID) String() string
type CORSConfig ¶
type CORSConfig struct {
AllowCredentials bool `json:"allow_credentials" toml:"allow_credentials"`
AllowedOrigins []string `json:"allowed_origins" toml:"allowed_origins"`
AllowedMethods []string `json:"allowed_methods" toml:"allowed_methods"`
AllowedHeaders []string `json:"allowed_headers" toml:"allowed_headers"`
ExposedHeaders []string `json:"exposed_headers" toml:"exposed_headers"`
MaxAge time.Duration `json:"max_age" toml:"max_age"`
}
type Config ¶
type Config struct {
// Core identity
AppName string `json:"app_name" toml:"app_name"`
BaseURL string `json:"base_url" toml:"base_url"`
BasePath string `json:"base_path" toml:"base_path"`
Secret string `json:"secret" toml:"secret"`
Database DatabaseConfig `json:"database" toml:"database"`
Logger LoggerConfig `json:"logger" toml:"logger"`
Session SessionConfig `json:"session" toml:"session"`
Security SecurityConfig `json:"security" toml:"security"`
EventBus EventBusConfig `json:"event_bus" toml:"event_bus"`
Plugins PluginsConfig `json:"plugins" toml:"plugins"`
// RouteMappings defines plugin-to-route mappings.
// Each route specifies which plugins should execute hooks for that endpoint.
// This enables fully declarative plugin routing in both standalone and library modes.
RouteMappings []RouteMapping `json:"route_mappings" toml:"route_mappings"`
// PreParsedConfigs stores the original typed plugin config objects.
// This allows skipping mapstructure unmarshalling and preserving type safety.
// Key: plugin ID, Value: typed config struct passed to Auth.New()
PreParsedConfigs map[string]any `json:"-" toml:"-"`
// CoreDatabaseHooks allows you to hook into database operations for users, accounts, sessions, and verifications.
CoreDatabaseHooks *CoreDatabaseHooksConfig `json:"-" toml:"-"`
}
Config holds the core configuration for GoBetterAuth.
type ConfigManager ¶
type ConfigManager interface {
// Init initializes the config manager, creating initial config if necessary
Init() error
// GetConfig returns the current configuration
GetConfig() *Config
// Load reloads the configuration from the storage
Load() error
// Update updates a specific configuration key with a new value
Update(key string, value any) error
// UpdateWithResult updates config and returns the updated config to avoid redundant GetConfig() calls
UpdateWithResult(key string, value any, result **Config) error
// Watch returns a channel that emits config updates
Watch(ctx context.Context) (<-chan *Config, error)
}
type ContextKey ¶
type ContextKey string
const ( ContextUserID ContextKey = "user_id" ContextSessionID ContextKey = "session_id" ContextSessionToken ContextKey = "session_token" ContextRequestContext ContextKey = "request_context" ContextAuthSuccess ContextKey = "auth.success" ContextAuthSignOut ContextKey = "auth.sign_out" ContextAuthIdempotentSkipTokensMint ContextKey = "auth.idempotent_skip_tokens_mint" )
func (ContextKey) String ¶
func (k ContextKey) String() string
type CoreDatabaseHooksConfig ¶
type CoreDatabaseHooksConfig struct {
Users *UserDatabaseHooksConfig
Accounts *AccountDatabaseHooksConfig
Sessions *SessionDatabaseHooksConfig
Verifications *VerificationDatabaseHooksConfig
}
type DatabaseConfig ¶
type DatabaseConfig struct {
Provider string `json:"provider" toml:"provider"`
URL string `json:"url" toml:"url"`
MaxOpenConns int `json:"max_open_conns" toml:"max_open_conns"`
MaxIdleConns int `json:"max_idle_conns" toml:"max_idle_conns"`
ConnMaxLifetime time.Duration `json:"conn_max_lifetime" toml:"conn_max_lifetime"`
}
type Event ¶
type Event struct {
ID string `json:"id"`
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
Payload json.RawMessage `json:"payload"`
Metadata map[string]string `json:"metadata"`
}
Event represents data to be published or received via the EventBus
type EventBus ¶
type EventBus interface {
EventPublisher
EventSubscriber
}
EventBus combines publisher and subscriber functionality
type EventBusConfig ¶
type EventBusConfig struct {
Prefix string `json:"prefix" toml:"prefix"`
MaxConcurrentHandlers int `json:"max_concurrent_handlers" toml:"max_concurrent_handlers"`
Provider events.EventBusProvider `json:"provider" toml:"provider"`
GoChannel *GoChannelConfig `json:"go_channel" toml:"go_channel"`
SQLite *SQLiteConfig `json:"sqlite" toml:"sqlite"`
PostgreSQL *PostgreSQLConfig `json:"postgres" toml:"postgres"`
Redis *RedisConfig `json:"redis" toml:"redis"`
Kafka *KafkaConfig `json:"kafka" toml:"kafka"`
NATS *NatsConfig `json:"nats" toml:"nats"`
RabbitMQ *RabbitMQConfig `json:"rabbitmq" toml:"rabbitmq"`
}
type EventHandler ¶
EventHandler processes events
type EventPublisher ¶
EventPublisher defines the interface for publishing events
type EventSubscriber ¶
type EventSubscriber interface {
Subscribe(eventType string, handler EventHandler) (SubscriptionID, error)
Unsubscribe(eventType string, id SubscriptionID)
Close() error
}
EventSubscriber defines the interface for subscribing to events
type GoChannelConfig ¶
type GoChannelConfig struct {
BufferSize int `json:"buffer_size" toml:"buffer_size"`
}
type Hook ¶
type Hook struct {
// Stage determines when this hook is executed
Stage HookStage
// PluginID identifies a plugin capability (e.g., "session.auth", "bearer.auth", "csrf.protect").
// If set, hook only executes if PluginID is in ctx.Route.Metadata["plugins"].
// If empty, hook executes based on Matcher logic.
PluginID string
// Matcher optionally filters when this hook should run (optional).
// Checked after PluginID filtering, if present.
Matcher HookMatcher
// Handler is the function that executes the hook
Handler HookHandler
// Order determines execution order when multiple hooks are at the same stage
// Lower order values execute first (0 is before 1, which is before 2, etc.).
// Order is local to the plugin (only compared against other hooks with same PluginID).
Order int
// Async determines if this hook runs in a background goroutine without blocking the response.
// Async hooks are for side-effects only (logging, analytics, events, webhooks, secondary storage).
// Must be false for all auth validation, CSRF, rate-limiting, and critical security hooks.
// Async hooks execute with a timeout to prevent leaks and have no access to response writer.
Async bool
}
Hook defines a request lifecycle hook that can be registered by plugins. Hooks provide a clean mechanism for plugins to intercept and modify the request lifecycle without tight coupling to the router.
Execution Semantics:
- Hooks execute in three phases: HookOnRequest → HookBefore (route handling) → HookAfter (before response)
- Within each stage, hooks are sorted by PluginID first (grouping), then by Order within each plugin
- Order values are plugin-local: comparing order only makes sense between hooks with the same PluginID
- If a hook's Handler returns an error, it is logged but does not stop further hook execution
- If a hook sets ctx.Handled=true, execution of subsequent hooks at that stage stops
- Hooks without a PluginID execute for all routes; hooks with PluginID only execute if listed in route metadata
- If a hook's Matcher returns false, the hook is skipped for that request
- Errors returned by HookHandler should not panic; they are handled gracefully
- Async hooks execute in background goroutines and do not block the response (side-effects only)
type HookHandler ¶
type HookHandler func(reqCtx *RequestContext) error
HookHandler is the function that executes a hook. It receives the request context and can modify request state, set UserID, populate Values, or set the Handled flag to short-circuit further processing.
type HookMatcher ¶
type HookMatcher func(reqCtx *RequestContext) bool
HookMatcher is a function that determines whether a hook should execute for a given request context. It allows hooks to be conditionally applied based on path, method, headers, or other request properties.
type HookStage ¶
type HookStage int
HookStage defines when a hook should be executed in the request lifecycle
const ( // HookOnRequest is executed for every request at the very start HookOnRequest HookStage = iota // HookBefore is executed before route matching and handling HookBefore // HookAfter is executed after route handling but before response is sent HookAfter // HookOnResponse is executed after the response has been written HookOnResponse )
type KafkaConfig ¶
type Logger ¶
type Logger interface {
// Debug logs a message at debug level with optional key-value pairs.
Debug(msg string, args ...any)
// Info logs a message at info level with optional key-value pairs.
Info(msg string, args ...any)
// Warn logs a message at warn level with optional key-value pairs.
Warn(msg string, args ...any)
// Error logs a message at error level with optional key-value pairs.
Error(msg string, args ...any)
}
Logger defines an interface for logging operations, allowing users to plug in different logging implementations such as slog, zerolog, or others.
type LoggerConfig ¶
type LoggerConfig struct {
Level string `json:"level" toml:"level"`
}
type Message ¶
type Message struct {
UUID string
Payload []byte // Message payload (serialized data)
Metadata map[string]string
}
Message represents a message in the pub/sub system.
type MiddlewareProvider ¶
MiddlewareProvider is an interface for plugins that provide global middleware
type NatsConfig ¶
type NatsConfig struct {
URL string `json:"url" toml:"url"`
}
type Plugin ¶
type Plugin interface {
Metadata() PluginMetadata
Config() any
Init(ctx *PluginContext) error
Close() error
}
Plugin is the base interface all plugins must implement
type PluginContext ¶
type PluginContext struct {
DB bun.IDB
Logger Logger
EventBus EventBus
ServiceRegistry ServiceRegistry
GetConfig func() *Config
}
PluginContext is the context passed to plugins during initialization.
type PluginID ¶
type PluginID string
const ( PluginConfigManager PluginID = "config_manager" PluginSecondaryStorage PluginID = "secondary_storage" PluginEmail PluginID = "email" PluginCSRF PluginID = "csrf" PluginEmailPassword PluginID = "email_password" PluginOAuth2 PluginID = "oauth2" PluginSession PluginID = "session" PluginJWT PluginID = "jwt" PluginBearer PluginID = "bearer" PluginRateLimit PluginID = "ratelimit" )
type PluginMetadata ¶
PluginMetadata contains metadata about a plugin
type PluginOption ¶
type PluginOption func(p Plugin)
type PluginRegistry ¶
type PluginRegistry interface {
Register(p Plugin) error
InitAll() error
RunMigrations(ctx context.Context) error
DropMigrations(ctx context.Context) error
Plugins() []Plugin
GetConfig() *Config
CloseAll()
GetPlugin(pluginID string) Plugin
}
PluginRegistry manages plugin registration and lifecycle
type PluginWithConfigWatcher ¶
PluginWithConfigWatcher is an optional interface that plugins can implement to receive real-time config updates. When the config is updated in the database, the ConfigManager will call OnConfigUpdate with the new config. Plugins should use this callback to update their own config structs using ParsePluginConfig, which ensures their internal config stays synchronized without changing pointer references.
type PluginWithHooks ¶
type PluginWithHooks interface {
Hooks() []Hook
}
PluginWithHooks is an optional interface that plugins can implement to provide request lifecycle hooks.
type PluginWithMiddleware ¶
PluginWithMiddleware is an optional interface for plugins that provide global middleware
type PluginWithMigrations ¶
type PluginWithMigrations interface {
Migrations(ctx context.Context, dbProvider string) (*embed.FS, error)
}
PluginWithMigrations is an optional interface for plugins that have database migrations
type PluginWithRoutes ¶
type PluginWithRoutes interface {
Routes() []Route
}
PluginWithRoutes is an optional interface for plugins that provide HTTP routes
type PluginsConfig ¶
PluginsConfig maps plugin IDs to their configurations
type PostgreSQLConfig ¶
type PostgreSQLConfig struct {
URL string `json:"url" toml:"url"`
}
type PubSub ¶
type PubSub interface {
// Publish sends a message to the specified topic
Publish(ctx context.Context, topic string, msg *Message) error
// Subscribe returns a channel that receives messages from the specified topic.
// The channel should be closed when the subscription is cancelled or closed.
Subscribe(ctx context.Context, topic string) (<-chan *Message, error)
// Close closes the pub/sub and cleans up resources
Close() error
}
PubSub is a generic publish-subscribe interface.
type RabbitMQConfig ¶
type RabbitMQConfig struct {
URL string `json:"url" toml:"url"`
}
type RedisConfig ¶
type RequestContext ¶
type RequestContext struct {
// Core HTTP components
Request *http.Request
ResponseWriter http.ResponseWriter
// Parsed request metadata
Path string
Method string
Headers http.Header
// User information (may be nil if not authenticated)
UserID *string
ClientIP string
// Generic key-value storage for hooks to share data
Values map[string]any
// Route is the matched route, assigned by the router after route matching.
// Plugins use Route.Metadata["plugins"] to determine if they should execute.
Route *Route
// Handled flag indicates whether a hook has handled the request
// and subsequent handlers should not be called
Handled bool
// Response capture fields allow handlers and hooks to override
// the final HTTP response written to the client
ResponseStatus int
ResponseHeaders http.Header
ResponseBody []byte
ResponseReady bool
ResponseData any
}
RequestContext provides a structured abstraction for passing context through request lifecycle hooks. It encapsulates all request-related information and provides control mechanisms for hooks.
func GetRequestContext ¶
func GetRequestContext(ctx context.Context) (*RequestContext, bool)
GetRequestContext retrieves the RequestContext from a context.Context
func (*RequestContext) SetJSONResponse ¶
func (reqCtx *RequestContext) SetJSONResponse(status int, payload any)
SetJSONResponse marshals the provided payload and stores it as the captured response body with the appropriate content type header. If marshaling fails, it falls back to a 500 Internal Server Error response.
func (*RequestContext) SetResponse ¶
func (reqCtx *RequestContext) SetResponse(status int, headers http.Header, body []byte)
SetResponse sets the captured response fields that will be written once the request lifecycle (including hooks) completes.
func (*RequestContext) SetUserIDInContext ¶
func (reqCtx *RequestContext) SetUserIDInContext(userID string)
SetUserIDInContext sets the user ID in both the RequestContext and the underlying Go context.
type RouteMapping ¶
type RouteMapping struct {
// Path is the route path (e.g., "/auth/me", "/auth/sign-in")
Path string `json:"path" toml:"path"`
// Method is the HTTP method (e.g., "GET", "POST", "PUT", "DELETE")
Method string `json:"method" toml:"method"`
// Plugins is the list of plugin IDs that should execute for this route.
// Plugin IDs follow the format "{plugin_name}.{operation}" (e.g., "session.auth", "csrf.protect")
Plugins []string `json:"plugins" toml:"plugins"`
}
RouteMapping defines which plugins should execute for a specific route. Used in both standalone and library modes to declaratively map routes to plugins. Standalone: via config.toml [[route_mappings]] table Library: via config.RouteMappings or WithRouteMappings option Example:
[[route_mappings]] path = "/auth/me" method = "GET" plugins = ["session.auth", "bearer.auth"]
type SQLiteConfig ¶
type SQLiteConfig struct {
DBPath string `json:"db_path" toml:"db_path"`
}
type SecondaryStorage ¶
type SecondaryStorage interface {
// Get retrieves the value associated with the given key.
Get(ctx context.Context, key string) (any, error)
// Set stores a value with an optional time-to-live (TTL).
Set(ctx context.Context, key string, value any, ttl *time.Duration) error
// Delete removes the value associated with the given key.
Delete(ctx context.Context, key string) error
// Incr increments an integer value associated with the given key.
Incr(ctx context.Context, key string, ttl *time.Duration) (int, error)
// TTL retrieves the time-to-live (TTL) for the given key.
TTL(ctx context.Context, key string) (*time.Duration, error)
// Close closes the storage and releases any resources.
Close() error
}
SecondaryStorage defines an interface for secondary storage operations.
type SecondaryStorageType ¶
type SecondaryStorageType string
const ( SecondaryStorageTypeMemory SecondaryStorageType = "memory" SecondaryStorageTypeDatabase SecondaryStorageType = "database" SecondaryStorageTypeCustom SecondaryStorageType = "custom" )
type SecurityConfig ¶
type SecurityConfig struct {
TrustedOrigins []string `json:"trusted_origins" toml:"trusted_origins"`
TrustedHeaders []string `json:"trusted_headers" toml:"trusted_headers"`
TrustedProxies []string `json:"trusted_proxies" toml:"trusted_proxies"`
CORS CORSConfig `json:"cors" toml:"cors"`
}
type ServiceID ¶
type ServiceID string
const ( // CORE ServiceUser ServiceID = "user_service" ServiceAccount ServiceID = "account_service" ServiceSession ServiceID = "session_service" ServiceVerification ServiceID = "verification_service" ServiceToken ServiceID = "token_service" // EMAIL ServicePassword ServiceID = "password_service" ServiceMailer ServiceID = "mailer_service" // JWT ServiceJWT ServiceID = "jwt_service" // CONFIG ServiceConfigManager ServiceID = "config_manager_service" // STORAGE ServiceSecondaryStorage ServiceID = "secondary_storage_service" )
type ServiceRegistry ¶
type Session ¶
type Session struct {
bun.BaseModel `bun:"table:sessions"`
ID string `json:"id" bun:"column:id,pk"`
UserID string `json:"user_id" bun:"column:user_id"`
Token string `json:"token" bun:"column:token"`
ExpiresAt time.Time `json:"expires_at" bun:"column:expires_at"`
IPAddress *string `json:"ip_address" bun:"column:ip_address"`
UserAgent *string `json:"user_agent" bun:"column:user_agent"`
CreatedAt time.Time `json:"created_at" bun:"column:created_at,default:current_timestamp"`
UpdatedAt time.Time `json:"updated_at" bun:"column:updated_at,default:current_timestamp"`
User User `json:"-" bun:"rel:belongs-to,join:user_id=id"`
}
type SessionConfig ¶
type SessionConfig struct {
CookieName string `json:"cookie_name" toml:"cookie_name"`
ExpiresIn time.Duration `json:"expires_in" toml:"expires_in"` // Sliding window per activity
UpdateAge time.Duration `json:"update_age" toml:"update_age"` // How often to check/update
CookieMaxAge time.Duration `json:"cookie_max_age" toml:"cookie_max_age"` // Absolute max age of the cookie
Secure bool `json:"secure" toml:"secure"`
HttpOnly bool `json:"http_only" toml:"http_only"`
SameSite string `json:"same_site" toml:"same_site"`
}
type SocialProviderConfig ¶
type SocialProviderConfig struct {
Enabled bool `json:"enabled" toml:"enabled"`
ClientID string `json:"client_id" toml:"client_id"`
ClientSecret string `json:"client_secret" toml:"client_secret"`
RedirectURL string `json:"redirect_url" toml:"redirect_url"`
Scopes []string `json:"scopes" toml:"scopes"`
}
type SubscriptionID ¶
type SubscriptionID uint64
SubscriptionID identifies a specific event handler subscription for removal
type User ¶
type User struct {
bun.BaseModel `bun:"table:users"`
ID string `json:"id" bun:"column:id,pk"`
Name string `json:"name" bun:"column:name"`
Email string `json:"email" bun:"column:email"`
EmailVerified bool `json:"email_verified" bun:"column:email_verified"`
Image *string `json:"image" bun:"column:image"`
Metadata json.RawMessage `json:"metadata" bun:"column:metadata"`
CreatedAt time.Time `json:"created_at" bun:"column:created_at,default:current_timestamp"`
UpdatedAt time.Time `json:"updated_at" bun:"column:updated_at,default:current_timestamp"`
}
type UserDatabaseHooksConfig ¶
type Verification ¶
type Verification struct {
bun.BaseModel `bun:"table:verifications"`
ID string `json:"id" bun:"column:id,pk"`
UserID *string `json:"user_id" bun:"column:user_id"`
Identifier string `json:"identifier" bun:"column:identifier"` // email or other identifier
Token string `json:"token" bun:"column:token"`
Type VerificationType `json:"type" bun:"column:type"`
ExpiresAt time.Time `json:"expires_at" bun:"column:expires_at"`
CreatedAt time.Time `json:"created_at" bun:"column:created_at,default:current_timestamp"`
UpdatedAt time.Time `json:"updated_at" bun:"column:updated_at,default:current_timestamp"`
User *User `json:"-" bun:"rel:belongs-to,join:user_id=id"`
}
type VerificationDatabaseHooksConfig ¶
type VerificationDatabaseHooksConfig struct {
BeforeCreate func(verification *Verification) error
AfterCreate func(verification Verification) error
}
type VerificationType ¶
type VerificationType string
const ( TypeEmailVerification VerificationType = "email_verification" TypePasswordResetRequest VerificationType = "password_reset_request" TypeEmailResetRequest VerificationType = "email_reset_request" )
func (VerificationType) String ¶
func (vt VerificationType) String() string