keys

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 23, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package keys is the SQLite-backed store of self-serve API keys for the data API: it creates and revokes keys, authenticates requests by the SHA-256 hash of the presented key (the secret itself is never stored), counts requests per key and UTC day against a daily cap, keeps a 30-day per-endpoint request history, lets an operator change caps and block owners, and keeps revoked keys for 30 days with when they were revoked and by which role (owner, admin or operator).

A Store implements dataapi.Authenticator, dataapi.Quota and dataapi.Usage. Active keys are indexed in memory, so a revoked key (or a blocked owner's keys) stops working at once in the same process. Usage counts are kept in memory and written to the database periodically, on day rollover and on Close.

The store also keeps accounts. Each sign-in identity (an EVE character or a Discord user) belongs to one account, so the keys and limits of every linked identity are shared. For an identity the store keeps only the provider, the id, the EVE owner hash and the link date. For an account it keeps the creation time and, for testers, the time tester evidence was last seen. An account can be deleted with everything the store holds for it: by its owner (DeleteAccountOf), unless it is blocked, or by the operator (DeleteAccount), which keeps a block on its sign-in ids.

The database file belongs to one Store in one process at a time: the in-memory counts are the source of truth for today and overwrite the stored rows. On shutdown stop the HTTP server first (so no request is still being counted), then Close the Store.

Index

Constants

View Source
const (
	ProviderEVE     = "eve"
	ProviderDiscord = "discord"
)

Sign-in providers an identity can come from.

View Source
const (
	// RevokerOwner: the key's owner revoked it.
	RevokerOwner = "owner"
	// RevokerAdmin: an admin revoked it through a front end's admin panel (operator API, X-Actor: admin).
	RevokerAdmin = "admin"
	// RevokerOperator: revoked through the operator API with no admin panel involved (the token holder).
	RevokerOperator = "operator"
	// RevokerUnknown: revoked before the store recorded the role.
	RevokerUnknown = "unknown"
)

Values of OwnedKey.RevokedBy: the role that revoked a key, never who.

View Source
const AccountPattern = `^acct:[0-9a-f]{16}$`

AccountPattern is the shape of a canonical account id: "acct:" and 16 lowercase hex digits.

View Source
const (

	// MaxCreatesPerDay is how many keys one owner may create per UTC day, revoked ones
	// included. Keys an operator issues still count toward it when the owner creates one
	// later the same day; only the operator's own Issue call is exempt from the check.
	MaxCreatesPerDay = 10
)
View Source
const MaxDailyCap = 10_000_000

MaxDailyCap is the largest daily cap a key may have.

View Source
const MaxOpsListBytes = 900 << 10

MaxOpsListBytes budgets the operator key list answer (GET /admin/v1/ops/keys) as a.write encodes it: the web server reads at most 1 MiB of it. The blocked owners and the revoked keys are cut to fit, newest kept, and the answer says so ("blocked_capped": true, "capped": true); the active keys are never cut, so the answer holds the budget unless they alone exceed it (a warning is logged then, and whenever a list is cut). On the revoked view the revoked keys are fitted before the blocked owners. MaxRevokedListed caps the revoked rows too.

View Source
const MaxRevokedListed = 1000

MaxRevokedListed caps ListRevoked: the web server reads a bounded admin response, so a longer list would be cut. The newest revocations are the ones kept. The operator list also cuts by size (MaxOpsListBytes).

View Source
const OwnerPattern = `^(?:acct:[0-9a-f]{16}|(?:eve|discord):[A-Za-z0-9._-]{1,128})$`

OwnerPattern is the accepted shape of a key owner id: a canonical account ("acct:" and 16 lowercase hex digits) or a sign-in identity ("eve:<character id>", "discord:<user id>"), the form keys were owned by before accounts existed. A sign-in id that is linked to an account acts for that account. Callers that validate an owner before it reaches the store (for example an admin proxy) use this constant so the rule cannot drift.

View Source
const TesterValidity = 7 * 24 * time.Hour

TesterValidity is how long Discord tester evidence keeps an account a tester. Every Discord sign-in or bot interaction that carries the evidence refreshes it.

Variables

View Source
var (
	// ErrBadIdentity: the provider is not eve or discord, the subject is not a decimal id that fits 64 bits, the
	// owner hash is malformed or given for a Discord identity, or tester evidence is given for an EVE identity.
	ErrBadIdentity = errors.New("invalid identity")
	// ErrIdentityChanged: an EVE identity came without an owner hash, or, outside a fresh sign-in, with one that is not
	// the stored one (the character changed hands). The user has to sign in again.
	ErrIdentityChanged = errors.New("identity changed hands or carries no owner hash")
	// ErrNoAccount: a key request named an account id the store does not hold.
	ErrNoAccount = errors.New("no such account")
)
View Source
var (
	// ErrDiscordConflict: the account would hold two Discord identities.
	ErrDiscordConflict = errors.New("account already has another Discord identity")
	// ErrNotLinked: the identity to unlink is not attached to the caller's account.
	ErrNotLinked = errors.New("identity is not linked to this account")
	// ErrCurrentIdentity: the identity to unlink is the one the caller is signed in with.
	ErrCurrentIdentity = errors.New("the identity in use cannot be unlinked")
)
View Source
var (
	ErrTooManyKeys = errors.New("too many active keys")
	ErrNotFound    = errors.New("no such key")
	ErrBadOwner    = errors.New("invalid owner id")
	ErrBadName     = errors.New("invalid key name")
	// ErrCreateLimit is returned by Create when the owner already created MaxCreatesPerDay
	// keys today (UTC).
	ErrCreateLimit = errors.New("too many keys created today")
	// ErrOwnerBlocked is returned by Create for an owner an operator has blocked.
	ErrOwnerBlocked = errors.New("owner is blocked")
	// ErrBadCap is returned by SetCap for a cap outside 0..10_000_000.
	ErrBadCap = errors.New("invalid daily cap")
	// ErrBadDays is returned by Usage for a window outside 1..30 days.
	ErrBadDays = errors.New("invalid usage window")
)
View Source
var ErrBadActor = errors.New("invalid actor")

ErrBadActor is returned by RevokeAny and Block for an actor other than "", RevokerOperator and RevokerAdmin.

Functions

func AdminHandler

func AdminHandler(s *Store, token string, log *slog.Logger) http.Handler

AdminHandler returns the token-protected admin API of the store, meant for a private listener that is not published on the internet. It returns nil when token is empty: there is no unauthenticated admin API.

Every request is checked, in order: a CF-Connecting-IP header (the request came through the tunnel) is refused with 403; the Authorization bearer token is compared in constant time (401 otherwise); owner routes need a valid X-User-ID (400 otherwise). Responses are JSON and never cached. Only the create responses carry a key secret.

Owner routes (X-User-ID: an account id acct:<16 hex digits>, or a legacy discord:<id> (acts for its linked account) or eve:<id> (acts only on keys held under that exact id; a create still counts the whole account's caps and blocks); creating a key for an unknown account is 404 no_account):

GET    /admin/v1/keys          list the owner's keys
POST   /admin/v1/keys          create a key; optional X-User-Name header, percent-encoded
DELETE /admin/v1/keys/{id}     revoke one of the owner's keys

Operator routes (token only):

GET    /admin/v1/ops/keys                  the active keys, the blocked owners and the per-owner limit;
                                           ?status=revoked: the keys revoked in the last 30 days instead,
                                           newest first, each with revoked_at and revoked_by
                                           (?status=active or none: the default; else 400 invalid_status).
                                           The answer stays within MaxOpsListBytes unless the active keys alone
                                           exceed it (they are never cut): the revoked keys (at most
                                           1000) and the blocked owners are cut to fit, newest kept, and a cut
                                           list adds "capped": true (revoked keys) or "blocked_capped": true
POST   /admin/v1/ops/keys                  issue a key for an owner
PATCH  /admin/v1/ops/keys/{id}             change the daily cap
DELETE /admin/v1/ops/keys/{id}             revoke any key
GET    /admin/v1/ops/keys/{id}/usage       request history (?days=1..30, default 30)
GET    /admin/v1/ops/usage                 the day history of every key with usage (?days=1..30,
                                           default 30): 200 {"days":N,"keys":{"<key id>":[day rows]}}
PUT    /admin/v1/ops/owners/{owner}/block  block an owner and revoke their keys
DELETE /admin/v1/ops/owners/{owner}/block  unblock an owner
DELETE /admin/v1/ops/owners/{owner}        delete all of an owner's key records (active and
                                           revoked), their usage and block-list entry;
                                           200 {"owner","keys_deleted"}, 404 not_found if none
DELETE /admin/v1/ops/accounts/{account}     delete an account with its identities and every key record of
                                           its owner set (active and revoked) and their usage; a blocked
                                           account's block stays on its sign-in ids; 200 {"account",
                                           "keys_deleted","identities"}, 404 no_account

Account routes (token only). The caller, a trusted service such as the web front end or the bot, has verified the identity it names and is the only source of the identity fields; the API cannot check them. Bodies are JSON with known fields only. An identity is {"provider":"eve"|"discord","subject":"<numeric id>","owner_hash"?}; an EVE identity must carry its owner hash. Account routes never log identity or account ids.

POST /admin/v1/accounts/resolve  {"provider","subject","owner_hash"?,"sign_in"?,"tester"?}: the account of
                                 an identity, created on first use; 200 {"account","identities":[{"provider",
                                 "subject","linked_at"}],"tester"}; 400 invalid_identity; 409 identity_changed
POST /admin/v1/accounts/link     {"identity":{...},"link":{...}}: attach a freshly verified identity to the
                                 caller's account, merging accounts and their keys; 200 {"account","identities",
                                 "merged":[...],"tester"}; 400 invalid_identity; 403 owner_blocked;
                                 409 identity_changed | discord_conflict
