keysmith

package module
v1.9.0 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 25 Imported by: 4

README

Keysmith, a Go library for API key management with hashed storage, rotation, scopes and rate limits

Keysmith: API key management for Go

Keysmith is a Go library for API key management. You create, validate, rotate, suspend and revoke API keys, give them permission scopes and policies (rate limits, lifetimes, rotation grace periods), and record usage, all scoped to a tenant. The store only ever holds a SHA-256 hash and the last four characters, so the raw key exists in exactly one place: the result you get back from create or rotate. It works on its own with net/http middleware, or as a Forge extension with an admin REST API.

CI Go Reference GitHub Stars

Documentation · Quick start · Features · Middleware · Policies · Forge · REST API · FAQ

Quick start

You need Go 1.26 or later.

go get github.com/xraph/keysmith@latest

Save this as main.go. It creates a key for one tenant, validates it, then rotates it with a one-hour grace window:

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/xraph/keysmith"
	"github.com/xraph/keysmith/key"
	"github.com/xraph/keysmith/rotation"
	"github.com/xraph/keysmith/scope"
	"github.com/xraph/keysmith/store/memory"
)

func main() {
	eng, err := keysmith.NewEngine(keysmith.WithStore(memory.New()))
	if err != nil {
		log.Fatal(err)
	}

	ctx := keysmith.WithTenant(context.Background(), "billing", "acme")

	// Scopes are defined per tenant before a key can carry them.
	if err := eng.CreateScope(ctx, &scope.Scope{Name: "deploy:write"}); err != nil {
		log.Fatal(err)
	}

	// The raw key only exists in this result. The store keeps a hash.
	created, err := eng.CreateKey(ctx, &keysmith.CreateKeyInput{
		Name:        "CI deploy key",
		Prefix:      "ak",
		Environment: key.EnvTest,
		Scopes:      []string{"deploy:write"},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("created key ending in", created.Key.Hint)

	res, err := eng.ValidateKey(ctx, created.RawKey)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("tenant:", res.Key.TenantID, "scopes:", res.Scopes)

	// Rotate, and keep the old key working for an hour.
	rotated, err := eng.RotateKey(ctx, created.Key.ID, rotation.ReasonScheduled, keysmith.WithGrace(time.Hour))
	if err != nil {
		log.Fatal(err)
	}

	old, err := eng.ValidateKey(ctx, created.RawKey)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("old key still valid:", old.ViaPreviousKey, "new key ends in", rotated.Key.Hint)
}

Run it:

go mod tidy
go run .
# created key ending in e918
# tenant: acme scopes: [deploy:write]
# old key still valid: true new key ends in 702a

Your hints will differ. Hand created.RawKey to your user once, show them the hint from then on, and if they lose the key, you rotate it, because there is no way to read it back out of the store and that's the whole point of hashing it.

Keys look like <prefix>_<environment>_<64 hex characters>, where the environment is key.EnvLive, key.EnvTest or key.EnvStaging. Want a different format or hash? Pass your own with keysmith.WithKeyGenerator or keysmith.WithHasher.

Why Keysmith?

Issuing an API key is easy. Running them for years is the part that hurts: you need to rotate a leaked key without breaking every client at once, know which tenant a key belongs to, cap what it can do, and prove what happened to it. Keysmith handles that lifecycle so your handlers only have to ask "is this key valid, and what can it do?".

You need Keysmith gives you
Keys you never store in plain text A SHA-256 hash in the store, plus a four-character hint you can show in a UI
Rotation without an outage RotateKey with a grace window, from WithGrace, the key's policy, or 24 hours by default
A full key lifecycle Create, validate, rotate, suspend, reactivate, revoke and expire, with ErrKeyInactive, ErrKeyExpired and friends to tell them apart
Permission scopes Per-tenant scopes assigned to keys, returned on validation, and checked by RequireScopes middleware
Limits per key Policies with rate limits (through a RateLimiter you supply), a maximum key lifetime, allowed scopes and a rotation grace period
Usage analytics Per-request usage records with daily and monthly aggregation
Multi-tenant isolation Tenant and app from Forge's scope or from keysmith.WithTenant
Your database PostgreSQL, SQLite and MongoDB through Grove, and an in-memory store
Hooks for audit and metrics Lifecycle hooks with ready-made audit, metrics and Warden bridges
To see what it's doing OpenTelemetry spans for validation and every key state change, and six metrics on Forge's /_/metrics, with no key material, IDs or tenants on any of them

Protect HTTP routes

The middleware package works with any net/http router. APIKeyAuth reads the key from Authorization: Bearer ... or X-API-Key, validates it, and puts the result on the request context. RequireScopes then checks the key holds every scope you list.

mux := http.NewServeMux()
mux.Handle("POST /deploy",
	middleware.APIKeyAuth(eng)(
		middleware.RequireScopes("deploy:write")(deployHandler),
	),
)

Inside the handler, middleware.ResultFromContext(r.Context()) gives you the *keysmith.ValidationResult with the key, its scopes and its policy. The middleware answers ErrRateLimited with a 429 and ErrKeyExpired with a 403. Everything else, a missing key, an unknown one, a suspended or revoked one, gets a 401. If a client presented a key that was rotated but is still inside its grace window, ViaPreviousKey is true and GraceEnds tells you when it stops working, which is a good moment to warn them.

Policies and scopes

A policy is a named set of limits in a tenant. You create one with eng.CreatePolicy(ctx, &policy.Policy{...}) and attach it to a key through CreateKeyInput.PolicyID. These fields are enforced today:

Policy field Where it's enforced
RateLimit, RateLimitWindow On every ValidateKey, when you passed a RateLimiter with keysmith.WithRateLimiter. Without one the limit is stored and never checked, and eng.RateLimiterConfigured() tells you which case you're in
MaxKeyLifetime On CreateKey. An expiry beyond it is refused, and a key with no expiry gets one at the cap
AllowedScopes On CreateKey and AssignScopes. A key can't carry a scope the policy doesn't list
GracePeriod On RotateKey, when you don't pass WithGrace

The policy also stores AllowedIPs, AllowedOrigins, AllowedMethods, AllowedPaths, BurstLimit, DailyQuota, MonthlyQuota and RotationPeriod, and returns them on validation, but Keysmith doesn't enforce them yet. If you need any of them now, read them off res.Policy in your handler after validation and enforce them there, since the validation result hands you the whole policy along with the key and its scopes.

Scopes are plain names. Each tenant has its own set (deploy:write, billing:read), and a scope can name a parent. Create them with CreateScope, then attach them with CreateKeyInput.Scopes or AssignScopes.

Usage and rotation history

Record a request against a key with eng.RecordUsage(ctx, &usage.Record{...}), carrying the endpoint, method, status code, IP and latency. Read them back with QueryUsage, or roll them up by day or month with AggregateUsage. Every rotation leaves a rotation.Record you can list with ListRotations.

CleanupExpiredKeys marks keys past their expiry as expired. EndGrace closes a rotated key's grace window early, which is what you want when the old key leaked.

Storage backends

Every backend implements store.Store, with Keys(), Policies(), Usages(), Rotations() and Scopes() plus Migrate, Ping and Close.

Backend Package Constructor
In-memory store/memory memory.New()
PostgreSQL store/postgres postgres.New(db *pgdriver.PgDB) or postgres.NewFromDSN(ctx, dsn)
SQLite store/sqlite sqlite.New(db *grove.DB); open the database with sqlite.DSN(path)
MongoDB store/mongo mongo.New(db *grove.DB)

Call Migrate(ctx) before first use, or let the Forge extension do it. For SQLite, build the DSN with sqlite.DSN so you get a busy timeout and foreign keys. Without them a write can fail with SQLITE_BUSY under load.

Hooks

Pass hooks to the engine with keysmith.WithExtension. A hook implements plugin.Plugin (just Name()) and any of these: KeyCreated, KeyCreateFailed, KeyValidated, KeyValidationFailed, KeyRotated, KeyRevoked, KeySuspended, KeyReactivated, KeyExpired, KeyRateLimited, PolicyCreated, PolicyUpdated, PolicyDeleted and Shutdown.

Three come with the library:

Package Constructor What it does
audit_hook audithook.New(recorder) Sends lifecycle events to any Recorder, such as an adapter over Chronicle
observability observability.NewMetricsExtension() Counts lifecycle events as unlabelled go-utils counters. The engine's own metrics (below) cover the same ground
warden_hook wardenhook.New(bridge) Keeps Warden role assignments for API key subjects in sync

The audit and Warden hooks each declare a small local interface, so Keysmith never imports Chronicle or Warden.

Use it with Forge

The extension package registers Keysmith as a Forge extension. It reads extensions.keysmith (or a legacy top-level keysmith key), builds a store from the *grove.DB in the container, provides *keysmith.Engine for injection, mounts the REST API under /keysmith, and runs Migrate on start.

go get github.com/xraph/keysmith@latest github.com/xraph/grove/extension@latest
import (
	grove "github.com/xraph/grove/extension"
	audithook "github.com/xraph/keysmith/audit_hook"
	keysmith "github.com/xraph/keysmith/extension"
)

// Send key lifecycle events wherever your audit trail lives.
recorder := audithook.RecorderFunc(func(ctx context.Context, ev *audithook.AuditEvent) error {
	log.Printf("keysmith audit: %s %s", ev.Action, ev.ResourceID)
	return nil
})

app := forge.NewApp(forge.AppConfig{
	Name: "billing-api",
	Extensions: []forge.Extension{
		grove.New(), // extensions.grove: driver and dsn
		keysmith.New(
			keysmith.WithHookExtension(audithook.New(recorder)),
		),
	},
})

List Grove first, because Keysmith looks up the database during Register. Without a Grove database, pass a store yourself with keysmith.WithEngineOptions(keysmith.WithStore(...)) from the root package. Authsome takes the same engine through authsome.WithKeysmith.

The REST API has no authentication of its own. Put your auth middleware in front of the /keysmith group, or turn the routes off with disable_routes and call the engine from your own handlers.

Tracing and metrics

In a Forge app there's nothing to set up. Forge installs the tracer provider, and the extension hands the engine app.Metrics(), so you get these spans under each request's server span and these series on /_/metrics:

Span Attributes
validate key, with a check rate limit child when a limiter runs keysmith.result, keysmith.validation.via_previous_key
create key, rotate key, revoke key, suspend key, reactivate key keysmith.result, plus the rotation reason and grace window on rotate key
expire keys, one root span per CleanupExpiredKeys run that finds work keysmith.keys.expired, keysmith.keys.failed
Metric Labels
forge_keysmith_key_validations_total result (ok, invalid, inactive, expired, rate_limited, cancelled, error), via_previous_key
forge_keysmith_key_validation_duration_seconds result
forge_keysmith_key_operations_total operation (create, rotate, revoke, suspend, reactivate, expire), result (ok, not_found, rejected, conflict, cancelled, error)
forge_keysmith_rate_limit_checks_total result (allowed, denied, cancelled, error)
forge_keysmith_hook_errors_total event, one per lifecycle hook
forge_keysmith_last_used_updates_failed_total none

Without Forge, pass keysmith.WithMetrics(m) with any go-utils metrics sink and set a global tracer provider. A store outage during validation is recorded as result="error", so it won't hide among bad keys, and the caller gets the store's error back, not ErrInvalidKey. A client that hangs up is cancelled, so it doesn't count as an outage. The observability docs list every attribute and label value.

Configuration

extensions:
  keysmith:
    base_path: /keysmith
    disable_routes: false
    disable_migrate: false
    grove_database: primary     # optional; the default *grove.DB otherwise
    dashboard:
      tenant_id: acme           # tenant for dashboard sessions with no org
      app_id: billing
Key Option Default
base_path WithBasePath(path) /keysmith
disable_routes WithDisableRoutes() false
disable_migrate WithDisableMigrate() false
grove_database WithGroveDatabase(name) the default *grove.DB
dashboard.tenant_id, dashboard.app_id WithDashboardTenant(tenantID, appID) empty, so a dashboard session without an org is refused

WithEngineOptions, WithHookExtension, WithLogger, WithConfig and WithRequireConfig round out the options. The Forge dashboard shows Keysmith through a React plugin in forge-dashboard; read MIGRATION.md before you upgrade from the templ dashboard.

REST API

With the Forge extension, these routes sit under the base path (/keysmith by default):

Area Routes
Keys POST /v1/keys, GET /v1/keys, GET /v1/keys/:keyId, DELETE /v1/keys/:keyId
Key state POST /v1/keys/:keyId/rotate, /revoke, /suspend, /reactivate
Validation POST /v1/keys/validate
Policies POST /v1/policies, GET /v1/policies, GET, PUT and DELETE /v1/policies/:policyId
Scopes POST /v1/scopes, GET /v1/scopes, DELETE /v1/scopes/:scopeId, POST and DELETE /v1/keys/:keyId/scopes
Usage GET /v1/keys/:keyId/usage, GET /v1/keys/:keyId/usage/aggregate, GET /v1/usage
Rotations GET /v1/keys/:keyId/rotations

Every route carries OpenAPI metadata, so it shows up in your Forge app's spec. The REST API reference has the request and response shapes.

Documentation

Topic Where
Getting started Getting started, architecture, full example
Subsystems Keys, policies, scopes, rotation, usage, plugins, observability
Storage PostgreSQL, SQLite, MongoDB, memory, custom store
Integration Forge extension, middleware, multi-tenancy

The Go API is on pkg.go.dev.

Examples

Example What it shows
basic The engine on its own with the memory store
forge Keysmith as a Forge extension

Run one from the repository root with GOWORK=off go run ./_examples/basic.

FAQ

How do I generate and validate API keys in Go?

Create an engine with a store, call CreateKey to get the raw key once, and call ValidateKey with whatever the client sends you. For HTTP services, wrap your handlers in middleware.APIKeyAuth.

How do I rotate an API key without breaking clients?

Call RotateKey with a grace window. The old key keeps validating until the window ends, and the validation result tells you when a client is still using it. For a leaked key, rotate with rotation.ReasonCompromise and close the window with EndGrace, or revoke the key outright.

Does Keysmith store API keys in plain text?

No. It stores a SHA-256 hash and the last four characters. The raw key only appears in what CreateKey and RotateKey return.

Can Keysmith rate limit API keys?

Yes, when you give the engine a RateLimiter with keysmith.WithRateLimiter and the key's policy sets RateLimit. Keysmith defines the interface, so you can back it with Redis or anything else you already run.

Can I use Keysmith without Forge?

Yes. The engine, the stores and the middleware are plain Go and net/http. The extension package is only there for Forge apps.

Contributing

Issues and focused pull requests are welcome on GitHub.

git clone https://github.com/xraph/keysmith.git
cd keysmith
GOWORK=off go test ./...       # memory and sqlite
make test-backends             # adds postgres and mongo in Docker
GOWORK=off go vet ./...
golangci-lint run ./...

Without KEYSMITH_TEST_PG_DSN or KEYSMITH_TEST_MONGO_URI, the PostgreSQL and MongoDB subtests are never created, so go test -v won't even print a SKIP for them. make test-backends starts the containers and sets both for you. Don't write a full fake key as one string literal in a test: one shaped like a live payment provider key trips GitHub push protection, so build it from parts. AGENTS.md explains that, the lint rules that bite and the release steps. Commits follow Conventional Commits.

License

MIT. See LICENSE.

Keysmith is part of the Forge forgery stack, built by Rex Raphael and contributors.

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

View Source
const InstrumentationName = "github.com/xraph/keysmith"

InstrumentationName is the tracer name on every span keysmith starts.

Variables

View Source
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

func WithTenant

func WithTenant(ctx context.Context, appID, tenantID string) context.Context

WithTenant sets the tenant scope on the context for standalone usage (without Forge). This is the non-Forge equivalent of forge.Scope.

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 NewEngine

func NewEngine(opts ...Option) (*Engine, error)

NewEngine creates a new Keysmith engine with the given options.

func (*Engine) AggregateUsage

func (e *Engine) AggregateUsage(ctx context.Context, filter *usage.QueryFilter) ([]*usage.Aggregation, error)

AggregateUsage returns aggregated usage statistics.

func (*Engine) AssignScopes

func (e *Engine) AssignScopes(ctx context.Context, keyID id.KeyID, scopeNames []string) error

AssignScopes assigns scopes to a key by name.

func (*Engine) CleanupExpiredKeys

func (e *Engine) CleanupExpiredKeys(ctx context.Context) error

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

func (e *Engine) CreatePolicy(ctx context.Context, pol *policy.Policy) error

CreatePolicy creates a new key policy.

func (*Engine) CreateScope

func (e *Engine) CreateScope(ctx context.Context, s *scope.Scope) error

CreateScope creates a permission scope.

func (*Engine) DeletePolicy

func (e *Engine) DeletePolicy(ctx context.Context, polID id.PolicyID) error

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

func (e *Engine) DeleteScope(ctx context.Context, scopeID id.ScopeID) error

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

func (e *Engine) EndGrace(ctx context.Context, keyID id.KeyID) (int64, error)

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) GetKey

func (e *Engine) GetKey(ctx context.Context, keyID id.KeyID) (*key.Key, error)

GetKey returns a key by ID.

func (*Engine) GetPolicy

func (e *Engine) GetPolicy(ctx context.Context, polID id.PolicyID) (*policy.Policy, error)

GetPolicy returns a policy by ID.

func (*Engine) Health

func (e *Engine) Health(ctx context.Context) error

Health checks the health of the engine by pinging its store.

func (*Engine) ListKeys

func (e *Engine) ListKeys(ctx context.Context, filter *key.ListFilter) ([]*key.Key, error)

ListKeys returns keys matching the filter.

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

func (e *Engine) ListScopes(ctx context.Context, filter *scope.ListFilter) ([]*scope.Scope, error)

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

func (e *Engine) RateLimiterConfigured() bool

RateLimiterConfigured reports whether a RateLimiter was injected. Without one, a policy's RateLimit is stored and never enforced.

func (*Engine) ReactivateKey

func (e *Engine) ReactivateKey(ctx context.Context, keyID id.KeyID) error

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

func (e *Engine) RecordUsage(ctx context.Context, rec *usage.Record) error

RecordUsage records a single usage event for a key.

func (*Engine) RemoveScopes

func (e *Engine) RemoveScopes(ctx context.Context, keyID id.KeyID, scopeNames []string) error

RemoveScopes removes scopes from a key by name.

func (*Engine) RevokeKey

func (e *Engine) RevokeKey(ctx context.Context, keyID id.KeyID, reason string) error

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) Start

