Documentation
¶
Overview ¶
Package limes is an object-capability authorisation primitive designed for agentic workloads.
A Capability is a short-lived, signed token granting its bearer a specific set of operations on a specific set of resources, with explicit budget and lifetime caveats. It is delegable — a parent issues a strictly narrower child to a sub-agent — and individually revocable. Compared with long-lived API keys or role-based access it gives:
- Per-call attribution: every call carries the capability ID, so audit and cost roll up by it, down to which run of which agent spent what.
- Budget enforcement at the authorisation boundary: the Meter refuses a charge once a ceiling is exhausted, rather than business logic discovering it three calls deep.
- Mid-flight revocation: a compromised agent's capability is revoked, together with everything it delegated.
- Sub-capabilities without an operator: an orchestrator narrows its own authority for each sub-agent it spawns, through the Issuer with no admin API in the path, or offline as a Biscuit block with no call at all.
Wire format is JWT compact serialization with Ed25519 signatures (`alg: Ed25519`, RFC 9864), or a Biscuit v3 sealing such a token; docs/standards.md lists every standard followed. Verification is local: the signature is checked against a resolved public key, and the store reads, whether the capability is revoked and, for a Biscuit, whether its copy is, go through short-lived caches (CachedRevocationChecker, CachedBiscuitRevocationChecker), so consumers scale horizontally without contending on a shared store.
Packages ¶
Two standards the verifier relies on are packages of their own, usable without the rest: github.com/oleg-tkachuk/limes/jwks for key sets (RFC 7517) and github.com/oleg-tkachuk/limes/dpop for proof of possession (RFC 9449). github.com/oleg-tkachuk/limes/inspect reads a token without verifying it, for tooling. memstore, storetest and metertest serve stores; dpop/dpoptest and jwks/jwkstest check a replay cache, a nonce source and a key resolver. Two modules of their own ship stores with a driver the root module does not resolve: github.com/oleg-tkachuk/limes/pgstore, every store contract over PostgreSQL, and github.com/oleg-tkachuk/limes/redisreplay, a replay cache over Redis.
What you implement ¶
Three contracts are required; a consumer that implements them needs nothing else:
- Store — capability records and revocations; an Issuer needs only its IssuanceStore half. IsRevoked answers for the capability's whole delegation chain, at most MaxDelegationDepth ancestors deep. No transactions required; an in-memory map is a valid implementation. A store that scopes calls to a tenant refuses one whose request names another with ErrScopeMismatch.
- UsageStore — request and spend counters and the charges ledger; the union of Meter, MeterReader, TenantBudgets and UsageHousekeeping. Every charge and request also counts against each ancestor. Generic in TX, the consumer's transaction handle; see "No-transaction mode" below.
- KeyResolver — public verification keys. jwks.Static ships as a working implementation and supports rotation via SetKey / RemoveKey; jwks.Remote fetches an issuer's JWKS for verifiers that run apart from it.
Eight more are needed only with the feature they serve: BiscuitRevocationStore to accept Biscuit tokens, CopyUsageReader to show a Biscuit copy's own counters, LedgerReader to sum a tenant's charges for billing and reports, RecordCensus to count capability records by disposition across tenants, TenantRecordEraser and TenantUsageEraser to remove a tenant, dpop.ReplayCache to share DPoP replay state across replicas (dpop.MemoryReplayCache ships for one process), and dpop.Nonces to require server nonces (dpop.HMACNonces ships, and works across replicas holding the same keys).
A Capability crosses the API by value, in and out: no caller can alter one another holds, and one that is not there is an error, never nil.
Every amount is Nanos, an int64 count of billionths of the unit (UnitCode, an ISO 4217 code; XXX for a budget that is not money), so spend sums, compares and refunds exactly, with no rounding left to the consumer.
Every read names what it reads — Store.GetRecord a capability, MeterReader.GetUsage the counters, TenantBudgets.GetTenantBudget a ceiling — so one type may implement Store and UsageStore together, or each apart as memstore does.
No-transaction mode ¶
Meter.Charge and Meter.Settle take an onCharged callback that runs inside the charge, receiving the consumer's transaction handle and the charge's receipt (Charged), so a side effect such as an outbox write commits atomically with the spend and names the charge. The module never constructs, inspects or constrains that handle — it only threads it back. That is what keeps this package free of any database dependency.
A consumer with no transactional storage instantiates UsageStore[struct{}] and always passes nil for onCharged. Everything else behaves identically; only the atomic-side-effect guarantee is forgone, and that limitation is store-dependent rather than a degradation of the primitive.
Guarantees ¶
Rejections are matchable sentinels, so a consumer can map each to its own transport status, or let Verifier.ErrorResponse map it to HTTP. A charge rejected by any ceiling — the capability's, an ancestor's, the tenant's — leaves every counter and the ledger unmutated, so retrying after a rejection is safe. Delegation needs OpShare on the parent and can only narrow: any widening returns ErrDelegationTooWide, and a differing unit code returns ErrUnitCodeMismatch rather than converting.
Enforcing caveats ¶
Caveats.Check and Caveats.CheckSource are the single definition of what the caveats mean at use time; delegation narrowing (Narrows) uses the same resource matcher (MatchResource), so the two cannot disagree.
See README.md for a five-minute walkthrough and examples/quickstart for it as a runnable program.
Example (BackdatedCharges) ¶
A test of a report needs charges at chosen times. Build the stores on a clock the test sets, record a capability that outlives the period, and move the clock forward before each charge: a charge carries the store's time, so it lands where the clock says, and no test writes a store's tables.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/oleg-tkachuk/limes"
"github.com/oleg-tkachuk/limes/memstore"
)
func main() {
ctx := context.Background()
start := time.Date(2026, time.March, 2, 9, 0, 0, 0, time.UTC) // a Monday
at := start
clock := func() time.Time { return at }
records, err := memstore.NewStore(memstore.StoreConfig{Now: clock})
if err != nil {
log.Fatal(err)
}
usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records, Now: clock})
if err != nil {
log.Fatal(err)
}
ledger := memstore.NewLedger(usage)
// The meter does not read expiry, but a capability that expired before
// a charge's time would make a report the test cannot reproduce live.
const period = 14 * 24 * time.Hour
c := limes.Capability{
ID: uuid.New(), IssuedAt: start, ExpiresAt: start.Add(period),
Subject: limes.Principal{Type: limes.PrincipalTypeService, TenantID: uuid.New(), Subject: "svc:reporting"},
Caveats: limes.Caveats{Requests: limes.RequestCeiling{Unlimited: true}, Budget: limes.BudgetCeiling{Unlimited: true}, UnitCode: "USD"},
}
if err := records.Insert(ctx, limes.InsertRequest{Capability: c}); err != nil {
log.Fatal(err)
}
subject, err := limes.StoredMeterSubject(ctx, records, c.ID)
if err != nil {
log.Fatal(err)
}
for _, day := range []int{0, 1, 8} { // Monday, Tuesday, and Tuesday a week on
at = start.AddDate(0, 0, day)
if _, err := usage.Charge(ctx, limes.ChargeRequest{Subject: subject, Amount: limes.MustParseAmount("1.5")}, nil); err != nil {
log.Fatal(err)
}
}
page, err := ledger.SumCharges(ctx, limes.SumChargesRequest{
TenantID: c.Subject.TenantID, From: start, To: start.Add(period), Bucket: limes.ChargeBucketWeek,
})
if err != nil {
log.Fatal(err)
}
for _, sum := range page.Items {
fmt.Println(sum.BucketStart.Format(time.DateOnly), sum.Charged, sum.Charges)
}
}
Output: 2026-03-02 3 2 2026-03-09 1.5 1
Example (Charge) ¶
package main
import (
"context"
"crypto/ed25519"
"errors"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/oleg-tkachuk/limes"
"github.com/oleg-tkachuk/limes/dpop"
"github.com/oleg-tkachuk/limes/jwks"
"github.com/oleg-tkachuk/limes/memstore"
)
// The issuer's name, and the audience every example's tokens are minted for.
const (
exampleIssuerName = "example-issuer"
exampleAudience = "my-service"
)
// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
issuer *limes.Issuer
verifier *limes.Verifier
records *memstore.Store
usage *memstore.UsageStore[struct{}]
tenantID uuid.UUID
}
func newExampleStack() exampleStack {
key, err := limes.GenerateKey()
kid, pub, priv := key.KeyID, key.Public(), key.Private
if err != nil {
log.Fatal(err)
}
records, err := memstore.NewStore(memstore.StoreConfig{})
if err != nil {
log.Fatal(err)
}
usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
if err != nil {
log.Fatal(err)
}
signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
if err != nil {
log.Fatal(err)
}
issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
if err != nil {
log.Fatal(err)
}
replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
if err != nil {
log.Fatal(err)
}
proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
if err != nil {
log.Fatal(err)
}
verifier, err := limes.NewVerifier(limes.VerifierConfig{
Keys: exampleKeys(kid, pub),
Revocations: records,
TrustedIssuers: []string{exampleIssuerName},
AcceptBiscuit: true,
BiscuitRevocations: records,
DPoP: proofs,
})
if err != nil {
log.Fatal(err)
}
return exampleStack{
issuer: issuer, verifier: verifier, records: records,
usage: usage, tenantID: uuid.New(),
}
}
// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
if err != nil {
log.Fatal(err)
}
return vc
}
// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
IssuedBy: limes.Principal{Subject: "operator@example.com"},
Subject: limes.Principal{
Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
},
Audience: []string{exampleAudience},
TTL: 15 * time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet, limes.OpShare},
ResourcePrefixes: []string{"corpus/public/"},
Requests: limes.RequestCeiling{Max: 500},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("25.00")},
UnitCode: "USD",
},
})
c, token := cResult.Capability, cResult.Token
if err != nil {
log.Fatal(err)
}
return c, token
}
func main() {
ctx := context.Background()
s := newExampleStack()
_, token := s.issue()
// The ceilings come from what the verifier accepted, never from the caller.
subject := s.verified(token).MeterSubject()
charge := func(amount string) (limes.ChargeReceipt, error) {
return s.usage.Charge(ctx, limes.ChargeRequest{
Subject: subject, Amount: limes.MustParseAmount(amount), Op: "search",
}, nil)
}
receipt, err := charge("0.35")
fmt.Println(err, receipt.SpentAmount)
// Over the ceiling: refused, and no counter moves.
_, err = charge("999")
fmt.Println(errors.Is(err, limes.ErrBudgetExceeded))
refund, err := s.usage.Refund(ctx, limes.RefundRequest{ChargeID: receipt.ChargeID, Remaining: true})
fmt.Println(err, refund.Amount)
}
// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
if err != nil {
log.Fatal(err)
}
return keys
}
Output: <nil> 0.35 true <nil> 0.35
Example (ProofOfPossession) ¶
package main
import (
"context"
"crypto/ed25519"
"crypto/rand"
"errors"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/oleg-tkachuk/limes"
"github.com/oleg-tkachuk/limes/dpop"
"github.com/oleg-tkachuk/limes/jwks"
"github.com/oleg-tkachuk/limes/memstore"
)
// The issuer's name, and the audience every example's tokens are minted for.
const (
exampleIssuerName = "example-issuer"
exampleAudience = "my-service"
)
// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
issuer *limes.Issuer
verifier *limes.Verifier
records *memstore.Store
usage *memstore.UsageStore[struct{}]
tenantID uuid.UUID
}
func newExampleStack() exampleStack {
key, err := limes.GenerateKey()
kid, pub, priv := key.KeyID, key.Public(), key.Private
if err != nil {
log.Fatal(err)
}
records, err := memstore.NewStore(memstore.StoreConfig{})
if err != nil {
log.Fatal(err)
}
usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
if err != nil {
log.Fatal(err)
}
signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
if err != nil {
log.Fatal(err)
}
issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
if err != nil {
log.Fatal(err)
}
replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
if err != nil {
log.Fatal(err)
}
proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
if err != nil {
log.Fatal(err)
}
verifier, err := limes.NewVerifier(limes.VerifierConfig{
Keys: exampleKeys(kid, pub),
Revocations: records,
TrustedIssuers: []string{exampleIssuerName},
AcceptBiscuit: true,
BiscuitRevocations: records,
DPoP: proofs,
})
if err != nil {
log.Fatal(err)
}
return exampleStack{
issuer: issuer, verifier: verifier, records: records,
usage: usage, tenantID: uuid.New(),
}
}
// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
if err != nil {
log.Fatal(err)
}
return vc
}
// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
IssuedBy: limes.Principal{Subject: "operator@example.com"},
Subject: limes.Principal{
Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
},
Audience: []string{exampleAudience},
TTL: 15 * time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet, limes.OpShare},
ResourcePrefixes: []string{"corpus/public/"},
Requests: limes.RequestCeiling{Max: 500},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("25.00")},
UnitCode: "USD",
},
})
c, token := cResult.Capability, cResult.Token
if err != nil {
log.Fatal(err)
}
return c, token
}
func main() {
ctx := context.Background()
s := newExampleStack()
const method, url = "POST", "https://data.example.com/objects/get"
// The capability is bound to a key the agent keeps.
_, agentKey, _ := ed25519.GenerateKey(rand.Reader)
jkt, _ := dpop.Thumbprint(agentKey.Public())
tokenResult, err := s.issuer.Issue(ctx, limes.IssueRequest{
IssuedBy: limes.Principal{Subject: "operator@example.com"},
Subject: limes.Principal{Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "worker"},
Audience: []string{exampleAudience}, TTL: 15 * time.Minute,
Caveats: limes.Caveats{Ops: []limes.Op{limes.OpGet}},
ConfirmationJKT: jkt,
})
token := tokenResult.Token
if err != nil {
log.Fatal(err)
}
// Without a proof the token is refused: a copy of it is useless.
_, err = s.verifier.Verify(ctx, limes.VerifyRequest{Token: token, Audience: exampleAudience})
fmt.Println(errors.Is(err, dpop.ErrRequired))
// The client signs a fresh proof per request.
proof, _ := dpop.Sign(ctx, agentKey, dpop.ProofRequest{Method: method, URL: url, Token: token, IssuedAt: time.Now()})
req := limes.VerifyRequest{
Token: token, Audience: exampleAudience,
DPoP: dpop.Proof{JWT: proof, Method: method, URL: url},
}
_, err = s.verifier.Verify(ctx, req)
fmt.Println(err)
// The same proof a second time is a replay.
_, err = s.verifier.Verify(ctx, req)
fmt.Println(errors.Is(err, dpop.ErrReplayed))
}
// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
if err != nil {
log.Fatal(err)
}
return keys
}
Output: true <nil> true
Example (Revoke) ¶
package main
import (
"context"
"crypto/ed25519"
"errors"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/oleg-tkachuk/limes"
"github.com/oleg-tkachuk/limes/dpop"
"github.com/oleg-tkachuk/limes/jwks"
"github.com/oleg-tkachuk/limes/memstore"
)
// The issuer's name, and the audience every example's tokens are minted for.
const (
exampleIssuerName = "example-issuer"
exampleAudience = "my-service"
)
// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
issuer *limes.Issuer
verifier *limes.Verifier
records *memstore.Store
usage *memstore.UsageStore[struct{}]
tenantID uuid.UUID
}
func newExampleStack() exampleStack {
key, err := limes.GenerateKey()
kid, pub, priv := key.KeyID, key.Public(), key.Private
if err != nil {
log.Fatal(err)
}
records, err := memstore.NewStore(memstore.StoreConfig{})
if err != nil {
log.Fatal(err)
}
usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
if err != nil {
log.Fatal(err)
}
signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
if err != nil {
log.Fatal(err)
}
issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
if err != nil {
log.Fatal(err)
}
replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
if err != nil {
log.Fatal(err)
}
proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
if err != nil {
log.Fatal(err)
}
verifier, err := limes.NewVerifier(limes.VerifierConfig{
Keys: exampleKeys(kid, pub),
Revocations: records,
TrustedIssuers: []string{exampleIssuerName},
AcceptBiscuit: true,
BiscuitRevocations: records,
DPoP: proofs,
})
if err != nil {
log.Fatal(err)
}
return exampleStack{
issuer: issuer, verifier: verifier, records: records,
usage: usage, tenantID: uuid.New(),
}
}
// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
if err != nil {
log.Fatal(err)
}
return vc
}
// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
IssuedBy: limes.Principal{Subject: "operator@example.com"},
Subject: limes.Principal{
Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
},
Audience: []string{exampleAudience},
TTL: 15 * time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet, limes.OpShare},
ResourcePrefixes: []string{"corpus/public/"},
Requests: limes.RequestCeiling{Max: 500},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("25.00")},
UnitCode: "USD",
},
})
c, token := cResult.Capability, cResult.Token
if err != nil {
log.Fatal(err)
}
return c, token
}
func main() {
ctx := context.Background()
s := newExampleStack()
c, token := s.issue()
err := s.records.Revoke(ctx, limes.RevokeRequest{
CapabilityID: c.ID, Note: "agent looping", Actor: "operator@example.com", CascadeChildren: true,
})
fmt.Println(err)
_, err = s.verifier.Verify(ctx, limes.VerifyRequest{Token: token, Audience: exampleAudience})
fmt.Println(errors.Is(err, limes.ErrRevoked))
}
// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
if err != nil {
log.Fatal(err)
}
return keys
}
Output: <nil> true
Index ¶
- Constants
- Variables
- func AllByPrincipal(ctx context.Context, l CapabilityLister, req ListByPrincipalRequest) iter.Seq2[Capability, error]
- func AllTenantBudgets(ctx context.Context, l TenantBudgetLister, req ListTenantBudgetsRequest) iter.Seq2[TenantBudget, error]
- func Attenuate(token string, a Attenuation) (string, error)
- func ExactPercent(pct float64) *big.Rat
- func IsUnitCode(u string) bool
- func MatchResource(prefix, uri string) bool
- func Narrows(parent, child Capability) error
- func PageLimit(requested int) int
- func ValidateExternalRef(ref string) error
- func WriteErrorResponse(w http.ResponseWriter, resp ErrorResponse)
- type AgentPrincipal
- type Attenuation
- type AuthScheme
- type AuthorizeRequest
- type BiscuitCopy
- type BiscuitRequest
- type BiscuitResult
- type BiscuitRevocation
- type BiscuitRevocationCacheConfig
- type BiscuitRevocationLookup
- type BiscuitRevocationStore
- type BudgetCeiling
- type BumpReceipt
- type BumpRequest
- type CachedBiscuitRevocationChecker
- type CachedRevocationChecker
- type Capability
- type CapabilityLister
- type Caveats
- type ChargeBucket
- type ChargeByRefRequest
- type ChargeReceipt
- type ChargeRecord
- type ChargeRequest
- type ChargeSum
- type Charged
- type CheckRequest
- type ContextSigner
- type CopyCeiling
- type CopyUsage
- type CopyUsageReader
- type CopyUsageRequest
- type CopyUsageResult
- type CountRecordsRequest
- type DelegateRequest
- type DeleteUsageRequest
- type Effect
- type EraseTenantRequest
- type EraseTenantResult
- type EraseTenantUsageRequest
- type ErrorResponse
- type HeldCopy
- type InsertRequest
- type InstrumentOption
- type IssuanceStore
- type IssueRequest
- type IssueResult
- type Issuer
- type IssuerConfig
- type KeyResolver
- type LedgerReader
- type ListByPrincipalRequest
- type ListReservationsRequest
- type ListTenantBudgetsRequest
- type Meter
- type MeterReader
- type MeterSubject
- type Nanos
- type Op
- type OverrunPolicy
- type Page
- type Principal
- type PrincipalType
- type Problem
- type PurgeOrphansRequest
- type PurgeOrphansResult
- type PurgeRequest
- type PurgeResult
- type Reason
- type Record
- type RecordCensus
- type RecordCount
- type RecordReader
- type RefundReceipt
- type RefundRequest
- type ReleaseExpiredRequest
- type ReleaseExpiredResult
- type ReleaseRequest
- type RequestCeiling
- type Reservation
- type ReserveRequest
- type Revocation
- type RevocationCacheConfig
- type RevocationLookup
- type RevokeBiscuitRequest
- type RevokeRequest
- type SetTenantBudgetRequest
- type SettleRequest
- type Signer
- type SignerConfig
- type SigningKey
- type Store
- type SumChargesRequest
- type TenantBudget
- type TenantBudgetFilter
- type TenantBudgetLister
- type TenantBudgets
- type TenantRecordEraser
- type TenantUsageEraser
- type TokenType
- type Usage
- type UsageHousekeeping
- type UsageStore
- type VerifiedCapability
- type Verifier
- func (v *Verifier) Authorize(ctx context.Context, req AuthorizeRequest) (VerifiedCapability, error)
- func (v *Verifier) BiscuitCopy(ctx context.Context, token string) (BiscuitCopy, error)
- func (v *Verifier) DPoPNonce(ctx context.Context) (string, error)
- func (v *Verifier) ErrorResponse(ctx context.Context, err error, scheme AuthScheme) ErrorResponse
- func (v *Verifier) Verify(ctx context.Context, req VerifyRequest) (VerifiedCapability, error)
- type VerifierConfig
- type VerifyRequest
Examples ¶
Constants ¶
const ( DefaultListLimit = 50 MaxListLimit = 500 )
Page sizes of a list: Limit 0 means DefaultListLimit, anything above MaxListLimit is clamped to it, and a negative Limit is the request's error (Validate).
const AbstractUnitCode = "XXX"
AbstractUnitCode is the unit of a budget that is not money — requests, credits, anything metered: ISO 4217's XXX, "no currency involved".
const AuthorizationHeader = "Authorization"
AuthorizationHeader is the request header a token travels in.
const DefaultGeneration = 1
DefaultGeneration is the Generation of a capability issued without one, and of every delegated capability.
const DefaultHousekeepingLimit = 1_000
DefaultHousekeepingLimit is how many rows one housekeeping batch — Store.Purge, UsageHousekeeping.ReleaseExpired or PurgeOrphans — handles when the request names no limit.
const DefaultIssueTTL = 15 * time.Minute
DefaultIssueTTL is IssuerConfig.DefaultTTL when it is zero.
const DefaultLeeway = 30 * time.Second
DefaultLeeway is VerifierConfig.Leeway when it is zero.
const DefaultMaxTokenBytes = 16 << 10
DefaultMaxTokenBytes bounds the token a verifier will look at. A capability with a few dozen resource entries is a couple of kilobytes; anything far larger is refused before any decoding is spent on it.
const DefaultRealm = "limes"
DefaultRealm is the realm of a verifier whose config names none.
const DefaultReservationTTL = 5 * time.Minute
DefaultReservationTTL is how long a reservation lasts when the request does not say.
const DefaultRevocationCacheEntries = 100_000
DefaultRevocationCacheEntries bounds the cache. Past it, the oldest entry makes room for the new one: a flood of distinct ids costs extra lookups, never unbounded memory, and never the answers cached for everyone else.
const DefaultRevocationCacheTTL = 2 * time.Second
DefaultRevocationCacheTTL bounds how long a cached answer is served. A revocation therefore takes effect within this window on every verifier that did not perform it.
const MaxBiscuitBlocks = 32
MaxBiscuitBlocks bounds the attenuation blocks a Biscuit may carry. Each block is a revocation id the verifier looks up, and anyone holding a copy can append blocks offline, so an unbounded chain is a lookup per block on every request. Attenuate refuses to go past it, and the verifier refuses a token that has.
const MaxCopyUsageIDs = MaxListLimit
MaxCopyUsageIDs is the most revocation ids one CopyUsage reads, as a page of any listing holds.
const MaxDelegationDepth = 64
MaxDelegationDepth is the most ancestors a capability may have: how far IsRevoked walks, and how deep Delegate mints.
const MaxExternalRefBytes = 200
MaxExternalRefBytes bounds ChargeRequest.ExternalRef: long enough for any UUID, ULID or provider call id, short enough to index.
const MaxIJSONInteger = jose.MaxIJSONInteger
MaxIJSONInteger is the largest integer a token carries: 2^53 − 1, the largest an IEEE 754 double holds exactly. RFC 7493 (I-JSON) §2.2 keeps numbers within it so that every JSON reader — JavaScript's among them — reads the same value; larger amounts travel as decimal strings (Nanos).
const MaxLeeway = 5 * time.Minute
MaxLeeway is the widest VerifierConfig.Leeway a verifier takes: a token is accepted for at most this long past its expiry, which is what lets a store purge it safely after MinPurgeRetention.
const MinPurgeRetention = MaxLeeway + PurgeClockSkew
MinPurgeRetention is the shortest retention Purge takes: no verifier accepts a token longer than MaxLeeway past its expiry, by a clock no more than PurgeClockSkew behind the store's, so a capability expired for longer can lose its revocation entry unseen.
const NanosPerUnit = 1_000_000_000
NanosPerUnit is how many nanos make one unit.
const PurgeClockSkew = time.Minute
PurgeClockSkew is how far the clock of the store that purges may run ahead of a verifier's: the store measures expiry by its own clock, the verifier by its own, and a revocation must outlive the token on both.
Variables ¶
var ( ErrOpNotAllowed error = &familyError{"limes: operation not allowed", ErrCaveatViolation} ErrResourceNotAllowed error = &familyError{"limes: resource not allowed", ErrCaveatViolation} ErrSourceIPNotAllowed error = &familyError{"limes: source IP not allowed", ErrCaveatViolation} ErrIdempotencyKeyRequired error = &familyError{"limes: idempotency key required", ErrCaveatViolation} ErrTaintedReadNotAllowed error = &familyError{"limes: tainted read not allowed", ErrCaveatViolation} )
Request-time caveat rejections. Each also matches ErrCaveatViolation.
var ( // ErrUnauthenticated is the family of every refusal of the presented // credentials, token or proof: the one errors.Is a consumer needs to // answer them all as unauthenticated. ErrUnauthenticated = errors.New("limes: unauthenticated") // ErrInvalidToken — the token is not genuine or not this verifier's // to accept: a bad signature, an unknown kid, an untrusted issuer, a // malformed header or claims. One sentinel for all, so a caller cannot // tell an attacker which check failed. ErrInvalidToken error = &familyError{"limes: invalid token", ErrUnauthenticated} // ErrProofOfPossession is the family of every proof-of-possession // refusal: the token may be genuine, the proof that its holder sent it // is what failed. The verifier wraps each dpop refusal in it. ErrProofOfPossession error = &familyError{"limes: proof of possession refused", ErrUnauthenticated} ErrExpired error = &familyError{"limes: expired", ErrUnauthenticated} ErrNotYetValid error = &familyError{"limes: not yet valid", ErrUnauthenticated} ErrRevoked error = &familyError{"limes: revoked", ErrUnauthenticated} // ErrDelegationTooDeep — a capability with more than MaxDelegationDepth // ancestors on record. Its root lies past the bounded IsRevoked walk, // so a revocation there would go unseen: it is refused as revoked, // never accepted, and Delegate refuses to mint one. A store answers it // as an error, which no revocation cache keeps. ErrDelegationTooDeep error = &familyError{"limes: delegation chain too deep", ErrRevoked} ErrAudienceMismatch error = &familyError{"limes: audience mismatch", ErrUnauthenticated} ErrCaveatViolation = errors.New("limes: caveat violation") ErrBudgetExceeded = errors.New("limes: budget exceeded") ErrDelegationTooWide = errors.New("limes: child wider than parent") // ErrUnitCodeMismatch — two units that would have to be one: a // delegated child declaring a unit_code other than its parent's, or an // amount in a unit other than the one a counter (a capability's, an // ancestor's, a tenant's) already counts in. Nothing converts between // currencies, so cross-currency delegation is refused at issuance and a // cross-currency charge at the Meter. ErrUnitCodeMismatch = errors.New("limes: unit_code mismatch") // ErrInvalidRequest is a request malformed in itself: a missing tenant, // an empty audience entry, a negative TTL, a thumbprint that is not one, // a not-before past the expiry, caveats or an amount that do not // validate. Nothing was done. Every such error matches it, so a caller // maps the whole family to its "bad request" without listing it. A // request that is well formed but asks for more than is allowed has its // own sentinel instead: ErrDelegationTooWide, ErrUnitCodeMismatch. ErrInvalidRequest = errors.New("limes: invalid request") // ErrUnknownTenant is an issuance for a tenant that does not exist — // typically one whose provisioning has not finished. The Store returns it. ErrUnknownTenant = errors.New("limes: unknown tenant") // ErrTenantDeleted is an issuance for a tenant in the trash: it exists, // so its rows may be restored, but nothing new is minted for it. The // Store returns it. ErrTenantDeleted = errors.New("limes: tenant deleted") // ErrScopeMismatch is a store call whose context is scoped to a tenant // other than the one its request names — or to none — under a store // that keeps each tenant's rows apart. The rows are there; the call may // not see them. It is the caller's wiring that is wrong, not the // request, so it matches neither ErrInvalidRequest nor ErrNotFound, and // ErrorResponse answers it 500. ErrScopeMismatch = errors.New("limes: call scoped to another tenant than its request names") // ErrProofUnsupported is a key-bound capability presented to a verifier // with no DPoP configured: no proof the client sends could be checked. // It is the deployment's fault, not the request's, so it matches no // ErrUnauthenticated sentinel and ErrorResponse invites no retry. ErrProofUnsupported = errors.New("limes: key-bound capability, and this verifier checks no proof") )
Errors exposed to callers, which switch on them with errors.Is. A consumer maps them to its transport, or lets Verifier.ErrorResponse map them to HTTP.
Some are families that more specific sentinels match as well, so a caller branches on the one it needs:
ErrUnauthenticated the presented credentials were refused (401)
├── ErrNoCredentials none were presented
├── ErrInvalidToken the token is not genuine, or not this verifier's
│ └── ErrBiscuitAttenuation
├── ErrProofOfPossession the proof of possession is missing or wrong
│ └── ErrAuthSchemeMismatch
├── ErrRevoked
│ └── ErrDelegationTooDeep
└── ErrExpired, ErrNotYetValid, ErrAudienceMismatch
ErrCaveatViolation a caveat refuses the operation (403)
ErrBudgetExceeded, ErrTenantBudgetExceeded, ErrRequestLimitExceeded
a ceiling refuses the request (403)
ErrInvalidRequest the request itself is malformed
├── ErrInvalidCaveats, ErrInvalidAmount, ErrEffectConflict
└── ErrMalformedAuthorization the client's Authorization header (400)
ErrNotFound what the request names is not on record
└── ErrChargeNotFound, ErrReservationNotFound, ErrUsageNotFound, …
var ( // ErrInvalidAmount — an amount that is negative or above MaxNanos, or a // decimal that is not one (ParseAmount). Refunds are explicit; a // negative charge is not one. A malformed request, so it matches // ErrInvalidRequest. ErrInvalidAmount error = &familyError{"limes: invalid amount", ErrInvalidRequest} // ErrCounterOverflow — an amount, each valid on its own, that would take // a counter past MaxNanos. The request is well formed; the counter has // no room for it. ErrCounterOverflow = errors.New("limes: counter would pass MaxNanos") // ErrChargeNotFound — Refund, GetCharge or ChargeByRef named a charge // with no ledger row. ErrChargeNotFound error = &familyError{"limes: charge not found", ErrNotFound} // ErrRefundExceedsCharge — Refund would return more than the charge // took, counting earlier refunds of the same charge. ErrRefundExceedsCharge = errors.New("limes: refund exceeds charge") // ErrReservationNotFound — Settle named a reservation that does not // exist, was released, or has expired. One already settled is not an // error: Settle returns its charge again (ChargeReceipt.Replayed). ErrReservationNotFound error = &familyError{"limes: reservation not found", ErrNotFound} // ErrRequestLimitExceeded — the capability, a Biscuit copy or an // ancestor used every request its ceiling allows; the next was blocked. ErrRequestLimitExceeded = errors.New("limes: request limit exceeded") // ErrUsageNotFound — no usage row exists for this capability. // Get-only; bump/charge always inserts on first hit. ErrUsageNotFound error = &familyError{"limes: usage row not found", ErrNotFound} // ErrTenantBudgetExceeded — the tenant aggregate cap has been // hit. Distinct from ErrBudgetExceeded because the remedy differs: // an operator raises a tenant's ceiling with SetTenantBudget, while a // capability's ceiling is in its token and lifting it means issuing a // new one. ErrTenantBudgetExceeded = errors.New("limes: tenant aggregate budget exceeded") // ErrTenantBudgetNotFound — Get-only; the tenant has no ceiling set // and has never been charged. A tenant charged before any ceiling was // set has a row, Unlimited, with ResourceVersion 0. ErrTenantBudgetNotFound error = &familyError{"limes: tenant budget row not found", ErrNotFound} // ErrTenantBudgetVersionMismatch — SetTenantBudget's OCC guard refused the // write: the stored resource_version is not the one the caller read, so // someone else changed the cap in between. The caller re-reads and decides, // rather than silently overwriting a change it never saw. ErrTenantBudgetVersionMismatch = errors.New("limes: tenant budget resource_version mismatch") )
Usage-related sentinels; ErrBudgetExceeded lives in types.go.
var ErrAlreadyExists = errors.New("limes: already exists")
ErrAlreadyExists is Store.Insert given an id already on record.
var ErrAuthSchemeMismatch error = &familyError{"limes: token presented under the wrong scheme", ErrProofOfPossession}
ErrAuthSchemeMismatch — the scheme the token came under is not the one its binding calls for: a key-bound token as Bearer, which RFC 9449 §7.2 treats as a downgrade, or an unbound one as DPoP. Matches ErrProofOfPossession: the binding, not the token, is what the scheme fails.
var ErrBiscuitAttenuation error = &familyError{"limes: invalid Biscuit attenuation", ErrInvalidToken}
ErrBiscuitAttenuation — an attenuation block the verifier cannot honour: outside the vocabulary, malformed, or widening its parent. Matches ErrInvalidToken: the token is not one this verifier accepts.
var ErrCopyCountersNotMetered = errors.New("limes: copy limits need a verifier with MeterCopies")
ErrCopyCountersNotMetered — a Biscuit sets a copy's own request or budget limit, and the verifier was not told that its Meter counts copies. The limit would go unenforced, so the token is refused instead. The token is not at fault — a verifier with MeterCopies accepts it — so this is the server's configuration, outside ErrInvalidToken: ErrorResponse answers it as an internal error, and it has no Reason.
var ErrCursorRepeated = errors.New("limes: a page returned a cursor already fetched")
ErrCursorRepeated is a page that hands back a cursor the walk has already fetched — its own, or one further back. Following it would fetch the same pages forever, so the walk stops there.
var ErrEffectConflict error = &familyError{"limes: declared effect conflicts with the operation", ErrInvalidRequest}
ErrEffectConflict is a declared effect that contradicts a built-in operation's own ("put" declared a read), or one this package does not define. A declaration may name a built-in operation's effect, never change it: a write declared a read would shed IdempotencyKeyRequired. It is a programming error in the consumer, so it matches ErrInvalidRequest.
var ErrExternalRefConflict error = &familyError{"limes: external ref already names a different charge", ErrInvalidRequest}
ErrExternalRefConflict — Charge named an ExternalRef its capability already charged under, for a different amount, unit or tenant. One reference names one cost; replaying the first receipt would report the second as booked. Matches ErrInvalidRequest.
var ErrInvalidCaveats error = &familyError{"limes: invalid caveats", ErrInvalidRequest}
ErrInvalidCaveats is returned when a caveat bag is malformed: a budget outside 0..MaxNanos, a request limit outside 0..MaxIJSONInteger, an unknown unit, an empty resource entry, an unparsable CIDR, an operation name that is not well formed. It is a request error, not an authorisation decision — the capability was never minted — so it also matches ErrInvalidRequest.
var ErrMalformedAuthorization error = &familyError{"limes: malformed Authorization header", ErrInvalidRequest}
ErrMalformedAuthorization — an Authorization header that is not one scheme this module reads followed by one token68 credential. It is a malformed request (RFC 6750 §3.1 invalid_request), not a bad token, so it matches ErrInvalidRequest; ErrorResponse still answers it 400, as the client's fault, where the rest of that family is the server's own call.
var ErrNoCredentials error = &familyError{"limes: no credentials", ErrUnauthenticated}
ErrNoCredentials — the request carries no credential this module reads: no Authorization header, or one under a scheme other than Bearer or DPoP. RFC 6750 §3.1 answers it with a challenge and no error code: the client has not tried, so nothing it sent was wrong.
var ErrNotFound = errors.New("limes: not found")
ErrNotFound is the family of every not-found error: Store.GetRecord returns it, and the meter's not-found sentinels (ErrUsageNotFound and the others) match it under errors.Is. It lives in the core package rather than in a store implementation so that any consumer — in-memory, relational, or otherwise — can satisfy the Store contract using only what this package publishes.
The verifier never returns it: an id not on record is simply not revoked.
Functions ¶
func AllByPrincipal ¶ added in v0.2.0
func AllByPrincipal(ctx context.Context, l CapabilityLister, req ListByPrincipalRequest) iter.Seq2[Capability, error]
AllByPrincipal walks ListByPrincipal page by page, from req.Cursor, and yields every capability in its order. req.Limit sets the page size, not how many are yielded.
An error is yielded once, with a zero Capability, and ends the walk; breaking out of the loop fetches no further page. The cursor is the contract for paging across requests — an API handing a page token to its client — and this is for a caller that wants the whole list in one go.
Example ¶
package main
import (
"context"
"crypto/ed25519"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/oleg-tkachuk/limes"
"github.com/oleg-tkachuk/limes/dpop"
"github.com/oleg-tkachuk/limes/jwks"
"github.com/oleg-tkachuk/limes/memstore"
)
// The issuer's name, and the audience every example's tokens are minted for.
const (
exampleIssuerName = "example-issuer"
exampleAudience = "my-service"
)
// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
issuer *limes.Issuer
verifier *limes.Verifier
records *memstore.Store
usage *memstore.UsageStore[struct{}]
tenantID uuid.UUID
}
func newExampleStack() exampleStack {
key, err := limes.GenerateKey()
kid, pub, priv := key.KeyID, key.Public(), key.Private
if err != nil {
log.Fatal(err)
}
records, err := memstore.NewStore(memstore.StoreConfig{})
if err != nil {
log.Fatal(err)
}
usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
if err != nil {
log.Fatal(err)
}
signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
if err != nil {
log.Fatal(err)
}
issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
if err != nil {
log.Fatal(err)
}
replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
if err != nil {
log.Fatal(err)
}
proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
if err != nil {
log.Fatal(err)
}
verifier, err := limes.NewVerifier(limes.VerifierConfig{
Keys: exampleKeys(kid, pub),
Revocations: records,
TrustedIssuers: []string{exampleIssuerName},
AcceptBiscuit: true,
BiscuitRevocations: records,
DPoP: proofs,
})
if err != nil {
log.Fatal(err)
}
return exampleStack{
issuer: issuer, verifier: verifier, records: records,
usage: usage, tenantID: uuid.New(),
}
}
// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
if err != nil {
log.Fatal(err)
}
return vc
}
// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
IssuedBy: limes.Principal{Subject: "operator@example.com"},
Subject: limes.Principal{
Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
},
Audience: []string{exampleAudience},
TTL: 15 * time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet, limes.OpShare},
ResourcePrefixes: []string{"corpus/public/"},
Requests: limes.RequestCeiling{Max: 500},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("25.00")},
UnitCode: "USD",
},
})
c, token := cResult.Capability, cResult.Token
if err != nil {
log.Fatal(err)
}
return c, token
}
func main() {
ctx := context.Background()
s := newExampleStack()
for range 3 {
s.issue()
}
// One page per capability, to show the walk crossing pages.
const pageSize = 1
n := 0
for _, err := range limes.AllByPrincipal(ctx, s.records, limes.ListByPrincipalRequest{
TenantID: s.tenantID, PrincipalType: limes.PrincipalTypeAgent, Subject: "research-orchestrator",
Limit: pageSize,
}) {
if err != nil {
log.Fatal(err)
}
n++
}
fmt.Println(n)
}
// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
if err != nil {
log.Fatal(err)
}
return keys
}
Output: 3
func AllTenantBudgets ¶ added in v0.2.0
func AllTenantBudgets(ctx context.Context, l TenantBudgetLister, req ListTenantBudgetsRequest) iter.Seq2[TenantBudget, error]
AllTenantBudgets walks ListTenantBudgets page by page, from req.Cursor, and yields every row in its order, as AllByPrincipal does. Spend moves between pages, so a tenant may be yielded twice or not at all; see TenantBudgets.ListTenantBudgets.
func Attenuate ¶
func Attenuate(token string, a Attenuation) (string, error)
Attenuate appends a block that narrows token. It needs no key and no network; the result is a new token, and the input still works as before.
Example ¶
package main
import (
"context"
"crypto/ed25519"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/oleg-tkachuk/limes"
"github.com/oleg-tkachuk/limes/dpop"
"github.com/oleg-tkachuk/limes/jwks"
"github.com/oleg-tkachuk/limes/memstore"
)
// The issuer's name, and the audience every example's tokens are minted for.
const (
exampleIssuerName = "example-issuer"
exampleAudience = "my-service"
)
// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
issuer *limes.Issuer
verifier *limes.Verifier
records *memstore.Store
usage *memstore.UsageStore[struct{}]
tenantID uuid.UUID
}
func newExampleStack() exampleStack {
key, err := limes.GenerateKey()
kid, pub, priv := key.KeyID, key.Public(), key.Private
if err != nil {
log.Fatal(err)
}
records, err := memstore.NewStore(memstore.StoreConfig{})
if err != nil {
log.Fatal(err)
}
usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
if err != nil {
log.Fatal(err)
}
signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
if err != nil {
log.Fatal(err)
}
issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
if err != nil {
log.Fatal(err)
}
replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
if err != nil {
log.Fatal(err)
}
proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
if err != nil {
log.Fatal(err)
}
verifier, err := limes.NewVerifier(limes.VerifierConfig{
Keys: exampleKeys(kid, pub),
Revocations: records,
TrustedIssuers: []string{exampleIssuerName},
AcceptBiscuit: true,
BiscuitRevocations: records,
DPoP: proofs,
})
if err != nil {
log.Fatal(err)
}
return exampleStack{
issuer: issuer, verifier: verifier, records: records,
usage: usage, tenantID: uuid.New(),
}
}
// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
if err != nil {
log.Fatal(err)
}
return vc
}
// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
IssuedBy: limes.Principal{Subject: "operator@example.com"},
Subject: limes.Principal{
Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
},
Audience: []string{exampleAudience},
TTL: 15 * time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet, limes.OpShare},
ResourcePrefixes: []string{"corpus/public/"},
Requests: limes.RequestCeiling{Max: 500},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("25.00")},
UnitCode: "USD",
},
})
c, token := cResult.Capability, cResult.Token
if err != nil {
log.Fatal(err)
}
return c, token
}
func main() {
ctx := context.Background()
s := newExampleStack()
c, token := s.issue()
fullResult, err := s.issuer.Biscuit(context.Background(), limes.BiscuitRequest{Capability: s.verified(token)})
full := fullResult.Token
if err != nil {
log.Fatal(err)
}
// The holder narrows it offline: no key, no network.
narrow, err := limes.Attenuate(full, limes.Attenuation{
Ops: []limes.Op{limes.OpGet},
ResourcePrefixes: []string{"corpus/public/2026/"},
})
if err != nil {
log.Fatal(err)
}
got, err := s.verifier.Verify(ctx, limes.VerifyRequest{Token: narrow, Audience: exampleAudience})
fmt.Println(err, got.Capability().ID == c.ID, got.Capability().Caveats.Ops, got.Capability().Caveats.ResourcePrefixes)
}
// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
if err != nil {
log.Fatal(err)
}
return keys
}
Output: <nil> true [get] [corpus/public/2026/]
func ExactPercent ¶ added in v0.50.1
ExactPercent is pct as the decimal it is written as — the shortest decimal that reads back as the same float64 — so a threshold of 1.1 means eleven tenths exactly, not the binary fraction a hair above it. A store compares spent / max × 100 against it without rounding. It is nil for NaN or an infinity, which ListTenantBudgetsRequest.Validate refuses.
func IsUnitCode ¶ added in v0.3.0
IsUnitCode reports whether u is an ISO 4217 currency code, in its canonical upper-case spelling: "USD", "JPY", "XXX". Empty is not one: it names no unit, and no unit is ever assumed for it.
func MatchResource ¶
MatchResource reports whether uri falls under prefix, matched at a path- segment boundary: "corpus/public" covers "corpus/public" and "corpus/public/x", but not "corpus/public-secret". A prefix that already ends in "/" matches anything beneath it. An empty prefix matches nothing — a caveat that means "everything" is written by leaving the list empty, not by an empty entry, so a stray "" can never silently widen a capability.
A uri with a dot segment — "." or "..", percent-encoded or not, between slashes that may be encoded or backslashes — matches nothing: what it names depends on how the consumer resolves it, so the match it seems to make by its text proves nothing about where it leads.
This is the single definition of a resource match. Delegation narrowing and request-time enforcement both go through it, so the two can never disagree about what a prefix reaches.
Example ¶
package main
import (
"fmt"
"github.com/oleg-tkachuk/limes"
)
func main() {
fmt.Println(limes.MatchResource("corpus/public", "corpus/public/x"))
fmt.Println(limes.MatchResource("corpus/public", "corpus/public-secret"))
}
Output: true false
func Narrows ¶
func Narrows(parent, child Capability) error
Narrows reports whether `child` is a narrowing of `parent`: every operation child allows is allowed by parent; every resource child can touch is reachable by parent; the budget / request count are no larger, and the validity window starts no earlier and ends no later; the audience is a subset; tainted-read is no broader. Issuer.Delegate runs this check before signing the child token, so a misuse fails at issue time, not later.
The function is exposed at package level so external tooling can dry-run a delegation without going through the issuer.
func ValidateExternalRef ¶
ValidateExternalRef reports whether ref is usable as ChargeRequest.ExternalRef: empty, or printable text within MaxExternalRefBytes.
func WriteErrorResponse ¶ added in v0.3.0
func WriteErrorResponse(w http.ResponseWriter, resp ErrorResponse)
WriteErrorResponse writes resp: its status, challenges, nonce and problem.
Types ¶
type AgentPrincipal ¶
type AgentPrincipal struct {
// AgentType is a human-meaningful label ("research-orchestrator",
// "code-reviewer-bot"). Used for grouping, dashboards, anomaly
// baselines.
AgentType string
// AgentVersion pins the build / git-sha of the agent runtime.
AgentVersion string
// RunID is a UUID minted by the agent host at session start.
RunID uuid.UUID
// ParentAgentID is set when this agent was spawned by another via
// `OpShare`. Empty for top-level agents.
ParentAgentID uuid.UUID
// Model identifies the LLM behind the agent ("example-model-1",
// "gpt-5-pro"). Used for cost attribution.
Model string
// MCPClient is the agent host's user-agent ("example-host@1.4.0",
// "cursor@0.44"). Useful for compatibility tracking.
MCPClient string
// contains filtered or unexported fields
}
AgentPrincipal carries the fields specific to AI-agent principals. Threaded into audit + lineage so per-tool-call replay reconstructs the call graph without joining across half a dozen tables.
type Attenuation ¶
type Attenuation struct {
// Ops the token keeps.
Ops []Op
// ResourcePrefixes and ResourceURIs the token keeps. Setting either
// replaces both.
ResourcePrefixes []string
ResourceURIs []string
// Audience the token keeps.
Audience []string
// ExpiresAt, when earlier than the token's, becomes its expiry.
ExpiresAt time.Time
// ConfirmationJKT binds the token to a key (dpop.Thumbprint). Only an
// unbound token can be bound offline: a bound one stays bound to its key,
// because rebinding needs no key and anyone could do it.
ConfirmationJKT string
// Requests and Budget give this copy limits of its own, counted apart
// from its siblings' and within the capability's: requests made with
// it, and its spend in the capability's unit. nil sets none; a ceiling
// of 0 freezes the copy. Each must be within every limit already in
// force, and an Unlimited one is refused: a copy only narrows.
Requests *RequestCeiling
Budget *BudgetCeiling
}
Attenuation is one offline narrowing. Empty fields leave that dimension as it is; set ones replace it and must be within what the token already allows, or the verifier refuses the whole token.
type AuthScheme ¶ added in v0.3.0
type AuthScheme string
AuthScheme is the authentication scheme a token was presented under.
const ( // AuthSchemeUnspecified is a token that did not come from an // Authorization header — a gRPC metadata entry, a message field. // VerifyRequest checks no scheme for it; a key-bound token still needs // its DPoP proof. AuthSchemeUnspecified AuthScheme = "" // AuthSchemeBearer presents a bearer token (RFC 6750). AuthSchemeBearer AuthScheme = "Bearer" // AuthSchemeDPoP presents a key-bound token (RFC 9449 §7.1). AuthSchemeDPoP AuthScheme = "DPoP" )
func ParseAuthorization ¶ added in v0.3.0
func ParseAuthorization(header string) (AuthScheme, string, error)
ParseAuthorization reads an Authorization header value: the scheme, matched without regard to case (RFC 9110 §11.1), and its token68 credential. With ErrMalformedAuthorization it still returns the scheme it recognised, so ErrorResponse challenges under the scheme the client used.
func (AuthScheme) Validate ¶ added in v0.37.0
func (s AuthScheme) Validate() error
Validate refuses a scheme other than the ones ParseAuthorization returns, or AuthSchemeUnspecified, as ErrInvalidRequest.
type AuthorizeRequest ¶ added in v0.3.0
type AuthorizeRequest struct {
// Verify is the token and how it was presented.
Verify VerifyRequest
// Check is what the token is asked to allow.
Check CheckRequest
// ClientAddr is the client address the consumer observed. A capability
// constrained to source networks refuses the zero address.
ClientAddr netip.Addr
}
AuthorizeRequest is a VerifyRequest and the operation it is presented for.
type BiscuitCopy ¶
type BiscuitCopy struct {
// contains filtered or unexported fields
}
BiscuitCopy identifies one copy of a capability's Biscuit. Only Verifier.BiscuitCopy makes one, from a token whose signature chain it checked, so the revocation id and the capability it is revoked under always belong together: a caller cannot name another tenant's copy under its own capability. The zero BiscuitCopy names nothing.
func (BiscuitCopy) CapabilityID ¶
func (c BiscuitCopy) CapabilityID() uuid.UUID
CapabilityID is the capability the Biscuit seals.
func (BiscuitCopy) Copies ¶ added in v0.50.0
func (c BiscuitCopy) Copies() []CopyCeiling
Copies are the request and budget limits in force on the copy — its own and those of the copies it was attenuated from — innermost first; CopyUsageReader reads their counters.
func (BiscuitCopy) RevocationID ¶
func (c BiscuitCopy) RevocationID() []byte
RevocationID is the id of the copy's last block: listing it revokes this copy and every copy attenuated from it.
type BiscuitRequest ¶ added in v0.46.0
type BiscuitRequest struct {
// Capability is the capability to give a Biscuit form, as the verifier
// accepted it.
Capability VerifiedCapability
}
BiscuitRequest is the input shape for Issuer.Biscuit.
type BiscuitResult ¶ added in v0.46.0
type BiscuitResult struct {
// Token is the Biscuit, base64url.
Token string
// contains filtered or unexported fields
}
BiscuitResult is what Issuer.Biscuit mints.
type BiscuitRevocation ¶
type BiscuitRevocation struct {
CapabilityID uuid.UUID
RevocationID []byte
RevokedAt time.Time
Note string
Actor string
}
BiscuitRevocation is one revoked Biscuit copy, as RevokeBiscuit wrote it.
type BiscuitRevocationCacheConfig ¶ added in v0.13.0
type BiscuitRevocationCacheConfig struct {
// Upstream answers what the cache does not hold. Required.
Upstream BiscuitRevocationLookup
TTL time.Duration
MaxEntries int
Now func() time.Time
}
BiscuitRevocationCacheConfig configures a CachedBiscuitRevocationChecker; its fields read as RevocationCacheConfig's do, MaxEntries counting revocation ids.
type BiscuitRevocationLookup ¶
type BiscuitRevocationLookup interface {
// IsBiscuitRevoked reports whether any of revocationIDs — the blocks
// of one copy, authority first — has been revoked.
IsBiscuitRevoked(ctx context.Context, revocationIDs [][]byte) (bool, error)
}
BiscuitRevocationLookup answers whether any of a Biscuit's revocation ids is revoked. The ids it is asked about come from a Biscuit whose signature chain has already verified.
type BiscuitRevocationStore ¶
type BiscuitRevocationStore interface {
BiscuitRevocationLookup
// RevokeBiscuit lists the revocation id of one Biscuit copy, under the
// capability the copy belongs to — the pair Verifier.BiscuitCopy read
// from the token, which no caller can assemble. Idempotent. Returns
// ErrNotFound when the capability is not on record — or not visible to
// the caller — so a caller cannot revoke a copy of a capability it cannot
// see, and ErrInvalidRequest for a request naming no copy.
RevokeBiscuit(ctx context.Context, req RevokeBiscuitRequest) error
// GetBiscuitRevocation reads back what RevokeBiscuit wrote for one
// revocation id. ErrNotFound when the id is not revoked, or belongs to a
// capability the caller cannot see.
GetBiscuitRevocation(ctx context.Context, revocationID []byte) (BiscuitRevocation, error)
}
BiscuitRevocationStore persists revoked Biscuit copies. It is separate from Store so that a consumer without Biscuits implements nothing more.
type BudgetCeiling ¶ added in v0.27.0
type BudgetCeiling struct {
// Max is the most that may be spent; 0 allows nothing. Within
// 0..MaxNanos, and 0 when Unlimited.
Max Nanos
// Unlimited lifts the ceiling.
Unlimited bool
}
BudgetCeiling bounds an amount spent: at most Max, in the capability's unit, or none at all when Unlimited.
func (BudgetCeiling) Admits ¶ added in v0.27.0
func (c BudgetCeiling) Admits(amount Nanos) bool
Admits reports whether amount spent in all stays under the ceiling.
func (BudgetCeiling) String ¶ added in v0.27.0
func (c BudgetCeiling) String() string
String is the ceiling for logs and errors: its Max, or "unlimited".
func (BudgetCeiling) Validate ¶ added in v0.27.0
func (c BudgetCeiling) Validate() error
Validate refuses a Max beside Unlimited as ErrInvalidRequest, and a Max outside 0..MaxNanos as ErrInvalidAmount (Nanos.Validate), which matches ErrInvalidRequest too.
func (BudgetCeiling) Within ¶ added in v0.27.0
func (c BudgetCeiling) Within(outer BudgetCeiling) bool
Within reports whether c allows no more than outer.
type BumpReceipt ¶ added in v0.16.0
type BumpReceipt struct {
// RequestCount is the capability's request count after this one.
RequestCount int64
// contains filtered or unexported fields
}
BumpReceipt is what a Bump counted.
type BumpRequest ¶
type BumpRequest struct {
// Subject is the capability counted, with its ceiling and copy limits:
// VerifiedCapability.MeterSubject. Ancestors' ceilings come from their
// records.
Subject MeterSubject
}
BumpRequest is the input to Meter.Bump.
func (BumpRequest) Validate ¶ added in v0.68.0
func (r BumpRequest) Validate() error
Validate refuses a request whose subject names no capability, no tenant or an unknown unit.
type CachedBiscuitRevocationChecker ¶
type CachedBiscuitRevocationChecker struct {
// contains filtered or unexported fields
}
CachedBiscuitRevocationChecker caches IsBiscuitRevoked answers per revocation id, under the same TTL, bound and sharing of concurrent misses as CachedRevocationChecker. A token is revoked when any of its ids is, so each id is asked about on its own: the ids a token shares with the copies it was attenuated from stay cached, and a fresh attenuation — which anyone holding a copy can make offline — costs one upstream lookup, for its new block, rather than one for the whole token. A token none of whose ids is cached costs one lookup per block.
A revoked id's cached "live" answer lasts until the TTL, as in CachedRevocationChecker; Clear drops every entry at once.
func NewCachedBiscuitRevocationChecker ¶
func NewCachedBiscuitRevocationChecker(cfg BiscuitRevocationCacheConfig) (*CachedBiscuitRevocationChecker, error)
NewCachedBiscuitRevocationChecker wires a cache over cfg.Upstream, as NewCachedRevocationChecker does.
func (*CachedBiscuitRevocationChecker) Clear ¶
func (c *CachedBiscuitRevocationChecker) Clear()
Clear drops every entry; wire it to the same revocation notification as CachedRevocationChecker.Clear.
func (*CachedBiscuitRevocationChecker) IsBiscuitRevoked ¶
func (c *CachedBiscuitRevocationChecker) IsBiscuitRevoked(ctx context.Context, revocationIDs [][]byte) (bool, error)
IsBiscuitRevoked answers for each id in turn — a fresh cached answer, or one upstream lookup shared by every concurrent caller asking about that id — and stops at the first revoked. An upstream error is returned and never cached.
func (*CachedBiscuitRevocationChecker) Sweep ¶
func (c *CachedBiscuitRevocationChecker) Sweep() int
Sweep drops every expired entry and returns how many it dropped.
type CachedRevocationChecker ¶
type CachedRevocationChecker struct {
// contains filtered or unexported fields
}
CachedRevocationChecker is a small in-memory cache for IsRevoked answers.
A cached answer about a capability is also an answer about its ancestors, so revoking a parent reaches a child's cached "live" entry only when that entry expires — within the TTL, like any other revocation seen from another process — or when Clear drops it. There is no per-id drop: the cache does not know which entries are a revoked id's descendants.
Concurrent misses for the same id share one upstream call.
func NewCachedRevocationChecker ¶
func NewCachedRevocationChecker(cfg RevocationCacheConfig) (*CachedRevocationChecker, error)
NewCachedRevocationChecker wires a cache over cfg.Upstream. A negative TTL or bound is refused; a deployment that wants no cache passes its store to the verifier directly.
func (*CachedRevocationChecker) Clear ¶
func (c *CachedRevocationChecker) Clear()
Clear drops every entry. A consumer that learns of a revocation from elsewhere — a database notification, a message bus — calls it so the next check goes upstream at once instead of waiting out the TTL. It drops everything, not only the revoked id, because an answer cached for a descendant is also an answer about the revoked ancestor.
func (*CachedRevocationChecker) IsRevoked ¶
IsRevoked serves a fresh cached answer, or asks upstream once on behalf of every concurrent caller asking about the same id. An upstream error is returned to all of them and is never cached.
func (*CachedRevocationChecker) Sweep ¶
func (c *CachedRevocationChecker) Sweep() int
Sweep drops every expired entry and returns how many it dropped. Optional housekeeping — lookups skip stale entries, and every new answer drops the expired ones before it.
type Capability ¶
type Capability struct {
// ID is the unique identifier (UUIDv7 — sortable by issuance time
// so per-tenant indexes stay tight).
ID uuid.UUID
// Issuer is the service instance that minted the capability. Verifiers
// accept tokens whose Issuer is in the trusted set.
Issuer string
// Subject is the principal the capability authorises.
Subject Principal
// Audience pins which services the capability is valid for: a
// verifier refuses a token whose audience does not include the one it
// verifies for (VerifyRequest.Audience).
Audience []string
// Caveats is the conjunction of restrictions — every caveat must
// be satisfied by the request, and child capabilities can only
// add further caveats, never relax one.
Caveats Caveats
// IssuedAt / NotBefore / ExpiresAt are standard JWT-style times.
IssuedAt time.Time
NotBefore time.Time
ExpiresAt time.Time
// ParentID, if non-zero, points at the parent capability whose
// holder delegated this one. Narrowing is checked once, at
// delegation. At use, the lineage matters twice: revoking any
// ancestor revokes this capability (Store.IsRevoked answers for the
// whole chain), and every charge also debits each ancestor, so a
// parent's budget bounds the spend of everything delegated from it.
ParentID uuid.UUID
// Generation is an issuer-assigned counter (1 for a fresh root, 1 for
// every delegated child) carried in the token for audit. A consumer
// that rotates a capability may reissue with Generation+1 and revoke
// the previous one, and compare generations in its own records.
//
// It is NOT a fence token: nothing in this package compares it
// against a stored current value, so an older generation stays usable
// until it expires or is revoked. Revocation is the mechanism that
// stops a superseded capability.
Generation int64
// ConfirmationJKT, when set, binds the capability to a key: the RFC 7638
// thumbprint of the public key whose holder alone may present it. Every
// request must then carry a DPoP proof signed by that key (package dpop). A
// delegated child of a bound capability is bound too. Carried in the
// token as `cnf.jkt`, and absent from tokens that are not bound.
ConfirmationJKT string
// BiscuitRoot is set only on the token sealed inside a Biscuit (see
// Issuer.Biscuit): the base64url Ed25519 public key that roots the
// Biscuit's signature chain. A token carrying it is refused when
// presented on its own, so it cannot be lifted out of an attenuated
// Biscuit to shed the attenuation.
BiscuitRoot string
// Copies are the request and budget limits the attenuation blocks of a
// verified Biscuit set, innermost block first. Each is counted on its
// own, beside the capability's counters, by every Meter call that is
// handed them. Set by the verifier only; never part of a token or a
// stored record.
Copies []CopyCeiling
}
Capability is the in-memory representation of a token. The wire form is a JWT; this struct is what the Issuer fills before signing, and what Verifier.Verify returns inside a VerifiedCapability.
func (Capability) Clone ¶ added in v0.24.0
func (c Capability) Clone() Capability
Clone is c with slices and pointers of its own: a store keeps, and hands out, a capability no caller can alter through a slice it kept — the record is append-only.
type CapabilityLister ¶ added in v0.2.0
type CapabilityLister interface {
// ListByPrincipal returns one page (Store.ListByPrincipal).
ListByPrincipal(ctx context.Context, req ListByPrincipalRequest) (Page[Capability], error)
}
CapabilityLister lists a principal's capabilities a page at a time. Every Store is one.
type Caveats ¶
type Caveats struct {
// Ops is the set of operations the capability authorises. The
// issuer rejects an empty set: a capability that allows nothing is
// a mistake, not a credential.
Ops []Op
// ResourcePrefixes restricts the capability to URIs under any prefix
// in the slice, matched at a "/" segment boundary (MatchResource):
// "a/b" covers "a/b" and "a/b/c" but not "a/bc". Empty slice =
// unrestricted within the tenant; an empty ENTRY is invalid.
ResourcePrefixes []string
// ResourceURIs pins the capability to specific URIs. Used by
// hand-off flows where the parent passes exactly the artifacts the
// child needs.
ResourceURIs []string
// Requests caps the total number of times the capability may be
// used within its TTL; the zero value allows none, and
// RequestCeiling{Unlimited: true} lifts the cap.
Requests RequestCeiling
// Budget is the cost budget the capability authorises, in the
// currency identified by UnitCode, exact to the nano; the zero value
// allows no spend, and BudgetCeiling{Unlimited: true} lifts it. The
// Meter counts spend against it; once exhausted, a charge returns
// ErrBudgetExceeded.
Budget BudgetCeiling
// UnitCode pins the currency or unit: an ISO 4217 code (IsUnitCode),
// XXX ("no currency") for a budget that is not money. Empty names none:
// the capability counts requests (Meter.Bump) but no amount, and a
// Charge, Reserve or Settle on it is ErrInvalidRequest. A delegated
// child that names none takes its parent's.
UnitCode string
// AllowTaintedRead permits non-mutating operations on resources the
// consumer has flagged with prompt-injection / PII / secrets signals.
// Off by default. Enforced by Caveats.Check, which can only act on a
// taint signal the consumer supplies in CheckRequest.ResourceTainted
// — a consumer that tracks no taint flags gets no protection from
// this caveat, and should say so rather than imply it.
AllowTaintedRead bool
// IdempotencyKeyRequired forces the bearer to send an explicit
// idempotency key on mutating operations (CheckRequest.Mutating).
// Enforced by Caveats.Check from CheckRequest.HasIdempotencyKey.
IdempotencyKeyRequired bool
// SourceIPCIDR optionally pins the capability to clients whose
// observed IP is in the listed CIDRs. Empty = unconstrained.
// Enforced by Caveats.CheckSource; delegation requires each child
// network to lie inside a parent network.
SourceIPCIDR []string
}
Caveats is a typed bag of restrictions, AND-combined. Ops is required; an empty resource or source-network list places no restriction on its axis. The ceilings are explicit: a zero RequestCeiling or BudgetCeiling allows nothing, and only Unlimited lifts one.
It is written into a token as the limes_caveats claim (wire.go), so renaming a field changes nothing a token carries. A verifier refuses a member it does not know: a caveat it cannot enforce must not pass as one it did.
A child capability may only narrow caveats — set a tighter prefix, drop ops, lower the budget, shorten TTL. Issuer.Delegate enforces this through Narrows before it signs, and the verifier does for every Biscuit attenuation block, so an attempt to widen is refused, never minted.
func (Caveats) AllowsResource ¶
AllowsResource reports whether uri is reachable under the resource caveats: unrestricted caveats allow any uri; restricted ones require an exact ResourceURIs entry or a covering ResourcePrefixes entry. An empty uri, or one with a "." or ".." path segment, is never allowed by restricted caveats — an operation that cannot name what it touches cannot be shown to stay inside the scope.
func (Caveats) Check ¶
func (c Caveats) Check(req CheckRequest) error
Check evaluates every per-operation caveat against req and returns the first violation, as one of the sentinels above, or nil. A declared Effect that Op.ResolveEffect refuses returns ErrEffectConflict, which is a programming error rather than a caveat violation. It is the one definition of what the caveats mean at use time; a consumer that hand-rolls the same checks drifts from it.
The source-IP caveat is per connection rather than per operation, so it is checked separately by CheckSource. Budget and request-count caveats are stateful and enforced by the UsageStore.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/oleg-tkachuk/limes"
)
func main() {
caveats := limes.Caveats{
Ops: []limes.Op{limes.OpGet},
ResourcePrefixes: []string{"corpus/public/"},
}
fmt.Println(caveats.Check(limes.CheckRequest{Op: limes.OpGet, Resource: "corpus/public/report.pdf"}))
// A prefix matches at a "/" boundary only.
err := caveats.Check(limes.CheckRequest{Op: limes.OpGet, Resource: "corpus/public-secret/x"})
fmt.Println(errors.Is(err, limes.ErrResourceNotAllowed), errors.Is(err, limes.ErrCaveatViolation))
err = caveats.Check(limes.CheckRequest{Op: limes.OpPut, Resource: "corpus/public/report.pdf"})
fmt.Println(errors.Is(err, limes.ErrOpNotAllowed))
}
Output: <nil> true true true
func (Caveats) CheckSource ¶
CheckSource evaluates the source-IP caveat against the client address the consumer observed. An unknown address (the zero netip.Addr) fails a constrained capability closed: "we could not tell" is not "it matched".
func (Caveats) RestrictsResources ¶
RestrictsResources reports whether the caveats confine the capability to a set of resources. False means "any resource within the tenant".
type ChargeBucket ¶ added in v0.61.0
type ChargeBucket uint8
ChargeBucket is how SumCharges splits a period in time.
const ( // ChargeBucketUnspecified takes the default, ChargeBucketNone: a request // that names no bucket sums its period whole. ChargeBucketUnspecified ChargeBucket = iota ChargeBucketHour ChargeBucketDay ChargeBucketMonth // ChargeBucketWeek is an ISO week, from Monday 00:00 UTC. ChargeBucketWeek // ChargeBucketNone sums the period whole, in one bucket. ChargeBucketNone )
The buckets, each cut at UTC boundaries. A new one is appended, so that no bucket's number changes.
func (ChargeBucket) Start ¶ added in v0.61.0
func (b ChargeBucket) Start(t time.Time) time.Time
Start is where the bucket holding t begins, in UTC; the zero time for ChargeBucketNone, which holds the whole period, and for ChargeBucketUnspecified, which takes it.
func (ChargeBucket) String ¶ added in v0.61.0
func (b ChargeBucket) String() string
String is the bucket's name.
func (ChargeBucket) Validate ¶ added in v0.61.0
func (b ChargeBucket) Validate() error
Validate refuses a bucket this module does not define.
type ChargeByRefRequest ¶ added in v0.31.0
type ChargeByRefRequest struct {
// CapabilityID is the capability the charge was taken against.
CapabilityID uuid.UUID
// ExternalRef is the ChargeRequest.ExternalRef it was taken under.
ExternalRef string
}
ChargeByRefRequest names one charge by the reference it was taken under.
type ChargeReceipt ¶
type ChargeReceipt struct {
// ChargeID identifies the ledger row; pass it to Meter.Refund.
ChargeID uuid.UUID
// SpentAmount is the capability's spend as this call returns (its subtree's,
// when it has delegated children).
SpentAmount Nanos
// Replayed reports that the charge was already in the ledger — the same
// ExternalRef, or a reservation already settled — so this call moved
// nothing and ChargeID names the earlier charge.
Replayed bool
// Overrun reports that the charge crossed at least one ceiling, which
// only OverrunPolicyRecord allows.
Overrun bool
// TenantID and UnitCode are the charge's tenant and unit, so a caller
// holding only the receipt — logged, or passed on — can attribute it. A
// refusal by a ceiling or by onCharged still sets them.
TenantID uuid.UUID
UnitCode string
// contains filtered or unexported fields
}
ChargeReceipt is the result of a committed charge.
type ChargeRecord ¶
type ChargeRecord struct {
ID uuid.UUID
CapabilityID uuid.UUID
TenantID uuid.UUID
// Amount is what the charge took; RefundedAmount is what has been returned
// from it since.
Amount Nanos
RefundedAmount Nanos
UnitCode string
// ExternalRef is the charge's ChargeRequest.ExternalRef; empty for none.
ExternalRef string
// Overrun reports that the charge crossed a ceiling (OverrunPolicyRecord).
Overrun bool
// Op and Actor are what the charge was for and who made it, as the
// request named them; At is when it was committed.
Op Op
Actor string
At time.Time
// contains filtered or unexported fields
}
ChargeRecord is one charge as the ledger holds it.
type ChargeRequest ¶
type ChargeRequest struct {
// Subject is the capability charged — its tenant, its own ceiling, its
// unit and the presented copy's limits: VerifiedCapability.MeterSubject.
// No conversion happens: a counter the charge moves — the
// capability's, an ancestor's, the tenant's — that already counts in
// another unit refuses it with ErrUnitCodeMismatch, and so does a
// reservation.
Subject MeterSubject
// Amount is the cost, in the subject's unit. Within 0..MaxNanos
// (Nanos.Validate).
Amount Nanos
// Op and Actor are stamped on the ledger row for attribution only;
// both may be empty and neither affects enforcement.
Op Op
Actor string
// ExternalRef names the cost in the consumer's own records — the id of
// the call or the event that reported it — and makes the charge
// idempotent on (capability, ExternalRef). Empty: every call charges.
// At most MaxExternalRefBytes long.
ExternalRef string
// Overrun is what happens when Amount would cross a ceiling.
Overrun OverrunPolicy
}
ChargeRequest is the input to Meter.Charge.
func (ChargeRequest) Validate ¶ added in v0.68.0
func (r ChargeRequest) Validate() error
Validate refuses an amount out of range, an unknown overrun policy, an external ref past MaxExternalRefBytes, and a subject that names no capability, no tenant or no unit, or an unknown unit.
type ChargeSum ¶ added in v0.61.0
type ChargeSum struct {
// BucketStart is where the bucket begins, in UTC; the zero time when
// the request sums the period whole. A bucket at either end of the
// period sums only the charges within it.
BucketStart time.Time
UnitCode string
Op Op
Actor string
// CapabilityID is the capability charged.
CapabilityID uuid.UUID
// Charged is what the group's charges took; Refunded, what has been
// returned from them since. Charged less Refunded is the net.
Charged Nanos
Refunded Nanos
// Charges counts the group's charges.
Charges int64
// contains filtered or unexported fields
}
ChargeSum is one group's sums. The fields a request did not group by are zero.
type Charged ¶ added in v0.67.0
type Charged[TX any] struct { // Tx is the consumer's transaction handle the charge commits on. Tx TX // Receipt is the receipt the call returns once the charge commits: // ChargeID names the ledger row it writes. Replayed is never set, as a // replay runs no onCharged. Receipt ChargeReceipt // contains filtered or unexported fields }
Charged is what Meter.Charge and Meter.Settle hand onCharged: the charge it runs inside, before the call commits it.
type CheckRequest ¶
type CheckRequest struct {
// Op is the operation being performed.
Op Op
// Effect is what Op does to state, as the server declares it. Leave it
// EffectUnspecified for a built-in operation; see Op.ResolveEffect.
Effect Effect
// Resource is the URI the operation touches. Leave it empty only for an
// operation that is not bound to one resource; a resource-restricted
// capability refuses such an operation (see Caveats.AllowsResource).
// For an operation over a set — a listing, a batch — pass the URI or
// prefix that bounds the whole set.
Resource string
// HasIdempotencyKey reports whether the request carried an idempotency
// key. Consulted only when the caveats require one for a mutating
// operation.
HasIdempotencyKey bool
// ResourceTainted reports whether the consumer has flagged the resource
// with a prompt-injection / PII / secrets signal. A consumer with no
// taint tracking leaves it false — AllowTaintedRead then protects
// nothing, which is the consumer's to disclose.
ResourceTainted bool
}
CheckRequest describes one operation a bearer is attempting, in the terms the caveats are written in. The consumer fills it from its own request.
func (CheckRequest) Mutating ¶
func (req CheckRequest) Mutating() (bool, error)
Mutating reports whether the operation may change state, resolving its declared Effect. A consumer that gates anything else on the operation's effect — a lookup it runs only for reads — asks here, so that it and Check cannot disagree.
type ContextSigner ¶ added in v0.14.0
type ContextSigner interface {
crypto.Signer
SignContext(ctx context.Context, rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error)
}
ContextSigner is a crypto.Signer that signs under a context, as a key held in a KMS or an HSM does for its call over the network. Signer uses SignContext when its key has it, so a slow or cancelled call is bounded by the request that asked for the token.
type CopyCeiling ¶
type CopyCeiling struct {
// RevocationID is the block's revocation id.
RevocationID []byte
// Requests bounds the requests made with the copy; nil when the block
// set no request limit of its own. Never Unlimited: a copy only narrows.
Requests *RequestCeiling
// Budget bounds the copy's spend, in the capability's unit; nil when the
// block set no budget of its own. Never Unlimited.
Budget *BudgetCeiling
}
CopyCeiling is a limit one attenuation block of a Biscuit set on the copy it ends and every copy attenuated from it, all of which carry the block's revocation id. Its counters are keyed by that id, so siblings attenuated apart count apart, while the capability's own counters still bound them all together.
func (CopyCeiling) AdmitsBudget ¶ added in v0.52.0
func (c CopyCeiling) AdmitsBudget(amount Nanos) bool
AdmitsBudget reports whether amount spent with the copy in all stays under its own budget; with none of its own, any amount does.
func (CopyCeiling) AdmitsRequests ¶ added in v0.52.0
func (c CopyCeiling) AdmitsRequests(n int64) bool
AdmitsRequests reports whether n requests made with the copy in all stay under its own request limit; with none of its own, any number does.
type CopyUsage ¶
type CopyUsage struct {
RevocationID []byte
CapabilityID uuid.UUID
RequestCount int64
SpentAmount Nanos
ReservedAmount Nanos
}
CopyUsage is the counters of one Biscuit copy with limits of its own (see Meter, "Copies of a Biscuit"): the copy and every copy attenuated from it.
type CopyUsageReader ¶
type CopyUsageReader interface {
// CopyUsage returns the counters of each copy named that has been used,
// in no particular order; a copy never used has no counters and is
// absent from the result. It refuses what CopyUsageRequest.Validate
// refuses.
CopyUsage(ctx context.Context, req CopyUsageRequest) (CopyUsageResult, error)
}
CopyUsageReader reads the counters of Biscuit copies. It is separate from Meter so that a Meter which does not count copies implements nothing more.
type CopyUsageRequest ¶ added in v0.52.0
type CopyUsageRequest struct {
// RevocationIDs are the copies' revocation ids: at least one, at most
// MaxCopyUsageIDs, none empty.
RevocationIDs [][]byte
}
CopyUsageRequest names the copies CopyUsage reads.
func (CopyUsageRequest) Validate ¶ added in v0.52.0
func (r CopyUsageRequest) Validate() error
Validate refuses a request naming no copy, more than MaxCopyUsageIDs, or an empty revocation id, as ErrInvalidRequest.
type CopyUsageResult ¶ added in v0.52.0
type CopyUsageResult struct {
// Copies are the counters of the copies that have been used.
Copies []CopyUsage
// contains filtered or unexported fields
}
CopyUsageResult is what CopyUsage read.
type CountRecordsRequest ¶ added in v0.64.0
type CountRecordsRequest struct {
// TenantID is the tenant whose records are counted; AllTenants counts
// every tenant's instead. Exactly one of them is required.
TenantID uuid.UUID
AllTenants bool
// ExpiringWithin is the window ExpiringSoon counts in: an active record
// expiring within it of now. Zero counts none; negative is refused.
ExpiringWithin time.Duration
// ByTenant and ByPrincipalType group the counts by the record's tenant
// and by its subject's principal type as well.
ByTenant, ByPrincipalType bool
// Limit is the page size; see PageLimit. Negative is refused.
Limit int
// Cursor is the one the previous page returned; empty for the first.
Cursor string
}
CountRecordsRequest asks for one page of record counts.
func (CountRecordsRequest) Validate ¶ added in v0.64.0
func (r CountRecordsRequest) Validate() error
Validate refuses a request that names no tenant and not every tenant, or both, a negative window, or a negative page size.
type DelegateRequest ¶
type DelegateRequest struct {
// Parent is the capability the child narrows, as the verifier accepted
// it: only Verifier.Verify and Authorize make one.
Parent VerifiedCapability
// Subject is who the child authorises; its TenantID defaults to the
// parent's.
Subject Principal
// Audience defaults to the parent's, and must be a subset of it.
Audience []string
// Caveats are the child's; see InheritCaveats.
Caveats Caveats
// InheritCaveats copies the parent's caveats verbatim and ignores
// Caveats. It must be asked for: Caveats with no Ops is refused, never
// read as "inherit", which would replace every other field the caller
// set — a budget of 2 would become the parent's 25 without a word.
InheritCaveats bool
// TTL defaults to IssuerConfig.DefaultTTL, and is cut so the child
// expires no later than the parent.
TTL time.Duration
// NotBefore is raised, when earlier, to the parent's start: its
// NotBefore, else its IssuedAt.
NotBefore time.Time
// ConfirmationJKT binds the child to a key — typically the sub-agent's.
// Empty keeps the parent's binding, if it has one: a bound parent's
// child is always bound.
ConfirmationJKT string
}
DelegateRequest narrows from a parent capability the caller already holds. The issuer enforces narrowing (Narrows) before persisting so a runtime widening attempt fails before any token is emitted.
type DeleteUsageRequest ¶ added in v0.31.0
DeleteUsageRequest names the capability whose usage row DeleteUsage drops.
type Effect ¶
type Effect uint8
Effect is what an operation does to state. Check uses it twice: a mutating operation may need an idempotency key (Caveats.IdempotencyKeyRequired), and a read may be refused on a tainted resource (Caveats.AllowTaintedRead).
The server that performs the operation declares the effect, from the same place it defines the operation (a method option, a tool registry), and never from anything the bearer sends.
const ( // EffectUnspecified takes the operation's default: the fixed effect of a // built-in operation, and EffectWrite for a consumer-defined one, whose // effect this package cannot know — so an undeclared operation errs // towards the caveats that tighten mutations. It errs away from the one // that tightens reads: a tainted resource is refused only to a read, so // a consumer-defined operation that reads and is left undeclared is // never held to AllowTaintedRead. Declare EffectRead for one. EffectUnspecified Effect = iota // EffectRead leaves state unchanged. EffectRead // EffectWrite may change state. EffectWrite )
type EraseTenantRequest ¶ added in v0.60.0
type EraseTenantRequest struct {
TenantID uuid.UUID
// Note and Actor are written on every revocation entry, as
// RevokeRequest's are.
Note string
Actor string
}
EraseTenantRequest names the tenant EraseTenantRecords erases and who asked, recorded on each revocation it writes.
func (EraseTenantRequest) Validate ¶ added in v0.60.0
func (r EraseTenantRequest) Validate() error
Validate refuses a request naming no tenant.
type EraseTenantResult ¶ added in v0.60.0
type EraseTenantResult struct {
// Revoked counts the capabilities it revoked: those not revoked before.
Revoked int64
// Removed counts the records it removed; Kept, those it left on record
// for Purge.
Removed int64
Kept int64
// contains filtered or unexported fields
}
EraseTenantResult is what EraseTenantRecords did.
type EraseTenantUsageRequest ¶ added in v0.60.0
EraseTenantUsageRequest names the tenant EraseTenantUsage erases.
func (EraseTenantUsageRequest) Validate ¶ added in v0.60.0
func (r EraseTenantUsageRequest) Validate() error
Validate refuses a request naming no tenant.
type ErrorResponse ¶ added in v0.3.0
type ErrorResponse struct {
// Status is the HTTP status.
Status int
// Challenges are the WWW-Authenticate values, one per scheme; none for a
// refusal that is not about authentication.
Challenges []string
// DPoPNonce is the nonce to send in the DPoP-Nonce header, when the
// refusal asks for one.
DPoPNonce string
// Problem is the body.
Problem Problem
}
ErrorResponse is how to answer a refused request.
type HeldCopy ¶ added in v0.52.0
type HeldCopy struct {
// RevocationID is the copy's revocation id.
RevocationID []byte
// Budget is the copy's own budget the hold counts against; nil when the
// copy has none of its own.
Budget *BudgetCeiling
}
HeldCopy is a Biscuit copy a hold is on: a hold counts no requests, so it carries the copy's budget alone.
type InsertRequest ¶ added in v0.15.0
type InsertRequest struct {
Capability Capability
IssuedBy Principal
}
InsertRequest is a capability to record, and who asked for it.
func (InsertRequest) Validate ¶ added in v0.53.0
func (r InsertRequest) Validate() error
Validate refuses a capability with no id, as ErrInvalidRequest: nothing could read it back, and the zero id is what a root's ParentID holds.
type InstrumentOption ¶ added in v0.12.0
type InstrumentOption func(*meteringConfig)
InstrumentOption configures Instrument.
func WithMeterProvider ¶ added in v0.3.0
func WithMeterProvider(p metric.MeterProvider) InstrumentOption
WithMeterProvider creates the instruments on p rather than on the global provider (otel.GetMeterProvider).
func WithTenantAttribute ¶ added in v0.3.0
func WithTenantAttribute() InstrumentOption
WithTenantAttribute labels every measurement with tenant_id. Off by default: it multiplies each series by the number of tenants.
type IssuanceStore ¶ added in v0.15.0
type IssuanceStore interface {
// Insert records a capability at issuance. The persisted row holds
// the full claim set so admin tooling can show a capability without
// parsing the JWT, and so audit can correlate without
// keeping every issued token.
//
// req.IssuedBy is the principal that ASKED for the capability, which
// is not its Subject (the principal it authorises) and not its Issuer
// (the service that minted it). A delegated capability is requested by
// its parent's holder; a root one by an operator. It is issuance
// metadata, not a claim, so it travels beside the Capability rather
// than in it, which would change the token's frozen wire format.
//
// An id already on record returns ErrAlreadyExists and changes nothing.
// It refuses what InsertRequest.Validate refuses, a parent not on
// record as ErrInvalidRequest, a child expiring after its parent — or
// with no expiry under a parent that has one — or in another tenant as
// ErrDelegationTooWide, and a child that would have
// more than MaxDelegationDepth ancestors still on record — counted as
// IsRevoked counts them — as ErrDelegationTooDeep: Purge and the
// IsRevoked walk rely on a lineage that is whole, no deeper than the
// walk reaches, expires from the leaves up and stays in one tenant, so a
// store that scopes rows by tenant sees all of it. A refused record is
// not written. A
// store that knows tenants returns ErrUnknownTenant or ErrTenantDeleted
// for a tenant it cannot mint for.
Insert(ctx context.Context, req InsertRequest) error
// IsRevoked reports whether the capability, or any ancestor of it
// that is still on record, is in the revocation list. Answering for
// the chain is what makes revoking an orchestrator stop the workers it
// spawned. Verifiers gate every request on it, so implementations
// should make the walk a single round trip (a recursive query),
// bounded at MaxDelegationDepth ancestors: a capability with more
// still on record returns ErrDelegationTooDeep, never false. An id not
// on record is not revoked.
IsRevoked(ctx context.Context, id uuid.UUID) (bool, error)
}
IssuanceStore is what an Issuer needs of storage: to record what it mints, and to know whether a parent it delegates from has been revoked.
type IssueRequest ¶
type IssueRequest struct {
Subject Principal
// IssuedBy is the principal REQUESTING the capability — an operator for a
// root issuance, the parent's holder for a delegation. Distinct from
// Subject, which is who the capability authorises. Its Subject is
// required — a capability nobody requested cannot be audited — and the
// rest is recorded as given: an operator need not be a tenant's
// principal.
IssuedBy Principal
Audience []string
Caveats Caveats
TTL time.Duration // 0 → DefaultTTL; must reach the next whole second
NotBefore time.Time // zero → valid from issuance, no nbf claim
Generation int64 // 0 → DefaultGeneration
// ConfirmationJKT binds the capability to the key with this RFC 7638
// thumbprint (dpop.Thumbprint); empty issues a bearer capability.
ConfirmationJKT string
}
IssueRequest is the input shape for Issuer.Issue. Subject (with its TenantID), IssuedBy, Audience and Caveats are required; TTL, NotBefore, Generation and ConfirmationJKT are optional, and the issuer fills in ID, IssuedAt, ExpiresAt and Issuer, and Generation when it is zero. Caveats are validated (Caveats.Validate) before anything is persisted.
type IssueResult ¶ added in v0.46.0
type IssueResult struct {
Capability Capability
Token string
// contains filtered or unexported fields
}
IssueResult is what Issue and Delegate mint: the capability as recorded, to log or display, and its compact-form token, to ship to the agent.
type Issuer ¶
type Issuer struct {
// contains filtered or unexported fields
}
Issuer mints capabilities, in this order:
- Validate the request (caveats sane, parent narrowing if delegated).
- Sign the token, then persist the record, and only then return the token.
Signing first means a capability whose token cannot be minted — a signer that fails, or a token past MaxTokenBytes — is never recorded. The token is returned only once its record is written: if the store write fails, the signed token is dropped, so no token leaves the issuer that audit could not find or Revoke, which refuses an id not on record, could not stop.
func NewIssuer ¶
func NewIssuer(cfg IssuerConfig) (*Issuer, error)
NewIssuer validates wiring and returns a ready Issuer.
func (*Issuer) Biscuit ¶
func (i *Issuer) Biscuit(ctx context.Context, req BiscuitRequest) (BiscuitResult, error)
Biscuit returns the Biscuit form of a capability this issuer issued: the same capability — same ID, caveats, expiry and binding, revoked together with it — in a token its holder can attenuate offline. One copy can also be revoked on its own; see BiscuitRevocationStore.
Like Delegate it takes a VerifiedCapability, so what it signs is what this issuer signed and recorded before, never a value a caller assembled or widened. It refuses one another issuer named, one read from a Biscuit — wrapping a copy again would shed its attenuation and its revocation id; its holder attenuates it instead — and one that has expired or that the store reports revoked.
func (*Issuer) Delegate ¶
func (i *Issuer) Delegate(ctx context.Context, req DelegateRequest) (IssueResult, error)
Delegate mints a child capability narrower than the supplied parent, which the verifier accepted (VerifiedCapability), so its caveats are the ones its token carries. It does not trust it to be current, though: a parent that has expired, or that the store reports revoked (itself or any ancestor), delegates nothing; nor does a Biscuit copy (VerifiedCapability.FromBiscuit), which is ErrInvalidRequest, as Issuer.Biscuit answers it — its holder attenuates it instead. A parent another issuer named is ErrInvalidRequest: this issuer's store holds no record of it, so revoking it there would never reach the child. A child that would have more than MaxDelegationDepth ancestors is ErrDelegationTooDeep, refused by Store.Insert before it is on record. A parent whose caveats do not allow OpShare — the right to delegate — is ErrOpNotAllowed: delegating is the share operation, and a capability granted without it may not have the issuer mint a new principal any part of it. OpShare gates Delegate only: a Biscuit form of the capability (Issuer.Biscuit) is attenuated by whoever holds it, with no key and no OpShare, and a copy handed on is the same capability, not a new principal. Give a capability that must not be passed on no Biscuit form.
Example ¶
package main
import (
"context"
"crypto/ed25519"
"errors"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/oleg-tkachuk/limes"
"github.com/oleg-tkachuk/limes/dpop"
"github.com/oleg-tkachuk/limes/jwks"
"github.com/oleg-tkachuk/limes/memstore"
)
// The issuer's name, and the audience every example's tokens are minted for.
const (
exampleIssuerName = "example-issuer"
exampleAudience = "my-service"
)
// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
issuer *limes.Issuer
verifier *limes.Verifier
records *memstore.Store
usage *memstore.UsageStore[struct{}]
tenantID uuid.UUID
}
func newExampleStack() exampleStack {
key, err := limes.GenerateKey()
kid, pub, priv := key.KeyID, key.Public(), key.Private
if err != nil {
log.Fatal(err)
}
records, err := memstore.NewStore(memstore.StoreConfig{})
if err != nil {
log.Fatal(err)
}
usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
if err != nil {
log.Fatal(err)
}
signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
if err != nil {
log.Fatal(err)
}
issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
if err != nil {
log.Fatal(err)
}
replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
if err != nil {
log.Fatal(err)
}
proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
if err != nil {
log.Fatal(err)
}
verifier, err := limes.NewVerifier(limes.VerifierConfig{
Keys: exampleKeys(kid, pub),
Revocations: records,
TrustedIssuers: []string{exampleIssuerName},
AcceptBiscuit: true,
BiscuitRevocations: records,
DPoP: proofs,
})
if err != nil {
log.Fatal(err)
}
return exampleStack{
issuer: issuer, verifier: verifier, records: records,
usage: usage, tenantID: uuid.New(),
}
}
// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
if err != nil {
log.Fatal(err)
}
return vc
}
// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
IssuedBy: limes.Principal{Subject: "operator@example.com"},
Subject: limes.Principal{
Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
},
Audience: []string{exampleAudience},
TTL: 15 * time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet, limes.OpShare},
ResourcePrefixes: []string{"corpus/public/"},
Requests: limes.RequestCeiling{Max: 500},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("25.00")},
UnitCode: "USD",
},
})
c, token := cResult.Capability, cResult.Token
if err != nil {
log.Fatal(err)
}
return c, token
}
func main() {
ctx := context.Background()
s := newExampleStack()
_, token := s.issue()
// The orchestrator presents its token; the parent is what the verifier
// accepted.
parent := s.verified(token)
worker := limes.Principal{Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "sub-worker"}
childResult, err := s.issuer.Delegate(ctx, limes.DelegateRequest{
Parent: parent,
Subject: worker,
TTL: 2 * time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet},
ResourcePrefixes: []string{"corpus/public/2026/"},
Requests: limes.RequestCeiling{Max: 50},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("2.00")},
UnitCode: "USD",
},
})
child := childResult.Capability
fmt.Println(err, child.ParentID == parent.Capability().ID)
// Asking for more than the parent holds is refused.
_, err = s.issuer.Delegate(ctx, limes.DelegateRequest{
Parent: parent,
Subject: worker,
TTL: time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("999")},
UnitCode: "USD",
},
})
fmt.Println(errors.Is(err, limes.ErrDelegationTooWide))
}
// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
if err != nil {
log.Fatal(err)
}
return keys
}
Output: <nil> true true
func (*Issuer) Issue ¶
func (i *Issuer) Issue(ctx context.Context, req IssueRequest) (IssueResult, error)
Issue mints a top-level capability. Generation defaults to DefaultGeneration; see Capability.Generation for what it does and does not promise.
Example ¶
package main
import (
"context"
"crypto/ed25519"
"errors"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/oleg-tkachuk/limes"
"github.com/oleg-tkachuk/limes/jwks"
"github.com/oleg-tkachuk/limes/memstore"
)
// The issuer's name, and the audience every example's tokens are minted for.
const (
exampleIssuerName = "example-issuer"
exampleAudience = "my-service"
)
func main() {
ctx := context.Background()
key, _ := limes.GenerateKey()
kid, pub, priv := key.KeyID, key.Public(), key.Private
records, _ := memstore.NewStore(memstore.StoreConfig{}) // your Store
signer, _ := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
issuer, _ := limes.NewIssuer(limes.IssuerConfig{
Signer: signer, Store: records, IssuerName: exampleIssuerName,
})
verifier, _ := limes.NewVerifier(limes.VerifierConfig{
Keys: exampleKeys(kid, pub),
Revocations: records,
TrustedIssuers: []string{exampleIssuerName},
})
cResult, err := issuer.Issue(ctx, limes.IssueRequest{
IssuedBy: limes.Principal{Subject: "operator@example.com"},
Subject: limes.Principal{
Type: limes.PrincipalTypeAgent, TenantID: uuid.New(), Subject: "research-orchestrator",
},
Audience: []string{exampleAudience},
TTL: 15 * time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet},
ResourcePrefixes: []string{"corpus/public/"},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("25.00")},
UnitCode: "USD",
},
})
c, token := cResult.Capability, cResult.Token
if err != nil {
log.Fatal(err)
}
got, err := verifier.Verify(ctx, limes.VerifyRequest{Token: token, Audience: exampleAudience})
fmt.Println(err == nil, got.Capability().ID == c.ID)
_, err = verifier.Verify(ctx, limes.VerifyRequest{Token: token, Audience: "another-service"})
fmt.Println(errors.Is(err, limes.ErrAudienceMismatch))
}
// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
if err != nil {
log.Fatal(err)
}
return keys
}
Output: true true true
type IssuerConfig ¶
type IssuerConfig struct {
// Signer mints tokens (NewSigner), over a key in memory or one held in
// a KMS or an HSM.
Signer *Signer
// Store records what the issuer mints and answers whether a parent is
// revoked. Any Store is one.
Store IssuanceStore
// IssuerName is the value placed in the `iss` claim. Verifiers
// require this to be in their TrustedIssuers list.
IssuerName string
// DefaultTTL caps the lifetime when IssueRequest.TTL is zero.
// Zero: DefaultIssueTTL, short enough that revocation propagation
// latency rarely matters. Negative is refused.
DefaultTTL time.Duration
// MaxTokenBytes refuses to mint a token longer than this, which a
// verifier with the same limit (VerifierConfig.MaxTokenBytes) would
// refuse unread. Zero: DefaultMaxTokenBytes; negative is refused.
MaxTokenBytes int
// Now is the clock; tests override.
Now func() time.Time
}
IssuerConfig is the wiring for NewIssuer.
type KeyResolver ¶
type KeyResolver interface {
// PublicKey returns the Ed25519 public key for the given kid.
// Returns jwks.ErrUnknownKID if the key set does not contain the kid,
// which the verifier reads as the token's fault (ErrInvalidToken).
// Any other error is the resolver's: the verifier refuses the token
// with it as it is, the server's fault rather than the token's.
PublicKey(ctx context.Context, kid string) (ed25519.PublicKey, error)
}
KeyResolver resolves a JWKS `kid` to the public key the verifier should use. jwks.Static holds keys in memory; jwks.Remote fetches them from an issuer's JWKS endpoint with a cache.
Keys are Ed25519 for all of 1.x: the token format, not the key, names the algorithm, so another algorithm would be another token type (AcceptTokenTypes) rather than a key this interface returns.
type LedgerReader ¶ added in v0.61.0
type LedgerReader interface {
// SumCharges returns one page of sums, each one group's: the charges of
// req.TenantID taken in [req.From, req.To) — by the store's clock, as
// ChargeRecord.At has it — grouped by unit always, by req.Bucket, and
// by whichever of operation, actor and capability req names. Within a
// bucket the largest charged come first, so the first page of Limit
// rows is the top Limit; buckets come in time order. A refund counts in
// the group of the charge it returns, whenever it is made: a closed
// period's sums still move. A sum past MaxNanos is ErrCounterOverflow.
SumCharges(ctx context.Context, req SumChargesRequest) (Page[ChargeSum], error)
}
LedgerReader sums a tenant's charges ledger over a period, for billing and reporting: totals, the largest spenders by operation, actor or capability, and series by hour, day, week or month. It reads the ledger the Meter writes, so a consumer reports from it rather than from a store's tables. It is a type of its own, beside UsageStore rather than one of its interfaces: a reporter holds it, a meter does not. memstore and pgstore each ship one; metertest checks it when its Env sets one.
type ListByPrincipalRequest ¶
type ListByPrincipalRequest struct {
// TenantID, PrincipalType and Subject name the principal; all three are
// required (Validate).
TenantID uuid.UUID
PrincipalType PrincipalType
Subject string
// IncludeExpired and IncludeRevoked also list capabilities that have
// expired — at their ExpiresAt, by the store's clock — or that are
// revoked themselves (not through an ancestor).
IncludeExpired bool
IncludeRevoked bool
// Cursor is the one the previous page returned; empty for the first.
Cursor string
// Limit is the page size; see PageLimit.
Limit int
}
ListByPrincipalRequest is the input shape for Store.ListByPrincipal.
func (ListByPrincipalRequest) Validate ¶
func (r ListByPrincipalRequest) Validate() error
Validate reports whether the request names a principal. Every error matches ErrInvalidRequest.
type ListReservationsRequest ¶ added in v0.16.0
type ListReservationsRequest struct {
CapabilityID uuid.UUID
// Limit is the page size; see PageLimit. Negative is refused.
Limit int
// Cursor is the one the previous page returned; empty for the first.
Cursor string
}
ListReservationsRequest asks for one page of a capability's reservations.
func (ListReservationsRequest) Validate ¶ added in v0.16.0
func (r ListReservationsRequest) Validate() error
Validate refuses a request with no capability or a negative page size.
type ListTenantBudgetsRequest ¶
type ListTenantBudgetsRequest struct {
// Filter picks the rows; the zero value lists every tenant.
Filter TenantBudgetFilter
// ThresholdPct is the utilization TenantBudgetFilterAtOrPast lists from,
// 0..100. Any other filter takes none.
ThresholdPct float64
// Limit is the page size; see PageLimit. Negative is refused.
Limit int
// Cursor is the one the previous page returned; empty for the first.
Cursor string
}
ListTenantBudgetsRequest is the input shape for UsageStore.ListTenantBudgets.
func (ListTenantBudgetsRequest) Validate ¶ added in v0.13.0
func (r ListTenantBudgetsRequest) Validate() error
Validate refuses a request no page can answer: a negative page size, an unknown filter, a threshold outside 0..100, or a threshold beside a filter that takes none.
type Meter ¶
type Meter[TX any] interface { // Bump increments the request counter of the capability and of // each ancestor, and returns the capability's new count. When any // ceiling in the chain would be crossed it returns // ErrRequestLimitExceeded and nothing is mutated. Bump(ctx context.Context, req BumpRequest) (BumpReceipt, error) // Charge adds req.Amount to the spend counters of the capability, of // each ancestor and of the tenant aggregate, writes one charges-ledger // row, and runs onCharged — ALL atomically. Checks, in order: // // 1. Each Biscuit copy's own budget (req.Subject.Copies), innermost // first → ErrBudgetExceeded. // 2. The capability's own ceiling (req.Subject.Budget()) → // ErrBudgetExceeded. // 3. Each ancestor's ceiling → ErrBudgetExceeded. // 4. The tenant aggregate ceiling → ErrTenantBudgetExceeded. // // Any rejection, or an error from onCharged, leaves every counter and // the ledger untouched. onCharged runs inside the charge, atomically // with it, once every ceiling has passed. It receives the consumer's // transaction handle and the receipt the call will return (Charged), so // a side effect such as an outbox write commits with the spend and names // the ChargeID. The ledger row is written after onCharged returns: it is // not there yet to read. Pass nil to skip it. It must not call back into the Meter: an implementation may // hold its counters while onCharged runs, so a nested call can wait on // itself. // // The returned receipt carries the ledger row's ID; Refund takes it. // // With req.ExternalRef set, Charge is idempotent on (CapabilityID, // ExternalRef): a charge whose pair is already in the ledger moves // nothing, does not run onCharged, and returns that charge's receipt // with Replayed set — also once the capability is no longer on record, // as Settle replays a settled hold. The pair names one cost: arriving again with a // different Amount, UnitCode or TenantID it returns // ErrExternalRefConflict and moves nothing. With req.Overrun = OverrunPolicyRecord, a charge that // would cross a ceiling is committed anyway and the receipt reports it // (ChargeReceipt.Overrun); see OverrunPolicy. Charge(ctx context.Context, req ChargeRequest, onCharged func(ctx context.Context, c Charged[TX]) error) (ChargeReceipt, error) // Refund returns spend from one recorded charge to every counter that // charge debited (capability, ancestors, tenant aggregate), and records // the refund against the charge. req.Remaining refunds whatever // remains, which makes a full refund idempotent; a partial refund that // would take the total refunded past the charge returns // ErrRefundExceedsCharge and changes nothing. An unknown charge ID // returns ErrChargeNotFound, and a request naming neither an Amount nor // Remaining, or both, ErrInvalidRequest. The receipt carries the amount // actually refunded. // // A charge counted in a tenant accounting period that has since been // closed (SetTenantBudget with ResetSpend) is refunded to the // capability, its ancestors and its copies, but not to the tenant // aggregate: the current period never counted it. // // For a cost only known afterwards, prefer Reserve/Settle: a // reservation lapses on its own if the caller never settles it, where // an estimate charged up front stays charged. Refund(ctx context.Context, req RefundRequest) (RefundReceipt, error) // Reserve holds req.Amount against the capability, each ancestor and // the tenant aggregate, checking every ceiling against spend plus // what is already held. Holds count against later charges and // reservations exactly as spend does, until the reservation is // settled, released or expires. Rejections return the Charge // sentinels and hold nothing. Reserve(ctx context.Context, req ReserveRequest) (Reservation, error) // Settle ends a reservation and charges the actual cost in its place, // atomically: the hold is released and req.Amount is charged as // Charge would, with onCharged run inside. The actual cost may be // below, equal to or above the hold; above it, the excess must fit // every ceiling, or Settle returns the Charge sentinel and the // reservation stays in place for the caller to settle lower or // release — unless req.Overrun is OverrunPolicyRecord, when the cost is // charged anyway and the receipt reports it. // // Settle is idempotent on the reservation: settling one already // settled moves nothing, does not run onCharged, and returns the // receipt of the charge that settled it, with Replayed set. An unknown, // released or expired reservation returns ErrReservationNotFound, and so // does the zero id, which names none — never a direct charge. So does a // reservation made by another capability or tenant, or under other Biscuit copies // than req.Subject carries: a hold is settled by the copy that made it, // so a copy cannot settle a sibling's hold past its own limits. Settle(ctx context.Context, req SettleRequest, onCharged func(ctx context.Context, c Charged[TX]) error) (ChargeReceipt, error) // Release ends a reservation without charging. Idempotent: releasing // one that is gone is a no-op. So is releasing a reservation made by // another capability or tenant, or under other Biscuit copies than req.Subject // carries: it is none of this subject's, and stays held, so a copy // cannot free a sibling's hold to spend past its own limits. Release(ctx context.Context, req ReleaseRequest) error }
Meter enforces the request and budget ceilings (Caveats.Requests, Caveats.Budget) and keeps the charges ledger. Separated from Store so the issuance / revocation surface stays focused on identity, not runtime state.
Delegation trees ¶
A capability's counters cover its whole subtree: Bump and Charge apply to the capability AND to every ancestor in its delegation chain, and each ancestor's own ceiling — read from its stored record, not from the caller — must admit the increment. So an orchestrator holding 25.00 that delegates 2.00 to each of a hundred workers can still spend 25.00 in total, not 200.00. Narrowing at delegation bounds each child; this bounds the sum. An ancestor whose record is gone (Store.Purge, after expiry) no longer bounds anything — its children expired with it, since a child cannot outlive its parent. The capability itself must be on record: Bump, Charge, Reserve and Settle refuse one that is not — never recorded, or purged — with ErrNotFound, since none of its ancestors' ceilings could be read. A cost reported late is charged only while the record lasts, so keep records (PurgeRequest.ExpiredFor) as long as costs may still arrive.
Copies of a Biscuit ¶
A Biscuit's holder can give a copy limits of its own (CopyCeiling), which arrive through the Subject of BumpRequest, ChargeRequest and ReserveRequest (MeterSubject.Copies). Each is counted under its revocation id, apart from its siblings, and checked before the capability's ceilings; the capability's counters still take every copy's requests and spend. A charge or reservation records the copies it debited, so Refund, Settle, Release and ReleaseExpired return to them without being handed them again. A Meter that does not count copies must not be paired with a verifier that sets VerifierConfig.MeterCopies.
All methods are concurrency-safe. A rejected call mutates nothing, so a retry after rejection is safe. Each method first refuses a request whose Validate does, before it reads anything: a store that calls Validate enforces a rule added to it without a change of its own.
A store over a database holds at most one connection per call: the reads of the lineage, the ceilings and the ledger, the writes, and onCharged all run on the one transaction the call opens. A call that takes a second connection while it holds its transaction deadlocks once as many calls run as the pool has connections — each holds one and waits for another — which a large pool under a light load never shows. Run metertest against a pool of one or two connections to catch it.
type MeterReader ¶ added in v0.16.0
type MeterReader interface {
// GetCharge returns one charge of the ledger, by the ChargeID its
// receipt carried — the id Refund takes. ErrChargeNotFound when there
// is none.
GetCharge(ctx context.Context, chargeID uuid.UUID) (ChargeRecord, error)
// ChargeByRef returns the charge a capability took under an external
// reference (ChargeRequest.ExternalRef), so a reporter can reconcile
// its own records with the ledger. ErrChargeNotFound when there is none,
// an empty reference included: no charge is taken under no name.
ChargeByRef(ctx context.Context, req ChargeByRefRequest) (ChargeRecord, error)
// GetReservation returns a reservation still holding budget — open,
// or expired and not yet released, since until ReleaseExpired runs it
// still counts (ExpiresAt tells which). ErrReservationNotFound once it
// is settled or released.
GetReservation(ctx context.Context, reservationID uuid.UUID) (Reservation, error)
// ListReservations returns, a page at a time, every reservation
// GetReservation would return that was made against the capability
// itself — not its delegated children — soonest to expire first, and
// holds expiring at the same instant in ascending ID, compared bytewise.
// It refuses what ListReservationsRequest.Validate refuses.
ListReservations(ctx context.Context, req ListReservationsRequest) (Page[Reservation], error)
// GetUsage returns the current snapshot. Returns ErrUsageNotFound when no
// row exists for the capability — typically means it's never been
// used (no requests, no charges). For a capability with delegated
// children the counters include the children's usage.
GetUsage(ctx context.Context, capID uuid.UUID) (Usage, error)
}
MeterReader reads back what the Meter recorded: charges, reservations and counters. A reporter or an admin view depends on it alone.
type MeterSubject ¶ added in v0.17.0
type MeterSubject struct {
// contains filtered or unexported fields
}
MeterSubject is what a Meter counts a request against: a verified capability's id and tenant, its request and budget ceilings, its unit and the limits of the Biscuit copy presented. Only VerifiedCapability.MeterSubject and StoredMeterSubject make one, so a ceiling is the one a verified token carries or a RecordReader reads back — never a value a caller sets on the subject itself. A store reads it through its methods. The zero MeterSubject names no capability and no tenant; a Meter refuses it with ErrInvalidRequest rather than count it anywhere.
func StoredMeterSubject ¶ added in v0.33.0
func StoredMeterSubject(ctx context.Context, records RecordReader, capabilityID uuid.UUID) (MeterSubject, error)
StoredMeterSubject is what the Meter counts a capability against for a cost reported with no token in hand, read from the capability's record — records is the Store, or anything reading its records. It reads the record itself rather than take a Record, whose fields any caller can set, so the ceilings are the ones on record. A stored record names no Biscuit copy, so the subject carries none. A revoked capability still gives one: a cost already incurred belongs in the ledger. ErrNotFound when the capability is not on record.
func (MeterSubject) Budget ¶ added in v0.27.0
func (s MeterSubject) Budget() BudgetCeiling
Budget is the capability's own budget ceiling.
func (MeterSubject) CapabilityID ¶ added in v0.17.0
func (s MeterSubject) CapabilityID() uuid.UUID
CapabilityID is the capability the request counts against.
func (MeterSubject) Copies ¶ added in v0.17.0
func (s MeterSubject) Copies() []CopyCeiling
Copies are the limits of the Biscuit copy presented, a copy of them.
func (MeterSubject) Requests ¶ added in v0.27.0
func (s MeterSubject) Requests() RequestCeiling
Requests is the capability's own request ceiling.
func (MeterSubject) TenantID ¶ added in v0.17.0
func (s MeterSubject) TenantID() uuid.UUID
TenantID is the capability's tenant: its aggregate ceiling applies, and the ledger files the charge under it.
func (MeterSubject) UnitCode ¶ added in v0.17.0
func (s MeterSubject) UnitCode() string
UnitCode is the unit the capability counts in; empty when it names none, and then it counts requests alone.
type Nanos ¶
type Nanos int64
Nanos is an amount of a currency or metering unit (see UnitCode) in billionths of the unit: 1.5 USD is 1_500_000_000. Every amount the module counts — a charge, a ceiling, a hold, a refund — is Nanos, so spend sums, compares and refunds exactly, however many charges it takes. Float64 appears only at the edges, for display (Float64) and for a caller whose prices arrive as floats (AmountFromFloat).
In JSON, and so in a capability token, an amount is a decimal number of units in a string ("max_budget": "1.5"), written and read without a float64 in between.
const MaxNanos Nanos = 999_999_999_999_999_999
MaxNanos is the largest amount the module counts: just under a billion units, which is what a column of numeric(18,9) holds. Bounding every amount and counter by it leaves the sum of any two inside int64.
func AmountFromFloat ¶
AmountFromFloat converts an amount held as a float64 to Nanos, rounding to the nearest nano, halves to even. It reads the float as the shortest decimal that identifies it — 0.1 as "0.1", the sum 0.1+0.2 as "0.30000000000000004" — so an amount written with at most nine decimals and fifteen significant digits converts exactly. It refuses what Nanos.Validate refuses, and NaN and the infinities.
func MustParseAmount ¶
MustParseAmount is ParseAmount for an amount written into the program, a price or a test value: it panics on one ParseAmount refuses.
func ParseAmount ¶
ParseAmount reads a decimal amount of units — "0.35", "25", "1e-7" — into Nanos exactly. It refuses a negative amount, one finer than a nano and one beyond MaxNanos, rather than rounding any of them.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/oleg-tkachuk/limes"
)
func main() {
a, _ := limes.ParseAmount("0.1")
b, _ := limes.ParseAmount("0.2")
fmt.Println(a+b, int64(a+b))
// Finer than a nano is refused rather than rounded.
_, err := limes.ParseAmount("0.0000000001")
fmt.Println(errors.Is(err, limes.ErrInvalidAmount))
}
Output: 0.3 300000000 true
func (Nanos) Float64 ¶
Float64 is n in units, the nearest float64. For display and metrics only: never count with it.
func (Nanos) MarshalJSON ¶
MarshalJSON writes n as a JSON string holding a decimal number of units, "1.5": a string, so that a reader whose numbers are float64 — JavaScript's JSON.parse among them — cannot round it.
func (Nanos) MarshalText ¶
MarshalText writes n as a decimal number of units, as String does.
func (Nanos) String ¶
String writes n as a decimal number of units with no trailing zeros: "1.5", "0.000000001", "25".
func (*Nanos) UnmarshalJSON ¶
UnmarshalJSON reads what MarshalJSON writes, exactly, as ParseAmount does: a JSON string, never a number.
func (*Nanos) UnmarshalText ¶
UnmarshalText reads a decimal number of units exactly, as ParseAmount does: configuration that carries an amount — a YAML scalar, an environment variable — reads it without a float64 in between.
type Op ¶
type Op string
Op is an operation an agent may perform. The package defines a small built-in set (below); a consumer adds its own as namespaced names of the form "<namespace>:<name>" — "tool:search", "mcp:github/create_issue" — which Op.Validate accepts. A consumer-defined operation is treated as state-changing unless the server declares otherwise (Effect). Fine-grained method gating can still layer admin-authored policy on top.
const ( OpGet Op = "get" // read one resource OpPut Op = "put" // write one resource OpList Op = "list" // list resources under a prefix OpDelete Op = "delete" // delete a resource OpPresign Op = "presign" // mint a pre-signed URL for a resource OpTag Op = "tag" // label a resource OpSearch Op = "search" // query an index OpEmbed Op = "embed" // compute embeddings OpManage Op = "manage" // administer: tenants, policy )
The built-in operations. Get, List, Search and Embed read; the rest may change state (Op.ResolveEffect).
func (Op) ResolveEffect ¶
ResolveEffect returns the effect Check applies to op, given the effect the consumer declared for it. A built-in operation keeps its own effect, and a declaration may only repeat it; a consumer-defined operation takes the declaration, or EffectWrite when there is none.
type OverrunPolicy ¶
type OverrunPolicy uint8
OverrunPolicy is what a charge does when its amount would cross a ceiling — a Biscuit copy's, the capability's, an ancestor's or the tenant's.
const ( // OverrunPolicyUnspecified takes the default, OverrunPolicyReject: a // caller that says nothing never commits past a ceiling. OverrunPolicyUnspecified OverrunPolicy = iota // OverrunPolicyReject refuses the charge with the ceiling's sentinel and // changes nothing. Use it for a cost not yet incurred: the refusal is // what stops it. OverrunPolicyReject // OverrunPolicyRecord commits the charge past the ceiling and reports it in // ChargeReceipt.Overrun. Use it for a cost already incurred — reported // after the fact, out of the request path — where refusing would only // leave the ledger short of what was spent. The crossed ceiling then // refuses every later charge and reservation under OverrunPolicyReject. OverrunPolicyRecord )
func (OverrunPolicy) String ¶ added in v0.21.0
func (p OverrunPolicy) String() string
String names the policy for logs and errors.
func (OverrunPolicy) Validate ¶
func (p OverrunPolicy) Validate() error
Validate reports whether p is an OverrunPolicy this package defines. Implementations call it before touching state, so an unknown policy is never read as either one.
type Page ¶ added in v0.15.0
Page is one page of a list: its items, and the cursor that fetches the next page — empty on the last one. The cursor is the store's own, opaque to the caller.
type Principal ¶
type Principal struct {
// Type discriminates the principal kind.
Type PrincipalType
// TenantID is required for every principal — capability without
// tenant scope is malformed and the verifier rejects it.
TenantID uuid.UUID
// Subject is the stable identity (user UUID, agent run ID, service
// account name). Format depends on Type.
Subject string
// Agent fields are populated only for Type == PrincipalTypeAgent.
Agent *AgentPrincipal
// contains filtered or unexported fields
}
Principal identifies the subject the capability is issued to. It is written into a token as the limes_principal claim (wire.go). Agent principals have a richer shape than service principals: they carry the run / tool-call lineage that audit and cost attribution need.
func (Principal) Clone ¶ added in v0.24.0
Clone is p with agent details of its own: a store keeps, and hands out, a principal no caller can alter through a pointer it kept.
func (Principal) Validate ¶ added in v0.8.0
Validate reports whether p names a principal a capability can be issued to: one of the principal kinds, spelled as the constants spell it, and agent details only on an agent. The kind is part of the token, so a kind outside the set would be signed and then read by every verifier as it pleased.
type PrincipalType ¶
type PrincipalType string
PrincipalType discriminates Principal.Type.
const ( PrincipalTypeUnspecified PrincipalType = "" PrincipalTypeUser PrincipalType = "user" // a person PrincipalTypeAgent PrincipalType = "agent" // an AI agent; Principal.Agent says which PrincipalTypeService PrincipalType = "service" // a service account )
The principal kinds. The zero value, PrincipalTypeUnspecified, names none and is refused wherever a kind is required.
func (PrincipalType) Validate ¶ added in v0.21.0
func (t PrincipalType) Validate() error
Validate reports whether t is one of the principal kinds, spelled as the constants spell it. Every error matches ErrInvalidRequest.
type Problem ¶ added in v0.3.0
type Problem struct {
// Type is a URI naming the kind of problem.
Type string `json:"type"`
// Title is a short, fixed summary of the kind.
Title string `json:"title"`
// Status repeats the HTTP status.
Status int `json:"status"`
// Detail explains this occurrence, in terms safe to show the client.
Detail string `json:"detail,omitempty"`
// contains filtered or unexported fields
}
Problem is an RFC 9457 problem details object.
type PurgeOrphansRequest ¶ added in v0.16.0
type PurgeOrphansRequest struct {
// Limit bounds the rows deleted. Zero: DefaultHousekeepingLimit;
// negative is refused.
Limit int
}
PurgeOrphansRequest is one batch of UsageHousekeeping.PurgeOrphans.
type PurgeOrphansResult ¶ added in v0.16.0
type PurgeOrphansResult struct {
Purged int64
// contains filtered or unexported fields
}
PurgeOrphansResult is what one PurgeOrphans deleted.
type PurgeRequest ¶ added in v0.15.0
type PurgeRequest struct {
// ExpiredFor is how long a capability must have been expired to go.
// Zero: MinPurgeRetention; anything shorter, negative included, is
// refused.
ExpiredFor time.Duration
// Limit bounds the records removed. Zero: DefaultHousekeepingLimit; negative
// is refused.
Limit int
}
PurgeRequest is one batch of Store.Purge.
func (PurgeRequest) Retention ¶ added in v0.23.0
func (r PurgeRequest) Retention() time.Duration
Retention is how long a capability must have been expired to go: ExpiredFor, or MinPurgeRetention when it is zero.
func (PurgeRequest) Validate ¶ added in v0.15.0
func (r PurgeRequest) Validate() error
Validate refuses a retention shorter than MinPurgeRetention, other than zero, and a negative limit.
type PurgeResult ¶ added in v0.15.0
type PurgeResult struct {
// Purged is the number of capability records removed.
Purged int64
// contains filtered or unexported fields
}
PurgeResult is what one Purge removed.
type Reason ¶
type Reason string
Reason is a stable, machine-readable name for why a presented capability was refused. The sentinels stay the way to branch in Go; a Reason is what a consumer puts on the wire, so that a client in any language tells "this capability is spent" from "this request was malformed" without reading a message. The values are never renamed; new ones may be added.
const ( ReasonNoCredentials Reason = "no_credentials" ReasonMalformedAuthorization Reason = "malformed_authorization" ReasonInvalidToken Reason = "invalid_token" ReasonExpired Reason = "expired" ReasonNotYetValid Reason = "not_yet_valid" ReasonRevoked Reason = "revoked" ReasonAudienceMismatch Reason = "audience_mismatch" ReasonBiscuitAttenuation Reason = "biscuit_attenuation" ReasonProofRequired Reason = "proof_required" ReasonNonceRequired Reason = "nonce_required" ReasonAuthSchemeMismatch Reason = "auth_scheme_mismatch" ReasonProofInvalid Reason = "proof_invalid" ReasonProofReplayed Reason = "proof_replayed" ReasonOpNotAllowed Reason = "op_not_allowed" ReasonResourceNotAllowed Reason = "resource_not_allowed" ReasonSourceIPNotAllowed Reason = "source_ip_not_allowed" ReasonIdempotencyKeyRequired Reason = "idempotency_key_required" ReasonTaintedReadNotAllowed Reason = "tainted_read_not_allowed" ReasonRequestLimitExceeded Reason = "request_limit_exceeded" ReasonBudgetExceeded Reason = "budget_exceeded" ReasonTenantBudgetExceeded Reason = "tenant_budget_exceeded" )
The refusals a verifier, Caveats.Check or a Meter returns for a presented capability.
func ReasonOf ¶
ReasonOf returns the reason err refused a capability for, and false when err is no such refusal — nil, a store failure, or a programming error such as ErrInvalidRequest, which a consumer maps as it maps any other. The one member of that family that is a refusal, ErrMalformedAuthorization, has its reason.
func Reasons ¶
func Reasons() []Reason
Reasons returns every reason this package defines, in a fixed order, so a consumer can prove its own mapping covers them all.
func (Reason) Permanent ¶
Permanent reports whether a refusal for r stands for as long as the capability does: the same request with the same capability will be refused again, whatever happens elsewhere, so a caller stops rather than retries.
A refusal that something else can lift is not permanent: a budget frees as holds lapse or charges are refunded, a tenant ceiling is raised by an operator, a taint flag is cleared, a not-yet-valid capability becomes valid, a proof is signed afresh, the verifier fetches its keys again, and an idempotency key can be added to the request.
type Record ¶
type Record struct {
Capability Capability
// IssuedBy is the principal Insert was given.
IssuedBy Principal
// Revocation is the capability's own revocation entry; nil when it has
// none. A capability stopped only through an ancestor has none — ask
// IsRevoked whether it verifies.
Revocation *Revocation
}
Record is a capability as the Store holds it.
type RecordCensus ¶ added in v0.64.0
type RecordCensus interface {
// CountRecords returns one page of counts, each one group's: the
// records of req.TenantID, or of every tenant with req.AllTenants,
// grouped by tenant and by principal type when req asks, in that order,
// each ascending. A group with no record has no row, so the census of
// no record is an empty page.
CountRecords(ctx context.Context, req CountRecordsRequest) (Page[RecordCount], error)
}
RecordCensus counts capability records by what they are now — active, expired or revoked — for a platform operator's report: the records of one tenant or of every tenant, whole or by tenant and by principal type, with how many expire soon. It reads the store a consumer issues into, so a report counts through it rather than through a store's tables, whose columns are the store's own; a view over those tables could freeze its columns, but not take the store's clock or share these rules with every store. It is a type of its own, beside Store: a reporter holds it, an issuer does not. memstore and pgstore each ship one; storetest checks it when its Env sets one.
Each record counts under exactly one disposition, by the store's clock, the one expiry and purging are measured by:
- Revoked: the record, or an ancestor of it still on record, is revoked — what Store.IsRevoked answers, so a child of a revoked parent is revoked here, unlike ListByPrincipalRequest.IncludeRevoked, which reads a record's own entry. Revoked wins over expired.
- Expired: not revoked, and its ExpiresAt is zero or not after now. A child expires no later than its parent (Store.Insert), so no ancestor need be read for it.
- Active: neither.
A Biscuit copy's revocation is not a record's and counts nowhere.
type RecordCount ¶ added in v0.64.0
type RecordCount struct {
TenantID uuid.UUID
PrincipalType PrincipalType
// Total is every record of the group: Active, Expired and Revoked
// together, each record in one of them.
Total, Active, Expired, Revoked int64
// Delegated counts the group's records with a parent, whatever their
// disposition.
Delegated int64
// ExpiringSoon counts the active records expiring within the
// request's ExpiringWithin of now: a part of Active.
ExpiringSoon int64
// contains filtered or unexported fields
}
RecordCount is one group's counts. The fields a request did not group by are zero.
type RecordReader ¶ added in v0.42.0
type RecordReader interface {
// GetRecord reads back everything Insert and Revoke wrote about a
// capability: the capability, who issued it, and its own revocation
// entry if it has one. ErrNotFound when there is no record. For audit
// and admin reads, and for a reporter that needs a capability's
// ceilings; Verifier never calls it.
GetRecord(ctx context.Context, id uuid.UUID) (Record, error)
}
RecordReader reads one capability's record. Store implements it; a caller that only reads records — StoredMeterSubject — asks for no more.
type RefundReceipt ¶ added in v0.3.0
type RefundReceipt struct {
// ChargeID is the charge refunded from.
ChargeID uuid.UUID
// Amount is the amount this call returned; zero when nothing was
// left to refund.
Amount Nanos
// TenantID and UnitCode are the charge's.
TenantID uuid.UUID
UnitCode string
// contains filtered or unexported fields
}
RefundReceipt is the result of Meter.Refund.
type RefundRequest ¶
type RefundRequest struct {
ChargeID uuid.UUID
// Amount to refund, above zero and within MaxNanos (Nanos.Validate).
Amount Nanos
// Remaining refunds whatever of the charge is not yet refunded, which
// makes a full refund idempotent: once nothing remains, it returns
// nothing.
Remaining bool
}
RefundRequest is the input to Meter.Refund. It names either an Amount or Remaining, never both and never neither.
func (RefundRequest) Validate ¶ added in v0.19.0
func (r RefundRequest) Validate() error
Validate refuses a request that names no amount, or both an amount and Remaining.
type ReleaseExpiredRequest ¶ added in v0.16.0
type ReleaseExpiredRequest struct {
// Limit bounds the reservations released. Zero:
// DefaultHousekeepingLimit; negative is refused.
Limit int
}
ReleaseExpiredRequest is one batch of UsageHousekeeping.ReleaseExpired.
type ReleaseExpiredResult ¶ added in v0.16.0
type ReleaseExpiredResult struct {
Released int64
// contains filtered or unexported fields
}
ReleaseExpiredResult is what one ReleaseExpired released.
type ReleaseRequest ¶ added in v0.16.0
type ReleaseRequest struct {
// Subject is the capability releasing, as it reserved
// (VerifiedCapability.MeterSubject). A reservation of another
// capability, or of other copies, is left held.
Subject MeterSubject
ReservationID uuid.UUID
}
ReleaseRequest names the reservation to release, and who releases it.
func (ReleaseRequest) Validate ¶ added in v0.68.0
func (r ReleaseRequest) Validate() error
Validate refuses a request whose subject names no capability, no tenant or an unknown unit.
type RequestCeiling ¶ added in v0.27.0
type RequestCeiling struct {
// Max is the most requests allowed; 0 allows none. Within
// 0..MaxIJSONInteger, and 0 when Unlimited.
Max int64
// Unlimited lifts the ceiling.
Unlimited bool
}
RequestCeiling bounds a count of requests: at most Max, or none at all when Unlimited.
func (RequestCeiling) Admits ¶ added in v0.27.0
func (c RequestCeiling) Admits(n int64) bool
Admits reports whether n requests in all stay under the ceiling.
func (RequestCeiling) String ¶ added in v0.27.0
func (c RequestCeiling) String() string
String is the ceiling for logs and errors: its Max, or "unlimited".
func (RequestCeiling) Validate ¶ added in v0.27.0
func (c RequestCeiling) Validate() error
Validate refuses a negative or out-of-range Max, and a Max beside Unlimited, as ErrInvalidRequest.
func (RequestCeiling) Within ¶ added in v0.27.0
func (c RequestCeiling) Within(outer RequestCeiling) bool
Within reports whether c allows no more than outer: anything fits under an unlimited ceiling, and under a bounded one only a bounded ceiling no higher.
type Reservation ¶
type Reservation struct {
ID uuid.UUID
CapabilityID uuid.UUID
TenantID uuid.UUID
// Amount is held in UnitCode.
Amount Nanos
UnitCode string
// Op and Actor are what Settle will stamp on the charge.
Op Op
Actor string
// Copies are the Biscuit copies the hold is also on, innermost first,
// with the budget each is held against.
Copies []HeldCopy
ExpiresAt time.Time
}
Reservation is a committed hold, as Reserve made it and as GetReservation and ListReservations read it back.
type ReserveRequest ¶
type ReserveRequest struct {
// Subject is the capability the hold counts against
// (VerifiedCapability.MeterSubject). The reservation keeps the
// presented copy's limits, and Settle checks and charges them.
Subject MeterSubject
// Amount to hold, in the subject's unit. Within 0..MaxNanos
// (Nanos.Validate).
Amount Nanos
// TTL is how long the hold lasts if never settled or released.
// 0 = DefaultReservationTTL; negative is ErrInvalidRequest.
TTL time.Duration
// Op and Actor are carried onto the charge Settle records.
Op Op
Actor string
}
ReserveRequest is the input to Meter.Reserve.
func (ReserveRequest) Validate ¶ added in v0.68.0
func (r ReserveRequest) Validate() error
Validate refuses an amount out of range, a subject that names no capability, no tenant or no unit, or an unknown unit, and a negative TTL.
type Revocation ¶
type Revocation struct {
RevokedAt time.Time
Note string
Actor string
// Cascaded reports that the entry was written by a cascading revoke —
// of this capability or of an ancestor.
Cascaded bool
// contains filtered or unexported fields
}
Revocation is one entry of the revocation list, as Revoke wrote it.
type RevocationCacheConfig ¶ added in v0.13.0
type RevocationCacheConfig struct {
// Upstream answers what the cache does not hold. Required.
Upstream RevocationLookup
// TTL is how long an answer is served. Zero: DefaultRevocationCacheTTL.
TTL time.Duration
// MaxEntries bounds the cache. Zero: DefaultRevocationCacheEntries.
MaxEntries int
// Now is the clock. Nil: time.Now.
Now func() time.Time
}
RevocationCacheConfig configures a CachedRevocationChecker.
type RevocationLookup ¶
type RevocationLookup interface {
// IsRevoked reports whether id, or any ancestor of it, is revoked. An
// error is no answer: the verifier refuses rather than assume not.
IsRevoked(ctx context.Context, id uuid.UUID) (bool, error)
}
RevocationLookup is the seam the cache wraps. A persistent store implements it; tests substitute a stub. Like Store.IsRevoked, it answers for the capability's whole delegation chain.
type RevokeBiscuitRequest ¶
type RevokeBiscuitRequest struct {
// Copy is the copy to revoke, as Verifier.BiscuitCopy read it.
Copy BiscuitCopy
// Note and Actor are stored for audit, as on RevokeRequest.
Note string
Actor string
}
RevokeBiscuitRequest is the input shape for BiscuitRevocationStore.RevokeBiscuit.
func (RevokeBiscuitRequest) Validate ¶ added in v0.43.0
func (r RevokeBiscuitRequest) Validate() error
Validate refuses a request naming no copy as ErrInvalidRequest.
type RevokeRequest ¶
type RevokeRequest struct {
CapabilityID uuid.UUID
// Note is an operator-supplied label ("compromise", "rotation",
// "policy-change"). Stored on the revocation row for audit.
Note string
// Actor identifies who triggered the revocation (a user UUID or
// an automation principal). Stored on the row.
Actor string
// CascadeChildren records a revocation entry for every descendant as
// well. Descendants stop verifying regardless; see Store.Revoke.
CascadeChildren bool
}
RevokeRequest is the input shape for Store.Revoke.
type SetTenantBudgetRequest ¶
type SetTenantBudgetRequest struct {
TenantID uuid.UUID
// Ceiling is the tenant's new ceiling; the zero value freezes its
// spend, BudgetCeiling{Unlimited: true} lifts it.
Ceiling BudgetCeiling
// UnitCode pins the currency. Empty keeps the tenant's, or, for a
// tenant that has none yet, leaves it to the first charge or hold to
// pin.
UnitCode string
// PeriodEnd pins a closing time for the accounting window; zero leaves
// it open-ended (typical for "just set the cap, I'll roll later").
PeriodEnd time.Time
// ResetSpend=true zeroes the spend and rolls PeriodStart to
// now. false leaves the counter alone — the cap changes mid-
// window.
ResetSpend bool
// ExpectedVersion is the optimistic-concurrency guard, checked
// atomically with the write. 0 asserts no ceiling has been set yet (the
// budget is missing, or holds only counted spend) and is itself a
// conflict if one has, so two operators editing one ceiling cannot
// overwrite each other unseen.
ExpectedVersion int64
}
SetTenantBudgetRequest is the input for UsageStore.SetTenantBudget.
func (SetTenantBudgetRequest) Validate ¶ added in v0.39.0
func (r SetTenantBudgetRequest) Validate() error
Validate refuses a request with no tenant, a ceiling that does not validate, a unit that is not an ISO 4217 code, or a negative ExpectedVersion, as ErrInvalidRequest. A store calls it before reading the tenant's row, so a malformed request is refused as one, never as a version conflict.
type SettleRequest ¶
type SettleRequest struct {
// Subject is the capability settling, whose ceiling the charge meets
// (VerifiedCapability.MeterSubject). A reservation of another
// capability is ErrReservationNotFound.
Subject MeterSubject
ReservationID uuid.UUID
// Amount is the actual cost to charge. Within 0..MaxNanos (Nanos.Validate).
Amount Nanos
// Overrun is what happens when Amount would cross a ceiling.
Overrun OverrunPolicy
}
SettleRequest is the input to Meter.Settle.
func (SettleRequest) Validate ¶ added in v0.68.0
func (r SettleRequest) Validate() error
Validate refuses a subject that names no capability, no tenant or no unit, or an unknown unit, an amount out of range, and an unknown overrun policy. The zero ReservationID is not refused here: it names no reservation, which Settle reports as ErrReservationNotFound.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer holds the key an Issuer mints capability tokens with: it encodes a Capability as the token format and signs it with an Ed25519 key, which may be in memory or held in a KMS or an HSM behind crypto.Signer. It signs only for an Issuer (IssuerConfig.Signer). Verifiers pick up the public keys (statically or from a JWKS endpoint) and validate locally — no RPC per request.
func NewSigner ¶ added in v0.14.0
func NewSigner(cfg SignerConfig) (*Signer, error)
NewSigner builds a Signer, refusing an empty kid or a key that is not Ed25519.
type SignerConfig ¶ added in v0.14.0
type SignerConfig struct {
// KeyID is the kid tokens carry, by which verifiers find the key.
// Required.
KeyID string
// Key signs. Its public key must be Ed25519: an ed25519.PrivateKey, or
// a crypto.Signer over a key held elsewhere — one that also implements
// ContextSigner is given the request's context. Required.
Key crypto.Signer
}
SignerConfig configures a Signer.
type SigningKey ¶ added in v0.3.0
type SigningKey struct {
KeyID string
Private ed25519.PrivateKey
}
SigningKey is an Ed25519 key and the kid it is published under. It holds the private key alone, and Public derives the public half from it, so the two cannot disagree.
func GenerateKey ¶ added in v0.12.0
func GenerateKey() (SigningKey, error)
GenerateKey makes a fresh Ed25519 signing key under the kid jwks.KeyID derives for it. It lives only as long as the process unless saved.
func LoadKey ¶ added in v0.13.0
func LoadKey(path string) (SigningKey, error)
LoadKey reads the PKCS#8 PEM signing key at path, under the kid jwks.KeyID derives from it; a deployment that names its kids sets SigningKey.KeyID on the result. For a key that lives only as long as the process, GenerateKey; a deployment that may have no key file chooses between the two itself, so a missing mount is never silently a fresh key.
func (SigningKey) Format ¶ added in v0.65.0
func (k SigningKey) Format(f fmt.State, verb rune)
Format prints k with its private key redacted.
func (SigningKey) Public ¶ added in v0.3.0
func (k SigningKey) Public() ed25519.PublicKey
Public is the public half of Private; nil when Private is not an Ed25519 private key.
type Store ¶
type Store interface {
IssuanceStore
RecordReader
// Revoke adds an entry to the revocation list. Idempotent: a
// duplicate revoke is a no-op. An id not on record — or not visible
// to the caller — returns ErrNotFound and writes nothing, so a caller
// is never told "revoked" about a capability nothing stopped.
//
// Every descendant stops verifying either way, because IsRevoked
// walks the chain. CascadeChildren=true additionally writes a
// revocation entry for each descendant, so the audit trail names
// every capability that was stopped, not only the one an operator
// picked.
Revoke(ctx context.Context, req RevokeRequest) error
// Purge removes the capabilities expired for longer than the request's
// retention (PurgeRequest.Retention) — their records, their revocation
// entries and those of their Biscuit copies — at most req.Limit of
// them, and reports how many records it removed. A housekeeping worker
// calls it until a batch comes back short of the limit. The retention
// is how long an expired capability stays on record for audit, and
// never less than MinPurgeRetention: a verifier still accepts a token
// for up to its Leeway past expiry, and a revocation purged before
// that would let a revoked token through again.
//
// Purge never removes a capability while a child of it stays on record:
// a child expires no later than its parent (Insert refuses one that
// would not), and among records expiring together Purge takes the
// deepest first. The IsRevoked walk and the Meter's lineage, which stop
// at the first ancestor no longer on record, therefore miss no
// revocation and no ceiling. Once records go, UsageHousekeeping.PurgeOrphans
// releases what the meter still holds for them.
Purge(ctx context.Context, req PurgeRequest) (PurgeResult, error)
// ListByPrincipal lists the capabilities issued to one principal, in
// ascending id order, a page at a time: at most Limit of them, and the
// cursor of the next page, empty on the last. The cursor is the store's
// own and opaque.
ListByPrincipal(ctx context.Context, req ListByPrincipalRequest) (Page[Capability], error)
}
Store persists capability records and revocation entries. Consumers supply the implementation: memstore ships an in-memory one, and the module github.com/oleg-tkachuk/limes/pgstore one over PostgreSQL.
Records are append-mostly: capabilities aren't mutated after issuance except via Revoke. Revocations are checked by the verifier on every request; put a CachedRevocationChecker in front to keep the per-call cost flat.
type SumChargesRequest ¶ added in v0.61.0
type SumChargesRequest struct {
// TenantID is the tenant whose charges are summed. Required: a sum
// across tenants is not this request.
TenantID uuid.UUID
// From and To bound the period, From included, To not; From before To.
From, To time.Time
// Bucket splits the period at UTC boundaries; ChargeBucketNone sums it
// whole, and so does ChargeBucketUnspecified, which takes it.
Bucket ChargeBucket
// ByOp, ByActor and ByCapability group the sums by the charge's Op,
// Actor and capability as well.
ByOp, ByActor, ByCapability bool
// Limit is the page size; see PageLimit. Negative is refused.
Limit int
// Cursor is the one the previous page returned; empty for the first.
Cursor string
}
SumChargesRequest asks for one page of a tenant's charge sums.
func (SumChargesRequest) Validate ¶ added in v0.61.0
func (r SumChargesRequest) Validate() error
Validate refuses a request with no tenant, a period that is empty or unbounded, a bucket this module does not define, or a negative page size.
type TenantBudget ¶
type TenantBudget struct {
TenantID uuid.UUID
// Ceiling bounds the tenant's spend in the current period. A tenant
// whose ceiling was never set counts Unlimited.
Ceiling BudgetCeiling
SpentAmount Nanos
// ReservedAmount is held by the tenant's open reservations.
ReservedAmount Nanos
UnitCode string
PeriodStart time.Time
// PeriodEnd is when the accounting window closes; zero for an
// open-ended one.
PeriodEnd time.Time
// UpdatedAt is when the store last wrote the aggregate, by its clock.
UpdatedAt time.Time
// ResourceVersion is the OCC token a caller passes back as
// SetTenantBudgetRequest.ExpectedVersion. Zero means no ceiling has
// ever been set — the row, if there is one, holds only what Charge and
// Reserve counted.
ResourceVersion int64
// contains filtered or unexported fields
}
TenantBudget is the snapshot view of the tenant aggregate cap. Amounts are Nanos, exact; a store keeps them exactly too (an integer or decimal column), so that sums do not drift.
func (TenantBudget) Utilization ¶ added in v0.19.0
func (b TenantBudget) Utilization() (pct float64, limited bool)
Utilization is how much of the ceiling is spent, in percent, clamped to 0..100: a tenant past its ceiling, or with a ceiling of 0, is at 100. limited is false for an unlimited tenant, which has no utilization to report; pct is then 0.
type TenantBudgetFilter ¶ added in v0.47.0
type TenantBudgetFilter uint8
TenantBudgetFilter picks the rows ListTenantBudgets lists.
const ( // TenantBudgetFilterUnspecified takes the default, TenantBudgetFilterAll: a // request that names no filter lists every tenant. TenantBudgetFilterUnspecified TenantBudgetFilter = iota // TenantBudgetFilterAll lists every tenant: no filter. TenantBudgetFilterAll // TenantBudgetFilterAtOrPast lists tenants with a ceiling spent to // ListTenantBudgetsRequest.ThresholdPct or past it. TenantBudgetFilterAtOrPast // TenantBudgetFilterUnlimited lists tenants with no ceiling. TenantBudgetFilterUnlimited )
func (TenantBudgetFilter) String ¶ added in v0.47.0
func (f TenantBudgetFilter) String() string
String names the filter for logs and errors.
func (TenantBudgetFilter) Validate ¶ added in v0.47.0
func (f TenantBudgetFilter) Validate() error
Validate refuses a filter this package does not define, as ErrInvalidRequest.
type TenantBudgetLister ¶ added in v0.2.0
type TenantBudgetLister interface {
// ListTenantBudgets returns one page (TenantBudgets.ListTenantBudgets).
ListTenantBudgets(ctx context.Context, req ListTenantBudgetsRequest) (Page[TenantBudget], error)
}
TenantBudgetLister lists tenant budgets a page at a time. Every TenantBudgets is one.
type TenantBudgets ¶
type TenantBudgets interface {
// GetTenantBudget returns the tenant-aggregate snapshot. Returns
// ErrTenantBudgetNotFound when no row exists (tenant has no cap
// configured AND has never been charged).
//
// The aggregate counts from the tenant's first charge or reservation,
// whether or not a ceiling has been set: Charge and Reserve create the
// row, with no ceiling and ResourceVersion 0, when it is missing. A
// ceiling set afterwards is measured against that spend and those holds.
GetTenantBudget(ctx context.Context, tenantID uuid.UUID) (TenantBudget, error)
// SetTenantBudget upserts the tenant cap. ResetSpend=true rolls
// the accounting period and zeroes the spend — operators call
// this on each billing close. ResetSpend=false adjusts the cap
// mid-cycle without affecting accumulated spend. It refuses what
// SetTenantBudgetRequest.Validate refuses — a ceiling outside
// 0..MaxNanos is ErrInvalidAmount — and changes nothing. A UnitCode
// other than the row's returns ErrUnitCodeMismatch while anything is
// held in the old unit, or spent in it and not reset.
SetTenantBudget(ctx context.Context, req SetTenantBudgetRequest) (TenantBudget, error)
// ListTenantBudgets returns tenant budget rows; a consumer joins its
// own tenant data to them by TenantID. Filter picks the rows:
// - TenantBudgetFilterUnspecified, TenantBudgetFilterAll: every one.
// - TenantBudgetFilterAtOrPast: rows with a ceiling — a bound or 0 — where
// spent / max * 100 >= ThresholdPct, exactly; a ceiling of 0 is
// spent through at any threshold. At a threshold of 0, every row
// with a ceiling.
// - TenantBudgetFilterUnlimited: rows whose ceiling is Unlimited.
// Limit is the page size, as PageLimit reads it.
// It refuses what ListTenantBudgetsRequest.Validate refuses.
//
// Rows come most at risk first. Ceilings of 0, which allow nothing,
// lead; bounded ceilings follow by spent / max descending, compared
// exactly as fractions — unclamped, so of two tenants past their
// ceilings the further one leads; unlimited ceilings, with no utilization, trail. Within each,
// ties go by tenant id. A page ends with a cursor that fetches
// the next one, empty on the last page. Spend moves between pages, so
// a tenant may show on two of them, or on none; a cursor is a place in
// the order, not a snapshot.
ListTenantBudgets(ctx context.Context, req ListTenantBudgetsRequest) (Page[TenantBudget], error)
}
TenantBudgets administers the tenant-aggregate ceilings Charge enforces.
type TenantRecordEraser ¶ added in v0.60.0
type TenantRecordEraser interface {
// EraseTenantRecords revokes every capability of the tenant not revoked
// yet, so no token of the tenant verifies again, and removes those of
// its records Purge would remove at MinPurgeRetention — each with its
// revocation entry and its Biscuit copies' — and no other: an id not on
// record is not revoked, so a record removed while its token may still
// verify would let that token through again. Purge takes the rest as
// it expires; a capability with no expiry stays on record, revoked. It
// changes no other tenant's records, and a second call revokes and
// removes nothing the first left.
EraseTenantRecords(ctx context.Context, req EraseTenantRequest) (EraseTenantResult, error)
}
TenantRecordEraser is a Store that erases one tenant's capability records, for a consumer removing the tenant. It is optional; storetest checks a Store that implements it.
type TenantUsageEraser ¶ added in v0.60.0
type TenantUsageEraser interface {
// EraseTenantUsage removes every counter, hold and charge of the
// tenant's capabilities and its Biscuit copies, and the tenant's
// aggregate: the charges ledger is the tenant's billing history, and
// it does not come back. It changes no other tenant's rows.
EraseTenantUsage(ctx context.Context, req EraseTenantUsageRequest) error
}
TenantUsageEraser is a UsageStore that erases one tenant's usage, for a consumer removing the tenant. It is optional; metertest checks a UsageStore that implements it.
type TokenType ¶
type TokenType string
TokenType is a token format, as the JWT "typ" header names it.
const TokenTypeV4 TokenType = "limes-cap-v4+jwt" //nolint:gosec // G101: a JWT type label, not a credential
TokenTypeV4 is the JWT "typ" header of the current token format, which every Signer of this module mints. The verifier requires a type it accepts (VerifierConfig.AcceptTokenTypes), so a different kind of EdDSA JWT signed by the same key — an ID token, another product's token — cannot be presented as a capability. It names the format's version: a token of another version is refused before a claim of it is read under the wrong schema.
v4 spells every ceiling out: an absent max_requests or max_budget is a ceiling of 0, and only unlimited_requests or unlimited_budget lifts it. v3, which read an absent bound as none, is not accepted.
type Usage ¶
type Usage struct {
CapabilityID uuid.UUID
RequestCount int64
SpentAmount Nanos
// ReservedAmount is held by open reservations (its subtree's, for a
// capability with delegated children).
ReservedAmount Nanos
// UnitCode is the unit the counters count in: the subject's, recorded
// by the first Bump, Charge or Reserve of a subject that names one, so
// a capability that has only been bumped reads its unit too. Empty
// while none has.
UnitCode string
// contains filtered or unexported fields
}
Usage is the snapshot view of a capability's runtime counters.
type UsageHousekeeping ¶
type UsageHousekeeping interface {
// DeleteUsage drops the capability's row and the counters of its Biscuit
// copies. Called when the capability is reaped. It releases the
// capability's open reservations first,
// returning their holds to its ancestors and its tenant: nothing could
// settle or release them afterwards. A capability with no row is no
// error: there is nothing to drop.
DeleteUsage(ctx context.Context, req DeleteUsageRequest) error
// PurgeOrphans deletes the usage rows of capabilities no longer on
// record (Store.Purge), with their copies' counters, releasing their
// reservations as DeleteUsage does, at
// most req.Limit of them; the caller loops while a batch comes back
// full.
PurgeOrphans(ctx context.Context, req PurgeOrphansRequest) (PurgeOrphansResult, error)
// ReleaseExpired releases the reservations past their expiry, at most
// req.Limit of them. Run on a timer: until it runs, an expired hold
// keeps counting against the ceilings, which errs towards refusing
// spend, never towards allowing too much.
ReleaseExpired(ctx context.Context, req ReleaseExpiredRequest) (ReleaseExpiredResult, error)
}
UsageHousekeeping reclaims usage rows of capabilities that are gone.
type UsageStore ¶
type UsageStore[TX any] interface { Meter[TX] MeterReader TenantBudgets UsageHousekeeping }
UsageStore is everything a consumer implements for runtime accounting. It is the union of narrower contracts so that code which only meters (an interceptor, a handler) can depend on Meter alone, a reporter on MeterReader, and code which only administers tenant ceilings on TenantBudgets.
func Instrument ¶ added in v0.12.0
func Instrument[TX any](inner UsageStore[TX], opts ...InstrumentOption) (UsageStore[TX], error)
Instrument wraps a UsageStore in OpenTelemetry instruments. Its instruments are created on the global MeterProvider unless WithMeterProvider names another, and carry no tenant unless WithTenantAttribute asks for it.
The result implements every interface of this package that inner does — the optional CopyUsageReader and TenantUsageEraser — so wrapping a store never hides what it can do. A decorator forwards a fixed list of optional interfaces, and a test fails when the list misses one the store has.
type VerifiedCapability ¶ added in v0.14.0
type VerifiedCapability struct {
// contains filtered or unexported fields
}
VerifiedCapability is a capability a Verifier accepted. Only Verify and Authorize make one, so a function that takes it — Issuer.Delegate — knows the token it came from was checked, rather than trusting a caller to have checked it.
func (VerifiedCapability) Capability ¶ added in v0.14.0
func (v VerifiedCapability) Capability() Capability
Capability is what the token grants: a copy, the caller's to change.
func (VerifiedCapability) FromBiscuit ¶ added in v0.25.0
func (v VerifiedCapability) FromBiscuit() bool
FromBiscuit reports whether the capability was read from a copy of a Biscuit rather than from a JWT. Issuer.Delegate and Issuer.Biscuit take only the latter.
func (VerifiedCapability) MeterSubject ¶ added in v0.17.0
func (v VerifiedCapability) MeterSubject() MeterSubject
MeterSubject is what the Meter counts this capability's requests against.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier is the production verifier — signature first, then claims, then issuer / tenant / time / audience / revocation gates. A deployment typically resolves keys through a cached JWKS (jwks.Remote) and checks revocation through a short-TTL cache (CachedRevocationChecker), so revocation propagates quickly without hammering the store. A consumer that mocks it declares the narrow interface it needs.
Verification is independent of the persistence layer apart from the revocation lookup, so the verifier scales horizontally without contention on the capability records.
func NewVerifier ¶ added in v0.36.0
func NewVerifier(cfg VerifierConfig) (*Verifier, error)
NewVerifier validates wiring and builds the verifier.
func (*Verifier) Authorize ¶ added in v0.3.0
func (v *Verifier) Authorize(ctx context.Context, req AuthorizeRequest) (VerifiedCapability, error)
Authorize is Verify followed by every caveat that can be checked without state: the operation, the resource, the idempotency key, the tainted read (Caveats.Check), and the client's address (Caveats.CheckSource). The request and budget limits are stateful: the Meter enforces them, through Bump and Charge, inside the consumer's own transaction.
Example ¶
package main
import (
"context"
"crypto/ed25519"
"errors"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/oleg-tkachuk/limes"
"github.com/oleg-tkachuk/limes/dpop"
"github.com/oleg-tkachuk/limes/jwks"
"github.com/oleg-tkachuk/limes/memstore"
)
// The issuer's name, and the audience every example's tokens are minted for.
const (
exampleIssuerName = "example-issuer"
exampleAudience = "my-service"
)
// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
issuer *limes.Issuer
verifier *limes.Verifier
records *memstore.Store
usage *memstore.UsageStore[struct{}]
tenantID uuid.UUID
}
func newExampleStack() exampleStack {
key, err := limes.GenerateKey()
kid, pub, priv := key.KeyID, key.Public(), key.Private
if err != nil {
log.Fatal(err)
}
records, err := memstore.NewStore(memstore.StoreConfig{})
if err != nil {
log.Fatal(err)
}
usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
if err != nil {
log.Fatal(err)
}
signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
if err != nil {
log.Fatal(err)
}
issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
if err != nil {
log.Fatal(err)
}
replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
if err != nil {
log.Fatal(err)
}
proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
if err != nil {
log.Fatal(err)
}
verifier, err := limes.NewVerifier(limes.VerifierConfig{
Keys: exampleKeys(kid, pub),
Revocations: records,
TrustedIssuers: []string{exampleIssuerName},
AcceptBiscuit: true,
BiscuitRevocations: records,
DPoP: proofs,
})
if err != nil {
log.Fatal(err)
}
return exampleStack{
issuer: issuer, verifier: verifier, records: records,
usage: usage, tenantID: uuid.New(),
}
}
// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
if err != nil {
log.Fatal(err)
}
return vc
}
// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
IssuedBy: limes.Principal{Subject: "operator@example.com"},
Subject: limes.Principal{
Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
},
Audience: []string{exampleAudience},
TTL: 15 * time.Minute,
Caveats: limes.Caveats{
Ops: []limes.Op{limes.OpGet, limes.OpShare},
ResourcePrefixes: []string{"corpus/public/"},
Requests: limes.RequestCeiling{Max: 500},
Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("25.00")},
UnitCode: "USD",
},
})
c, token := cResult.Capability, cResult.Token
if err != nil {
log.Fatal(err)
}
return c, token
}
func main() {
ctx := context.Background()
s := newExampleStack()
_, token := s.issue()
// One call: the token, then every caveat that needs no state.
authorize := func(op limes.Op, resource string) error {
_, err := s.verifier.Authorize(ctx, limes.AuthorizeRequest{
Verify: limes.VerifyRequest{Token: token, Audience: exampleAudience},
Check: limes.CheckRequest{Op: op, Resource: resource},
})
return err
}
fmt.Println(authorize(limes.OpGet, "corpus/public/report.pdf"))
fmt.Println(errors.Is(authorize(limes.OpDelete, "corpus/public/report.pdf"), limes.ErrOpNotAllowed))
fmt.Println(errors.Is(authorize(limes.OpGet, "corpus/private/x"), limes.ErrResourceNotAllowed))
}
// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
if err != nil {
log.Fatal(err)
}
return keys
}
Output: <nil> true true
func (*Verifier) BiscuitCopy ¶ added in v0.36.0
BiscuitCopy reads which copy token is, after checking it as Verify does — the sealed token's signature, issuer and tenant, then the Biscuit's signature chain — so that a revocation is only ever recorded against a capability the token genuinely carries. Expiry, audience and revocation are not checked: a copy can be revoked whatever its state. Nor is MeterCopies: a copy carrying limits is named, and can be revoked, on any verifier.
func (*Verifier) DPoPNonce ¶ added in v0.36.0
DPoPNonce is the nonce to send in the DPoP-Nonce header — with a response refused by dpop.ErrNonceRequired, and with any other for the client's next proof. Empty when the verifier issues none.
func (*Verifier) ErrorResponse ¶ added in v0.36.0
func (v *Verifier) ErrorResponse(ctx context.Context, err error, scheme AuthScheme) ErrorResponse
ErrorResponse is how to answer a request refused with err, presented under scheme. A refusal about the token is challenged under the scheme the client used (Bearer when it named none); one about the proof, under DPoP. A verifier with DPoP configured also advertises its algorithms in a DPoP challenge, and hands out a fresh nonce with use_dpop_nonce.
func (*Verifier) Verify ¶
func (v *Verifier) Verify(ctx context.Context, req VerifyRequest) (VerifiedCapability, error)
Verify checks the token itself: its signature, issuer and tenant, its validity window, its audience, the proof of possession a key-bound capability needs, and revocation. It returns the capability the token grants; a refusal is one of the package's sentinels, so the caller can branch on the failure mode.
Order matters: nothing in the claims is read until the signature over them has verified, so a forged JWT costs a header decode and one signature check, and no claim-parsing path is reachable by an attacker. A Biscuit is read further before its signatures are checked: its container and every block are decoded and their facts read as data, which MaxTokenBytes and MaxBiscuitBlocks bound; none of it is trusted until the signatures have verified. The checks that need no I/O come before the revocation lookup — the proof of possession among them, so a holder of a key-bound token without its key learns nothing of the token's revocation and causes no lookup — and the proof's id is recorded in the replay cache after it, so a revoked token never records one.
type VerifierConfig ¶
type VerifierConfig struct {
// Keys resolves JWKS kids to Ed25519 public keys.
Keys KeyResolver
// Revocations answers whether a capability — or any ancestor in its
// delegation chain — is revoked. Wrap the store in a
// CachedRevocationChecker to keep the per-call cost flat.
Revocations RevocationLookup
// TrustedIssuers is the set of issuer values the verifier accepts.
// Tokens whose `iss` claim is not in the set are rejected with
// ErrInvalidToken.
TrustedIssuers []string
// KeyIssuers binds each kid to the one issuer allowed to sign with it:
// a token whose kid is absent from the map, or whose `iss` differs from
// the kid's binding, is rejected. Empty binds no kid: enough with one
// trusted issuer. With several it is required, unless SharedKeys says otherwise:
// any trusted key signing for any trusted issuer is wrong for issuers
// that do not share a trust boundary, so it is never the default.
KeyIssuers map[string]string
// several issuers that do share one trust boundary. It and KeyIssuers
// exclude each other.
SharedKeys bool
// Now is the time function the verifier uses for nbf/exp. Tests
// override it; production passes time.Now.
Now func() time.Time
// Leeway widens the nbf / exp window to absorb clock skew between
// issuer and verifier. Zero: DefaultLeeway; negative or past MaxLeeway
// is refused.
Leeway time.Duration
// NoLeeway holds nbf and exp to the verifier's clock exactly, for an
// issuer that shares it. It excludes a Leeway.
NoLeeway bool
// MaxTokenBytes rejects longer tokens before decoding them. Default
// DefaultMaxTokenBytes when zero; negative is refused.
MaxTokenBytes int
// AcceptBiscuit accepts the Biscuit form (Issuer.Biscuit, Attenuate)
// beside the JWT. Off, a Biscuit is refused like any malformed token.
AcceptBiscuit bool
// MeterCopies admits Biscuits whose attenuation blocks set a copy's own
// request or budget limits (Attenuation.Requests, Attenuation.Budget).
// Set it only when the Meter that enforces caveats counts
// the copies a MeterSubject carries; off, such a token is refused with
// ErrCopyCountersNotMetered rather than accepted with its limits unkept
// — once it has passed every other check, so a token refused for what
// it is gets that refusal instead.
MeterCopies bool
// BiscuitRevocations answers whether a copy of a Biscuit is revoked on
// its own. Required when AcceptBiscuit is set; wrap the store in a
// CachedBiscuitRevocationChecker.
BiscuitRevocations BiscuitRevocationLookup
// AcceptTokenTypes are the token formats (the JWT "typ") the verifier
// accepts. Empty: TokenTypeV4. Every 1.x verifier accepts TokenTypeV4,
// so a later format ships in a minor release: verifiers accept it
// first, issuers mint it after. A type this package has no schema for
// is refused: a verifier cannot enforce a format it cannot read.
AcceptTokenTypes []TokenType
// DPoP checks the proof of possession a key-bound capability (one with
// a ConfirmationJKT) must come with. Without it the verifier refuses
// every key-bound capability with ErrProofUnsupported, rather than accept
// one as a bearer token. Built by dpop.NewVerifier, with its own clock.
DPoP *dpop.Verifier
// Realm names the protection space in every WWW-Authenticate challenge
// ErrorResponse gives (RFC 9110 §11.5). Empty: DefaultRealm. Only the
// characters RFC 6750 §3 allows in an auth-param value.
Realm string
}
VerifierConfig wires the verifier. Keys, Revocations and TrustedIssuers are required.
type VerifyRequest ¶ added in v0.3.0
type VerifyRequest struct {
// Token is the capability token, a JWT or a Biscuit.
Token string
// Audience is the service the call is hitting: the token's audience
// must include it.
Audience string
// DPoP is the request's proof of possession, needed when the
// capability is key-bound and ignored when it is not.
DPoP dpop.Proof
// Scheme is the Authorization scheme the token came under
// (ParseAuthorization). Set, it must match the binding: DPoP for a
// key-bound capability, Bearer for any other.
Scheme AuthScheme
}
VerifyRequest is a token as it arrived, and what it arrived for.
Source Files
¶
- authorization.go
- biscuit.go
- biscuit_revocation.go
- cache.go
- ceiling.go
- census.go
- challenge.go
- check.go
- delegate.go
- doc.go
- effect.go
- erase_tenant.go
- issuer.go
- keyloader.go
- ledger.go
- meter_subject.go
- metering_store.go
- metrics.go
- nanos.go
- pages.go
- reason.go
- redact.go
- resource.go
- signer.go
- store.go
- types.go
- usage.go
- validate.go
- verifier.go
- wire.go
Directories
¶
| Path | Synopsis |
|---|---|
|
biscuit
module
|
|
|
Package dpop implements proof of possession after RFC 9449 (DPoP): a client signs a short JWT over each request with a key it holds, and a server checks the proof against the key a token is bound to.
|
Package dpop implements proof of possession after RFC 9449 (DPoP): a client signs a short JWT over each request with a key it holds, and a server checks the proof against the key a token is bound to. |
|
dpoptest
Package dpoptest checks a dpop.ReplayCache and a dpop.Nonces against the package's contract, the way storetest checks a limes.Store.
|
Package dpoptest checks a dpop.ReplayCache and a dpop.Nonces against the package's contract, the way storetest checks a limes.Store. |
|
exchange
module
|
|
|
Package inspect reads a capability token for tooling — a CLI that shows a token, a debugger, a log viewer — without verifying it.
|
Package inspect reads a capability token for tooling — a CLI that shows a token, a debugger, a log viewer — without verifying it. |
|
internal
|
|
|
fields
Package fields lets the contract suites, storetest and metertest, prove that what they write and read back covers every field of the module's types.
|
Package fields lets the contract suites, storetest and metertest, prove that what they write and read back covers every field of the module's types. |
|
httpx
Package httpx names the HTTP header fields and Cache-Control directives the module's packages write and read, so each is spelt in one place.
|
Package httpx names the HTTP header fields and Cache-Control directives the module's packages write and read, so each is spelt in one place. |
|
jose
Package jose holds the JOSE pieces the module's packages share: the algorithm and key identifiers, base64url, the compact serialisation, and the public JWK with its RFC 7638 thumbprint.
|
Package jose holds the JOSE pieces the module's packages share: the algorithm and key identifiers, base64url, the compact serialisation, and the public JWK with its RFC 7638 thumbprint. |
|
link
Package link lets the module's own packages reach what the root does not export: limes/inspect the token reader, and metertest, storetest, memstore and the tests of memstore and pgstore the constructors of a MeterSubject and a BiscuitCopy.
|
Package link lets the module's own packages reach what the root does not export: limes/inspect the token reader, and metertest, storetest, memstore and the tests of memstore and pgstore the constructors of a MeterSubject and a BiscuitCopy. |
|
rfc3986
Package rfc3986 normalises a URI the way RFC 3986 §6.2.2 and §6.2.3 say, for comparing two spellings of one request URI.
|
Package rfc3986 normalises a URI the way RFC 3986 §6.2.2 and §6.2.3 say, for comparing two spellings of one request URI. |
|
watchdog
Package watchdog bounds one check of a contract suite.
|
Package watchdog bounds one check of a contract suite. |
|
Package jwks publishes and resolves the Ed25519 keys capability tokens are signed with, as an RFC 7517 JSON Web Key Set.
|
Package jwks publishes and resolves the Ed25519 keys capability tokens are signed with, as an RFC 7517 JSON Web Key Set. |
|
jwkstest
Package jwkstest checks a limes.KeyResolver against the module's contract, the way storetest checks a limes.Store.
|
Package jwkstest checks a limes.KeyResolver against the module's contract, the way storetest checks a limes.Store. |
|
Package memstore is an in-memory reference implementation of the limes storage contracts.
|
Package memstore is an in-memory reference implementation of the limes storage contracts. |
|
Package metertest checks a limes.UsageStore against the module's contract — its Meter, tenant budgets and housekeeping — the way testing/fstest checks an fs.FS.
|
Package metertest checks a limes.UsageStore against the module's contract — its Meter, tenant budgets and housekeeping — the way testing/fstest checks an fs.FS. |
|
otelmetric
module
|
|
|
pgstore
module
|
|
|
redisreplay
module
|
|
|
Package storetest checks a limes.Store — and, when it is one, a limes.BiscuitRevocationStore — against the module's contract, the way testing/fstest checks an fs.FS.
|
Package storetest checks a limes.Store — and, when it is one, a limes.BiscuitRevocationStore — against the module's contract, the way testing/fstest checks an fs.FS. |