POST /admin/v1/accounts/unlink   {"identity":{...},"unlink":{"provider","subject"}}: detach one; 200
                                 {"account","identities","tester"}; 400 invalid_identity; 403 owner_blocked;
                                 404 not_linked; 409 identity_changed | current_identity
POST /admin/v1/accounts/delete   {"identity":{...}}: delete the caller's account, its identities and every
                                 key row of its owner set (active and revoked) with their usage, at once;
                                 200 {"keys_deleted":N,"identities":[...]}; 400 invalid_identity; 403
                                 owner_blocked (nothing deleted); 404 no_account; 409 identity_changed

Account answers carry "blocked":true while the account or a legacy id of one of its identities is blocked.

The two operator revokes (DELETE .../keys/{id} and PUT .../block) take an optional X-Actor header with the acting role: "admin" (an admin in a front end's panel) or "operator" (the default). Any other value is 400 invalid_actor and changes nothing. The role becomes the revoked keys' revoked_by; an owner revoke is always "owner". X-Actor is asserted by the bearer-token holder: the API checks only that it is one of these values.

Types

type Account added in v0.4.0

type Account struct {
	ID            string           `json:"account"`
	Identities    []LinkedIdentity `json:"identities"`
	Tester        bool             `json:"tester"`
	TesterExpired bool             `json:"tester_expired,omitempty"`
	// Blocked is true while the account, or a legacy owner id of one of its identities, is on the block list. Its
	// owner can then neither change nor delete it.
	Blocked bool `json:"blocked,omitempty"`
}

Account is a canonical account with its identities, oldest link first. Tester is true while the account carries Discord tester evidence younger than TesterValidity; TesterExpired is true when it carries older evidence (the UI then asks for a Discord sign-in to refresh it).

type BlockedOwner

type BlockedOwner struct {
	Owner     string    `json:"owner"`
	OwnerName string    `json:"owner_name"`
	BlockedAt time.Time `json:"blocked_at"`
	// Identities are the sign-in identities of an account owner (absent for a legacy owner id).
	Identities []LinkedIdentity `json:"identities,omitempty"`
}

BlockedOwner is an owner an operator has blocked.

type DayUsage

type DayUsage struct {
	Day     string `json:"day"`
	Count   int    `json:"count"`
	Refused int    `json:"refused"`
	Charged int    `json:"charged"`
}

DayUsage is one UTC day of a key's history. Count is requests served, Refused requests answered 429 (a daily-quota refusal or a route rate limit) and Charged the quota spent (what counts toward the daily cap; a request rate limited after the quota check is charged and refused, not served).

type DeletedAccount added in v0.4.0

type DeletedAccount struct {
	KeysDeleted int              `json:"keys_deleted"`
	Identities  []LinkedIdentity `json:"identities"`
}

DeletedAccount is what a deletion removed: the number of key rows and the identities that were attached.

type EndpointUsage

type EndpointUsage struct {
	Endpoint string `json:"endpoint"`
	Count    int    `json:"count"`
	Refused  int    `json:"refused"`
}

EndpointUsage is one endpoint's total over the window: requests served and requests answered 429. For an "mcp:<tool>" row, Count is the tool calls attempted (see KeyUsage).

type Identity added in v0.4.0

type Identity struct {
	Provider  string `json:"provider"`
	Subject   string `json:"subject"`
	OwnerHash string `json:"owner_hash,omitempty"`
}

Identity is one sign-in identity: an EVE character or a Discord user. OwnerHash is the EVE owner hash of the sign-in (EVE only).

func (Identity) LogValue added in v0.4.0

func (id Identity) LogValue() slog.Value

LogValue keeps the owner hash out of any log line that carries an Identity.

type Key

type Key struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	CreatedAt time.Time `json:"created_at"`
	LastUsed  string    `json:"last_used,omitempty"` // UTC date YYYY-MM-DD
	DailyCap  int       `json:"daily_cap"`
	UsedToday int       `json:"used_today"` // requests charged against the daily cap
}