func (e *Engine) Start(_ context.Context) error

Start starts the engine and any background workers.

func (*Engine) Stop

func (e *Engine) Stop(ctx context.Context) error

Stop gracefully shuts down the engine.

func (*Engine) Store

func (e *Engine) Store() store.Store

Store returns the underlying composite store.

func (*Engine) SuspendKey

func (e *Engine) SuspendKey(ctx context.Context, keyID id.KeyID) error

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

func (e *Engine) UpdatePolicy(ctx context.Context, pol *policy.Policy) error

UpdatePolicy updates an existing policy.

func (*Engine) ValidateKey

func (e *Engine) ValidateKey(ctx context.Context, rawKey string) (*ValidationResult, error)

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.

func DefaultHasher

func DefaultHasher() Hasher

DefaultHasher returns a SHA-256 hasher.

type ID

type ID = id.ID

ID is the primary identifier type for all Keysmith entities.

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

func WithExtension(x plugin.Plugin) Option

WithExtension registers a lifecycle plugin with the engine.

func WithHasher

func WithHasher(h Hasher) Option

WithHasher sets the key hasher.

func WithKeyGenerator

func WithKeyGenerator(g KeyGenerator) Option

WithKeyGenerator sets the key generator.

func WithLogger

func WithLogger(l log.Logger) Option

WithLogger sets the logger.

func WithMetrics

func WithMetrics(m gometrics.Metrics) Option

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.

func WithStore

func WithStore(s store.Store) Option

WithStore sets the composite store.

type Prefix

type Prefix = id.Prefix

Prefix identifies the entity type encoded in a TypeID.

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.

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.

Jump to

Keyboard shortcuts

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