Documentation
¶
Overview ¶
Package key defines the API key management types and service.
Index ¶
Constants ¶
const ( ScopeCompletions = "completions" ScopeEmbeddings = "embeddings" ScopeModels = "models" ScopeAdmin = "admin" )
The scopes a key can hold. A key with no scopes given gets completions, embeddings and models. Admin is cross-tenant: an admin key administers every tenant, whichever tenant it belongs to.
Variables ¶
var ( ErrNotFound = errors.New("nexus: api key not found") // ErrRevoked and ErrExpired are returned by Validate only for the right // raw key. ErrRevoked = errors.New("nexus: api key revoked") ErrExpired = errors.New("nexus: api key expired") // ErrInvalid wraps every input the service refuses. ErrInvalid = errors.New("nexus: invalid api key input") // ErrDuplicate is returned by Store.Insert when a key with the same hash // is already stored. ErrDuplicate = errors.New("nexus: api key already exists") )
ErrNotFound reports that no API key matched. Stores return it from FindByID, TouchLastUsed and Update, never a nil key with a nil error. FindByPrefix reports none found as an empty slice instead. Validate also returns it for any raw key that matches no stored hash, so a caller cannot tell an unknown prefix from a wrong key.
Functions ¶
func KnownScope ¶
KnownScope reports whether s is one of the scopes above.
func WellFormed ¶
WellFormed reports whether raw has the shape Create gives a key: "nxs_" and 64 lowercase hex digits.
Types ¶
type APIKey ¶
type APIKey struct {
ID id.KeyID `json:"id"`
TenantID id.TenantID `json:"tenant_id"`
Name string `json:"name"`
Prefix string `json:"prefix"` // "nxs_" + first 8 chars (for display)
Hash string `json:"-"` // SHA-256 of the raw key, hex (never exposed). The raw key carries 256 random bits, so an unsalted hash is enough.
Scopes []string `json:"scopes"` // ["completions", "embeddings", "models"]
Status Status `json:"status"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
APIKey represents a Nexus-issued API key.
type CreateInput ¶
type CreateInput struct {
TenantID string `json:"tenant_id"`
Name string `json:"name"`
Scopes []string `json:"scopes,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
// ExpiresAt, when set, must be in the future.
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
CreateInput is the input for creating an API key.
type Events ¶
type Events interface {
EmitKeyCreated(ctx context.Context, keyID id.KeyID, tenantID id.TenantID)
EmitKeyRevoked(ctx context.Context, keyID id.KeyID)
}
Events receives key lifecycle events. *plugin.Registry satisfies it.
type ListOptions ¶
type ListOptions struct {
TenantID string `json:"tenant_id,omitempty"`
Status Status `json:"status,omitempty"`
Limit int `json:"limit,omitempty"`
Cursor string `json:"cursor,omitempty"`
// Now is the clock reading the status filter derives expiry against.
// The zero value means the wall clock. key.Service sets it from its own
// clock, so ListPage and Count agree with Get under WithClock.
Now time.Time `json:"-"`
}
ListOptions configures key listing. An empty TenantID lists every tenant's keys. Lists are newest first and cursor paged.
func (*ListOptions) At ¶
func (o *ListOptions) At() time.Time
At returns the time expiry is derived against: Now, or the wall clock when Now is unset.
type ListResult ¶
ListResult is one page of keys. NextCursor is "" on the last page.
type Option ¶
type Option func(*service)
Option configures the key service.
func WithTenants ¶
func WithTenants(t TenantFinder) Option
WithTenants makes Create refuse a key for a tenant that does not exist.
type Service ¶
type Service interface {
// Create generates a new API key. Returns the full key (only time it's visible).
Create(ctx context.Context, input *CreateInput) (*APIKey, string, error)
// Ensure makes sure the key whose raw value is rawKey exists, so an
// operator can supply the first admin key from config. A key with that
// value, found by prefix and a constant-time hash compare, is returned
// with created false, in any status: a revoked key stays revoked.
// Otherwise Ensure checks in as Create does (tenant exists, scopes known,
// expiry), stores the key and reports it. A rawKey that is not "nxs_"
// and 64 lowercase hex digits gives ErrInvalid. No error carries rawKey.
Ensure(ctx context.Context, rawKey string, in *CreateInput) (k *APIKey, created bool, err error)
// Validate checks a raw API key and returns the associated key record.
Validate(ctx context.Context, rawKey string) (*APIKey, error)
// Get returns one key, with its expiry derived: an active key whose
// ExpiresAt has passed reads expired. Every store derives expiry the same
// way in its List and Count, so the status filter and the items agree.
Get(ctx context.Context, id string) (*APIKey, error)
// Revoke deactivates an API key.
Revoke(ctx context.Context, id string) error
// List returns keys for a tenant (hashed, never shows full key).
List(ctx context.Context, tenantID string) ([]*APIKey, error)
// ListPage returns one page of keys, newest first. The store applies the
// status filter with expiry derived, and every item carries its effective
// status: an active key whose ExpiresAt has passed is expired.
ListPage(ctx context.Context, opts *ListOptions) (*ListResult, error)
// Count returns how many keys match opts, with the same filter semantics
// as ListPage. Limit and Cursor are ignored.
Count(ctx context.Context, opts *ListOptions) (int, error)
// Rotate creates a replacement for an active key and revokes the old one.
// It is not atomic across backends: if the revoke fails, the new key is
// revoked too (best effort) and the error is returned.
Rotate(ctx context.Context, oldKeyID string) (*APIKey, string, error)
}
Service manages API key lifecycle.
func NewService ¶
type Status ¶
type Status string
Status represents the key's current state.
type Store ¶
type Store interface {
// Insert stores k. A key whose Hash is already stored is refused with
// ErrDuplicate, and nothing is written: the hash is unique in every store.
Insert(ctx context.Context, k *APIKey) error
FindByID(ctx context.Context, id string) (*APIKey, error)
// FindByPrefix returns every key whose prefix is prefix, in any status.
// Prefixes are not unique, so a caller must check the hash. None found
// is an empty slice and a nil error.
FindByPrefix(ctx context.Context, prefix string) ([]*APIKey, error)
// TouchLastUsed sets only LastUsedAt, so it can never undo a concurrent
// revoke. It returns ErrNotFound when the key does not exist.
TouchLastUsed(ctx context.Context, id string, at time.Time) error
Update(ctx context.Context, k *APIKey) error
Delete(ctx context.Context, id string) error
ListByTenant(ctx context.Context, tenantID string) ([]*APIKey, error)
List(ctx context.Context, opts *ListOptions) (*ListResult, error)
// Count returns how many keys match opts, with List's filter semantics.
// Limit and Cursor are ignored. nil opts counts every key.
Count(ctx context.Context, opts *ListOptions) (int, error)
}
Store is the persistence interface for API keys. Finders and Update return ErrNotFound when no key matches; Delete of an unknown id is a no-op.
List and Count derive expiry. Against a clock reading now:
- active means stored active and (no ExpiresAt or ExpiresAt after now);
- expired means stored expired, or stored active with ExpiresAt at or before now;
- revoked means stored revoked.
Every key List returns carries its effective status (see Effective).