Key is a stored key without its secret.

type KeyUsage

type KeyUsage struct {
	ID        string          `json:"id"`
	Days      []DayUsage      `json:"days"`      // oldest first, one entry per day in the window
	Endpoints []EndpointUsage `json:"endpoints"` // summed over the window, highest count first
}

KeyUsage is a key's request history. Over the window the day totals and the endpoint totals agree: the sum of Count (served requests, answered without a 429) and the sum of Refused (requests answered 429) over the endpoint rows that do NOT start with "mcp:" equal the same sums over the days. Rows "mcp:<tool>" are a sub-breakdown of the MCP route: they count tool calls attempted inside MCP requests (a call the upstream limiter then refuses is still counted) and are not requests.

type LinkedIdentity added in v0.4.0

type LinkedIdentity struct {
	Provider string    `json:"provider"`
	Subject  string    `json:"subject"`
	LinkedAt time.Time `json:"linked_at"`
}

LinkedIdentity is an identity attached to an account. The owner hash is never returned.

type Options

type Options struct {
	// DailyCap is copied into every new key (0 = no cap).
	DailyCap int
	// MaxPerOwner is the number of active keys one owner may hold (0 means 3).
	MaxPerOwner int
	// Now is the clock (nil: time.Now); tests inject it.
	Now func() time.Time
	// FlushEvery is how often usage counts are written (0 means 30 s).
	FlushEvery time.Duration
	// Logger receives key lifecycle events (nil: discarded).
	Logger *slog.Logger
}

