Documentation
¶
Overview ¶
Package keysmith is a composable API key management engine for the Forge ecosystem.
Keysmith handles the full lifecycle of API keys: generation, hashing, storage, validation, rotation, revocation, and usage analytics. Raw API keys are never persisted — only their SHA-256 hashes are stored, and the raw key is returned exactly once at creation time.
Quick Start ¶
Create an engine with an in-memory store (replace with postgres in production):
eng, err := keysmith.NewEngine(
keysmith.WithStore(memory.New()),
)
Set a tenant context and create a key:
ctx := keysmith.WithTenant(ctx, "my-app", "tenant-1")
result, err := eng.CreateKey(ctx, &keysmith.CreateKeyInput{
Name: "Production Key",
Prefix: "sk",
Environment: key.EnvLive,
})
fmt.Println(result.RawKey) // shown once — save it
Validate the key on incoming requests:
vr, err := eng.ValidateKey(ctx, rawKey) fmt.Println(vr.Key.TenantID, vr.Scopes)
Architecture ¶
The engine coordinates five subsystems, each backed by a pluggable store:
- key — core key entity, lifecycle states (active/rotated/expired/revoked/suspended)
- policy — rate limits, allowed scopes/IPs/origins, key lifetime constraints
- scope — hierarchical permission scopes assignable to keys
- usage — per-request usage records and aggregated analytics
- rotation — rotation history with grace-period tracking
Plugins ¶
The plugin package defines opt-in lifecycle hook interfaces. A plugin implements plugin.Plugin (Name) and any combination of hooks like plugin.KeyCreated or plugin.KeyRevoked. Built-in plugins:
- audit_hook — emits structured audit events to a [audithook.Recorder] backend
- observability — increments go-utils metric counters for each lifecycle event
- warden_hook — syncs scopes as Warden permissions and assigns roles to API keys
Telemetry ¶
The engine starts OpenTelemetry spans on the global tracer provider (validate key, create key, rotate key and the other key state changes), and records metrics when you pass WithMetrics. The Forge extension passes app.Metrics(), so in a Forge app both are on with no setup. Spans and metric labels carry bounded enums only, never key material, IDs or tenants.
Forge Integration ¶
The [extension] package adapts Keysmith as a forge.Extension, registering the engine in the DI container, mounting REST API routes, and running migrations on start. The [middleware] package provides HTTP middleware for API key validation.
Index ¶
- Constants
- Variables
- func WithTenant(ctx context.Context, appID, tenantID string) context.Context
- type Config
- type CreateKeyInput
- type Engine
- func (e *Engine) AggregateUsage(ctx context.Context, filter *usage.QueryFilter) ([]*usage.Aggregation, error)
- func (e *Engine) AssignScopes(ctx context.Context, keyID id.KeyID, scopeNames []string) error
- func (e *Engine) CleanupExpiredKeys(ctx context.Context) error
- func (e *Engine) CreateKey(ctx context.Context, input *CreateKeyInput) (*key.CreateResult, error)
- func (e *Engine) CreatePolicy(ctx context.Context, pol *policy.Policy) error
- func (e *Engine) CreateScope(ctx context.Context, s *scope.Scope) error
- func (e *Engine) DeletePolicy(ctx context.Context, polID id.PolicyID) error
- func (e *Engine) DeleteScope(ctx context.Context, scopeID id.ScopeID) error
- func (e *Engine) EndGrace(ctx context.Context, keyID id.KeyID) (int64, error)
- func (e *Engine) GetKey(ctx context.Context, keyID id.KeyID) (*key.Key, error)
- func (e *Engine) GetPolicy(ctx context.Context, polID id.PolicyID) (*policy.Policy, error)
- func (e *Engine) Health(ctx context.Context) error
- func (e *Engine) ListKeys(ctx context.Context, filter *key.ListFilter) ([]*key.Key, error)
- func (e *Engine) ListPolicies(ctx context.Context, filter *policy.ListFilter) ([]*policy.Policy, error)
- func (e *Engine) ListRotations(ctx context.Context, filter *rotation.ListFilter) ([]*rotation.Record, error)
- func (e *Engine) ListScopes(ctx context.Context, filter *scope.ListFilter) ([]*scope.Scope, error)
- func (e *Engine) QueryUsage(ctx context.Context, filter *usage.QueryFilter) ([]*usage.Record, error)
- func (e *Engine) RateLimiterConfigured() bool
- func (e *Engine) ReactivateKey(ctx context.Context, keyID id.KeyID) error
- func (e *Engine) RecordUsage(ctx context.Context, rec *usage.Record) error
- func (e *Engine) RemoveScopes(ctx context.Context, keyID id.KeyID, scopeNames []string) error
- func (e *Engine) RevokeKey(ctx context.Context, keyID id.KeyID, reason string) error
- func (e *Engine) RotateKey(ctx context.Context, keyID id.KeyID, reason rotation.Reason, ...) (*key.CreateResult, error)
- func (e *Engine) Start(_ context.Context) error
- func (e *Engine) Stop(ctx context.Context) error
- func (e *Engine) Store() store.Store
- func (e *Engine) SuspendKey(ctx context.Context, keyID id.KeyID) error
- func (e *Engine) UpdatePolicy(ctx context.Context, pol *policy.Policy) error
- func (e *Engine) ValidateKey(ctx context.Context, rawKey string) (*ValidationResult, error)
- type Hasher
- type ID
- type KeyGenerator
- type Option
- type Prefix
- type RateLimiter
- type RotateOption
- type ValidationResult
Constants ¶
const InstrumentationName = "github.com/xraph/keysmith"
InstrumentationName is the tracer name on every span keysmith starts.
Variables ¶
var ( // ErrInvalidKey is returned when the provided API key is not valid. ErrInvalidKey = errors.New("keysmith: invalid API key") // ErrKeyInactive is returned when the key is not in an active state. ErrKeyInactive = errors.New("keysmith: key is not active") // ErrKeyExpired is returned when the key has expired. ErrKeyExpired = errors.New("keysmith: key has expired") // ErrKeyRevoked is returned when the key has been permanently revoked. ErrKeyRevoked = errors.New("keysmith: key has been revoked") // ErrKeySuspended is returned when the key is temporarily suspended. ErrKeySuspended = errors.New("keysmith: key is suspended") // ErrRateLimited is returned when the key exceeds its rate limit. ErrRateLimited = errors.New("keysmith: rate limit exceeded") // ErrQuotaExceeded is returned when the key exceeds its usage quota. ErrQuotaExceeded = errors.New("keysmith: usage quota exceeded") // ErrInvalidStateTransition is returned for illegal key state changes. ErrInvalidStateTransition = errors.New("keysmith: invalid state transition") // ErrPolicyInUse is returned when deleting a policy that a key which is // not revoked (active, suspended or expired) still uses. ErrPolicyInUse = errors.New("keysmith: policy is used by keys that are not revoked") // ErrPolicyNameTaken is returned when a policy is created or renamed to a // name another policy in the same tenant already has. ErrPolicyNameTaken = errors.New("keysmith: a policy with this name already exists in the tenant") // ErrScopeNameTaken is returned when a scope is created with a name // another scope in the same tenant already has. ErrScopeNameTaken = errors.New("keysmith: a scope with this name already exists in the tenant") // ErrScopeHasChildren is returned when deleting a scope that other scopes // in its tenant name as their parent. ErrScopeHasChildren = errors.New("keysmith: other scopes name this scope as their parent") // ErrScopeAllowedByPolicy is returned when deleting a scope that a policy // in its tenant lists in AllowedScopes. Dropping the name from the policy // instead could empty its list, and an empty list allows every scope. ErrScopeAllowedByPolicy = errors.New("keysmith: a policy allows this scope") // ErrPolicyNotFound is returned when a policy cannot be found. ErrPolicyNotFound = store.ErrPolicyNotFound // ErrKeyNotFound is returned when a key cannot be found. ErrKeyNotFound = store.ErrKeyNotFound // ErrKeyConflict is returned when a key write lost a race: the key // changed between the engine reading it and writing it back, so the // write was refused rather than putting an older row over the newer one. ErrKeyConflict = store.ErrKeyConflict // ErrScopeNotFound is returned when a scope cannot be found. ErrScopeNotFound = store.ErrScopeNotFound // ErrKeyLifetimeExceeded is returned when a key is created with an expiry // further out than its policy's MaxKeyLifetime allows. ErrKeyLifetimeExceeded = errors.New("keysmith: expiry is beyond the policy's maximum key lifetime") // ErrScopeNotAllowed is returned when a scope is not permitted by the policy. ErrScopeNotAllowed = errors.New("keysmith: scope not allowed by policy") // ErrIPNotAllowed is returned when the IP address is not in the allowlist. ErrIPNotAllowed = errors.New("keysmith: IP address not allowed") // ErrOriginNotAllowed is returned when the origin is not in the allowlist. ErrOriginNotAllowed = errors.New("keysmith: origin not allowed") // ErrRotationNotFound is returned when a rotation record cannot be found. ErrRotationNotFound = store.ErrRotationNotFound )
Functions ¶
Types ¶
type Config ¶
type Config struct {
// DefaultPrefix is the default key prefix (e.g., "sk").
DefaultPrefix string
// DefaultEnvironment is the default key environment.
DefaultEnvironment key.Environment
// DefaultKeyLength is the byte count for the random portion of keys.
DefaultKeyLength int
}
Config holds configuration for the Keysmith engine.
type CreateKeyInput ¶
type CreateKeyInput struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Prefix string `json:"prefix"`
Environment key.Environment `json:"environment"`
PolicyID *id.PolicyID `json:"policy_id,omitempty"`
Scopes []string `json:"scopes,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
CreateKeyInput contains the parameters for creating a new API key.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the central Keysmith engine that coordinates all subsystems.
func (*Engine) AggregateUsage ¶
func (e *Engine) AggregateUsage(ctx context.Context, filter *usage.QueryFilter) ([]*usage.Aggregation, error)
AggregateUsage returns aggregated usage statistics.
func (*Engine) AssignScopes ¶
AssignScopes assigns scopes to a key by name.
func (*Engine) CleanupExpiredKeys ¶
CleanupExpiredKeys finds and marks expired keys. Keysmith never calls it: schedule it from your host.
Each run that finds expired keys, or fails to list them, is one root span, expire keys, linked to the span in ctx when there is one. A run that finds nothing, or whose caller cancelled it before the listing came back, starts no span, so a frequent schedule doesn't mint an empty trace every tick. A run where any key failed to expire with a real error gets an error status carrying the first such error's type.
The listing runs before the run's span exists, so it runs with the span stripped from ctx. A scheduler's ctx often comes from an extension's Start, which forge v1.12.9 calls inside a "start <ext>" span that ends at boot; kept, it would hang the listing query's db.client span off that finished trace on every idle tick. Cancellation and values carry over.
func (*Engine) CreateKey ¶
func (e *Engine) CreateKey(ctx context.Context, input *CreateKeyInput) (*key.CreateResult, error)
CreateKey generates a new API key, hashes it, stores the hash, and returns the raw key exactly once. The raw key is never persisted.
func (*Engine) CreatePolicy ¶
CreatePolicy creates a new key policy.
func (*Engine) CreateScope ¶
CreateScope creates a permission scope.
func (*Engine) DeletePolicy ¶
DeletePolicy deletes a policy by ID. It refuses with ErrPolicyInUse while any key that is not revoked uses the policy. A revoked key never validates again and cannot be deleted, so it does not hold the policy; it keeps the policy's ID after the delete. A key with RevokedAt set counts as revoked whatever its State says, as it does everywhere else in the engine.
func (*Engine) DeleteScope ¶
DeleteScope deletes a scope by ID and takes it off every key that holds it. It refuses with ErrScopeHasChildren while another scope in the same tenant names it as its parent, then with ErrScopeAllowedByPolicy while a policy in the tenant lists it in AllowedScopes. It answers ErrScopeNotFound for a missing scope.
func (*Engine) EndGrace ¶
EndGrace closes every open grace window on a key now, so every previous key stops validating. It returns how many windows it closed.
func (*Engine) ListPolicies ¶
func (e *Engine) ListPolicies(ctx context.Context, filter *policy.ListFilter) ([]*policy.Policy, error)
ListPolicies returns policies matching the filter.
func (*Engine) ListRotations ¶
func (e *Engine) ListRotations(ctx context.Context, filter *rotation.ListFilter) ([]*rotation.Record, error)
ListRotations returns rotation records matching the filter.
func (*Engine) ListScopes ¶
ListScopes returns scopes for the tenant.
func (*Engine) QueryUsage ¶
func (e *Engine) QueryUsage(ctx context.Context, filter *usage.QueryFilter) ([]*usage.Record, error)
QueryUsage queries usage records.
func (*Engine) RateLimiterConfigured ¶
RateLimiterConfigured reports whether a RateLimiter was injected. Without one, a policy's RateLimit is stored and never enforced.
func (*Engine) ReactivateKey ¶
ReactivateKey re-enables a suspended key. A key with RevokedAt set is revoked whatever its State says, and stays that way. The write is version-checked like RevokeKey's, so a key that changed after the read is refused with ErrKeyConflict.
func (*Engine) RecordUsage ¶
RecordUsage records a single usage event for a key.
func (*Engine) RemoveScopes ¶
RemoveScopes removes scopes from a key by name.
func (*Engine) RevokeKey ¶
RevokeKey permanently disables a key. Revocation is terminal: no state change leads out of it, and revoking twice is refused so hooks fire once. If the key changes between the read and the write, the revoke is refused with ErrKeyConflict and the caller can read the key again and retry.
func (*Engine) RotateKey ¶
func (e *Engine) RotateKey(ctx context.Context, keyID id.KeyID, reason rotation.Reason, opts ...RotateOption) (*key.CreateResult, error)
RotateKey issues a new raw key for the same key record. The previous raw key keeps validating until its grace window ends. The window comes from WithGrace if given, else the key's policy GracePeriod, else 24 hours. A window of zero stops the previous key at once. A revoked or expired key cannot be rotated, and a key with RevokedAt set counts as revoked. If another write reaches the key between the read and the write (a revoke, for example), the rotation is refused with ErrKeyConflict and the key keeps the other write.
func (*Engine) SuspendKey ¶
SuspendKey temporarily disables an active key. Only an active key can be suspended; anything else, a revoked key above all, is refused. A key with RevokedAt set is revoked whatever its State says. The write is version-checked like RevokeKey's, so a key that changed after the read is refused with ErrKeyConflict.
func (*Engine) UpdatePolicy ¶
UpdatePolicy updates an existing policy.
func (*Engine) ValidateKey ¶
ValidateKey validates a raw API key and returns the key record if valid. This is the hot path — optimized for speed.
ErrInvalidKey means the key is not known. If the key store cannot answer, or ctx is cancelled or past its deadline, ValidateKey returns an error that wraps the cause (a store error, context.Canceled or context.DeadlineExceeded) and is not ErrInvalidKey, and it does not fire the KeyValidationFailed hook. Callers should answer such an error with a retryable 5xx, not a 401.
type Hasher ¶
type Hasher interface {
// Hash produces a deterministic hash of the raw key.
Hash(rawKey string) (string, error)
// Verify checks whether a raw key matches a stored hash.
Verify(rawKey, hash string) (bool, error)
}
Hasher hashes raw API keys for secure storage.
type KeyGenerator ¶
type KeyGenerator interface {
// Generate produces a raw API key string with the given prefix and environment.
Generate(prefix string, env key.Environment) (string, error)
}
KeyGenerator generates raw API key strings.
func DefaultKeyGenerator ¶
func DefaultKeyGenerator() KeyGenerator
DefaultKeyGenerator returns a generator producing keys in the format: {prefix}_{env}_{64 random hex chars} (e.g., "sk_live_a3f8b2c9...").
type Option ¶
type Option func(*Engine)
Option is a functional option for Engine.
func WithExtension ¶
WithExtension registers a lifecycle plugin with the engine.
func WithKeyGenerator ¶
func WithKeyGenerator(g KeyGenerator) Option
WithKeyGenerator sets the key generator.
func WithMetrics ¶
WithMetrics records keysmith's metrics on m: validations and their duration, lifecycle operations, rate limiter checks, failed hooks and failed last-used writes. In a Forge app pass app.Metrics() and the series land on /_/metrics with forge's namespace in front (forge_keysmith_key_validations_total). Keysmith's Forge extension does this for you. A nil m, the default, records nothing.
Spans need no option: the engine starts them on the global tracer provider, which forge.New installs.
func WithRateLimiter ¶
func WithRateLimiter(r RateLimiter) Option
WithRateLimiter sets the rate limiter.
type RateLimiter ¶
type RateLimiter interface {
// Allow returns true if the request is within rate limits.
Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, error)
// Remaining returns the number of remaining requests in the current window.
Remaining(ctx context.Context, key string, limit int, window time.Duration) (int, error)
}
RateLimiter checks whether a request is allowed under rate limits.
type RotateOption ¶
type RotateOption func(*rotateConfig)
RotateOption customises one call to RotateKey.
func WithGrace ¶
func WithGrace(d time.Duration) RotateOption
WithGrace sets how long the previous key keeps validating. Zero means it stops the moment the rotation is recorded, which is what a compromise rotation wants. A negative value is treated as zero.
func WithRotatedBy ¶
func WithRotatedBy(subject string) RotateOption
WithRotatedBy records who rotated the key.
type ValidationResult ¶
type ValidationResult struct {
Key *key.Key `json:"key"`
Scopes []string `json:"scopes"`
Policy *policy.Policy `json:"policy,omitempty"`
// ViaPreviousKey is true when the caller presented a key that has since
// been rotated and is still inside its grace window. A host can use it
// to warn the caller to switch to the new key.
ViaPreviousKey bool `json:"via_previous_key,omitempty"`
// GraceEnds is when the previous key stops validating. It is set only
// when ViaPreviousKey is true.
GraceEnds *time.Time `json:"grace_ends,omitempty"`
}
ValidationResult is returned from key validation.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
_examples
|
|
|
basic
command
Command basic demonstrates standalone Keysmith usage without Forge.
|
Command basic demonstrates standalone Keysmith usage without Forge. |
|
forge
command
Command forge demonstrates Keysmith as a Forge extension.
|
Command forge demonstrates Keysmith as a Forge extension. |
|
Package api provides Forge-style HTTP handlers for the Keysmith API.
|
Package api provides Forge-style HTTP handlers for the Keysmith API. |
|
Package audithook bridges Keysmith lifecycle events to an audit trail backend.
|
Package audithook bridges Keysmith lifecycle events to an audit trail backend. |
|
Package extension adapts Keysmith as a Forge extension.
|
Package extension adapts Keysmith as a Forge extension. |
|
contract
Package contract wires keysmith into the Forge dashboard's contract path.
|
Package contract wires keysmith into the Forge dashboard's contract path. |
|
Package id defines TypeID-based identity types for all Keysmith entities.
|
Package id defines TypeID-based identity types for all Keysmith entities. |
|
internal
|
|
|
groveset
Package groveset lists the columns a grove model-based UPDATE writes, with their values.
|
Package groveset lists the columns a grove model-based UPDATE writes, with their values. |
|
storetest
Package storetest runs a test against every keysmith store backend that is available.
|
Package storetest runs a test against every keysmith store backend that is available. |
|
Package key defines the core API key entity and its lifecycle states.
|
Package key defines the core API key entity and its lifecycle states. |
|
Package middleware provides Forge-compatible middleware for API key validation.
|
Package middleware provides Forge-compatible middleware for API key validation. |
|
Package observability provides a metrics extension for Keysmith lifecycle events.
|
Package observability provides a metrics extension for Keysmith lifecycle events. |
|
Package plugin defines lifecycle hook interfaces for Keysmith plugins.
|
Package plugin defines lifecycle hook interfaces for Keysmith plugins. |
|
Package policy defines key policies with rate limits, scopes, and restrictions.
|
Package policy defines key policies with rate limits, scopes, and restrictions. |
|
Package rotation defines key rotation records and reasons.
|
Package rotation defines key rotation records and reasons. |
|
Package scope defines permission scopes that can be assigned to API keys.
|
Package scope defines permission scopes that can be assigned to API keys. |
|
Package store defines the composite store interface for Keysmith.
|
Package store defines the composite store interface for Keysmith. |
|
memory
Package memory provides an in-memory implementation of store.Store for testing.
|
Package memory provides an in-memory implementation of store.Store for testing. |
|
mongo
Package mongo provides a MongoDB implementation of store.Store using grove ORM.
|
Package mongo provides a MongoDB implementation of store.Store using grove ORM. |
|
postgres
Package postgres provides a PostgreSQL implementation of store.Store using grove ORM.
|
Package postgres provides a PostgreSQL implementation of store.Store using grove ORM. |
|
sqlite
Package sqlite provides a SQLite implementation of store.Store using grove ORM.
|
Package sqlite provides a SQLite implementation of store.Store using grove ORM. |
|
Package usage defines API key usage records and aggregation types.
|
Package usage defines API key usage records and aggregation types. |
|
Package wardenhook bridges Keysmith key lifecycle events to Warden authorization.
|
Package wardenhook bridges Keysmith key lifecycle events to Warden authorization. |