authkit

package module
v0.130.1 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 11 Imported by: 0

README

AuthKit

Embedded auth library for Go services: users, sessions, MFA, passkeys, device keys, OAuth/OIDC and Solana login, RBAC permission groups, API keys, signed documents and delegated tokens, running in your process against your Postgres (18+) and Redis. Tests exercise the embedded HTTP handlers directly; AuthKit owns its PostgreSQL migration source and runs it through migratekit.

One module, github.com/open-rails/authkit, includes the core and every adapter. One root release tag versions them together; adapter import paths are unchanged. Framework dependencies enter an application's build only when it imports the corresponding adapter. The embedded engine uses River for PostgreSQL maintenance; the root and verify packages remain engine-free.

For local tests, run scripts/check.sh. Applications call embedded.ApplyMigrations with migration credentials before constructing the engine, then call runtime.Start(ctx) before serving and runtime.Close() at shutdown.

See verification trust and key ownership for local versus external identity, application delegation boundaries, and key rotation. See authentication workflows for first-factor continuations, atomic registration, and workflow test coverage.

Redis-compatible stores must support atomic GETDEL and atomic Lua (EVAL/EVALSHA); proof claims and counters depend on those guarantees. For Garnet, enable both --lua true and --lua-transaction-mode true. Garnet's configuration reference describes the transaction mode that locks script keys for execution.

Migrations

import (
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/open-rails/authkit/embedded"
)

ownerPool, _ := pgxpool.New(ctx, migrationDSN)
runtimePool, _ := pgxpool.New(ctx, applicationDSN)
err := embedded.ApplyMigrations(ctx, ownerPool, "profiles", embedded.MigrationOptions{
	RuntimePool: runtimePool,
})
// Pass runtimePool to embedded.Deps{Postgres: runtimePool} when constructing the runtime.

AuthKit owns the embedded migration source, migratekit runner, migration ledger and target schema creation. The call is idempotent and must complete before embedded.New; consumers do not import AuthKit migrations or migratekit. Pre-v1 schemas must be rebuilt for the fresh baseline; AuthKit never drops existing application data automatically.

RuntimePool identifies the application's existing database user through its active connection. Both pools must connect to the same database. Initialization grants that user AuthKit's schema, table, sequence and function permissions directly, plus the runtime objects of managed River. AuthKit creates no database roles or memberships, and the host needs no AuthKit-specific GRANT script. The same normal application login and pool can serve other embedded libraries. Both pools remain host-owned. Omit RuntimePool for migration-only setup with access provisioned separately; runtime credentials never need migration rights.

PostgreSQL maintenance

AuthKit runs CleanupExpiredAuthState through River on startup and hourly. It removes expired sessions, terminal credentials and expired retained history; Redis and in-memory TTL state keep their local expiry behavior. AuthKit also owns the fixed 30-day recoverable account deletion lifecycle and its durable application callbacks. There is no separate purge adapter to register.

With no River dependency supplied, ApplyMigrations also applies River's own migrations to public. New constructs an owned worker client without starting it or running DDL. Call runtime.Start(ctx) before serving; runtime.Close() cancels its workers and releases only AuthKit-owned resources. Runtime pools need data access, while initialization uses separate migration credentials. Config.River.Schema and MigrationOptions.RiverSchema select a custom managed River schema; Config.River.CleanupInterval defaults to one hour.

Applications sharing River with other libraries compose one worker configuration:

ownership := embedded.RiverFromHost()
err := embedded.ApplyMigrations(ctx, ownerPool, "profiles", embedded.MigrationOptions{
	River: ownership, RuntimePool: runtimePool,
})
// The host initializes its River schema through River's migrator.
runtime, err := embedded.New(cfg, embedded.Deps{Postgres: runtimePool, Redis: rdb, River: ownership})
jobs, err := riverhelpers.New(ctx, runtimePool, &river.Config{Schema: "public"},
    runtime.RiverJobs(), billing.RiverJobs())
err = runtime.Start(ctx) // checks composition; never starts the host client
err = jobs.Start(ctx)
// On shutdown: stop jobs before runtime.Close().

RiverJobs contributes AuthKit's worker, queue and periodic schedule to the neutral github.com/open-rails/helpers/river composer. Producer binding happens inside composition; no per-library binding call or hand-written host cron is needed. The passed host configuration owns the River schema. The registry supports one AuthKit engine; duplicate registration fails explicitly. With RiverFromHost, the host also owns River database permissions; AuthKit provisions only its identity schema, leaving the shared fleet's access unchanged.

Every replica sharing a River schema must carry the same complete periodic schedule set. River's elected leader alone schedules periodic jobs. Separate managed AuthKit and OpenRails clients with different schedules in the same public fleet can starve each other's maintenance. Use the composed host client above, or explicitly separate their River schemas. A managed AuthKit fleet is appropriate when its replicas all run the same AuthKit workers and schedules.

Construction

embedded.New(cfg, deps) returns the local *embedded.Runtime, which owns pools, keys, River and lifecycle. runtime.Client() returns the engine-free authkit.Client operation view. That view does not expose local configuration, bootstrap or resource access. Creating it starts no additional engine.

Set HTTP policy in the runtime constructor, then obtain and mount its routes:

cfg.HTTP = authhttp.Config{
    TrustedProxies: []string{"10.0.0.0/8"}, // or DirectPeerIP when no proxy is present
    Mount: authhttp.MountOptions{APIPrefix: "/api/v1", RefreshCookie: true},
}
runtime, err := embedded.New(cfg, embedded.Deps{Postgres: pg, Redis: rdb, Email: mailer})
if err != nil {
    return err
}
defer runtime.Close()
client := runtime.Client() // application user/group/token operations

routes, err := authkitgin.Routes(runtime)
if err != nil {
    return err
}
router := gin.New()
if err := routes.Mount(router); err != nil {
    return err
}
// Compose runtime.RiverJobs() with the host fleet, or start managed workers.
// Application middleware uses runtime.Verifier(); domain code uses client.

Use the same pattern with authkitfiber.Routes(runtime).Mount(app) or authkithttp.Routes(runtime).Mount(mux) for a standard http.ServeMux or Chi router. Handle the error returned by Routes before calling Mount. The net/http, Gin, and Fiber adapters all ship in the root module.

Prefer Config.HTTP for policy known at construction. ConfigureHTTP supports provisioning dependencies that become available later and is one-shot. A failed build consumes the attempt and closes partial HTTP resources; the operation client remains available. Calling HTTPRoutes before configuration seals HTTP disabled and returns an error. Configure before obtaining route bundles; configuration after Close is refused. The runtime closes its HTTP resources before its engine resources.

The HTTP policy chooses groups, API prefix, exclusions, wrappers and refresh cookies once through authhttp.Config.Mount. The runtime derives concrete routes from that policy and enabled identity features. Native route inspection shows the actual inventory. JWKS remains at /.well-known/jwks.json, browser OIDC under /oidc, and published documents at their standard root path; mount AuthKit on the host root router. No catch-all is installed.

embedded.New initializes explicitly declared group containment and the root singleton in one transaction. Omitted or empty RBAC leaves shared topology intact. Apply migrations before constructing a database-backed runtime. Construction never grants user roles or restores revoked permissions. Use client.OperatorAssignGroupRole and client.OperatorUnassignGroupRole for explicit trusted operator commands; request paths use the actor-checked *As methods. Operator describes the host's authority; it is not a built-in persona or role.

The runtime wraps a private engine and exposes only lifecycle, route, verifier, job and construction dependencies. It has no public business methods, Genesis, database, configuration or signer accessors. The HTTP transport receives its local engine capability only while the runtime constructs it. This release adds no remote AuthKit client or standalone service.

Native user JWTs establish identity; group memberships, roles and permissions are always resolved live when a route requires permission. Native tokens do not carry permission authority. The experimental RootPermissionSnapshot API has been removed. Machine and delegated credentials retain their separate verified permission ceilings and scope bindings.

Bans prevent login and refresh. An existing native identity JWT remains valid until expiry (15 minutes by default), including on a permission route if its current grant remains assigned. Revoking a role takes effect immediately at the next permission check. Account liveness can still be explicitly requested with RequiredLive, OptionalLive, or IsLive; it is not automatically added to admin routes. Ownership mutations retain their current valid-owner invariants.

Select optional coarse entitlement claims explicitly:

embedded.TokenConfig{EntitlementAllowlist: []string{"premium"}}

Only names actually granted by the entitlement provider are included. Empty configuration skips that provider lookup during minting and omits the claim; directory/admin provider results remain unfiltered. The allowlist is limited to 32 distinct names, 128 UTF-8 bytes per name and 2048 encoded JSON bytes. Provider failure also omits the claim while allowing login; omission is not a successful empty-grant lookup. These are token-time billing snapshots until refresh, not live permission checks. Per-product ownership belongs in the billing query API.

Verification in a host

runtime.Verifier() is a *verify.Verifier; verify imports no Postgres or Redis, so a pure resource server depends on it alone. verify.Required/Optional and their authkitgin and authkitfiber equivalents put verify.Claims in the request context. Optional permits a missing credential but rejects a present invalid credential; it never downgrades an invalid token to anonymous access. Required accepts any supported principal, including machine principals, so user-only handlers must also check the result of UserClaims (or claims.IsUser() with net/http).

RequirePermission resolves the group name once and authorizes the immutable UUID: a user is checked live against GroupID, a group-bound API key must match the scope, an unbound delegated token is authorized from its own permissions. AuthorityIssuer is this deployment's Token.Issuer; verify.PermissionScopeFromContext hands the handler the authorized scope.

For a consumer's provider-neutral interface, the same verifier implements AuthenticateRequest(context.Context, *http.Request) (auth.Principal, error) using github.com/open-rails/helpers/auth. The result exposes immutable identity metadata and optional auth.PermissionChecker access. Can checks a host-resolved auth.Scope{Authority: issuer, ID: immutableGroupID} and permission against live native assignments or the verified machine credential's exact scope and ceiling. The runtime wires its native checker automatically. Verify-only hosts can use WithPermissionChecker(client, authorityIssuer). Neither identity nor scope selects an application's billing account or grants permission by itself.

AuthenticateRequestLive explicitly applies immediate account liveness; the ordinary method retains the stateless user-session policy. Hosts that already verified the request with trusted middleware may explicitly call PrincipalFromVerifiedClaims after their admission policy. Those claims must come from complete verification of this same unchanged request under the host's intended issuer, audience, assurance and sender-proof policy. This handoff avoids consuming a single-use DPoP proof twice. Ordinary authentication never trusts ambient context claims. Retain the resulting principal only for that request.

Fiber v3

Install AuthKit at the chosen root version, then import github.com/open-rails/authkit/adapters/fiber. See the single-module upgrade instructions if the application previously required an adapter module.

The middleware and typed accessors mirror the Gin adapter. Configure the local runtime once as above, then register its inventory:

routes, err := authkitfiber.Routes(runtime)
if err != nil {
    return err
}
app := fiber.New()
app.Get("/api/me", authkitfiber.Required(runtime.Verifier()), func(c fiber.Ctx) error {
    user, ok := authkitfiber.UserClaims(c)
    if !ok {
        return fiber.ErrUnauthorized
    }
    return c.JSON(fiber.Map{"user_id": user.UserID})
})
if err := routes.Mount(app); err != nil {
    return err
}

All installed endpoints appear in app.GetRoutes(true), named with authkitfiber.RouteNamePrefix. Mount takes the root *fiber.App; HTTP policy was already supplied to ConfigureHTTP. Exact method/path conflicts, unsupported patterns and disabled HTTP methods are rejected before registration. Put host catch-alls after mounting. Unmatched requests follow Fiber's native routing.

Claims(c) returns all verified claims, UserClaims(c) returns only user claims, and Principal(c) exposes the authenticated principal. They read the standard context available through c.Context(), so downstream Go services can also use verify.ClaimsFromContext(c.Context()). RequiredLive adds the same live account checks as the Gin and net/http middleware; its constructor returns an error when the verifier has no liveness source. RequirePermission applies the same permission policy using a Fiber scope resolver.

Gin and Fiber's UserClaimsData names both alias verify.UserClaimsData, and their accessors delegate to verify.UserClaimsFromContext. Only UserID is guaranteed populated on a successful user result. Profile fields are normally absent with Required/Optional; RequiredLive loads the current email, verification flag, and username but does not refresh token entitlements or MFA claims. See user-claim presence and freshness.

Surfaces

  • docs/api-endpoints.md — generated route table plus wire notes; CI fails when stale.
  • docs/naming-policy.md — user/group naming, renames and aliases.
  • docs/ownership.md — role replacement and final-owner protection.
  • SEMVER.md — what the version contract covers.
  • SECURITY.md — reporting and the CI gates.

MountOptions{RefreshCookie: true} moves the rotating refresh token out of every response body into an HttpOnly+Secure+SameSite=Lax cookie (authkit_rt) path-scoped to the mount's POST /token, which requires the cookie and rejects body refresh tokens. Native mounts require body tokens and never consume refresh cookies. DELETE /logout and a refresh failing with user_banned clear it; an unknown-token 401 never does. The SPA and mount must share an origin. Cookie-mode JSON mutations reject cross-origin, opaque-origin and cross-site requests before consuming credentials. Omitted Origin remains valid for non-browser clients unless fetch metadata indicates another site. Origin comparison uses the deployment scheme and request host or configured frontend origin; forwarded origin headers are never trusted. Browser OIDC callbacks retain their state-cookie binding. Cookie mode is off by default.

Mounted JSON API bodies require Content-Type: application/json (parameters such as charset=utf-8 are allowed), including when cookie mode is off. Empty-body routes retain their existing behavior. JSON clients using body tokens continue to work across origins when the host allows them.

Browser OIDC

GET /oidc/{provider}/login[?return_to=/app/path][&ui=popup&popup_nonce=…] → provider → /oidc/{provider}/callback (GET, or POST for form_post) → 302 to Frontend.BaseURL + OIDCReturnPath (default /login/callback):

  • success: #access_token=…&refresh_token=…&expires_in=…&provider=…[&return_to=…] (no refresh_token with the refresh cookie);
  • error: #error=<code>&flow=login|link&provider=…; 2fa_enrollment_required carries enrollment_token, enrollment_expires_in, allowed_methods instead of an access token;
  • popup: postMessage of {type: "AUTHKIT_OIDC_RESULT", access_token, …, nonce} or {type: "AUTHKIT_OIDC_ERROR", error, flow, provider, nonce}.

return_to must be app-relative. Linking a provider to an existing account is POST /api/v1/oidc/{provider}/link/start: it needs fresh authentication (403 step_up_required) and an existing link for the same issuer must be unlinked first (409 provider_change_requires_unlink). Completion requires that same session to remain live and fresh. Successful linking returns an empty 204 for JSON, or redirects with #flow=link&result=success&provider=…; it retains the existing session and issues no tokens or refresh cookie. See credential and recovery grants.

RBAC

Config.RBAC is []embedded.PersonaDef. Each persona is a permission namespace (org:members:read) with a role catalog; non-root personas name one Parent; root is the parentless singleton with AuthKit's built-in owner role. Capabilities opt a persona into the generated API-key, remote-application and custom-role routes; Creation.Enabled mounts POST /<persona>. Assignments are rows keyed by persona and role name: treat both as durable identifiers and never rename in place; removed names fail closed without deleting rows. One role per subject per group; who may create a group is the host's decision.

An enabled remote application can own its immutable controlling group. Its signed app-self token can use that group's existing member add, role-change, removal, member-list and role-list endpoints. Mutations recheck current grants and the credential's permission ceiling in the same transaction as the write; both the replaced and requested roles must fit. Delegated user tokens do not inherit the application's ownership. Registration invitations still require a native user. A remote owner assignment in another group is rejected and never counts as a remaining owner; ordinary ancestor permission grants are unchanged.

Signed documents and delegated tokens

documents.NewService signs, persists and re-verifies an immutable JSON envelope (type, iss, aud, opaque payload) with the engine's live key. Pass it in authhttp.Config.Documents; MountHandler then serves GET|HEAD /.well-known/authkit/documents/{digest} to the remote applications pinned in Config.Documents.Readers (by id, proven domain or root-registered issuer — never slug). Receivers use documents.NewResolver and verify.Verifier.VerifyDocument. The resolver guards nil/default transports against private and reserved destinations, including current DNS answers. ResolverOptions.AllowHTTP is the existing development opt-in for local HTTP and private destinations. An explicit custom transport retains the host's network policy; resolver timeouts, redirect bounds, response caps and verification still apply.

POST /api/v1/delegated/token mounts when Config.Delegated.Audiences is set and requires the one host seam:

deps.DelegatedAuthorization = func(ctx context.Context, req authkit.DelegationRequest) (authkit.DelegationGrant, error) {
	if !mayDelegate(ctx, req.UserID, req.RequestedGrant) {
		return authkit.DelegationGrant{}, authkit.ErrDelegationRefused // 403 delegation_refused; any other error is 503
	}
	return authkit.DelegationGrant{Permissions: []string{"resource:read"}}, nil
}