Options configures Open.

type OwnedKey

type OwnedKey struct {
	Key
	Owner        string `json:"owner"`
	OwnerName    string `json:"owner_name"`
	RefusedToday int    `json:"refused_today"` // requests answered 429 today
	// RevokedAt and RevokedBy are set only for a revoked key (ListRevoked). RevokedBy is the
	// role: RevokerOwner, RevokerAdmin, RevokerOperator or RevokerUnknown.
	RevokedAt *time.Time `json:"revoked_at,omitempty"`
	RevokedBy string     `json:"revoked_by,omitempty"`
	// Identities are the sign-in identities of an account owner (operator list only; absent for a legacy owner id).
	Identities []LinkedIdentity `json:"identities,omitempty"`
}

OwnedKey is a Key with its owner, for the operator audit.

type ResolveOptions added in v0.4.0

type ResolveOptions struct {
	// SignIn says the identity comes from a fresh, verified sign-in.
	SignIn bool
	// Tester is Discord tester evidence from that sign-in or from a bot interaction: true marks the account a tester
	// now, false clears the mark, nil leaves it. Discord identities only.
	Tester *bool
}

ResolveOptions qualifies a Resolve.

type Store

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

Store is the key store. It is safe for concurrent use.

func Open

func Open(path string, opt Options) (*Store, error)

Open opens (creating if needed) the key database at path, loads the active keys and today's usage counts, and starts the usage flusher.

func (*Store) Authenticate

func (s *Store) Authenticate(r *http.Request) (dataapi.Caller, error)

Authenticate implements dataapi.Authenticator: no key is anonymous; a key this store does not hold (including any key without the ecd_ prefix, so a key file can answer for it) is dataapi.ErrInvalidAPIKey.

func (*Store) Block

func (s *Store) Block(ctx context.Context, owner, actor string) (int, error)

Block revokes every active key of owner's scope (an account, or the account a linked sign-in id belongs to, with every legacy owner id of its identities) and refuses new ones until Unblock. actor is the acting role, as for RevokeAny; it is stored as RevokedBy of every key revoked. It returns the number of keys revoked. Blocking an owner already blocked revokes any stragglers.

func (*Store) Blocked

func (s *Store) Blocked(ctx context.Context) ([]BlockedOwner, error)

Blocked lists the blocked owners, most recently blocked first (by blocked_at, then rowid: a block moved by an account merge or delete gets a new rowid but keeps its blocked_at).

func (*Store) Close

func (s *Store) Close() error

Close stops the flusher, writes the pending usage counts and closes the database. It is safe to call more than once.

func (*Store) Create

func (s *Store) Create(ctx context.Context, owner, ownerName, name string) (Key, string, error)

Create makes a new key for owner and returns it with its secret. The secret is shown only here; the store keeps just its hash. ownerName is the owner's display name for the operator audit (may be empty). An owner may create MaxCreatesPerDay keys per UTC day (ErrCreateLimit), however many of them they revoked. Limits are counted over the whole account when owner is an account or a linked sign-in id, whichever id asks; ErrNoAccount for an unknown account id.

func (*Store) CreateLimitRetryAfter

func (s *Store) CreateLimitRetryAfter() time.Duration

CreateLimitRetryAfter is the time until the per-day create counter resets (00:00 UTC).

func (*Store) DeleteAccount added in v0.4.0

func (s *Store) DeleteAccount(ctx context.Context, acct string) (DeletedAccount, error)

DeleteAccount is the operator's deletion of account acct: the same rows as DeleteAccountOf. A blocked account is deleted too, but its block is kept on every sign-in id of the account, so the person is still blocked when they sign in again. ErrNoAccount when acct is not an account the store holds.

func (*Store) DeleteAccountOf added in v0.4.0

func (s *Store) DeleteAccountOf(ctx context.Context, current Identity) (DeletedAccount, error)