The request {audiences, ttl_seconds, delegate_certificate_der_b64url, requested_grant} is clamped to the configured audiences and TTL bounds; AuthKit signs only the grant plus every published document digest, bound to the delegate's leaf certificate as cnf: {"x5t#S256": …} (RFC 8705). A bound token verifies only when r.TLS.PeerCertificates[0] hashes to that value — terminate TLS on the resource server with tls.Config{ClientAuth: tls.RequestClientCert} or stricter; anything else fails sender_proof_required.

Browser clients can use the same route without client certificates when the issuer enables Config.Delegated.AllowDPoP. A validated DPoP proof binds the result to a browser key as cnf.jkt; direct resource calls use the DPoP authorization scheme and a fresh proof for each request. The host authorizer still decides every permission and must handle the JWK binding with a nil DelegateCertificate. See browser delegation for the exact wire profile, receiver configuration and browser key lifecycle.

Application self-registration

Config.Applications = ApplicationsConfig{SelfRegistration: true, OrgPersona: "org"} mounts POST /api/v1/applications/register {"domain": "cozy.art"}. The server fetches https://<domain>/.well-known/authkit/application.json; that fetch is the domain-control proof (https, no redirects, SSRF-guarded). The document declares issuer, one of jwks_uri/public_keys, and a requested slug (default: the hostname) claimed like any org slug. The result is a registered-tier remote application plus a service-owned OrgPersona group. Re-registering the same domain re-proves the root and refreshes the keys — that is key rotation; a keypair never rotates itself. Deps.ApplicationAdmission is the host's cost gate.

Device keys

Config.DeviceKeys.Enabled mounts RouteDeviceKeys for native clients. POST /api/v1/device-keys/enroll/begin (email + public key → emailed code) and enroll/finish (code + signature; an MFA-protected account must also present its second factor) enrol a per-machine key. login/begin + login/finish exchange a signed challenge for a short access token and nothing else — no refresh session. GET /api/v1/device-keys, DELETE /api/v1/device-keys/{id} and POST /api/v1/device-keys/revoke-others manage keys; a revoked machine cannot revoke its replacement.

Passkey ceremonies