DeleteAccountOf deletes the account of current, the identity the caller is signed in with (self-service). It removes, in one transaction:

  • the account row;
  • its identities (ids, owner hashes, link dates);
  • every key row of its owner set (the account and the legacy owner ids of its identities), active or revoked, with their usage and endpoint usage rows.

The keys stop authenticating at once, and their in-memory counters are dropped. An EVE identity must carry the stored owner hash, else ErrIdentityChanged: a session from before the character changed hands cannot delete the new owner's account. ErrNoAccount when the identity belongs to no account (none is created). ErrOwnerBlocked, with nothing changed, when the account or a legacy owner id of one of its identities is blocked. Nothing about a deleted account is kept; an identity that signs in again starts a new, empty account. The log line carries no id.

func (*Store) IdentitiesOf added in v0.4.0

func (s *Store) IdentitiesOf(ctx context.Context, accounts []string) (map[string][]LinkedIdentity, error)

IdentitiesOf returns the identities of each account id in accounts, oldest link first. Ids that are not account ids are skipped, and accounts without identities are absent from the map.

func (*Store) Issue

func (s *Store) Issue(ctx context.Context, owner, ownerName, name string) (Key, string, error)

Issue is Create for an operator: the key is logged under the actor "operator" and the per-day create limit does not apply.

func (s *Store) Link(ctx context.Context, current, add Identity) (Account, []string, error)

Link attaches add, a freshly verified sign-in, to the account of current (the identity the caller is signed in with). It returns that account and the ids of the accounts merged into it (empty, never nil).

  • add is new: it joins the account.
  • add is already in the account: an EVE owner hash that differs is replaced (the fresh login proves ownership).
  • add is an EVE character in another account with a different owner hash: it changed hands. Only the identity moves; its previous account keeps what it holds and is not merged.
  • add belongs to another account: that account merges in. Its identities and every key row it owns (active and revoked) move, its tester evidence counts if it is newer, and it is deleted. A merge never revokes: an account above the per-owner key limit keeps every key, and Create refuses new ones until it is under the limit. Today's creations add up because Create counts rows.

The caller guarantees where the arguments come from. add comes only from the identity verified in this link's own OAuth callback (EVE: the character id and owner hash of a verified token; Discord: the user from /users/@me with the code-exchange token), never from client input. The OAuth state is single-use, short-lived and bound server-side to the session that started the link. current comes from the server-verified session.

Refusals change nothing: ErrBadIdentity, ErrIdentityChanged (an EVE side without its owner hash, or a stale current), ErrOwnerBlocked (either account is blocked), ErrDiscordConflict (the result would hold two Discord identities).

func (*Store) List

func (s *Store) List(ctx context.Context, owner string) ([]Key, error)

List returns owner's active keys, newest first, with today's usage, including those under the legacy owner ids of an account's identities.

func (*Store) ListAll

func (s *Store) ListAll(ctx context.Context) ([]OwnedKey, error)

ListAll returns every active key with its owner, newest first (operator audit).

func (*Store) ListRevoked added in v0.4.0

func (s *Store) ListRevoked(ctx context.Context) ([]OwnedKey, error)

ListRevoked returns the keys revoked in the last 30 days (the rows the store still keeps), most recently revoked first (keys revoked at the same instant: the newest-inserted first), with RevokedAt, RevokedBy and the fields ListAll returns (operator audit). At most MaxRevokedListed rows come back, the first in that order.

func (*Store) MaxPerOwner

func (s *Store) MaxPerOwner() int

MaxPerOwner is the number of active keys one owner may hold.

func (*Store) PurgeOwner

func (s *Store) PurgeOwner(ctx context.Context, owner string) (int, error)

PurgeOwner deletes every key row of owner (active and revoked), their usage and endpoint usage rows and the owner's block-list entry, and drops them from the in-memory index and counters at once. It returns the number of keys deleted; ErrNotFound when the owner has no key row and no block-list entry. It is an exact-owner operation: it does not extend to an account's scope.

func (*Store) Record

func (s *Store) Record(id, endpoint string, refused bool)