/api/v1/passkeys/* covers browser login, registration and management. AuthKit's HTTP transport drives the private engine ceremonies; Runtime does not expose workflow primitives to embedding applications. Every finish consumes its ceremony once and only for the purpose for which it was begun.

Liveness

verify.Required is stateless: a banned or deleted user keeps a valid access token until it expires (at most one access TTL). For a surface that cannot accept that window:

// Config.HTTP wires the local engine as the liveness source.
requiredLive, err := authkitgin.RequiredLive(srv.Verifier()) // verify.RequiredLive for net/http

It denies banned, deleted, reserved and unknown accounts on the next request and hands the handler fresh Username/Email/EmailVerified. Fail-closed: one UserLivenessByIDs read per request, no cache, a lookup error denies. A standalone verify.NewVerifier() still needs an explicit WithLiveness(client); without a source, live middleware construction returns verify.ErrLivenessUnconfigured. Hosts can replace a service verifier's source with the same setter. Attaching the source does not change stateless middleware.

Choose the scope where the check runs by mounting middleware, with no global configuration switch or implicit admin-role policy:

  • verify.Required and verify.Optional keep native-user token checks stateless.
  • verify.RequiredLive requires credentials and checks native-user liveness.
  • verify.OptionalLive admits anonymous requests without a lookup; presented credentials must verify, and native users must pass the liveness check.

The Gin and Fiber adapters expose matching RequiredLive and OptionalLive constructors. Mount the returned native middleware on routes, groups, or the whole application using the framework's usual registration methods.

requiredLive, err := verify.RequiredLive(srv.Verifier())
if err != nil { return err }
optionalLive, err := verify.OptionalLive(srv.Verifier())
if err != nil { return err }
mux.Handle("/admin/", requiredLive(adminHandler)) // explicit sensitive-route policy
mux.Handle("/profile", optionalLive(profileHandler))
// Alternatively, wrap the unwrapped application handler instead of its routes:
handler := optionalLive(applicationHandler)

Mount on a route, a subtree/group, or the outer application handler according to the host's policy; choose one scope to avoid redundant lookups. Anonymous requests through OptionalLive remain anonymous. Invalid credentials, banned accounts, and liveness-backend failures are refused instead of becoming anonymous. These checks do not grant admin permissions; authorization remains a separate route policy. Verified machine/external principals retain the existing verifier behavior and do not acquire a native-user directory lookup.

AuthKit's built-in root-permission operations resolve permissions live, without an implicit account-ban lookup. Existing native access tokens authenticate until expiry; bans prevent login and refresh. Hosts can explicitly select the live middleware above when they need immediate account revocation. Deleted or reserved users cannot perform authority mutations, and ownership transitions retain their stricter valid-owner checks.

Sessions across issuers

Deployments sharing one account schema under different issuers (separate site logins, shared accounts) each set Token.AccountIssuers to the same issuer set.

Operation Refresh sessions revoked on
DELETE /logout, DELETE /user/sessions[/{id}], RevokeIssuerSessions, session-cap eviction, refresh reuse this issuer
AdminRevokeAccountSessions[As] (POST /admin/users/{user_id}/sessions/revoke), password change/reset/admin set, contact change, ban, deletion every account issuer

The emergency revoke also revokes device keys and returns authkit.AccountSessionRevocation: covered issuers, per-issuer counts, and live sessions left under unlisted issuers (nonzero means incomplete configuration). Each revoked session is recorded under its own issuer, plus one account_sessions_revoked event.

Revocation stops refresh and step-up re-authentication at once. It does not recall issued access tokens: verify.Required accepts them until exp (AccessTokenDuration), and RequiredLive/AllowLive check account liveness and live permissions, not sessions. Removing the account's roles cuts privileged access immediately. Bans block login and refresh; existing native access tokens retain their remaining lifetime unless explicit live-account verification is used.

Recoverable account deletion

Every accepted account deletion is soft for exactly 30 days. Ownership must be transferred, or the group deleted, before its last eligible owner can delete their account. Repeating deletion does not restart the clock. AuthKit revokes existing sessions immediately, retains the identity for recovery, and schedules a River finalizer for that account's exact deadline.

Pass optional OnSoftDelete, OnHardDelete and OnRestore functions in embedded.Deps. Each receives (context.Context, authkit.UserDeletion) and returns an error. The payload contains a deletion generation ID, UserID, DeletedAt and PurgeAt. Soft callbacks must preserve recoverable host data; restore callbacks undo reversible soft work. Hard callbacks run after the deadline and before physical identity purge, so they can remove host foreign keys. Final purge waits for all required applications to finish successfully.

Callbacks run at least once, outside database transactions, in lifecycle order for each user and application. They must be idempotent and honor cancellation; River retries errors without losing the cleanup. Nil means no application work for that stage. The host does not poll a backlog or acknowledge events.

Token.AccountIssuers identifies deployments sharing account lifecycle. Each issuer must compose its River fleet once before deletion affects it; AuthKit remembers that issuer's River schema and queues callbacks directly into it, even while the application is offline. Separate River schemas are supported, but AuthKit and every participating fleet must address the same physical database for atomic insertion. Binding verifies that identity, including schema-bound pool copies. An issuer may bind a different River schema once it has no active deletion generations or pending callbacks. The transition is atomic and fences old runtime producers; active work must finish first.

Terminal generation and delivery history uses AuthKit's internal 90-day retention and bounded maintenance batches. Active generations and unfinished callbacks are never expired; old completed River jobs safely no-op afterward.

Trusted operators can use client.OperatorRestoreUsers; authorized HTTP administrators use POST /admin/users/{user_id}/restore. Recovery before the deadline invalidates that generation's finalizer without reviving revoked sessions. Once finalization starts after the deadline, restoration is refused. There is no public immediate-purge operation or configurable retention period.

A deleted user can prove their identity through the existing password, passwordless, passkey, external-login or Solana login flow. Existing MFA still applies. Successful proof returns 409 account_recovery_required with an opaque recovery object instead of a session. Submit its token to POST /account/recovery/confirm to restore explicitly, then sign in normally. The one-use confirmation expires within ten minutes and before the deletion deadline; it is bound to that issuer, credential version and deletion generation. It cannot authenticate API requests, enroll new MFA factors, or refresh a session. Required but missing MFA enrollment needs operator recovery; no enrollment access token is issued for a deleted account. Login never restores implicitly.

Documentation

Overview

Package authkit defines the public contracts shared by AuthKit hosts, the embedded engine, HTTP transports, and verification code.

The root package owns Client, domain and wire types, typed identifiers, policy vocabulary, and the error catalog. It also owns the small shared operations on those contracts: credential parsing, permission matching, naming-policy evaluation and response encoding. Importing it does not construct an engine or connect to a database.

Construct the engine with embedded.New. Call embedded.ApplyMigrations before construction so AuthKit owns its schema and migration runner. Mount authentication routes through authhttp or a framework adapter, and use verify for credential verification and request authorization. Database access, token issuance, and authentication workflows are implemented by embedded, not this package.

Client describes host operations implemented by embedded.Runtime. A host can define a smaller interface for the operations it needs; Client membership does not define the full supported API. Documented concrete operations are also covered by the repository's SEMVER.md contract.

The root package remains independent of PostgreSQL drivers and framework adapters. Document aliases and the internal error model share their canonical definitions without importing the engine. The dependency guard tests preserve this boundary for both authkit and verify.

Index

Constants

View Source
const (
	ImportStatusInserted   ImportUserStatus = "inserted"
	ImportStatusSkipped    ImportUserStatus = "skipped"
	ImportStatusRejected   ImportUserStatus = "rejected"
	AdminUserStatusActive  AdminUserStatus  = "active"     // not deleted, not banned
	AdminUserStatusBanned  AdminUserStatus  = "banned"     // not deleted, currently banned
	AdminUserStatusDeleted AdminUserStatus  = "deleted"    // soft-deleted
	AdminUserStatusAny     AdminUserStatus  = "any"        // no deleted/banned predicate
	AdminUserSortCreatedAt AdminUserSort    = "created_at" // default
	AdminUserSortLastLogin AdminUserSort    = "last_login"
	AdminUserSortUsername  AdminUserSort    = "username"
	AdminUserSortEmail     AdminUserSort    = "email"
)
View Source
const (
	ErrorTypeInvalidRequest = "invalid_request_error"
	ErrorTypeAuthentication = "authentication_error"
	ErrorTypeAuthorization  = "authorization_error"
	ErrorTypeRateLimit      = "rate_limit_error"
	ErrorTypeAPI            = "api_error"
)

Error type categories, aligned with openrails' / Stripe's taxonomy strings.

View Source
const (
	SubjectKindUser      SubjectKind = "user"
	SubjectKindRemoteApp SubjectKind = "remote_application"

	// RootPersona is the single built-in persona: every deployment has exactly
	// one root group, the parentless ancestor of every other group.
	RootPersona Persona = "root"

	// OwnerRole is the role every persona ships: it holds the persona's whole
	// namespace (`<persona>:*`) and nothing else.
	OwnerRole Role = "owner"
)
View Source
const (
	RemoteAppModeJWKS   = "jwks"
	RemoteAppModeStatic = "static"
)

Remote-application trust modes (#74). A remote_application is a federation PRINCIPAL whose credential is a key, with exactly one trust source:

jwks   — keys fetched + refreshed from JWKSURI; rotation is publishing a new
         kid at the same URL.
static — authorized_keys-style human-managed PEM list for principals without
         a JWKS endpoint; manual rotation by design.
View Source
const (
	ApplicationTierRegistered = "registered"
	ApplicationTierApproved   = "approved"
)

Application capability tiers (#264).

View Source
const (
	ApplicationTrustRootManual = "manual"
	ApplicationTrustRootDomain = "domain"
	ApplicationTrustRootUser   = "user"
)

Application trust roots (#264): the authority that rotates keys.

View Source
const (
	// ServiceJWTTokenUse is the required `token_use` claim for service JWTs.
	ServiceJWTTokenUse = "service"
	// DefaultServiceJWTLifetime is the recommended lifetime for first-party
	// machine-to-machine service JWTs.
	DefaultServiceJWTLifetime = 15 * time.Minute
)
View Source
const (
	DefaultUsernameMinLength = 4
	DefaultUsernameMaxLength = 30
	UsernameMaxLengthCeiling = 64
)

Username length defaults and the ceiling for a configured maximum.

View Source
const (
	ActionUpdateUsername       = "update_username"
	ActionRequestPasswordReset = "request_password_reset"
	ActionRequestVerification  = "request_verification"
)

ActionAvailability reports whether a cooldown-gated action is currently allowed; it rides on GET /me and on 429 error metadata. Action names carried by ActionAvailability.

View Source
const ApplicationWellKnownPath = "/.well-known/authkit/application.json"

ApplicationWellKnownPath is where a domain-registered application serves its ApplicationDocument. Fetching it over HTTPS IS the domain-control proof.

View Source
const MaxRemoteApplicationIssuerLen = 512

MaxRemoteApplicationIssuerLen bounds a remote-application issuer identifier. Registration refuses longer values and the verifier never consults the store for them (ak#297).

View Source
const PermWildcard = "*"

PermWildcard is the wildcard CHARACTER used inside namespace-anchored globs (`org:*`, `org:members:*`, `org:*:read`, `root:*`). A bare standalone `*` is NOT a valid grant — it is rejected everywhere.

View Source
const UserRecoveryPeriod = 30 * 24 * time.Hour

UserRecoveryPeriod is the fixed interval in which an accepted account deletion can be restored. Repeated deletion does not extend it.

View Source
const UsernamePattern = "^[A-Za-z][A-Za-z0-9_]*$"

UsernamePattern is the fixed character rule for interactive usernames, compatible with Go RE2 and JavaScript `u`/`v` regular expressions and HTML `pattern`. Length is governed separately by UsernamePolicy.

Variables

View Source
var (
	// ErrInvalidAccessToken indicates an API key that does not exist, has a bad
	// secret, or whose owning permission group is gone. Deliberately indistinguishable from
	// a malformed token so callers learn nothing from the error.
	ErrInvalidAccessToken = E(CodeInvalidToken)
	// ErrAccessTokenRevoked indicates the API key was explicitly revoked.
	ErrAccessTokenRevoked = E(CodeAccessTokenRevoked)
	// ErrAccessTokenExpired indicates the API key is past its expires_at.
	ErrAccessTokenExpired = E(CodeAccessTokenExpired)
)
View Source
var (
	CodeTwoFAEnrollmentRequired           = def("2fa_enrollment_required", 403, "Two-factor authentication must be enrolled to continue.")
	CodeTwoFAFactorExists                 = def("2fa_factor_exists", 409, "A two-factor authentication method is already enrolled. Remove it before enrolling a replacement.")
	CodeTwoFAMethodUnavailable            = def("2fa_method_unavailable", 400, "That two-factor method is unavailable.")
	CodeTwoFARequired                     = def("2fa_required", 403, "Two-factor authentication is required.")
	CodeAbandonFailed                     = def("abandon_failed", 500, "The registration could not be abandoned.")
	CodeAccessTokenHasSub                 = def("access_token_has_sub", 401, "An access token must not carry a subject.")
	CodeAccessTokenWrongTyp               = def("access_token_wrong_typ", 401, "The token type is wrong for an access token.")
	CodeAccountAuthorityEscalation        = def("account_authority_escalation", 403, "That account holds authority you do not.")
	CodeAccountDisabled                   = def("account_disabled", 401, "This account is disabled.")
	CodeAccountRecoveryExpired            = def("account_recovery_expired", 409, "The account recovery window has ended.")
	CodeAccountRecoveryRequired           = def("account_recovery_required", 409, "Confirm account recovery before signing in.")
	CodeAccountExistsLinkRequired         = def("account_exists_link_required", 409, "An account with this email already exists. Sign in and link the provider.")
	CodeAccountRegistrationInviteNotFound = def("account_registration_invite_not_found", 404, "The registration invite was not found.")
	CodeAddressMismatch                   = def("address_mismatch", 400, "The address does not match.")
	CodeAddressRequired                   = def("address_required", 400, "An address is required.")
	CodeApplicationDocumentFetchFailed    = def("application_document_fetch_failed", 502, "The application document could not be fetched.")
	CodeApplicationDocumentInvalid        = def("application_document_invalid", 400, "The application document is invalid.")
	CodeApplicationDomainConflict         = def("application_domain_conflict", 409, "That domain already belongs to another application.")
	CodeApplicationDomainInvalid          = def("application_domain_invalid", 400, "The application domain is invalid.")
	CodeApplicationIssuerConflict         = def("application_issuer_conflict", 409, "That issuer already belongs to another application.")
	CodeApplicationRegistrationDisabled   = def("application_registration_disabled", 403, "Application registration is disabled.")
	CodeApplicationSlugConflict           = def("application_slug_conflict", 409, "That application slug is taken.")
	CodeAuthRequiredForLink               = def("auth_required_for_link", 401, "Sign in before linking a provider.")
	CodeAuthenticationFailed              = def("authentication_failed", 401, "Authentication failed.")
	CodeAuthenticationRequired            = def("authentication_required", 401, "Authentication is required.")
	CodeAvatarURLInvalid                  = def("avatar_url_invalid", 400, "The avatar URL is invalid.")
	CodeBadAudience                       = def("bad_audience", 401, "The token audience is not accepted.")
	CodeBadIssuer                         = def("bad_issuer", 401, "The token issuer is not trusted.")
	CodeBootstrapDatabaseNotEmpty         = def("bootstrap_database_not_empty", 409, "The database is not empty; bootstrap refused.")
	CodeCannotRemoveLastAdminRole         = def("cannot_remove_last_admin_role", 409, "The last owner cannot be removed.")
	CodeCannotRemoveLastOwner             = def("cannot_remove_last_owner", 409, "The last owner cannot be removed.")
	CodeCannotUnlinkLastLoginMethod       = def("cannot_unlink_last_login_method", 400, "The last login method cannot be unlinked.")
	CodeChallengeExpired                  = def("challenge_expired", 401, "The challenge has expired.")
	CodeChallengeFailed                   = def("challenge_failed", 500, "The challenge could not be created.")
	CodeConfirmationWrongTokenType        = def("confirmation_wrong_token_type", 401, "This token type does not accept a confirmation claim.")
	CodeConflictingSubject                = def("conflicting_subject", 401, "The token carries conflicting subjects.")
	CodeCustomRoleGrantCrossPersona       = def("custom_role_grant_cross_persona", 400, "A custom role may only grant permissions in its own persona.")
	CodeCustomRoleGrantOutsideCatalog     = def("custom_role_grant_outside_catalog", 400, "A custom role grant is outside the persona catalog.")
	CodeCustomRoleIsCatalogRole           = def("custom_role_is_catalog_role", 400, "A catalog role cannot be redefined as a custom role.")
	CodeCustomRoleNameInvalid             = def("custom_role_name_invalid", 400, "The custom role name is invalid.")
	CodeCustomRolesNotSupported           = def("custom_roles_not_supported", 400, "This persona does not support custom roles.")
	CodeDatabaseError                     = def("database_error", 500, "An internal error occurred. Please try again.")
	CodeDelegatedAccessHasRoles           = def("delegated_access_has_roles", 401, "A delegated token must not carry roles.")
	CodeDelegatedAccessHasUserTier        = def("delegated_access_has_user_tier", 401, "A delegated token must not carry a user tier.")
	CodeDelegatedAccessWrongTyp           = def("delegated_access_wrong_typ", 401, "The token type is wrong for delegated access.")
	CodeDelegatedDocumentUnavailable      = def("delegated_document_unavailable", 503, "The delegated document is unavailable.")
	CodeDelegatedMintFailed               = def("delegated_mint_failed", 500, "The delegated token could not be minted.")
	CodeDelegatedTokenTooLarge            = def("delegated_token_too_large", 500, "The delegated token is too large.")
	CodeDelegationAuthorizerUnavailable   = def("delegation_authorizer_unavailable", 503, "Delegation is unavailable.")
	CodeDelegationRefused                 = def("delegation_refused", 403, "The delegation was refused.")
	CodeDeviceKeysDisabled                = def("device_keys_disabled", 403, "Device keys are disabled.")
	CodeDuplicateClaim                    = def("duplicate_claim", 401, "The document carries a duplicate claim.")
	CodeEmailAlreadyVerified              = def("email_already_verified", 409, "The email address is already verified.")
	CodeEmailDeliveryFailed               = def("email_delivery_failed", 502, "The email could not be delivered.")
	CodeEmailInUse                        = def("email_in_use", 400, "That email address is already in use.")
	CodeEmailPasswordResetUnavailable     = def("email_password_reset_unavailable", 503, "Password reset by email is unavailable.")
	CodeEmailRegistrationUnavailable      = def("email_registration_unavailable", 500, "Email registration is unavailable.")
	CodeEmailSenderUnavailable            = def("email_sender_unavailable", 503, "Email sending is unavailable.")
	CodeEmailUnavailable                  = def("email_unavailable", 503, "Email is unavailable.")
	CodeEmailUnchanged                    = def("email_unchanged", 400, "The email address is unchanged.")
	CodeEmailVerificationSendFailed       = def("email_verification_failed", 500, "The verification email could not be sent.")
	CodeEmailVerificationUnavailable      = def("email_verification_unavailable", 500, "Email verification is unavailable.")
	CodeTwoFAEnableFailed                 = def("enable_2fa_failed", 500, "Two-factor authentication could not be enabled.")
	CodeEntitlementFilterUnavailable      = def("entitlement_filter_unavailable", 400, "Entitlement filtering is unavailable.")
	CodeExternalInvitesDisabled           = def("external_invites_disabled", 403, "Invite links are disabled.")
	CodeFailedToDelete                    = def("failed_to_delete", 500, "The delete failed.")
	CodeFailedToList                      = def("failed_to_list", 500, "The list could not be loaded.")
	CodeFailedToListSignins               = def("failed_to_list_signins", 500, "Sign-ins could not be listed.")
	CodeFailedToLogout                    = def("failed_to_logout", 500, "Logout failed.")
	CodeFailedToRequestPhoneChange        = def("failed_to_request_phone_change", 400, "The phone change could not be requested.")
	CodeFailedToRevoke                    = def("failed_to_revoke", 500, "The revoke failed.")
	CodeFailedToRevokeAll                 = def("failed_to_revoke_all", 500, "The sessions could not be revoked.")
	CodeFailedToUnban                     = def("failed_to_unban", 500, "The user could not be unbanned.")
	CodeFailedToUnlink                    = def("failed_to_unlink", 500, "The provider could not be unlinked.")
	CodeFailedToUpdatePreferredLanguage   = def("failed_to_update_preferred_language", 400, "The preferred language could not be updated.")
	CodeForbidden                         = def("forbidden", 403, "You do not have permission to perform this action.")
	CodeGroupCreationRefused              = def("group_creation_refused", 403, "Group creation was refused.")
	CodeInviteLinkExpired                 = def("group_invite_link_expired", 400, "The invite link has expired.")
	CodeInviteLinkNotFound                = def("group_invite_link_not_found", 404, "The invite link was not found.")
	CodeInviteLinkRevoked                 = def("group_invite_link_revoked", 400, "The invite link was revoked.")
	CodeGroupSlugApplicationManaged       = def("group_slug_application_managed", 409, "The group slug is managed by its application.")
	CodeGroupSlugInvalid                  = def("group_slug_invalid", 400, "The group slug is invalid.")
	CodeGroupSlugReserved                 = def("group_slug_reserved", 403, "That group slug is reserved.")
	CodeGroupSlugTaken                    = def("group_slug_taken", 409, "That group slug is taken.")
	CodeInsufficientRoleAuthority         = def("insufficient_role_authority", 403, "You do not have the authority for this role change.")
	CodeInternalError                     = def("internal_error", 500, "An internal error occurred. Please try again.")
	CodeInvalidAddress                    = def("invalid_address", 400, "The address is invalid.")
	CodeInvalidAudiences                  = def("invalid_audiences", 400, "The audiences are invalid.")
	CodeInvalidBaseURL                    = def("invalid_base_url", 500, "The frontend base URL is invalid.")
	CodeInvalidBootstrapManifest          = def("invalid_bootstrap_manifest", 400, "The bootstrap manifest is invalid.")
	CodeInvalidChallenge                  = def("invalid_challenge", 401, "The challenge is invalid.")
	CodeInvalidCode                       = def("invalid_code", 400, "The code is invalid.")
	CodeInvalidConfirmation               = def("invalid_confirmation", 401, "The confirmation claim is invalid.")
	CodeInvalidCredentials                = def("invalid_credentials", 401, "Invalid credentials.")
	CodeInvalidDelegateCertificate        = def("invalid_delegate_certificate", 400, "The delegate certificate is invalid.")
	CodeInvalidEmail                      = defParam("invalid_email", 400, "email", "The email address is invalid.")
	CodeInvalidExpiry                     = def("invalid_expiry", 400, "The expiry is invalid.")
	CodeInvalidIdentifier                 = def("invalid_identifier", 400, "The identifier must be an email address or a phone number.")
	CodeInvalidInvite                     = def("invalid_invite", 400, "The invite is invalid.")
	CodeInvalidMessageEncoding            = def("invalid_message_encoding", 400, "The message encoding is invalid.")
	CodeInvalidTwoFAMethod                = def("invalid_method", 400, "The method is invalid.")
	CodeInvalidOrExpiredCode              = def("invalid_or_expired_code", 400, "The code is invalid or has expired.")
	CodeInvalidOrExpiredToken             = def("invalid_or_expired_token", 400, "The token is invalid or has expired.")
	CodeInvalidPassword                   = def("invalid_password", 401, "The password is incorrect.")
	CodeInvalidPhoneNumber                = defParam("invalid_phone_number", 400, "phone_number", "The phone number is invalid.")
	CodeInvalidPreferredLanguage          = defParam("invalid_preferred_language", 400, "preferred_language", "The preferred language is invalid.")
	CodeInvalidProvider                   = def("invalid_provider", 400, "The provider is invalid.")
	CodeInvalidRefreshToken               = def("invalid_refresh_token", 401, "The refresh token is invalid.")
	CodeInvalidRemoteApplication          = def("invalid_remote_application", 400, "The remote application is invalid.")
	CodeInvalidRequest                    = def("invalid_request", 400, "The request is invalid.")
	CodeInvalidRequestedGrant             = def("invalid_requested_grant", 400, "The requested grant is invalid.")
	CodeInvalidRole                       = def("invalid_role", 400, "The role is invalid.")
	CodeInvalidServiceJWT                 = def("invalid_service_jwt", 401, "The service token is invalid.")
	CodeInvalidSignature                  = def("invalid_signature", 401, "The signature is invalid.")
	CodeInvalidSignatureEncoding          = def("invalid_signature_encoding", 400, "The signature encoding is invalid.")
	CodeInvalidState                      = def("invalid_state", 400, "The state is invalid.")
	CodeInvalidToken                      = def("invalid_token", 401, "The authentication token is invalid.")
	CodeInvalidUI                         = def("invalid_ui", 400, "The ui parameter is invalid.")
	CodeInvalidUntil                      = def("invalid_until", 400, "The until value is invalid.")
	CodeLivenessUnavailable               = def("liveness_unavailable", 401, "Account status could not be verified.")
	CodeMalformedPayload                  = def("malformed_payload", 401, "The token payload is malformed.")
	CodeMalformedPermissions              = def("malformed_permissions", 401, "The permissions claim is malformed.")
	CodeMissingAudience                   = def("missing_audience", 401, "The token carries no audience.")
	CodeMissingDelegatedSub               = def("missing_delegated_sub", 401, "The delegated token carries no subject.")
	CodeMissingExp                        = def("missing_exp", 401, "The token carries no expiry.")
	CodeMissingFields                     = def("missing_fields", 400, "Required fields are missing.")
	CodeMissingIAT                        = def("missing_iat", 401, "The token carries no issued-at.")
	CodeMissingKID                        = def("missing_kid", 401, "The token names no key.")
	CodeMissingName                       = def("missing_name", 400, "A name is required.")
	CodeMissingNBF                        = def("missing_nbf", 401, "The token carries no not-before.")
	CodeMissingSessionID                  = def("missing_session_id", 400, "A session id is required.")
	CodeMissingSidClaim                   = def("missing_sid_claim", 400, "The token carries no session.")
	CodeMissingSigner                     = def("missing_signer", 500, "No signing key is configured.")
	CodeMissingSub                        = def("missing_sub", 401, "The token carries no subject.")
	CodeMissingToken                      = def("missing_token", 401, "A bearer token is required.")
	CodeMissingTokenTyp                   = def("missing_token_typ", 401, "The token carries no type.")
	CodeNameAdmissionRefused              = def("name_admission_refused", 403, "That name was refused.")
	CodeNotAuthenticated                  = def("not_authenticated", 401, "Authentication is required.")
	CodeNotDelegatedAccessToken           = def("not_delegated_access_token", 401, "The token is not a delegated access token.")
	CodeNotFound                          = def("not_found", 404, "The requested resource was not found.")
	CodeNotGroupMember                    = def("not_group_member", 403, "The subject is not a member of the group.")
	CodeNotImplemented                    = def("not_implemented", 501, "Not implemented.")
	CodeOIDCBeginFailed                   = def("oidc_begin_failed", 400, "The provider login could not be started.")
	CodeOIDCExchangeFailed                = def("oidc_exchange_failed", 401, "The provider login could not be completed.")
	CodeOwnerSlugTaken                    = defParam("owner_slug_taken", 400, "username", "That name is taken.")
	CodePasskeyCloneDetected              = def("passkey_clone_detected", 401, "The passkey appears to have been cloned.")
	CodePasskeyFailed                     = def("passkey_failed", 500, "The passkey operation failed.")
	CodePasskeyNotFound                   = def("passkey_not_found", 404, "The passkey was not found.")
	CodePasskeyUserVerificationRequired   = def("passkey_user_verification_required", 401, "The passkey must verify the user.")
	CodePasswordChangeFailed              = def("password_change_failed", 400, "The password could not be changed.")
	CodePasswordResetRequired             = def("password_reset_required", 401, "A password reset is required before you can sign in.")
	CodePasswordTooShort                  = defParam("password_too_short", 400, "password", "The password is too short.")
	CodePasswordTooCommon                 = defParam("password_too_common", 400, "password", "The password is too common.")
	CodePasswordContainsIdentifier        = defParam("password_contains_identifier", 400, "password", "The password must not contain your username or email.")
	CodePasswordRequirementsUnmet         = defParam("password_requirements_unmet", 400, "password", "The password does not meet the character requirements.")
	CodePasswordTooLong                   = defParam("password_too_long", 400, "password", "The password is too long.")
	CodePasswordlessDisabled              = def("passwordless_disabled", 403, "Passwordless login is disabled.")
	CodePendingRegistrationNotFound       = def("pending_registration_not_found", 404, "No pending registration was found.")
	CodeGroupNotFound                     = def("permission_group_not_found", 404, "The permission group was not found.")
	CodePermissionNotGranted              = def("permission_not_granted", 403, "The token claims a permission it was not granted.")
	CodePhoneTwoFAUnavailable             = def("phone_2fa_unavailable", 500, "SMS two-factor authentication is unavailable.")
	CodePhoneAlreadyVerified              = def("phone_already_verified", 409, "The phone number is already verified.")
	CodePhoneNumberRequired               = def("phone_and_code_required", 400, "A phone number is required.")
	CodePhoneInUse                        = def("phone_in_use", 400, "That phone number is already in use.")
	CodePhoneNumberMustBeE164             = def("phone_number_must_be_e164", 400, "The phone number must be in E.164 format.")
	CodePhoneRegistrationUnavailable      = def("phone_registration_unavailable", 500, "Phone registration is unavailable.")
	CodePhoneUnavailable                  = def("phone_unavailable", 503, "Phone is unavailable.")
	CodePhoneUnchanged                    = def("phone_unchanged", 400, "The phone number is unchanged.")
	CodePhoneVerificationSendFailed       = def("phone_verification_failed", 500, "The verification SMS could not be sent.")
	CodePhoneVerificationUnavailable      = def("phone_verification_unavailable", 500, "Phone verification is unavailable.")
	CodePKCEGenerationFailed              = def("pkce_generation_failed", 500, "The login could not be started.")
	CodePreferredLanguageLookupFailed     = def("preferred_language_lookup_failed", 500, "The preferred language could not be read.")
	CodeProviderAlreadyLinked             = def("provider_already_linked", 409, "That provider identity is already linked to another account.")
	CodeProviderChangeRequiresUnlink      = def("provider_change_requires_unlink", 409, "Unlink the current provider account before linking another.")
	CodeProviderError                     = def("provider_error", 400, "The provider returned an error.")
	CodeProviderLinkFailed                = def("provider_link_failed", 500, "The provider could not be linked.")
	CodeProviderNotLinked                 = def("provider_not_linked", 400, "That provider is not linked.")
	CodeRateLimited                       = def("rate_limited", 429, "Too many requests. Please try again later.")
	CodeRegenerateCodesFailed             = def("regenerate_codes_failed", 500, "Backup codes could not be regenerated.")
	CodeRegistrationDisabled              = def("registration_disabled", 403, "Registration is currently disabled.")
	CodeRemoteApplicationAccessHasSubject = def("remote_application_access_has_subject", 401, "A remote-application token must not carry a subject.")
	CodeRemoteApplicationIssuerConflict   = def("remote_application_issuer_conflict", 409, "That issuer already belongs to another remote application.")
	CodeRemoteApplicationNotFound         = def("remote_application_not_found", 404, "The remote application was not found.")
	CodeRenameRateLimited                 = def("rename_rate_limited", 429, "Too many renames. Please try again later.")
	CodeRenamesDisabled                   = def("renames_disabled", 403, "Renames are disabled.")
	CodeReservedIssuer                    = def("reserved_issuer", 400, "That issuer is reserved.")
	CodeRoleAssignmentEscalation          = def("role_assignment_escalation", 403, "That role confers authority you do not hold.")
	CodeRoleNotAssignable                 = def("role_not_assignable", 400, "The role cannot be assigned in this group.")
	CodeTwoFASetupCodeSendFailed          = def("send_code_failed", 500, "The code could not be sent.")
	CodeSenderProofRequired               = def("sender_proof_required", 401, "The token requires sender proof.")
	CodeServiceJWTLifetimeExceeded        = def("service_jwt_lifetime_exceeded", 401, "The service token lifetime is too long.")
	CodeSessionCreationFailed             = def("session_creation_failed", 500, "The session could not be created.")
	CodeSIWSAddressMismatch               = def("siws_address_mismatch", 400, "The wallet address does not match the challenge.")
	CodeSIWSChallengeExpired              = def("siws_challenge_expired", 401, "The sign-in challenge has expired.")
	CodeSIWSChallengeMismatch             = def("siws_challenge_mismatch", 401, "The sign-in challenge does not match.")
	CodeSIWSChallengeNotFound             = def("siws_challenge_not_found", 401, "The sign-in challenge was not found.")
	CodeSIWSDomainInvalid                 = def("siws_domain_invalid", 401, "The sign-in domain is invalid.")
	CodeSIWSSignatureInvalid              = def("siws_signature_invalid", 401, "The wallet signature is invalid.")
	CodeSIWSTimestampInvalid              = def("siws_timestamp_invalid", 401, "The sign-in timestamp is invalid.")
	CodeSMSDeliveryFailed                 = def("sms_delivery_failed", 502, "The SMS could not be delivered.")
	CodeSMSSenderUnavailable              = def("sms_unavailable", 503, "SMS sending is unavailable.")
	CodeStateStoreFailed                  = def("state_store_failed", 500, "The login state could not be stored.")
	CodeStepUpFailed                      = def("step_up_failed", 500, "Step-up verification failed.")
	CodeStepUpRequired                    = def("step_up_required", 403, "Additional verification is required to continue.")
	CodeAccessTokenExpired                = def("token_expired", 401, "The authentication token has expired.")
	CodeTokenIssueFailed                  = def("token_issue_failed", 500, "The token could not be issued.")
	CodeTokenNotYetValid                  = def("token_not_yet_valid", 401, "The token is not yet valid.")
	CodeAccessTokenRevoked                = def("token_revoked", 401, "The access token has been revoked.")
	CodeTTLExceedsDelegateCertificate     = def("ttl_exceeds_delegate_certificate", 400, "The TTL exceeds the delegate certificate.")
	CodeUnauthenticated                   = def("unauthenticated", 401, "Authentication is required.")
	CodeUnauthorized                      = def("unauthorized", 401, "Authentication is required.")
	CodeUnknownGroupPersona               = def("unknown_group_persona", 400, "Unknown group persona.")
	CodeUnknownKID                        = def("unknown_kid", 401, "The token names an unknown key.")
	CodeUnknownProvider                   = def("unknown_provider", 400, "Unknown provider.")
	CodeUnknownRole                       = def("unknown_role", 400, "Unknown role.")
	CodeUnsupportedTokenTyp               = def("unsupported_token_typ", 401, "The token type is not supported.")
	CodeUserBanned                        = def("user_banned", 401, "This account is banned.")
	CodeUserCreationFailed                = def("user_creation_failed", 500, "The user could not be created.")
	CodeUserLookupFailed                  = def("user_lookup_failed", 500, "The user could not be loaded.")
	CodeUserNotFound                      = def("user_not_found", 404, "User not found.")
	CodeUserReferenced                    = def("user_referenced", 409, "The user is still referenced.")
	CodeUserRoleNotFound                  = def("user_role_not_found", 404, "The user does not hold that role.")
	CodeUsernameCannotContainAt           = defParam("username_cannot_contain_at", 400, "username", "The username cannot contain @.")
	CodeUsernameCannotStartWithPlus       = defParam("username_cannot_start_with_plus", 400, "username", "The username cannot start with +.")
	CodeUsernameInUse                     = def("username_in_use", 400, "That username is already in use.")
	CodeUsernameInvalidCharacters         = defParam("username_invalid_characters", 400, "username", "The username contains invalid characters.")
	CodeUsernameMustStartWithLetter       = defParam("username_must_start_with_letter", 400, "username", "The username must start with a letter.")
	CodeUsernameNotAllowed                = defParam("username_not_allowed", 400, "username", "That username is not allowed.")
	CodeUsernameTooLong                   = defParam("username_too_long", 400, "username", "The username is too long.")
	CodeUsernameTooShort                  = defParam("username_too_short", 400, "username", "The username is too short.")
	CodeVerificationLinkExpired           = def("verification_link_expired", 410, "The verification link has expired.")
	CodeVerificationRequired              = def("verification_required", 403, "Verify your contact details to continue.")
	CodeWalletAlreadyLinked               = def("wallet_already_linked", 409, "That wallet is already linked to another account.")
	CodeWalletChangeRequiresUnlink        = def("wallet_change_requires_unlink", 409, "Unlink your current wallet before connecting another.")
)

The catalog: every wire code with its HTTP status and message.

View Source
var (
	ErrApplicationDocumentFetchFailed    = E(CodeApplicationDocumentFetchFailed)
	ErrApplicationDocumentInvalid        = E(CodeApplicationDocumentInvalid)
	ErrApplicationDomainConflict         = E(CodeApplicationDomainConflict)
	ErrApplicationDomainInvalid          = E(CodeApplicationDomainInvalid)
	ErrApplicationIssuerConflict         = E(CodeApplicationIssuerConflict)
	ErrApplicationRegistrationDisabled   = E(CodeApplicationRegistrationDisabled)
	ErrApplicationSlugConflict           = E(CodeApplicationSlugConflict)
	ErrBootstrapDatabaseNotEmpty         = E(CodeBootstrapDatabaseNotEmpty)
	ErrGroupSlugApplicationManaged       = E(CodeGroupSlugApplicationManaged)
	ErrGroupSlugTaken                    = E(CodeGroupSlugTaken)
	ErrGroupSlugReserved                 = E(CodeGroupSlugReserved)
	ErrGroupSlugInvalid                  = E(CodeGroupSlugInvalid)
	ErrGroupCreationRefused              = E(CodeGroupCreationRefused)
	ErrAvatarURLInvalid                  = E(CodeAvatarURLInvalid)
	ErrCannotRemoveLastAdminRole         = E(CodeCannotRemoveLastAdminRole)
	ErrAccountRegistrationInviteNotFound = E(CodeAccountRegistrationInviteNotFound)
	ErrAccountRecoveryExpired            = E(CodeAccountRecoveryExpired)
	ErrCustomRoleGrantCrossPersona       = E(CodeCustomRoleGrantCrossPersona)
	ErrCustomRoleGrantOutsideCatalog     = E(CodeCustomRoleGrantOutsideCatalog)
	ErrCustomRoleIsCatalogRole           = E(CodeCustomRoleIsCatalogRole)
	ErrCustomRoleNameInvalid             = E(CodeCustomRoleNameInvalid)
	ErrCustomRolesNotSupported           = E(CodeCustomRolesNotSupported)
	ErrEmailAlreadyVerified              = E(CodeEmailAlreadyVerified)
	ErrEmailDeliveryFailed               = E(CodeEmailDeliveryFailed)
	ErrEmailInUse                        = E(CodeEmailInUse)
	ErrEmailSenderUnavailable            = E(CodeEmailSenderUnavailable)
	ErrEntitlementFilterUnavailable      = E(CodeEntitlementFilterUnavailable)
	ErrExternalInvitesDisabled           = E(CodeExternalInvitesDisabled)
	ErrGroupNotFound                     = E(CodeGroupNotFound)
	ErrInsufficientRoleAuthority         = E(CodeInsufficientRoleAuthority)
	ErrInvalidBootstrapManifest          = E(CodeInvalidBootstrapManifest)
	ErrInvalidExpiry                     = E(CodeInvalidExpiry)
	ErrInvalidInvite                     = E(CodeInvalidInvite)
	ErrInvalidRole                       = E(CodeInvalidRole)
	ErrInvalidUntil                      = E(CodeInvalidUntil)
	ErrInviteLinkExpired                 = E(CodeInviteLinkExpired)
	ErrInviteLinkNotFound                = E(CodeInviteLinkNotFound)
	ErrInviteLinkRevoked                 = E(CodeInviteLinkRevoked)
	ErrMissingName                       = E(CodeMissingName)
	ErrMissingSigner                     = E(CodeMissingSigner)
	ErrNotGroupMember                    = E(CodeNotGroupMember)
	ErrOwnerSlugTaken                    = E(CodeOwnerSlugTaken)
	ErrPasskeyCloneDetected              = E(CodePasskeyCloneDetected)
	ErrPasskeyNotFound                   = E(CodePasskeyNotFound)
	ErrPasskeyUserVerificationRequired   = E(CodePasskeyUserVerificationRequired)
	ErrPasswordlessDisabled              = E(CodePasswordlessDisabled)
	ErrDeviceKeysDisabled                = E(CodeDeviceKeysDisabled)
	ErrPasswordResetRequired             = E(CodePasswordResetRequired)
	ErrPendingRegistrationNotFound       = E(CodePendingRegistrationNotFound)
	ErrPhoneAlreadyVerified              = E(CodePhoneAlreadyVerified)
	ErrPhoneInUse                        = E(CodePhoneInUse)
	ErrUsernameInUse                     = E(CodeUsernameInUse)
	ErrRegistrationDisabled              = E(CodeRegistrationDisabled)
	ErrRemoteApplicationIssuerConflict   = E(CodeRemoteApplicationIssuerConflict)
	ErrRemoteApplicationNotFound         = E(CodeRemoteApplicationNotFound)
	ErrRenameRateLimited                 = E(CodeRenameRateLimited)
	ErrRenamesDisabled                   = E(CodeRenamesDisabled)
	ErrNameAdmissionRefused              = E(CodeNameAdmissionRefused)
	ErrReservedIssuer                    = E(CodeReservedIssuer)
	ErrRoleAssignmentEscalation          = E(CodeRoleAssignmentEscalation)
	ErrAccountAuthorityEscalation        = E(CodeAccountAuthorityEscalation)
	ErrRoleNotAssignable                 = E(CodeRoleNotAssignable)
	ErrSMSDeliveryFailed                 = E(CodeSMSDeliveryFailed)
	ErrSMSSenderUnavailable              = E(CodeSMSSenderUnavailable)
	ErrStepUpRequired                    = E(CodeStepUpRequired)
	ErrTwoFAFactorExists                 = E(CodeTwoFAFactorExists)
	ErrTwoFAEnrollmentRequired           = E(CodeTwoFAEnrollmentRequired)
	ErrUnknownGroupPersona               = E(CodeUnknownGroupPersona)
	ErrUnknownRole                       = E(CodeUnknownRole)
	ErrUserBanned                        = E(CodeUserBanned)
	ErrUserNotFound                      = E(CodeUserNotFound)
	ErrUserReferenced                    = E(CodeUserReferenced)
	ErrUserRoleNotFound                  = E(CodeUserRoleNotFound)
	ErrVerificationLinkExpired           = E(CodeVerificationLinkExpired)
	ErrSIWSAddressMismatch               = E(CodeSIWSAddressMismatch)
	ErrSIWSChallengeExpired              = E(CodeSIWSChallengeExpired)
	ErrSIWSChallengeMismatch             = E(CodeSIWSChallengeMismatch)
	ErrSIWSChallengeNotFound             = E(CodeSIWSChallengeNotFound)
	ErrSIWSDomainInvalid                 = E(CodeSIWSDomainInvalid)
	ErrSIWSSignatureInvalid              = E(CodeSIWSSignatureInvalid)
	ErrSIWSTimestampInvalid              = E(CodeSIWSTimestampInvalid)
	ErrWalletAlreadyLinked               = E(CodeWalletAlreadyLinked)
	ErrWalletChangeRequiresUnlink        = E(CodeWalletChangeRequiresUnlink)
	ErrProviderAlreadyLinked             = E(CodeProviderAlreadyLinked)
	ErrProviderChangeRequiresUnlink      = E(CodeProviderChangeRequiresUnlink)
	ErrInvalidCredentials                = E(CodeInvalidCredentials)
	ErrAccountExistsLinkRequired         = E(CodeAccountExistsLinkRequired)
	ErrProviderLinkFailed                = E(CodeProviderLinkFailed)
	ErrUserCreationFailed                = E(CodeUserCreationFailed)
	ErrInvalidIdentifier                 = E(CodeInvalidIdentifier)
	ErrEmailRegistrationUnavailable      = E(CodeEmailRegistrationUnavailable)
	ErrPhoneRegistrationUnavailable      = E(CodePhoneRegistrationUnavailable)
	ErrEmailVerificationSendFailed       = E(CodeEmailVerificationSendFailed)
	ErrPhoneVerificationSendFailed       = E(CodePhoneVerificationSendFailed)
	ErrInvalidTwoFAMethod                = E(CodeInvalidTwoFAMethod)
	ErrPhoneNumberRequired               = E(CodePhoneNumberRequired)
	ErrPhoneNumberMustBeE164             = E(CodePhoneNumberMustBeE164)
	ErrInvalidCode                       = E(CodeInvalidCode)
	ErrPhoneTwoFAUnavailable             = E(CodePhoneTwoFAUnavailable)
	ErrTwoFASetupCodeSendFailed          = E(CodeTwoFASetupCodeSendFailed)
	ErrTwoFAEnableFailed                 = E(CodeTwoFAEnableFailed)
	ErrTwoFAMethodUnavailable            = E(CodeTwoFAMethodUnavailable)
	ErrInternalError                     = E(CodeInternalError)
	ErrNotFound                          = E(CodeNotFound)
	ErrForbidden                         = E(CodeForbidden)
	ErrNotAuthenticated                  = E(CodeNotAuthenticated)
	ErrRateLimited                       = E(CodeRateLimited)
	ErrInvalidRequest                    = E(CodeInvalidRequest)
)

Sentinels — the identities Go callers match with errors.Is.

View Source
var ErrDelegationRefused = E(CodeDelegationRefused)

ErrDelegationRefused is returned (or wrapped) by a DelegationAuthorizer to refuse a mint as a policy decision; any other error is an authorizer outage.

View Source
var ErrInvalidRemoteApplication = E(CodeInvalidRemoteApplication)

ErrInvalidRemoteApplication indicates a malformed remote_application registration payload.

View Source
var ErrInvalidServiceJWT = E(CodeInvalidServiceJWT)

ErrInvalidServiceJWT indicates a presented service JWT failed verification.

Functions

func APIKeyMarker

func APIKeyMarker(prefix string) string

APIKeyMarker returns the leading marker that identifies an API key for the given application prefix: "<prefix>_st_" when prefix is non-empty, else "st_".

func DescribeCode added in v0.98.0

func DescribeCode(code Code) (status int, message string, ok bool)

DescribeCode reports a code's catalog status and message.

func ErrorTypeForStatus

func ErrorTypeForStatus(status int) string

ErrorTypeForStatus maps an HTTP status to its error-type category (the same inference openrails performs).

func FormatAPIKey

func FormatAPIKey(prefix, keyID, secret string) string

FormatAPIKey assembles the full presented token: <marker><key_id>_<secret>.

func HasAPIKeyPrefix

func HasAPIKeyPrefix(prefix, token string) bool

HasAPIKeyPrefix reports whether token carries the API-key marker for prefix. Used by middleware to route to the API-key path before attempting JWT verification.

func LanguageFromContext added in v0.99.0

func LanguageFromContext(ctx context.Context) (string, bool)

LanguageFromContext reads the request language attached by WithLanguage.

func ParseAPIKey

func ParseAPIKey(prefix, token string) (keyID, secret string, ok bool)

ParseAPIKey splits a presented token into its key_id and secret. key_id and secret are base62 (no underscores), so the first "_" after the marker is the unambiguous delimiter. ok is false if the token lacks the marker or either part is empty.

func PublicDisplayName added in v0.92.0

func PublicDisplayName(refs map[string]PublicUserRef, id string) string

PublicDisplayName renders id's display name against a PublicUsersByIDs result, including for ids the batch did not resolve at all (never-existed accounts, which are absent from the map rather than tombstoned). It is the whole author-name branch a caller would otherwise write around every lookup.

func ValidRemoteApplicationIssuer added in v0.98.0

func ValidRemoteApplicationIssuer(iss string) bool

ValidRemoteApplicationIssuer reports whether iss has the shape every registered remote-application issuer has: an absolute http(s) URL with a host, at most MaxRemoteApplicationIssuerLen bytes, no whitespace or control characters. Registration enforces it; the verifier applies the same rule to a token's self-asserted `iss` before any store lookup.

func WithLanguage added in v0.99.0

func WithLanguage(ctx context.Context, language string) context.Context

WithLanguage attaches a request language to ctx.

func WriteError added in v0.98.0

func WriteError(w http.ResponseWriter, err error)

WriteError writes err as the canonical error envelope — the ONE writer behind authhttp and verify.

Types

type APIKey

type APIKey struct {
	ID          string
	KeyID       string
	Name        string
	Role        Role
	Permissions []string
	CreatedBy   string
	CreatedAt   time.Time
	LastUsedAt  *time.Time
	ExpiresAt   *time.Time
	RevokedAt   *time.Time
}

type APIKeyMintOptions

type APIKeyMintOptions struct {
	Name      string
	Role      Role
	CreatedBy string
	ExpiresAt *time.Time
}

type AccountRegistrationInvite added in v0.72.0

type AccountRegistrationInvite struct {
	ID         string
	Email      string
	InvitedBy  string
	ExpiresAt  time.Time
	RevokedAt  *time.Time
	ConsumedAt *time.Time
	ConsumedBy *string
	// Persona/InstanceSlug/Role describe an OPTIONAL group role the code also grants
	// on consume (#147 register+join). Empty for a plain registration invite.
	Persona      Persona
	InstanceSlug string
	Role         Role
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

type AccountRegistrationInviteCreated added in v0.72.0

type AccountRegistrationInviteCreated struct {
	ID        string
	Code      string
	URL       string
	Email     string
	ExpiresAt time.Time
	// Persona/InstanceSlug/Role echo the optional group grant carried by the code.
	Persona      Persona
	InstanceSlug string
	Role         Role
}

type AccountSessionRevocation added in v0.102.0

type AccountSessionRevocation struct {
	// Issuers is the exact issuer scope covered, this deployment's first.
	Issuers []string `json:"issuers"`
	// RevokedSessions counts revoked refresh sessions per covered issuer.
	RevokedSessions map[string]int `json:"revoked_sessions"`
	// RevokedDeviceKeys counts revoked device keys; they are not issuer-bound.
	RevokedDeviceKeys int `json:"revoked_device_keys"`
	// UnlistedIssuerSessions counts live sessions left under issuers outside
	// Issuers; nonzero means the account issuer configuration is incomplete.
	UnlistedIssuerSessions int `json:"unlisted_issuer_sessions"`
}

AccountSessionRevocation reports an account-wide emergency revocation across the configured account issuers (TokenConfig.AccountIssuers). Issued access tokens are not revoked; they stay valid until they expire.

type ActionAvailability added in v0.98.0

type ActionAvailability struct {
	Action            string     `json:"action"`
	Allowed           bool       `json:"allowed"`
	Reason            string     `json:"reason,omitempty"`
	RetryAfterSeconds int64      `json:"retry_after_seconds,omitempty"`
	NextAllowedAt     *time.Time `json:"next_allowed_at,omitempty"`
	Limit             *int       `json:"limit,omitempty"`
	Remaining         *int       `json:"remaining,omitempty"`
	WindowSeconds     *int64     `json:"window_seconds,omitempty"`
	CooldownSeconds   *int64     `json:"cooldown_seconds,omitempty"`
}

type AdminListUsersResult

type AdminListUsersResult struct {
	Users  []AdminUser `json:"users"`
	Total  int64       `json:"total"`
	Limit  int         `json:"limit"`
	Offset int         `json:"offset"`
}

type AdminUser

type AdminUser struct {
	ID              string     `json:"id"`
	Email           *string    `json:"email"` // Nullable for phone-only users
	PhoneNumber     *string    `json:"phone_number"`
	Username        *string    `json:"username"`
	DiscordUsername *string    `json:"discord_username"`
	EmailVerified   bool       `json:"email_verified"`
	PhoneVerified   bool       `json:"phone_verified"`
	BannedAt        *time.Time `json:"banned_at,omitempty"`
	BannedUntil     *time.Time `json:"banned_until,omitempty"`
	BanReason       *string    `json:"ban_reason,omitempty"`
	BannedBy        *string    `json:"banned_by,omitempty"`
	DeletedAt       *time.Time `json:"deleted_at"`
	CreatedAt       time.Time  `json:"created_at"`
	UpdatedAt       time.Time  `json:"updated_at"`
	LastLogin       *time.Time `json:"last_login"`
	Roles           []string   `json:"roles"`
	RemovedRoles    []string   `json:"removed_roles,omitempty"`
	Entitlements    []string   `json:"entitlements"`
	// PreferredLanguage carries the user's stored language preference through from
	// the loaded user row, so callers (e.g. GET /me) need not issue a separate
	// language read (#228). Omitted from JSON when unset.
	PreferredLanguage *string `json:"preferred_language,omitempty"`
	// AvatarURL is the host-supplied avatar URL/key string (#262).
	AvatarURL *string `json:"avatar_url,omitempty"`
}

type AdminUserListOptions

type AdminUserListOptions struct {
	Page        int
	PageSize    int
	Search      string          // ILIKE over username/email/phone_number
	Role        Role            // root_role slug (e.g. "admin"); empty = no role filter
	Status      AdminUserStatus // empty = non-deleted (historical default)
	Sort        AdminUserSort   // empty = created_at
	Desc        bool            // true = descending
	Entitlement string          // empty = no entitlement filter; else provider-backed
}

type AdminUserSort

type AdminUserSort string

type AdminUserStatus

type AdminUserStatus string

type ApplicationDocument added in v0.88.0

type ApplicationDocument struct {
	// Slug is the REQUESTED handle — a free claim through the same
	// availability + anti-squat gates as any org (slugs and domains are
	// separate). Empty defaults to the serving domain's hostname.
	Slug string `json:"slug"`
	// DisplayName is free-form, non-unique metadata.
	DisplayName string `json:"display_name,omitempty"`
	// Issuer is the application's token `iss`; its host must be the serving
	// domain outside dev-like environments.
	Issuer string `json:"issuer"`
	// JWKSURI XOR PublicKeys: exactly one trust source.
	JWKSURI    string         `json:"jwks_uri,omitempty"`
	PublicKeys []RemoteAppKey `json:"public_keys,omitempty"`
	// DocumentEndpoint is the optional signed-document base URL.
	DocumentEndpoint string `json:"document_endpoint,omitempty"`
}

ApplicationDocument is the well-known application.json a self-registering application serves at https://<domain>/.well-known/authkit/application.json. Unknown fields are ignored (forward-compatible).

type BootstrapManifest

type BootstrapManifest struct {
	Users              []BootstrapManifestUser              `json:"users" yaml:"users"`
	RemoteApplications []BootstrapManifestRemoteApplication `json:"remote_applications" yaml:"remote_applications"`
	// Dev carries dev-only runtime fixtures (#266). NOT part of the apply-once
	// reconcile: hosts read it at every boot and honor it only in a dev
	// environment (fail-closed).
	Dev BootstrapManifestDev `json:"dev,omitempty" yaml:"dev,omitempty"`
}

type BootstrapManifestDev added in v0.89.0

type BootstrapManifestDev struct {
	// StaticEntitlements are entitlement names seeded into every access token
	// via a static EntitlementsProvider — billing/entitlement E2E fixtures as
	// reviewable YAML (formerly the AUTHKIT_STATIC_ENTITLEMENTS env CSV, #266).
	StaticEntitlements []string `json:"static_entitlements,omitempty" yaml:"static_entitlements,omitempty"`
}

BootstrapManifestDev is the dev-only fixture section of a bootstrap manifest.

type BootstrapManifestRemoteApplication

type BootstrapManifestRemoteApplication struct {
	Slug       string         `json:"slug" yaml:"slug"`
	Issuer     string         `json:"issuer" yaml:"issuer"`
	JWKSURI    string         `json:"jwks_uri" yaml:"jwks_uri"`
	PublicKeys []RemoteAppKey `json:"public_keys" yaml:"public_keys"`
	Enabled    *bool          `json:"enabled" yaml:"enabled"`
	RootRole   string         `json:"root_role" yaml:"root_role"`
}

type BootstrapManifestResult

type BootstrapManifestResult struct {
	DryRun              bool `json:"dry_run"`
	AlreadyApplied      bool `json:"already_applied"`
	UsersCreated        int  `json:"users_created"`
	UsersUpdated        int  `json:"users_updated"`
	PasswordsSet        int  `json:"passwords_set"`
	PasswordsKept       int  `json:"passwords_kept"`
	RootRoleAssignments int  `json:"root_role_assignments"`
	RemoteApplications  int  `json:"remote_applications"`
	RemoteAppRootRoles  int  `json:"remote_application_root_roles"`
}

type BootstrapManifestUser

type BootstrapManifestUser struct {
	Email         string                 `json:"email" yaml:"email"`
	PhoneNumber   string                 `json:"phone_number" yaml:"phone_number"`
	Username      string                 `json:"username" yaml:"username"`
	EmailVerified bool                   `json:"email_verified" yaml:"email_verified"`
	PhoneVerified bool                   `json:"phone_verified" yaml:"phone_verified"`
	Banned        bool                   `json:"banned" yaml:"banned"`
	BannedAt      *time.Time             `json:"banned_at" yaml:"banned_at"`
	BannedUntil   *time.Time             `json:"banned_until" yaml:"banned_until"`
	BanReason     *string                `json:"ban_reason" yaml:"ban_reason"`
	BannedBy      *string                `json:"banned_by" yaml:"banned_by"`
	Metadata      map[string]any         `json:"metadata" yaml:"metadata"`
	Password      *BootstrapUserPassword `json:"password" yaml:"password"`
	// RootRole assigns one root permission-group role to this user by name.
	// "owner" (the built-in apex, root:*) is seeded SEED-IF-ABSENT; any other
	// name is assigned as a same-named catalog role of the root persona.
	RootRole string `json:"root_role" yaml:"root_role"`
}

type BootstrapReconcileOptions

type BootstrapReconcileOptions struct {
	DryRun bool
	// StartupOnly applies initial seed data at most once per database schema.
	// Leave false for ordinary operator/CLI applies.
	StartupOnly bool
	// Name labels its completion receipt; another name does not rerun genesis.
	// Empty means "default".
	Name string
}

type BootstrapUserPassword

type BootstrapUserPassword struct {
	Plaintext     string `json:"plaintext" yaml:"plaintext"`
	Hash          string `json:"hash" yaml:"hash"`
	HashAlgo      string `json:"hash_algo" yaml:"hash_algo"`
	ResetRequired bool   `json:"reset_required" yaml:"reset_required"`
	// Enforce makes the password DESIRED-STATE (#89): re-asserted on every
	// reconcile. Default false = SEED-ONCE — the password is applied only when
	// the user is first created, so a password rotated out of band (via the
	// admin API) is never reverted to the manifest value on a later reconcile.
	// Must not be combined with ResetRequired (forcing a reset every run is
	// nonsensical).
	Enforce bool `json:"enforce" yaml:"enforce"`
}

type Client

type Client interface {
	// --- users ---
	CreateUser(ctx context.Context, email, username string) (*User, error)
	GetUserByEmail(ctx context.Context, email string) (*User, error)
	GetUserByPhone(ctx context.Context, phone string) (*User, error)
	GetUserByUsername(ctx context.Context, username string) (*User, error)
	// GetUserMetadata reads application-owned JSON under trusted host authority.
	// Hosts select public fields explicitly; the metadata map is not a public profile.
	GetUserMetadata(ctx context.Context, userID string) (map[string]any, error)
	// SoftDeleteUsers begins the fixed 30-day recoverable account lifecycle:
	// per-item BEST-EFFORT — the returned OpResults pinpoint the failures; the
	// outer error is a whole-call failure only (e.g. no store).
	SoftDeleteUsers(ctx context.Context, userIDs []string) ([]OpResult, error)
	MarkEmailVerified(ctx context.Context, id string) error
	// UpdateAvatarURL sets (or clears, with nil) the user's avatar URL/key
	// string (#262). Blob storage/validation is the host's job.
	UpdateAvatarURL(ctx context.Context, id string, avatarURL *string) error
	UpdateEmail(ctx context.Context, id, email string) error
	UpdateUsername(ctx context.Context, id, username string) error
	UpdateImportedUser(ctx context.Context, userID string, input ImportUserInput) (*User, error)
	ImportUsers(ctx context.Context, inputs []ImportUserInput) (ImportUsersResult, error)
	// UsersByIDs resolves many user IDs to slim display projections in ONE
	// query; missing IDs are absent. PRIVILEGED — the projection carries Email;
	// render other users with PublicUsersByIDs.
	UsersByIDs(ctx context.Context, ids []string) (map[string]UserRef, error)
	// PublicUsersByIDs is the PUBLIC-SAFE twin (#268): no email; soft-deleted
	// users come back as tombstones, banned users normally, unknown ids absent.
	PublicUsersByIDs(ctx context.Context, ids []string) (map[string]PublicUserRef, error)
	// UserLivenessByIDs is the batch account-liveness read behind verify's
	// per-request liveness gate (#267). Errors PROPAGATE so authorization
	// callers fail closed; unknown ids are absent and a gate treats that as a
	// denial.
	UserLivenessByIDs(ctx context.Context, ids []string) (map[string]UserLiveness, error)
	UpsertPasswordHash(ctx context.Context, userID, hash, algo string) error

	// --- admin directory ---
	AdminGetUser(ctx context.Context, id string) (*AdminUser, error)
	AdminListUsers(ctx context.Context, opts AdminUserListOptions) (*AdminListUsersResult, error)
	// AdminRevokeAccountSessions revokes the user's refresh sessions on every
	// account issuer plus device keys. Unchecked: the host authorizes the actor.
	AdminRevokeAccountSessions(ctx context.Context, userID string) (AccountSessionRevocation, error)
	AdminSetPassword(ctx context.Context, userID, new string) error
	BanUser(ctx context.Context, userID string, reason *string, until *time.Time, bannedBy string) error
	UnbanUser(ctx context.Context, userID string) error

	// --- trusted host operator operations ---
	// OperatorApplyBootstrapManifest reconciles authority under explicit trusted operator authority.
	// It is never invoked implicitly by runtime construction or exposed over HTTP.
	OperatorApplyBootstrapManifest(ctx context.Context, manifest BootstrapManifest, opts BootstrapReconcileOptions) (BootstrapManifestResult, error)
	// OperatorAssignGroupRole and OperatorUnassignGroupRole use trusted host-operator
	// authority, not a persona or role named operator. Hosts authorize the operator; request
	// actors use the corresponding actor-checked *As methods. Subject MFA and
	// final-owner invariants still apply. These methods add no HTTP exposure.
	OperatorAssignGroupRole(ctx context.Context, group GroupRef, subject Subject, role Role) error
	OperatorUnassignGroupRole(ctx context.Context, group GroupRef, subject Subject, role Role) error
	// OperatorRestoreUsers restores soft-deleted accounts before their fixed
	// recovery deadline under explicit trusted host authority.
	OperatorRestoreUsers(ctx context.Context, userIDs []string) ([]OpResult, error)

	// --- root roles (actor-checked) ---
	// Assign/RemoveRolesBySlugAs are batch-native (#219/#222): the no-escalation
	// check (#136) runs PER ITEM and each OpResult carries its own authority error.
	AssignRolesBySlugAs(ctx context.Context, actorUserID string, userIDs []string, role Role) ([]OpResult, error)
	RemoveRolesBySlugAs(ctx context.Context, actorUserID string, userIDs []string, role Role) ([]OpResult, error)
	UpsertRoleBySlug(ctx context.Context, name string, role Role, description *string) error
	// RoleSlugsByUsers returns each user's LIVE configured root role slugs in
	// ONE call (#220); users with no roles are absent; errors PROPAGATE (#136).
	RoleSlugsByUsers(ctx context.Context, userIDs []string) (map[string][]string, error)

	// --- permission groups ---
	CreatePermissionGroup(ctx context.Context, req CreatePermissionGroupRequest) (string, error)
	ResolveGroupIDForSlug(ctx context.Context, group GroupRef) (string, error)
	GroupInstanceForSlug(ctx context.Context, group GroupRef) (GroupInstance, error)
	UpdateGroupInstanceAs(ctx context.Context, actorUserID, groupID string, update GroupInstanceUpdate) (GroupInstance, error)
	// SoftDeleteGroupInstanceByID retires a nonroot subtree without removing its
	// rows or name reservations. Repeated calls retain the original DeletedAt.
	// The trusted host owns admission, retention and eventual hard deletion.
	SoftDeleteGroupInstanceByID(ctx context.Context, groupID string) (GroupInstance, error)
	// DeleteGroupInstanceByID is a trusted host-operator mutation.
	DeleteGroupInstanceByID(ctx context.Context, groupID string, opts DeletePermissionGroupOptions) error
	GroupInstanceByID(ctx context.Context, groupID string) (GroupInstance, error)
	AssignGroupRoleAs(ctx context.Context, actorUserID string, group GroupRef, subject Subject, role Role) error
	UnassignGroupRoleAs(ctx context.Context, actorUserID string, group GroupRef, subject Subject, role Role) error
	RemoveGroupSubjectAs(ctx context.Context, actorUserID string, group GroupRef, subject Subject) error
	ListGroupMembers(ctx context.Context, group GroupRef) ([]GroupMember, error)
	ListSubjectGroups(ctx context.Context, subject Subject) ([]SubjectGroupMembership, error)
	Can(ctx context.Context, subject Subject, group GroupRef, perm Perm) (bool, error)
	CanOnGroup(ctx context.Context, subject Subject, groupID string, perm Perm) (bool, error)
	ListEffectivePermissions(ctx context.Context, subject Subject, group GroupRef) ([]string, error)
	CreateGroupInviteLink(ctx context.Context, req CreateGroupInviteLinkRequest) (GroupInviteLinkCreated, error)
	ListGroupInviteLinks(ctx context.Context, group GroupRef) ([]GroupInviteLink, error)
	RevokeGroupInviteLink(ctx context.Context, group GroupRef, linkID string) error

	// --- tokens (#214: Mint* = signing a JWT; session creation is not a Mint) ---
	MintAccessToken(ctx context.Context, userID string, extra map[string]any) (string, time.Time, error)
	MintRemoteApplicationAccessToken(ctx context.Context, p RemoteApplicationAccessParams) (string, error)
	MintServiceJWT(ctx context.Context, opts ServiceJWTMintOptions) (string, ServiceJWTClaims, error)

	// --- API keys ---
	MintAPIKeyWithOptions(ctx context.Context, group GroupRef, opts APIKeyMintOptions) (APIKey, string, error)
	ListAPIKeys(ctx context.Context, group GroupRef) ([]APIKey, error)
	RevokeAPIKey(ctx context.Context, group GroupRef, tokenID string) (bool, error)
	ResolveAPIKey(ctx context.Context, keyID, secret string) (string, []string, error)

	// --- identity providers ---
	// ImportUnverifiedSolanaLinks preserves host migration associations without
	// turning them into credentials; only a subsequent SIWS proof verifies a link.
	ImportUnverifiedSolanaLinks(ctx context.Context, inputs []ImportUnverifiedSolanaLinkInput) (ImportUnverifiedSolanaLinksResult, error)
	LinkProviderByIssuer(ctx context.Context, userID, issuer, providerSlug, subject string, email *string) error

	// --- remote applications (federation issuers) ---
	UpsertRemoteApplication(ctx context.Context, in RemoteApplication) (*RemoteApplication, error)
	GetRemoteApplication(ctx context.Context, issuer string) (*RemoteApplication, error)
	ResolveRemoteApplicationAuthority(ctx context.Context, appID string) (RemoteApplicationAuthority, error)
}

Client is the portable application operation contract returned by embedded.Runtime.Client. The local implementation calls the private engine directly; a future remote implementation can preserve these typed operations. Inputs and results carry no process resources. Privileged operations require trusted host authority; this contract does not expose them over HTTP. Adding methods changes the contract implemented by consumer fakes.

type Code added in v0.98.0

type Code = errmodel.Code

func Codes added in v0.98.0

func Codes() []Code

Codes lists every catalogued code (authkit + documents), sorted.

type CreateAccountRegistrationInviteRequest added in v0.72.0

type CreateAccountRegistrationInviteRequest struct {
	Email     string
	InvitedBy string
	ExpiresIn time.Duration
	// Persona/InstanceSlug/Role, when all set, make this a register+join invite: the
	// minted code ALSO grants the given role in that permission group on consume
	// (#147). The minting actor must hold that group's members:manage (no-escalation);
	// a role-carrying invite does NOT require general root:users:invite. Leave empty
	// for a plain registration invite (root:users:invite gated).
	Persona      Persona
	InstanceSlug string
	Role         Role
}

type CreateGroupInviteLinkRequest

type CreateGroupInviteLinkRequest struct {
	Persona      Persona
	InstanceSlug string
	Role         Role
	ExpiresIn    time.Duration
	InvitedBy    string
}

type CreatePermissionGroupRequest

type CreatePermissionGroupRequest struct {
	Persona            Persona
	InstanceSlug       string
	ParentPersona      Persona
	ParentInstanceSlug string
	OwnerSubjectID     string
	// OwnerSubjectKind selects the owner principal kind: "user" (default) or
	// "remote_application" (#264 service-owned orgs — an application principal
	// owning its own permission group).
	OwnerSubjectKind SubjectKind
	// DisplayName is free-form, non-unique group metadata (#264 naming
	// doctrine: vanity naming lives here, never on the slug).
	DisplayName string
}

type CustomRoleDef added in v0.98.0

type CustomRoleDef struct {
	Role        Role
	Permissions []string
	RequiresMFA bool
}

CustomRoleDef defines (or redefines) a per-group custom role: its grant patterns, all in the group's persona namespace, and whether holding it requires an enrolled second factor (mirrors RoleDef.RequiresMFA, #247).

type DelegatedAccessParams

type DelegatedAccessParams struct {
	// Issuer becomes the `iss` claim: the AuthKit issuer that signed the token.
	// Must match a remote_application registered with the validating resource server.
	// Required when minting via the free function; the *Service mint method
	// defaults it to the Service's configured Issuer when empty.
	Issuer string
	// Audiences becomes the `aud` claim: the target resource API(s), e.g.
	// "openrails", "tensorhub", or "gen-orchestrator".
	Audiences []string
	// DelegatedSubject becomes `delegated_sub`: the issuer-side subject id.
	// Required. No local account is implied in the receiving service.
	DelegatedSubject string
	// Permissions becomes the `permissions` claim: an array of resource-defined
	// permission strings (NOT OAuth's space-delimited `scope`). Receiving
	// services validate these against their own permission set.
	Permissions []string
	// Documents becomes the top-level `documents` claim: versioned document
	// type -> canonical sha256 digest. AuthKit transports and validates these
	// references but does not resolve or interpret their payload schemas.
	Documents map[string]string
	// Attributes carries app-specific JSON inline. AuthKit transports values
	// without interpreting or resolving them; the consuming app owns their schema.
	// Reserved well-known keys: `tier` (opaque entitlement-tier string), `roles`
	// (a uuid array; prefer the typed Roles field below), and `documents` (use the
	// top-level Documents field above). Everything else is free-form per consuming
	// app. Values are arbitrary JSON.
	Attributes map[string]any
	// Roles is a convenience for emitting the delegated subject's role UUIDs into
	// `attributes.roles` (a JSON array of UUID strings). Equivalent to setting
	// Attributes["roles"] yourself; when both are set this typed field wins.
	Roles []string
	// TTL is the token lifetime. Defaults to 15m when zero.
	TTL time.Duration
	// JTI becomes the `jti` claim (token identifier). Optional to SET, but
	// always PRESENT on the minted token: when empty the minter generates a
	// fresh uuidv7, so a receiving service can revoke any delegated token by
	// id without a per-issuer "does this one have a jti" carve-out.
	JTI string
	// NotBefore, when set, becomes the `nbf` claim. Optional.
	NotBefore time.Time
	// ConfirmationCertificateSHA256, when set, binds the token to the delegate's
	// X.509 certificate as RFC 8705 `cnf.x5t#S256`; verification then requires
	// that exact leaf as the TLS peer. When both confirmation fields are nil,
	// the token is an unbound bearer.
	ConfirmationCertificateSHA256 *[32]byte
	// ConfirmationJWKThumbprintSHA256 binds the token to a DPoP key (RFC 9449).
	// Mutually exclusive with ConfirmationCertificateSHA256.
	ConfirmationJWKThumbprintSHA256 *[32]byte
}

type DelegationAuthorizer added in v0.98.0

type DelegationAuthorizer func(context.Context, DelegationRequest) (DelegationGrant, error)

DelegationAuthorizer is the single host seam of the delegated mint route.

type DelegationGrant added in v0.98.0

type DelegationGrant struct {
	Permissions []string
	Attributes  map[string]any
	Documents   map[string]string
}

DelegationGrant is the complete authority AuthKit signs for one request.

type DelegationRequest added in v0.98.0

type DelegationRequest struct {
	UserID                        string
	Audiences                     []string
	TTL                           time.Duration
	ConfirmationCertificateSHA256 [32]byte
	// ConfirmationJWKThumbprintSHA256 is set only after validating a DPoP proof.
	// DelegateCertificate is nil on this browser-capable path.
	ConfirmationJWKThumbprintSHA256 *[32]byte
	DelegateCertificate             *x509.Certificate
	RequestedGrant                  json.RawMessage
}

DelegationRequest is what POST /delegated/token asks the host to authorize (ak#277). Audiences and TTL are already clamped; the certificate or DPoP sender proof is validated; RequestedGrant is the client's opaque, host-schema object that AuthKit never copies into the token.

type DeletePermissionGroupOptions added in v0.88.0

type DeletePermissionGroupOptions struct {
	// ReleaseSlug applies to every canonical name in the deleted subtree;
	// prior aliases retain their original expiry.
	ReleaseSlug bool
}

DeletePermissionGroupOptions controls the delete-time naming rule (#264): by DEFAULT a deleted group's slug is TOMBSTONED to its uuid forever (fail-safe — published references can never be re-claimed by someone else). ReleaseSlug frees the name (and drops the group's own tombstones) instead; that is safe ONLY for names nothing ever referenced, and the judgment is the host's: a released name re-created by a different owner is live and "live slugs win" in slug resolution, so any dangling published reference to the old group now resolves to the new owner (#308). authkit never deletes a group on its own.

type DocumentEnvelope added in v0.86.0

type DocumentEnvelope = documents.Envelope

type DocumentReference added in v0.86.0

type DocumentReference = documents.Reference

type Error added in v0.98.0

type Error = errmodel.Error

func AsError added in v0.98.0

func AsError(err error) *Error

AsError returns the *Error in err's chain, or nil.

func E added in v0.98.0

func E(code Code, opts ...ErrorOption) *Error

E builds an Error for a catalogued code; the status comes from the catalog.

func Recode added in v0.98.0

func Recode(err error, code Code, opts ...ErrorOption) *Error

Recode re-tags err with a route-specific code, keeping err as the cause.

type ErrorEnvelope

type ErrorEnvelope struct {
	Error ErrorObject `json:"error"`
}

ErrorEnvelope is the top-level error response: {"error": {...}}.

func ErrorEnvelopeFor added in v0.98.0

func ErrorEnvelopeFor(err error) (int, ErrorEnvelope)

ErrorEnvelopeFor derives the wire status and envelope for err: an *Error keeps its status, code, param (the catalog's default when unset) and metadata; a plain 500 — and anything that is not an *Error — is emitted as internal_error, so the operation name never reaches the wire.

type ErrorObject

type ErrorObject struct {
	Type     string         `json:"type"`
	Code     string         `json:"code"`
	Message  string         `json:"message"`
	Param    *string        `json:"param,omitempty"`
	Metadata map[string]any `json:"metadata,omitempty"`
}

ErrorObject is the nested error detail carried under the top-level "error" key.

type ErrorOption added in v0.98.0

type ErrorOption = errmodel.Option

func WithCause added in v0.98.0

func WithCause(cause error) ErrorOption

func WithMeta added in v0.98.0

func WithMeta(key string, value any) ErrorOption

func WithMetadata added in v0.98.0

func WithMetadata(m map[string]any) ErrorOption

func WithParam added in v0.98.0

func WithParam(param string) ErrorOption

func WithStatus added in v0.98.0

func WithStatus(status int) ErrorOption

type FormerNameRetentionConfig added in v0.98.0

type FormerNameRetentionConfig struct {
	Mode     FormerNameRetentionMode `json:"mode,omitempty" koanf:"mode"`
	Duration *time.Duration          `json:"duration,omitempty" koanf:"duration"`
}

type FormerNameRetentionMode added in v0.98.0

type FormerNameRetentionMode string

FormerNameRetentionMode controls reservation and forwarding after a rename.

const (
	FormerNamesFinite          FormerNameRetentionMode = "finite"
	FormerNamesForever         FormerNameRetentionMode = "forever"
	FormerNamesImmediate       FormerNameRetentionMode = "immediate"
	DefaultRenameInterval                              = 72 * time.Hour
	DefaultFormerNameRetention                         = 90 * 24 * time.Hour
)

type GroupInstance added in v0.93.0

type GroupInstance struct {
	// DeletedAt marks retained inactive state; only trusted ID reads include it.
	DeletedAt    *time.Time
	ID           string
	Persona      Persona
	InstanceSlug string
	DisplayName  string
}

GroupInstance is one persona instance's own identity (#269): the addressing pair a caller already holds, plus the uuid a HOST needs to own rows about the group in its own (or a sibling service's) ledger — openrails' `customer_id` being the case that forced it. Group ids never appear in a PATH; this type is how a caller who already has authority over an instance LEARNS its id.

type GroupInstanceUpdate added in v0.98.0

type GroupInstanceUpdate struct {
	Slug        *string `json:"slug,omitempty"`
	DisplayName *string `json:"display_name,omitempty"`
}

GroupInstanceUpdate changes group settings atomically against a captured UUID.

type GroupInviteLink struct {
	ID                string
	PermissionGroupID string
	Role              Role
	InvitedBy         string
	// RedeemedAt is non-nil once the single-use link has been redeemed (#235;
	// replaces the former Uses 0/1 counter).
	RedeemedAt *time.Time
	ExpiresAt  *time.Time
	RevokedAt  *time.Time
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

type GroupInviteLinkCreated

type GroupInviteLinkCreated struct {
	ID   string
	Code string
	URL  string
}

type GroupMember

type GroupMember struct {
	SubjectID   string
	SubjectKind SubjectKind
	Role        Role
}

type GroupRef added in v0.98.0

type GroupRef struct {
	Persona  Persona
	Instance string
}

GroupRef addresses one permission-group instance: the persona plus its instance slug. The root group has no instance.

func RootGroup added in v0.98.0

func RootGroup() GroupRef

RootGroup addresses the deployment's root group.

func (GroupRef) IsRoot added in v0.98.0

func (g GroupRef) IsRoot() bool

func (GroupRef) String added in v0.98.0

func (g GroupRef) String() string

type ImportUnverifiedSolanaLinkInput added in v0.97.1

type ImportUnverifiedSolanaLinkInput struct {
	UserID          string
	Address         string
	Source          string
	SourceID        string
	SourceCreatedAt *time.Time
}

ImportUnverifiedSolanaLinkInput is a migration-only Solana identity claim. Importing reserves the address but does not make it a login method; the user must prove ownership through the normal SIWS flow before AuthKit trusts it.

type ImportUnverifiedSolanaLinkResult added in v0.97.1

type ImportUnverifiedSolanaLinkResult struct {
	Index   int
	UserID  string
	Address string
	Status  ImportUnverifiedSolanaLinkStatus
	Reason  string
}

ImportUnverifiedSolanaLinkResult is the outcome for one input row.

type ImportUnverifiedSolanaLinkStatus added in v0.97.1

type ImportUnverifiedSolanaLinkStatus string

ImportUnverifiedSolanaLinkStatus is the per-row outcome of a legacy Solana identity import.

const (
	ImportUnverifiedSolanaLinkInserted ImportUnverifiedSolanaLinkStatus = "inserted"
	ImportUnverifiedSolanaLinkSkipped  ImportUnverifiedSolanaLinkStatus = "skipped"
	ImportUnverifiedSolanaLinkRejected ImportUnverifiedSolanaLinkStatus = "rejected"
)

type ImportUnverifiedSolanaLinksResult added in v0.97.1

type ImportUnverifiedSolanaLinksResult struct {
	Results  []ImportUnverifiedSolanaLinkResult
	Inserted int
	Skipped  int
	Rejected int
}

ImportUnverifiedSolanaLinksResult aggregates per-row wallet import outcomes.

type ImportUserInput

type ImportUserInput struct {
	Email         string
	PhoneNumber   string
	Username      string
	EmailVerified bool
	PhoneVerified bool
	BannedAt      *time.Time
	BannedUntil   *time.Time
	BanReason     *string
	BannedBy      *string
	Metadata      map[string]any
	CreatedAt     *time.Time
	UpdatedAt     *time.Time

	// Optional pre-hashed credential to import alongside the user (bulk legacy
	// migration). When PasswordHash is non-empty and the user row is inserted,
	// ImportUsers stores it verbatim. The verify-time whitelist (argon2id/bcrypt,
	// else legacy-reset-required) still governs login; bulk import does not
	// re-validate the hash, matching single-row UpsertPasswordHash.
	PasswordHash string
	HashAlgo     string
}

type ImportUserResult

type ImportUserResult struct {
	Index  int
	UserID string // set when Status == inserted
	Status ImportUserStatus
	Reason string // set for skipped/rejected (machine-ish: "duplicate_in_batch", "already_exists", or a validation code)
}

type ImportUserStatus

type ImportUserStatus string

type ImportUsersResult

type ImportUsersResult struct {
	Results  []ImportUserResult
	Inserted int
	Skipped  int
	Rejected int
}

type InstanceCreationDef added in v0.90.0

type InstanceCreationDef struct {
	// Enabled mounts POST /<persona>. Off by default.
	Enabled bool
	// SlugPattern further restricts creatable slugs beyond the built-in
	// instance-slug rule: an unanchored regexp source, anchored (^...$) at
	// schema build. Empty = built-in rule only.
	SlugPattern string
	// ReservedSlugs are exact lowercase slugs creatable only by callers holding
	// ReservedEscalationRole in the root group (a list is config, a route is not).
	ReservedSlugs []string
	// ReservedEscalationRole is the root-group role that may create reserved
	// slugs. Empty = reserved slugs are not creatable through this route at all.
	ReservedEscalationRole Role
}

InstanceCreationDef opts a persona into the generated creation route (#263): POST /<persona> creates an instance with the authenticated user seeded as its owner. Only root-parented personas may enable it. AuthKit owns the anti-squat velocity limits (per-IP + per-user); host COST gates plug in through WithInstanceAdmission. Zero value = no creation route (existing behavior).

type ListPage added in v0.98.0

type ListPage[T any] struct {
	Object     string `json:"object"`
	Data       []T    `json:"data"`
	NextCursor string `json:"next_cursor,omitempty"`
}

ListPage is the one list envelope: {object:"list", data:[...], next_cursor?}. A present next_cursor means another page exists; pass it back as ?cursor=.

func NewListPage added in v0.98.0

func NewListPage[T any](items []T, nextCursor string) ListPage[T]

NewListPage wraps items (never null: an empty page marshals as []).

type MFAStatus

type MFAStatus struct {
	Enabled        bool
	Satisfied      bool
	AllowedMethods []string
}

type NameAdmissionRequest added in v0.98.0

type NameAdmissionRequest struct {
	OwnerKind     string
	Persona       Persona
	OwnerID       string // Empty only before a new group/account is created.
	ActorID       string
	CurrentName   string
	RequestedName string
	Operation     NameOperation
}

NameAdmissionRequest is the namespace admission hook's operation context. Group creation cost/enrollment hooks remain creation-only.

type NameAlias added in v0.98.0

type NameAlias struct {
	Name      string     `json:"name"`
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
}

type NameOperation added in v0.98.0

type NameOperation string
const (
	NameCreate NameOperation = "create"
	NameRename NameOperation = "rename"
)

type NameResolution added in v0.98.0

type NameResolution struct {
	ID             string     `json:"id"`
	CanonicalName  string     `json:"canonical_name"`
	IsAlias        bool       `json:"is_alias"`
	AliasExpiresAt *time.Time `json:"alias_expires_at,omitempty"`
}

NameResolution always addresses one immutable owner. An alias points directly to that owner; CanonicalName reflects its current spelling, never an alias chain. AliasExpiresAt is nil for canonical names and permanent aliases; IsAlias tells them apart. Expired aliases are not resolutions.

type NamingConfig added in v0.98.0

type NamingConfig struct {
	Enabled        *bool                     `json:"enabled,omitempty" koanf:"enabled"`
	RenameInterval *time.Duration            `json:"rename_interval,omitempty" koanf:"rename_interval"`
	FormerNames    FormerNameRetentionConfig `json:"former_names,omitempty" koanf:"former_names"`
}

NamingConfig is deployment-wide policy for users and group instances. Pointers distinguish omission (defaults) from explicit false/zero. Durations use Go time.Duration in embedded configuration.

func (NamingConfig) Normalize added in v0.98.0

func (c NamingConfig) Normalize() (NamingPolicy, error)

Normalize validates once at the configuration boundary. An empty retention object or finite-without-duration selects 90 days. Duration-without-mode means finite; finite zero means immediate. Other modes cannot specify a duration.

type NamingPolicy added in v0.98.0

type NamingPolicy struct {
	Enabled                 bool
	RenameInterval          time.Duration
	FormerNameRetentionMode FormerNameRetentionMode
	FormerNameRetention     time.Duration
}

NamingPolicy is the validated, immutable in-process policy. HTTP responses expose NamingPolicyInfo through NamingState instead of Go duration values.

func (NamingPolicy) CheckRename added in v0.98.0

func (p NamingPolicy) CheckRename(lastRenamedAt *time.Time, now time.Time) error

CheckRename is shared by user/group mutations under their owner lock. Callers authorize and detect a same-canonical-name no-op before checking this policy. Trusted import updates skip this check, never namespace ownership checks.

func (NamingPolicy) FormerNameExpiresAt added in v0.98.0

func (p NamingPolicy) FormerNameExpiresAt(now time.Time) *time.Time

FormerNameExpiresAt captures the promise made at rename time. Nil means forever. Immediate returns now: request-time lookup/claim use the same strict now.Before(deadline) boundary. Later policy changes never rewrite this value.

func (NamingPolicy) State added in v0.98.0

func (p NamingPolicy) State(last *time.Time, now time.Time) NamingState

type NamingPolicyInfo added in v0.100.0

type NamingPolicyInfo struct {
	Enabled                    bool                    `json:"enabled"`
	FormerNameRetentionMode    FormerNameRetentionMode `json:"former_name_retention_mode"`
	FormerNameRetentionSeconds float64                 `json:"former_name_retention_seconds"`
}

NamingPolicyInfo is the policy information used by account/settings UIs. Rename timing is already reported by NextRenameAt and RetryAfterSeconds; the current cadence is available in the action's cooldown_seconds field.

type NamingState added in v0.98.0

type NamingState struct {
	Aliases           []NameAlias      `json:"aliases,omitempty"`
	Policy            NamingPolicyInfo `json:"policy"`
	Allowed           bool             `json:"allowed"`
	NextRenameAt      *time.Time       `json:"next_rename_at,omitempty"`
	RetryAfterSeconds int64            `json:"retry_after_seconds"`
}

type OpResult added in v0.80.0

type OpResult struct {
	ID  string
	Err error
}

OpResult is the per-item outcome of a batch mutation (#219/#222): batch writes return one OpResult per requested ID so partial failure is expressible — a bare single error on a bulk write would hide which item failed. Err == nil means the item succeeded.

As JSON, Err marshals as its wire code (#197), so errors.Is against authkit sentinels survives the round-trip; a non-Error (or a 500) degrades to internal_error.

func (OpResult) MarshalJSON added in v0.80.0

func (r OpResult) MarshalJSON() ([]byte, error)

func (*OpResult) UnmarshalJSON added in v0.80.0

func (r *OpResult) UnmarshalJSON(b []byte) error

type PasswordlessStartRequest

type PasswordlessStartRequest struct {
	Identifier         string
	Mode               string
	ReturnTo           string
	PreferredLanguage  string
	AccountInviteToken string
}

type PasswordlessStartResult

type PasswordlessStartResult struct {
	Sent    bool
	Channel string
	Code    string
	LinkURL string
}

type Perm added in v0.98.0

type Perm string

Perm is a concrete permission (`org:members:read`) or a grant pattern (`org:members:*`, `org:*`).

func (Perm) Matches added in v0.98.0

func (p Perm) Matches(grant Perm) bool

Matches reports whether grant authorizes this CONCRETE permission. The grant may be a literal (`org:members:read`) or a namespace-anchored glob where `*` wildcards a whole segment (`org:members:*`, `org:*:read`, `org:*`). The namespace (segment 0) must be a literal — a bare `*` (or a `*` namespace) never matches. A two-segment glob `ns:*` matches every concrete `ns:…` perm.

This is the shared, authz-critical matcher used by both the engine's RBAC checks and the verification layer's permission-coverage checks.

func (Perm) Persona added in v0.98.0

func (p Perm) Persona() Persona

Persona returns the permission's first segment: its namespace.

type Persona added in v0.98.0

type Persona string

Persona is a permission-group persona name (`root`, `org`, `merchant`): the first permission segment and the namespace every grant is anchored in.

func (Persona) OwnerGrant added in v0.98.0

func (p Persona) OwnerGrant() Perm

OwnerGrant is the namespace-pure owner grant for a persona: `<persona>:*`.

type PersonaCapabilities added in v0.72.0

type PersonaCapabilities struct {
	APIKeys            bool
	RemoteApplications bool
	CustomRoles        bool
}

PersonaCapabilities are opt-in generated management capabilities for a persona.

type PreferredLanguage

type PreferredLanguage struct {
	Language string
}

type Principal added in v0.72.0

type Principal struct {
	Kind    PrincipalKind `json:"kind"`
	Issuer  string        `json:"issuer,omitempty"`
	Subject string        `json:"subject,omitempty"`
}

Principal is the small generic-auth shape host adapters expose.

type PrincipalKind added in v0.72.0

type PrincipalKind string

PrincipalKind is the broad AuthKit credential class for a verified request.

const (
	PrincipalKindUser              PrincipalKind = "user"
	PrincipalKindAPIKey            PrincipalKind = "api_key"
	PrincipalKindRemoteApplication PrincipalKind = "remote_application"
	PrincipalKindDelegated         PrincipalKind = "delegated"
	PrincipalKindService           PrincipalKind = "service"
)

type PublicUserRef added in v0.92.0

type PublicUserRef struct {
	ID string
	// Username is "" when unset OR when the user is a tombstone — see Deleted.
	// Prefer DisplayName over reading this directly.
	Username  string
	AvatarURL string // "" if unset or tombstoned
	CreatedAt time.Time
	// Deleted marks a TOMBSTONE: the id resolved to a soft-deleted account, so
	// the reference is not dangling — but every other field is zero, including
	// CreatedAt. Nothing about a deleted account is published. Callers render
	// DisplayName and show nothing else.
	Deleted bool
}

PublicUserRef is the PUBLIC-SAFE batch user projection (#268): the display identity of a user as other users may see it. It deliberately has NO email field — the type, not an `omitempty` tag or a caller's discipline, is what makes it safe to nest inside a response body.

Every field here is public by nature: a username, an avatar and a join date. Derived assets (thumbnail sizes, CDN rewrites) stay host-owned — authkit stores one avatar string (#262) and does not know a host's image pipeline.

func (PublicUserRef) DisplayName added in v0.92.0

func (r PublicUserRef) DisplayName() string

DisplayName is the name to render for a user, with the fallback both consumer hosts had independently hand-rolled: the username when there is one, else a stable, non-identifying `user-<first 8 of id>`. Tombstoned and unnamed users take the fallback.

type RedeemGroupInviteLinkResult

type RedeemGroupInviteLinkResult struct {
	Persona      Persona
	InstanceSlug string
	Role         Role
}

type RegisteredApplication added in v0.88.0

type RegisteredApplication struct {
	Application     RemoteApplication
	OrgPersona      Persona
	OrgInstanceSlug string
	// Created is false for an idempotent re-registration (the boot-time
	// self-heal / rotation-from-root path).
	Created bool
}

RegisteredApplication is the result of a (re-)registration: the application row plus its service-owned org (the permission group the application principal owns).

type RegistrationMode added in v0.72.0

type RegistrationMode string

RegistrationMode is the public native-user self-registration policy (#147). It governs ONLY public self-registration; operators can always create users through privileged APIs, bootstrap, or manual DB operations regardless of mode.

Open       — anyone may self-register.
InviteOnly — self-registration requires a valid unbound account-registration
             invite code.
Closed      — no public self-registration at all.

The former AdminOnly / AdminBootstrapOnly / ManifestOnly modes were removed (#147): they described operator-side creation, not a public self-registration policy, and are subsumed by "use the privileged APIs" under any mode.

const (
	RegistrationModeOpen       RegistrationMode = "open"
	RegistrationModeInviteOnly RegistrationMode = "invite_only"
	RegistrationModeClosed     RegistrationMode = "closed"
)

type RegistrationVerificationPolicy added in v0.72.0

type RegistrationVerificationPolicy string

RegistrationVerificationPolicy controls whether a newly-registered contact must be verified.

const (
	RegistrationVerificationNone     RegistrationVerificationPolicy = "none"
	RegistrationVerificationOptional RegistrationVerificationPolicy = "optional"
	RegistrationVerificationRequired RegistrationVerificationPolicy = "required"
)

type RemoteAppKey

type RemoteAppKey struct {
	KID          string `json:"kid,omitempty" yaml:"kid,omitempty"`
	PublicKeyPEM string `json:"public_key_pem" yaml:"public_key_pem"`
}

RemoteAppKey is one entry of a static-mode principal's human-managed key list (stored as jsonb; edited like an authorized_keys file).

type RemoteApplication

type RemoteApplication struct {
	ID                string
	Slug              string
	PermissionGroupID string // controlling permission-group id
	Issuer            string // OIDC iss
	JWKSURI           string // OIDC jwks_uri (jwks mode only)
	// Mode is the trust source: RemoteAppModeJWKS (fetch from JWKSURI) XOR
	// RemoteAppModeStatic (human-managed PublicKeys list). Never both.
	Mode string
	// PublicKeys is the static-mode key list (empty in jwks mode).
	PublicKeys []RemoteAppKey
	Enabled    bool
	// DisplayName is free-form, non-unique vanity metadata (#264). The slug is
	// the public handle; the uuid is the internal join key.
	DisplayName string
	// Tier is the application's capability tier: ApplicationTierRegistered
	// (self-registered; zero default capability — authenticate + documents
	// only) or ApplicationTierApproved (an admin act on the host).
	Tier string
	// TrustRoot is what can rotate this application's keys (#264):
	// ApplicationTrustRootManual (admin/bootstrap-managed),
	// ApplicationTrustRootDomain (re-fetching Domain's application.json
	// re-proves control and adopts current keys), or
	// ApplicationTrustRootUser (the owning user's authenticated session).
	// Never the keypair alone.
	TrustRoot string
	// Domain is the trust-root location for domain-rooted applications (the
	// canonical registration input; empty otherwise). Domains and slugs are
	// SEPARATE: the domain proves identity, the slug is a claimed handle.
	Domain string
	// DocumentEndpoint is the application's optional signed-document base URL
	// declared in its application.json.
	DocumentEndpoint string
	// RootVerifiedAt is the last successful trust-root proof (zero when the
	// root was never proven, e.g. manual registrations).
	RootVerifiedAt time.Time
	CreatedAt      time.Time
	UpdatedAt      time.Time
}

RemoteApplication is a registered federation principal: an external issuer authkit trusts to mint delegated/remote-application tokens. It is a plain data view; persistence and lifecycle live in core.

type RemoteApplicationAccessParams

type RemoteApplicationAccessParams struct {
	// Issuer becomes the `iss` claim: the remote_application's OIDC issuer,
	// registered with the validating resource server. Required when minting via
	// the free function; the *Service mint method defaults it to the Service's
	// configured Issuer when empty.
	Issuer string
	// Audiences becomes the `aud` claim: the target resource API(s).
	Audiences []string
	// TTL is the token lifetime. Defaults to 15m when zero.
	TTL time.Duration
	// JTI, when set, becomes the `jti` claim. Optional.
	JTI string
	// NotBefore, when set, becomes the `nbf` claim. Optional.
	NotBefore time.Time
	// Permissions, when non-nil, becomes the `permissions` claim: a DOWN-SCOPING
	// request for least-privilege (#76 amendment). The stored grant is the
	// ceiling; effective = this claim, but EVERY claimed perm must be within the
	// stored grant — an out-of-grant claimed perm REJECTS the token at verify (a
	// remote application access token can never widen). nil/absent => no claim
	// => full stored ceiling (backward-compatible with v0.28.0 tokens).
	Permissions []string
}

type RemoteApplicationAuthority added in v0.83.0

type RemoteApplicationAuthority struct {
	PermissionGroupID string
	AuthorityIssuer   string
	Permissions       []string
	Persona           Persona
	InstanceSlug      string
}

RemoteApplicationAuthority is a remote_application's STORED authority: its role-resolved effective permissions plus the owning permission-group INSTANCE they are bound to (#248). InstanceSlug is "" for singleton personas (root). Exact-instance binding only; descendant/walk-down authority is deliberately deferred.

type ResolvedAPIKey

type ResolvedAPIKey struct {
	APIKeyID string
	KeyID    string
	// PermissionGroupID is the controlling permission-group id.
	PermissionGroupID string
	AuthorityIssuer   string
	// Persona / InstanceSlug identify the owning permission-group INSTANCE the
	// key was minted on (#248). InstanceSlug is "" for singleton personas (root).
	// The verify layer binds the key's token-carried permissions to this exact
	// instance; descendant/walk-down authority is deliberately deferred.
	Persona      Persona
	InstanceSlug string
	Role         Role
	Permissions  []string
}

ResolvedAPIKey is the API-key resolution result. Permissions is the key's role resolved to its effective permission set AT VERIFY TIME (so a role edit is reflected immediately — perms are never frozen into the key).

type Role added in v0.98.0

type Role string

Role is a role slug in a persona's catalog (`owner`, `admin`) or a group's custom-role name.

type ServiceJWTClaims

type ServiceJWTClaims struct {
	Issuer      string
	Subject     string
	Audiences   []string
	IssuedAt    time.Time
	NotBefore   time.Time
	ExpiresAt   time.Time
	JTI         string
	TokenUse    string
	Permissions []string
	Scope       []string
}

ServiceJWTClaims is the canonical AuthKit claim shape for caller-minted machine-to-machine JWTs. Permissions are requested capabilities; receiving services must still intersect them with server-side grants.

type ServiceJWTMintOptions

type ServiceJWTMintOptions struct {
	Subject     string
	Audiences   []string
	Permissions []string
	Lifetime    time.Duration
	NotBefore   time.Time
	IssuedAt    time.Time
	JTI         string
}

type Session

type Session struct {
	ID                  string
	FamilyID            string
	CreatedAt           time.Time
	LastAuthenticatedAt *time.Time
	LastUsedAt          time.Time
	ExpiresAt           *time.Time
	RevokedAt           *time.Time
	UserAgent           *string
	IPAddr              *string
}

Session is a sanitized session view (no tokens). Part of the wire contract.

type SignedDocument added in v0.86.0

type SignedDocument = documents.SignedDocument

type SolanaLinkedAccount added in v0.98.0

type SolanaLinkedAccount struct {
	Provider            string     `json:"provider"`
	Issuer              string     `json:"issuer"`
	Address             string     `json:"address"`
	Verified            bool       `json:"verified"`
	VerifiedAt          *time.Time `json:"verified_at"`
	PrimarySNSName      *string    `json:"primary_sns_name"`
	SNSResolutionStatus string     `json:"sns_resolution_status"`
	SNSResolvedAt       *time.Time `json:"sns_resolved_at"`
	SNSStale            bool       `json:"sns_stale"`
	SNSError            *string    `json:"sns_error"`
}

SolanaLinkedAccount is the AuthKit-owned normalized metadata for a SIWS-linked wallet.

type StepUpTwoFactorOption added in v0.98.0

type StepUpTwoFactorOption struct {
	Method         string `json:"method"`
	IsDefault      bool   `json:"is_default,omitempty"`
	VerificationID string `json:"verification_id,omitempty"`
}

type StepUpTwoFactorOptions added in v0.98.0

type StepUpTwoFactorOptions struct {
	Methods       []string                `json:"methods,omitempty"`
	DefaultMethod string                  `json:"default_method,omitempty"`
	Options       []StepUpTwoFactorOption `json:"options,omitempty"`
}

StepUpTwoFactorOptions lists the second factors a step-up can use.

type Subject added in v0.98.0

type Subject struct {
	ID   string
	Kind SubjectKind
}

Subject is a principal that can hold roles in a permission group.

func RemoteAppSubject added in v0.98.0

func RemoteAppSubject(id string) Subject

func UserSubject added in v0.98.0

func UserSubject(id string) Subject

type SubjectGroupMembership

type SubjectGroupMembership struct {
	// GroupID is the instance's internal uuid (#269). It is a JOIN KEY, not an
	// address — every route stays slug-addressed — and it is reported only for
	// the caller's OWN memberships.
	GroupID      string
	Persona      Persona
	InstanceSlug string
	DisplayName  string
	Role         Role
}

type SubjectKind added in v0.98.0

type SubjectKind string

SubjectKind discriminates who holds a role in a permission group.

type TokenSet added in v0.98.0

type TokenSet struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int64  `json:"expires_in"`
	RefreshToken string `json:"refresh_token,omitempty"`
}

TokenSet is the one session-token envelope. A session-establishing route returns it as the whole body, or under "token_set" when the response says more (registration, step-up, device keys, SIWS/passwordless extras).

func NewTokenSet added in v0.98.0

func NewTokenSet(access, refresh string, exp time.Time) TokenSet

NewTokenSet builds a Bearer TokenSet whose expires_in is derived from exp.

type TwoFactorMethod added in v0.72.0

type TwoFactorMethod string

TwoFactorMethod is one second-factor channel a host enables.

const (
	TwoFactorEmail TwoFactorMethod = "email"
	TwoFactorSMS   TwoFactorMethod = "sms"
	TwoFactorTOTP  TwoFactorMethod = "totp"
)

type TwoFactorMode added in v0.72.0

type TwoFactorMode string

TwoFactorMode is the host's account-wide 2FA enrollment policy.

const (
	// TwoFactorDisabled turns 2FA off entirely: no user enrollment/challenge/
	// verify routes are usable.
	TwoFactorDisabled TwoFactorMode = "disabled"
	// TwoFactorOptional lets users enroll a second factor if they choose; an
	// un-enrolled user is not blocked from normal session use.
	TwoFactorOptional TwoFactorMode = "optional"
	// TwoFactorRequired forces every user to enroll a second factor before normal
	// session use. Existing un-enrolled users are challenged on their next
	// authenticated request (the session, not just signup, is gated).
	TwoFactorRequired TwoFactorMode = "required"
)

type User

type User struct {
	ID              string
	Email           *string // Nullable - phone-only users have NULL email
	PhoneNumber     *string
	Username        *string
	DiscordUsername *string
	EmailVerified   bool
	PhoneVerified   bool
	BannedAt        *time.Time
	BannedUntil     *time.Time
	BanReason       *string
	BannedBy        *string
	DeletedAt       *time.Time
	CreatedAt       time.Time
	UpdatedAt       time.Time
	LastLogin       *time.Time
	// PreferredLanguage is populated by the by-ID lookup (UserByID) only; other
	// lookups leave it nil. Nullable — NULL/unset when the user has no stored
	// language preference.
	PreferredLanguage *string
	// AvatarURL is the host-supplied avatar URL/key string (#262). Blob storage
	// stays host-owned; authkit stores only this string. Populated by the by-ID
	// lookup (UserByID) only, like PreferredLanguage.
	AvatarURL *string
}

User is the public user view returned by AuthKit lookups. Plain data: see #138 (contract inversion) — definitions live here in the lean, pgx-free contract package; the embedded engine aliases back to these.

type UserDeletion added in v0.124.0

type UserDeletion struct {
	ID        string
	UserID    string
	DeletedAt time.Time
	PurgeAt   time.Time
}

UserDeletion identifies one recoverable account-deletion generation. Hooks are delivered at least once. Use ID together with the hook name as an idempotency key; deleting again after restoration creates a new ID.

type UserLiveness added in v0.92.0

type UserLiveness struct {
	ID string
	// Allowed is the same account gate that guards token mint at login and
	// refresh: false for deleted, banned and reserved accounts. An expired
	// temporary ban is allowed (and cleared, exactly as the single-user gate
	// clears it).
	Allowed       bool
	Username      string // "" if unset
	Email         string // "" if unset (phone-only accounts have none)
	EmailVerified bool
	AvatarURL     string // "" if unset
}

UserLiveness is the per-request account-liveness verdict for one user, plus the identity fields that are fresh AS OF that same lookup (#267). It is what verify's liveness gate consumes, and what lets a host stop reaching for an admin-privileged read just to refresh a username or email onto a request.

The verdict is deliberately BOOLEAN: it does not report whether a denial was a ban, a deletion or a reservation. That distinction is an account-status question for an authenticated, entitled surface — not something an authentication gate should hand back to whoever presented the token.

type UserProfile added in v0.98.0

type UserProfile struct {
	ID                  string               `json:"id"`
	Username            string               `json:"username"`
	Email               *string              `json:"email"`
	PhoneNumber         *string              `json:"phone_number"`
	EmailVerified       bool                 `json:"email_verified"`
	PhoneVerified       bool                 `json:"phone_verified"`
	HasPassword         bool                 `json:"has_password"`
	DiscordUsername     *string              `json:"discord_username,omitempty"`
	SolanaAddress       *string              `json:"solana_address,omitempty"`
	SolanaLinkedAccount *SolanaLinkedAccount `json:"solana_linked_account,omitempty"`
	LinkedProviders     []string             `json:"linked_providers,omitempty"`
	EnabledProviders    []string             `json:"enabled_providers,omitempty"`
	Roles               []string             `json:"roles"`
	Entitlements        []string             `json:"entitlements"`
	AvatarURL           *string              `json:"avatar_url,omitempty"`
	UserAliases         []string             `json:"user_aliases,omitempty"`
	PreferredLanguage   *string              `json:"preferred_language,omitempty"`
	CreatedAt           *string              `json:"created_at,omitempty"`
	Naming              NamingState          `json:"naming"`
	Availability        []ActionAvailability `json:"availability,omitempty"`
	Security            UserSecurity         `json:"security"`
}

UserProfile is the caller's own account as GET /me returns it: identity, contact state, linked providers, roles/entitlements, naming state, cooldown-gated action availability, and the security view.

type UserRef added in v0.66.0

type UserRef struct {
	ID       string
	Username string // "" if unset
	Email    string // "" if unset
}

UserRef is a slim user projection (id + display fields) returned by batch lookups like Client.UsersByIDs — resolving many user IDs to display data in one query, without N+1 single fetches. Part of the wire contract.

It carries Email, so it is the PRIVILEGED batch projection: use it only where the caller is entitled to see addresses (admin surfaces, the account's own views). For anything rendered to other users — comment authors, gallery owners, public profiles — use PublicUserRef / Client.PublicUsersByIDs (#268), which has no email field at all.

type UserSecurity added in v0.98.0

type UserSecurity struct {
	LastAuthenticatedAt               *string                 `json:"last_authenticated_at,omitempty"`
	TimeUntilStepUpRequired           *int64                  `json:"time_until_step_up_required,omitempty"`
	StepUpRequiredForSensitiveActions bool                    `json:"step_up_required_for_sensitive_actions"`
	StepUpMethods                     []string                `json:"step_up_methods,omitempty"`
	StepUp2FA                         *StepUpTwoFactorOptions `json:"step_up_2fa,omitempty"`
	MFAEnabled                        bool                    `json:"mfa_enabled"`
	MFASatisfied                      bool                    `json:"mfa_satisfied"`
	MFAAllowedMethods                 []string                `json:"mfa_allowed_methods,omitempty"`
}

UserSecurity is the session/step-up/MFA view of the caller's own account, nested under UserProfile.Security.

type UsernamePolicy added in v0.130.0

type UsernamePolicy struct {
	MinLength int
	MaxLength int
}

UsernamePolicy is the operator-configured username length rule. Zero fields take defaults.

func (UsernamePolicy) Derive added in v0.130.0

func (p UsernamePolicy) Derive(s string) string

Derive turns arbitrary text into a username that satisfies Validate.

func (UsernamePolicy) Normalize added in v0.130.0

func (p UsernamePolicy) Normalize() (UsernamePolicy, error)

Normalize fills defaults and rejects an inconsistent policy.

func (UsernamePolicy) Validate added in v0.130.0

func (p UsernamePolicy) Validate(username string) error

Validate checks a trimmed username against the length policy and UsernamePattern. Length failures carry min_length/max_length metadata.

func (UsernamePolicy) ValidateImport added in v0.130.0

func (p UsernamePolicy) ValidateImport(username string) error

ValidateImport checks an operator-provisioned username: the configured minimum, a maximum of at least 64, and hyphens are also allowed.

func (UsernamePolicy) WithSuffix added in v0.130.0

func (p UsernamePolicy) WithSuffix(base, suffix string) string

WithSuffix appends suffix to a derived username, trimming base (and, under a very small maximum, suffix) so the result still satisfies Validate.

Directories

Path Synopsis
adapters
fiber
Package authkitfiber bridges AuthKit's net/http middleware to Fiber v3.
Package authkitfiber bridges AuthKit's net/http middleware to Fiber v3.
gin
Package authkitgin bridges AuthKit's net/http middleware to Gin.
Package authkitgin bridges AuthKit's net/http middleware to Gin.
http
Package authkithttp mounts a configured local Runtime on net/http or Chi.
Package authkithttp mounts a configured local Runtime on net/http or Chi.
twilio/internal/twiliocommon
Package twiliocommon holds the small helpers shared by AuthKit's Twilio email and SMS sender adapters (which are separate packages): request-language resolution, the app display label, and the default outbound HTTP client.
Package twiliocommon holds the small helpers shared by AuthKit's Twilio email and SMS sender adapters (which are separate packages): request-language resolution, the app display label, and the default outbound HTTP client.
riverjobs module
Package authprovider defines the external identity providers AuthKit's browser login flows delegate to.
Package authprovider defines the external identity providers AuthKit's browser login flows delegate to.
Package testing provides utilities for testing applications that use authkit.
Package testing provides utilities for testing applications that use authkit.
cmd
authkit-migrate command
Command authkit-migrate is AuthKit's repository-owned migration runner.
Command authkit-migrate is AuthKit's repository-owned migration runner.
Package documents defines AuthKit's generic immutable signed-document wire contract.
Package documents defines AuthKit's generic immutable signed-document wire contract.
Package dpop verifies the ES256/P-256 profile of RFC 9449 sender proofs.
Package dpop verifies the ES256/P-256 profile of RFC 9449 sender proofs.
Package embedded is the AuthKit engine: the concrete *engine a host constructs with New and holds directly, and the authkit/authhttp transport mounts.
Package embedded is the AuthKit engine: the concrete *engine a host constructs with New and holds directly, and the authkit/authhttp transport mounts.
internal
db
errmodel
Package errmodel is the one error model every AuthKit package speaks (ak#290): a Code, an Error carrying that code plus its HTTP status, an optional offending param, machine metadata and a cause, and the catalog that fixes each code's status and message.
Package errmodel is the one error model every AuthKit package speaks (ak#290): a Code, an Error carrying that code plus its HTTP status, an optional offending param, machine metadata and a cause, and the catalog that fixes each code's status and message.
migrations/postgres
Package postgres embeds AuthKit's private PostgreSQL schema migrations.
Package postgres embeds AuthKit's private PostgreSQL schema migrations.
netguard
Package netguard is the single outbound-network policy for AuthKit: the private/reserved address list, the resolve-then-dial SSRF guard, and the timeout-bounded HTTP client every package uses for fetches it does not fully control (JWKS, application documents, IdP endpoints).
Package netguard is the single outbound-network policy for AuthKit: the private/reserved address list, the resolve-then-dial SSRF guard, and the timeout-bounded HTTP client every package uses for fetches it does not fully control (JWKS, application documents, IdP endpoints).
passkeytest
Package passkeytest is a software WebAuthn authenticator for passkey integration tests: it answers real registration and assertion ceremonies with a P-256 key, so tests exercise the production ceremony code paths.
Package passkeytest is a software WebAuthn authenticator for passkey integration tests: it answers real registration and assertion ceremonies with a P-256 key, so tests exercise the production ceremony code paths.
siws
Package siws implements Sign In With Solana (SIWS) authentication.
Package siws implements Sign In With Solana (SIWS) authentication.
testclock
Package testclock is a settable clock for tests that would otherwise sleep through a TTL, grace window or rate-limit window.
Package testclock is a settable clock for tests that would otherwise sleep through a TTL, grace window or rate-limit window.
testdb
Package testdb owns AuthKit's Postgres integration-test harness.
Package testdb owns AuthKit's Postgres integration-test harness.
testdpop
Package testdpop creates genuine signed sender proofs for workflow tests.
Package testdpop creates genuine signed sender proofs for workflow tests.
testhttp
Package testhttp constructs isolated local runtimes for HTTP adapter tests.
Package testhttp constructs isolated local runtimes for HTTP adapter tests.
Package oidckit holds the browser-flow state shared by authhttp and its ephemeral stores: the pending-login record, its cache contract, and PKCE generation.
Package oidckit holds the browser-flow state shared by authhttp and its ephemeral stores: the pending-login record, its cache contract, and PKCE generation.
internal/commongen command
Command commongen regenerates ../../common_passwords.txt.gz from pinned SecLists (MIT) lists: run `go generate ./password`.
Command commongen regenerates ../../common_passwords.txt.gz from pinned SecLists (MIT) lists: run `go generate ./password`.
memory
Package memorylimiter is the in-memory sliding-window rate limiter over ratelimit.Limit buckets.
Package memorylimiter is the in-memory sliding-window rate limiter over ratelimit.Limit buckets.
redis
Package redislimiter is the Redis-backed sliding-window rate limiter over ratelimit.Limit buckets.
Package redislimiter is the Redis-backed sliding-window rate limiter over ratelimit.Limit buckets.

Jump to

Keyboard shortcuts

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