Record implements dataapi.Usage: it counts one request of key id on endpoint in today's per-endpoint and per-day totals, as served or (refused, a 429) refused. It does not touch the quota charge, which only Spend moves. An id the store does not hold is ignored. The counts are written by the same flusher as the quota counts.

func (*Store) RecordTool

func (s *Store) RecordTool(id, tool string)

RecordTool counts one tool call inside an MCP request under the endpoint "mcp:<tool>". The label is a sub-breakdown of the MCP route, not a request: it never moves the day's served, refused or charged counters, so the endpoint rows that do not start with "mcp:" still add up to the day totals. A call the upstream limiter then refuses is counted here as well. An id the store does not hold is ignored. The counts are written by the usage flusher.

func (*Store) Resolve added in v0.4.0

func (s *Store) Resolve(ctx context.Context, id Identity, opt ResolveOptions) (Account, error)

Resolve returns the account of id, creating one for an identity seen for the first time. An EVE identity must carry its owner hash (else ErrIdentityChanged); every stored EVE identity has one. When the given hash differs from the stored one, a fresh sign-in (opt.SignIn) moves the character to a new, empty account, because it changed hands, and what the old account holds stays there. Any other call is then ErrIdentityChanged and changes nothing. opt.Tester records Discord tester evidence on the account.

func (*Store) Revoke

func (s *Store) Revoke(ctx context.Context, owner, id string) error

Revoke revokes owner's key id; ErrNotFound when it is not in the owner's scope or already revoked.

func (*Store) RevokeAny

func (s *Store) RevokeAny(ctx context.Context, id, actor string) error

RevokeAny revokes any active key by id (operator). actor is the acting role: "" or RevokerOperator for the token holder, RevokerAdmin for an admin in a front end's panel; anything else is ErrBadActor. The role is stored as the key's RevokedBy and logged.

func (*Store) SetCap

func (s *Store) SetCap(ctx context.Context, id string, cap int) (Key, error)

SetCap changes the daily cap of an active key (0 = no cap; at most 10,000,000). It takes effect on the next Spend.

func (*Store) Spend

func (s *Store) Spend(id string) (bool, time.Duration)

Spend implements dataapi.Quota: it counts one request of key id against today's cap. An id the store does not hold is always allowed (and not counted). When the cap is reached it refuses with the time left until the end of the counted day (at least 1 s).

func (*Store) Unblock

func (s *Store) Unblock(ctx context.Context, owner string) error

Unblock lets owner create keys again; ErrNotFound when the owner is not blocked. It is an exact-owner operation: it removes the block entry of that owner id only, not of the rest of an account's scope.

func (s *Store) Unlink(ctx context.Context, current Identity, provider, subject string) (Account, error)

Unlink detaches the identity provider/subject from the account of current. The account keeps its keys and trial counters, including keys still recorded under the identity's legacy owner id. Unlinking the Discord login also clears the account's tester mark, which came from it. The detached identity gets a new, empty account (no tester mark) the next time it is resolved. Refusals change nothing: ErrBadIdentity, ErrIdentityChanged (current is stale or has no owner hash), ErrCurrentIdentity (the identity in use), ErrNotLinked, ErrOwnerBlocked (a blocked account cannot shed an identity).

func (*Store) Usage

func (s *Store) Usage(ctx context.Context, id string, days int) (KeyUsage, error)

Usage returns the request history of key id (active or revoked) over the last days UTC days (1..30), ending with the day the counters are on.

func (*Store) UsageAll added in v0.3.0

func (s *Store) UsageAll(ctx context.Context, days int) (map[string][]DayUsage, error)

UsageAll returns the day history of every key that has usage rows in the last days UTC days (1..30), keyed by key id (active or revoked). Each key gets the same day rows Usage returns: oldest first, one entry per day, zero-filled. Keys without rows are absent.

Jump to

Keyboard shortcuts

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