realmid

package module
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: May 8, 2026 License: MIT Imports: 20 Imported by: 0

README

@realmid/sdk — Go

Go SDK for verifying RealmID-issued JWTs. Sibling TypeScript SDK lives at ../ts/.

Install

go get github.com/Realm-ID/sdk/go

Usage

package main

import (
    "log"

    realmid "github.com/Realm-ID/sdk/go"
)

func main() {
    v, err := realmid.NewVerifier(realmid.Config{
        BaseURL:  "https://auth.realmid.dev",
        Audience: "your-partner-audience",
    })
    if err != nil {
        log.Fatal(err)
    }

    claims, err := v.Verify(accessToken)
    if err != nil {
        var verr *realmid.Error
        if errors.As(err, &verr) {
            // verr.Code in {malformed, wrong_algorithm, bad_signature,
            //   wrong_issuer, wrong_audience, expired, not_yet_valid,
            //   unknown_kid, jwks_fetch_failed}
        }
        return
    }

    // claims.Subject, claims.TenantID, claims.Role, claims.Extra["..."]
}

Runtime

Stdlib only — no third-party dependencies. Go 1.22+.

HTTP middleware

The full Realm handle (realmid.New(...)) ships an http.Handler middleware that handles /login, /logout, /token (refresh), and /mfa/verify end-to-end and verifies bearer tokens on every other route. Mount it once on your mux:

realm, err := realmid.New(realmid.Config{
    RealmID: os.Getenv("REALM_ID"),
    APIKey:  os.Getenv("REALM_API_KEY"),
})
if err != nil { log.Fatal(err) }

mw := realm.Middleware(realmid.MiddlewareOptions{
    ExemptPaths:       []string{"/health", "/public/*"},
    MFAProtectedPaths: []realmid.MFARule{{Path: "/admin/*"}},
    TokenDelivery:     "cookie", // or "body" for native / mobile clients
    // CookieName/Domain/Secure/SameSite all configurable; defaults are
    // realmid_refresh, HttpOnly, Secure=true, SameSite=Lax.
})

mux := http.NewServeMux()
mux.Handle("/me", mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    claims, _ := realmid.ClaimsFrom(r.Context())
    json.NewEncoder(w).Encode(claims)
})))
http.ListenAndServe(":3000", mux)

In "cookie" mode (default) the refresh token is set as HttpOnly; Secure; SameSite=Lax so browser JS can never read it and XSS cannot exfiltrate it. Use "body" only when a cookie isn't viable — native apps, CLIs, or truly cross-origin SPAs. See SPEC §10.2 for the full decision table.

What's in scope

Verifier-only callers can stay on realmid.NewVerifier(...) — no network calls beyond JWKS. The full handle (realmid.New(...)) layers the auth surface, management API, and the middleware above.

Tests

go test ./...

License

MIT — see the LICENSE at the repo root.

Documentation

Overview

Package realmid — error taxonomy (SPEC §3).

Every failure surfaced by the SDK is a *RealmError. Callers branch on Code (a stable string from the taxonomy in SPEC §3.1) and read envelope siblings (e.g. mfa_challenge_token) from Details directly, avoiding a second JSON parse.

Partner OTP primitive client (issue / view / verify) — see docs/proposals/partner-otp-primitive.md in the auth repo for design.

All three calls require a tenant-scoped user/service identity. The SDK supports both shapes:

  • UserBearer: legacy / public-client mode. The user's access JWT rides as Authorization: Bearer.
  • UserID: BFF mode. The SDK uses its cached platform token as bearer and forwards X-On-Behalf-Of-User: <UserID>.

Mirrors the dual-token semantics of the existing user-scoped endpoints (sessions list / revoke).

Package realmid is the Go SDK for Realm ID — covers login, refresh, MFA, verify, and the management surface (tenants, users, invitations, domains, API keys, config).

A partner application using this SDK should never need to call auth.realmid.dev directly. Construct one *Realm at startup with a realm id and API key; every operation on that handle threads the dual-token (API-key → short-lived platform-token) flow internally so the raw API key never crosses login traffic.

Usage:

realm, err := realmid.NewRealm(realmid.Config{
    RealmID: "01HXYZ...",
    APIKey:  "rk_live_...",
})
if err != nil { ... }
claims, err := realm.Verify(ctx, accessToken, nil)

Stdlib only.

Package realmid — platform-defined custom roles (ADR-040).

Realms own a `realm_roles` catalog. `RoleOwner` and `RoleMember` are the only system roles; everything else is partner-defined per realm. Use the named constants for the load-bearing system names; everywhere else `Role` is just a string alias so partners can declare their own names (e.g. "salesman", "dispatch") and pass them through.

Package realmid — access-token revocation cache (SPEC §6.7).

Server-side, RealmID revokes refresh tokens via POST /auth/logout. Access tokens are stateless RS256 JWTs, so once minted they verify on signature + exp alone until natural expiry. This client adds partner-side defense-in-depth: on logout, the access token's jti is parked in an in-memory cache with TTL = exp - now(). Subsequent requests presenting that jti are rejected without a server round trip.

Multi-pod caveat: this cache is per-process. A logout served by pod A does not propagate to pod B; a stolen access token can still be replayed against pod B for up to its remaining TTL. v1.1 will ship a Redis-backed swap-in for cross-pod coherence.

Index

Constants

View Source
const DefaultBaseURL = "https://auth.realmid.dev"

DefaultBaseURL is the canonical Realm ID issuer host.

View Source
const Version = "0.10.0"

Version is the published SDK version (semver). 0.10.0 corresponds to RealmID v0.7.0 — the two-endpoint auth surface (ADR-051). Breaking: the SDK now hits POST /auth/login {grant_type:"platform_api_key"} instead of the deleted POST /auth/platform-token, then refreshes via POST /auth/token with the refresh-token bearer. Refresh rotation is gated by the realm's `platform_refresh_rotates` config (default off, non-rotating). 0.5.0 was the platforms-namespace cut (ADR-044) and the signup_mode enum (ADR-045).

Variables

View Source
var (
	ErrRoleNotFound        = errors.New("realmid: role not found")
	ErrRoleExists          = errors.New("realmid: role already exists")
	ErrRoleInUse           = errors.New("realmid: role still attached to users/invitations")
	ErrSystemRoleImmutable = errors.New("realmid: system role is immutable")
)

Typed errors mirrored from the server taxonomy. Use errors.Is to branch on these.

View Source
var ErrTokenRevoked = errors.New("realmid: access token revoked")

ErrTokenRevoked is returned by TokensClient.GateRequest when the access token's jti is in the per-process revoked cache. Wrapped in a *RealmError so callers can unify on RealmError-style branching; errors.Is(err, ErrTokenRevoked) also works.

Functions

func IsCode

func IsCode(err error, code ErrorCode) bool

IsCode reports whether err is a *RealmError with the given Code.

func NormalizeOrigin

func NormalizeOrigin(raw string) string

NormalizeOrigin lowercases an origin and strips scheme + port + path to a bare hostname. Mirrors `domainmapping.Normalize` server-side.

Types

type APIKey

type APIKey struct {
	ID          string `json:"id"`
	DisplayName string `json:"display_name,omitempty"`
	Prefix      string `json:"prefix,omitempty"`
	// Secret is only present on creation.
	Secret    string   `json:"secret,omitempty"`
	Scopes    []string `json:"scopes,omitempty"`
	CreatedAt string   `json:"created_at,omitempty"`
	RevokedAt string   `json:"revoked_at,omitempty"`
}

APIKey is one entry returned from realm.APIKeys.* (SPEC §6.5).

type APIKeyCreate

type APIKeyCreate struct {
	DisplayName string   `json:"display_name"`
	Scopes      []string `json:"scopes,omitempty"`
}

APIKeyCreate is the create payload.

type APIKeysClient

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

APIKeysClient is realm.APIKeys.

func (*APIKeysClient) Create

func (c *APIKeysClient) Create(ctx context.Context, body APIKeyCreate) (*APIKey, error)

func (*APIKeysClient) List

func (c *APIKeysClient) List(ctx context.Context) ([]APIKey, error)

List returns every API key associated with this realm. List endpoints for api-keys are usually small and unpaginated; we accept either the {items, next_cursor} shape or a flat array.

func (*APIKeysClient) Revoke

func (c *APIKeysClient) Revoke(ctx context.Context, id string) error

type AdminClient added in v0.10.0

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

AdminClient is realm.Admin. ADR-048 / SPEC §7.5.

func (*AdminClient) ListEvents added in v0.10.0

ListEvents wraps GET /admin/events.

func (*AdminClient) ListPlatforms added in v0.10.0

ListPlatforms wraps GET /admin/platforms.

func (*AdminClient) Search added in v0.10.0

func (c *AdminClient) Search(ctx ctxpkg.Context, q string, limit int) (*AdminSearchResponse, error)

Search wraps GET /admin/search. limit ≤ 0 omits the param (server default applies).

func (*AdminClient) Stats added in v0.10.0

func (c *AdminClient) Stats(ctx ctxpkg.Context) (*AdminStats, error)

Stats wraps GET /admin/stats.

type AdminEventsResponse added in v0.10.0

type AdminEventsResponse struct {
	Items      []AuditEvent `json:"items"`
	NextCursor *string      `json:"next_cursor"`
}

AdminEventsResponse is the GET /admin/events envelope.

type AdminPlatformsResponse added in v0.10.0

type AdminPlatformsResponse struct {
	Items      []PlatformSummary `json:"items"`
	NextCursor *string           `json:"next_cursor"`
	Total      int               `json:"total"`
}

AdminPlatformsResponse is the GET /admin/platforms envelope.

type AdminSearchResponse added in v0.10.0

type AdminSearchResponse struct {
	Items []SearchHit `json:"items"`
}

AdminSearchResponse is the GET /admin/search response.

type AdminStats added in v0.10.0

type AdminStats struct {
	PlatformsCount int `json:"platforms_count"`
	TenantsCount   int `json:"tenants_count"`
	UsersCount     int `json:"users_count"`
	SessionsActive int `json:"sessions_active"`
	Events24h      int `json:"events_24h"`
}

AdminStats is the GET /admin/stats response.

type AuditEvent added in v0.10.0

type AuditEvent struct {
	ID          int64  `json:"id"`
	OccurredAt  int64  `json:"occurred_at"`
	Kind        string `json:"kind"`
	ActorUserID string `json:"actor_user_id,omitempty"`
	ActorLabel  string `json:"actor_label,omitempty"`
	PlatformID  string `json:"platform_id,omitempty"`
	TenantID    string `json:"tenant_id,omitempty"`
	TargetType  string `json:"target_type,omitempty"`
	TargetID    string `json:"target_id,omitempty"`
	Summary     string `json:"summary,omitempty"`
}

AuditEvent is one row in AdminEventsResponse.Items.

type AuthClient

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

AuthClient implements realm.Auth.* per SPEC §4.

func (*AuthClient) ListSessions

func (a *AuthClient) ListSessions(ctx ctxpkg.Context, req ListSessionsRequest) iter.Seq2[*SessionInfo, error]

ListSessions iterates sessions for the user named in req. Public- client realms set UserBearer; BFF realms set UserID and the SDK attaches the platform token + X-On-Behalf-Of-User (ADR-041 §7).

func (*AuthClient) Login

func (a *AuthClient) Login(ctx ctxpkg.Context, req LoginRequest) (*Session, error)

Login exchanges a provider token for a realm-scoped session. On a 412 mfa_required, returns *RealmError{Code: mfa_required} with Details["mfa_challenge_token"] populated.

func (*AuthClient) Logout

func (a *AuthClient) Logout(ctx ctxpkg.Context, req *LogoutRequest) error

Logout revokes a refresh token. If req.RefreshToken is empty, the caller's cookie / current session is used (server-side).

When req.AccessToken is set AND a RevocationCache is configured on the Realm, the access token's jti is added to the cache on successful logout — bridging the gap between user logout and the access token's stateless natural expiry (ADR-041 follow-up). Failure to push to the cache does NOT fail the logout call; the server-side refresh revocation is the load-bearing operation.

func (*AuthClient) MFAVerify

func (a *AuthClient) MFAVerify(ctx ctxpkg.Context, req MFAVerifyRequest) (*Session, error)

MFAVerify completes an MFA challenge. Same response shape as Login.

func (*AuthClient) MFAVerifyOTP added in v0.10.0

func (a *AuthClient) MFAVerifyOTP(ctx ctxpkg.Context, req MFAVerifyOTPRequest) (*Session, error)

MFAVerifyOTP completes an otp_internal second-factor challenge. Same response shape as Login. The first-factor login response carries an `mfa_challenge_token` and a `methods` list including "otp_internal" when the user is enrolled (per-user mfa_methods or per-role required_mfa_methods, gated by realms.config.otp_mfa_enabled).

func (*AuthClient) MintMFAChallenge

func (a *AuthClient) MintMFAChallenge(ctx ctxpkg.Context, req MFAChallengeRequest) (string, []string, error)

MintMFAChallenge calls POST /auth/mfa/challenge to mint a step-up challenge token for the verified access token. Used by the middleware when an MFA-protected route receives a token that lacks fresh MFA proof. Returns ("", nil, error) when the server hasn't shipped the endpoint yet — the middleware downgrades to a generic 412 envelope without a pre-minted challenge.

func (*AuthClient) OTPLogin added in v0.10.0

func (a *AuthClient) OTPLogin(ctx ctxpkg.Context, req OTPLoginRequest) (*Session, error)

OTPLogin exchanges an identifier + manager-issued OTP for a realm- scoped session. Single-factor variant of the partner OTP primitive (proposal §3.2.1). Gated server-side by realms.config.otp_login_enabled.

func (*AuthClient) RevokeSession

func (a *AuthClient) RevokeSession(ctx ctxpkg.Context, req RevokeSessionRequest) error

RevokeSession removes a session by id. The caller identifies the user either via a user-bearer JWT (legacy / public-client realms) or via UserID + the SDK's platform token (BFF realms; ADR-041 §7).

func (*AuthClient) Token

func (a *AuthClient) Token(ctx ctxpkg.Context, req TokenRequest) (*MintResult, error)

Token rotates a refresh token, optionally switching tenants and merging custom claims into the minted access token.

type Claims

type Claims struct {
	Issuer          string   `json:"iss,omitempty"`
	Subject         string   `json:"sub,omitempty"`
	Audience        string   `json:"aud,omitempty"`
	IssuedAt        int64    `json:"iat,omitempty"`
	NotBefore       int64    `json:"nbf,omitempty"`
	Expiry          int64    `json:"exp,omitempty"`
	JWTID           string   `json:"jti,omitempty"`
	AuthorizedParty string   `json:"azp,omitempty"`
	TenantID        string   `json:"tenant_id,omitempty"`
	Role            string   `json:"role,omitempty"`
	AMR             []string `json:"amr,omitempty"`
	ACR             string   `json:"acr,omitempty"`
	// MFAAt is the unix-seconds timestamp of the user's most recent
	// successful MFA challenge in this session. Zero means absent — the
	// session never completed MFA, or the server hasn't been upgraded
	// yet to emit this claim. SPEC §10.4.
	MFAAt int64          `json:"mfa_at,omitempty"`
	Extra map[string]any `json:"-"`
}

Claims is the verified token payload. Standard JWT fields plus the RealmID-specific extras (azp, tenant_id, role). Unknown fields land in Extra.

func ClaimsFrom

func ClaimsFrom(ctx context.Context) (*Claims, bool)

ClaimsFrom extracts the verified Claims from a request context. The second return is false if the middleware did not run on this request.

func (*Claims) HasMFA

func (c *Claims) HasMFA() bool

HasMFA reports whether the verified claims indicate the user passed an MFA challenge — either via amr containing "mfa" or any non-empty acr. This is a shape check; for freshness-aware gating, the middleware uses the mfa_at claim per SPEC §10.4.

type Config

type Config struct {
	// RealmID — required. Your realm's UUID-ish identifier.
	RealmID string

	// APIKey — required. The realm's API key (rk_live_...). Never sent
	// over login traffic; the SDK exchanges it once for a short-lived
	// platform token (SPEC §4.0).
	APIKey string

	// BaseURL overrides the issuer host. Default: DefaultBaseURL.
	BaseURL string

	// Origin is the value attached to the Origin header on auth calls.
	// If unset, derived from realm.Info().Audience on first use.
	Origin string

	// Logger is the *slog.Logger the SDK emits diagnostics to.
	// Default: a slog logger over io.Discard (no-op).
	Logger *slog.Logger

	// HTTPClient overrides the underlying http.Client (handy in tests
	// for fake transports). Default: 30s timeout.
	HTTPClient *http.Client

	// Leeway is the verifier's clock-skew tolerance for exp/nbf checks.
	// Default 30s.
	Leeway time.Duration

	// Clock overrides time.Now. Useful in tests.
	Clock func() time.Time

	// Revocation is an optional JTI denylist consulted by Verify after
	// signature + claim checks (ADR-041 follow-up). Lets partners stop
	// the bleed on stolen access tokens between user logout and natural
	// JWT expiry. Nil → no-op; verifier behaves as before. Pass
	// NewMemRevocationCache(nil) for a single-process default, or supply
	// a Redis/memcached-backed implementation for multi-replica deploys.
	Revocation RevocationCache
}

Config configures NewRealm. RealmID and APIKey are required; the rest have sensible defaults.

type ConfigClient

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

ConfigClient is realm.Config.

func (*ConfigClient) Update

func (c *ConfigClient) Update(ctx context.Context, patch ConfigPatch) error

Update issues PATCH /platforms/{id}/config.

type ConfigPatch

type ConfigPatch map[string]any

ConfigPatch is a partial patch of realm-level configuration (SPEC §6.5). The server enforces an allowlist of mutable keys; unknown keys are rejected with a 400.

type DomainClaim

type DomainClaim struct {
	Hostname   string     `json:"hostname"`
	ClaimToken string     `json:"claim_token,omitempty"`
	TxtRecord  *DomainTxt `json:"txt_record,omitempty"`
	Status     string     `json:"status,omitempty"`
}

DomainClaim is the response from realm.Domains.Claim. The TXT record fields (when populated) tell the partner what to provision in DNS to complete the verify step.

type DomainTxt

type DomainTxt struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

DomainTxt is the {name, value} pair the partner must publish at DNS for the verify step.

type DomainVerifyResult

type DomainVerifyResult struct {
	Hostname string `json:"hostname,omitempty"`
	Verified bool   `json:"verified,omitempty"`
	Status   string `json:"status,omitempty"`
}

DomainVerifyResult is returned from realm.Domains.Verify after the server reads the published TXT record.

type DomainsClient

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

DomainsClient is realm.Domains (SPEC §6.4).

func (*DomainsClient) Claim

func (c *DomainsClient) Claim(ctx context.Context, hostname string) (*DomainClaim, error)

Claim begins a domain claim and returns the TXT record to publish.

func (*DomainsClient) Verify

func (c *DomainsClient) Verify(ctx context.Context, claimToken string) (*DomainVerifyResult, error)

Verify finishes a previously-started domain claim.

type ErrorCode

type ErrorCode string

ErrorCode is a stable, machine-readable identifier for an SDK failure.

const (
	// Verifier codes (SPEC §3.1).
	ErrCodeMalformed       ErrorCode = "malformed"
	ErrCodeWrongAlgorithm  ErrorCode = "wrong_algorithm"
	ErrCodeBadSignature    ErrorCode = "bad_signature"
	ErrCodeWrongIssuer     ErrorCode = "wrong_issuer"
	ErrCodeWrongAudience   ErrorCode = "wrong_audience"
	ErrCodeExpired         ErrorCode = "expired"
	ErrCodeNotYetValid     ErrorCode = "not_yet_valid"
	ErrCodeUnknownKID      ErrorCode = "unknown_kid"
	ErrCodeJWKSFetchFailed ErrorCode = "jwks_fetch_failed"

	// Auth-flow codes.
	ErrCodeProviderTokenInvalid ErrorCode = "provider_token_invalid"
	ErrCodeMFARequired          ErrorCode = "mfa_required"
	ErrCodeSessionLimitReached  ErrorCode = "session_limit_reached"
	ErrCodeTenantRequired       ErrorCode = "tenant_required"
	ErrCodeTenantInvalid        ErrorCode = "tenant_invalid"
	ErrCodeAccountSuspended     ErrorCode = "account_suspended"
	ErrCodeAccountDeactivated   ErrorCode = "account_deactivated"
	ErrCodeRealmOriginMismatch  ErrorCode = "realm_origin_mismatch"
	ErrCodeMissingOrigin        ErrorCode = "missing_origin"

	// Partner OTP primitive (docs/proposals/partner-otp-primitive.md).
	ErrCodeInvalidOTP        ErrorCode = "invalid_otp"
	ErrCodeOTPExpired        ErrorCode = "otp_expired"
	ErrCodeOTPLocked         ErrorCode = "otp_locked"
	ErrCodeOTPNotFound       ErrorCode = "otp_not_found"
	ErrCodeInvalidPurpose    ErrorCode = "invalid_purpose"
	ErrCodeInvalidSubjectRef ErrorCode = "invalid_subject_ref"

	// Management / generic codes.
	ErrCodeUnauthorized ErrorCode = "unauthorized"
	ErrCodeForbidden    ErrorCode = "forbidden"
	ErrCodeNotFound     ErrorCode = "not_found"
	ErrCodeConflict     ErrorCode = "conflict"
	ErrCodeRateLimited  ErrorCode = "rate_limited"
	ErrCodeBadRequest   ErrorCode = "bad_request"
	ErrCodeNetwork      ErrorCode = "network"
	ErrCodeServerError  ErrorCode = "server_error"
)

type Invitation

type Invitation struct {
	ID       string `json:"id"`
	TenantID string `json:"tenant_id"`
	Email    string `json:"email"`
	Role     string `json:"role,omitempty"`
	Status   string `json:"status,omitempty"`
}

Invitation represents a pending tenant invite.

type InvitationCreate

type InvitationCreate struct {
	Email string `json:"email"`
	Role  string `json:"role,omitempty"`
}

InvitationCreate is the create payload for /tenants/{id}/invitations.

type InvitationsClient

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

InvitationsClient is realm.Tenants.Invitations.

func (*InvitationsClient) Create

func (c *InvitationsClient) Create(ctx context.Context, tenantID string, body InvitationCreate) (*Invitation, error)

func (*InvitationsClient) Delete

func (c *InvitationsClient) Delete(ctx context.Context, tenantID, invitationID string) error

func (*InvitationsClient) List

func (c *InvitationsClient) List(ctx context.Context, tenantID string) *Paginated[Invitation]

type IssueRequest added in v0.10.0

type IssueRequest struct {
	SubjectRef string
	Purpose    string

	// Auth: exactly one of UserID or UserBearer.
	UserID     string
	UserBearer string
	// OnBehalfOfIP, when set, rides as X-On-Behalf-Of-IP.
	OnBehalfOfIP string
}

IssueRequest names the entity an OTP is bound to and the partner-side purpose tag (free string, regex `^[a-z][a-z0-9_]{0,63}$`).

type IssueResponse added in v0.10.0

type IssueResponse struct {
	ID         string    `json:"id"`
	Value      string    `json:"value"`
	ExpiresAt  time.Time `json:"-"` // parsed from string field below
	ExpiresAtS string    `json:"expires_at"`
	Purpose    string    `json:"purpose"`
	SubjectRef string    `json:"subject_ref"`
}

IssueResponse mirrors api/internal/httpapi otpIssueResp.

type ListEventsParams added in v0.10.0

type ListEventsParams struct {
	PlatformID string
	TenantID   string
	ActorID    string
	Kind       []string
	Since      int64 // unix seconds; 0 → omit
	Until      int64
	Cursor     string
	Limit      int
}

ListEventsParams carries the filters for AdminClient.ListEvents.

type ListOriginsOptions

type ListOriginsOptions struct {
	RealmID string
	Cursor  string
	Limit   int
}

ListOriginsOptions parameterises Origins.List.

type ListPlatformsParams added in v0.10.0

type ListPlatformsParams struct {
	Q                  string
	Status             []string
	SignupMode         []string
	Domain             string
	OwnerUserID        string
	HasCustomDomain    *bool
	CreatedAfter       int64 // unix seconds; 0 → omit
	CreatedBefore      int64
	LastActivityAfter  int64
	LastActivityBefore int64
	Sort               string
	Cursor             string
	Limit              int
}

ListPlatformsParams carries the filter / pagination knobs for AdminClient.ListPlatforms (SPEC §7.5).

type ListSessionsRequest added in v0.7.0

type ListSessionsRequest struct {
	UserID       string
	UserBearer   string
	OnBehalfOfIP string
}

ListSessionsRequest selects the user whose sessions to list and how to attest the call.

Exactly one of UserID or UserBearer must be set:

  • UserID: BFF mode. The SDK uses its cached platform token as the bearer and sends X-On-Behalf-Of-User: <UserID>. Required when realm.config.require_bff_login=true (ADR-041 §7).
  • UserBearer: legacy / public-client mode. The user's access JWT rides as Authorization: Bearer. Subject is read from the JWT.

OnBehalfOfIP, when set, is forwarded as X-On-Behalf-Of-IP so the issuer's per-IP rate limits see the SPA's IP, not the BFF's egress (ADR-050 plan §8.2).

type LoginMethod

type LoginMethod string

LoginMethod is the upstream identity provider for a login call. "firebase" and "google" are supported today; others are roadmap.

const (
	LoginFirebase LoginMethod = "firebase"
	LoginGoogle   LoginMethod = "google"
)

type LoginRequest

type LoginRequest struct {
	Method        LoginMethod
	ProviderToken string
	Origin        string // optional override of the SDK-derived Origin header

	// TenantID disambiguates when the user is a member of multiple
	// tenants in the realm. When empty and the user has >1 tenants, the
	// auth server returns the tenant list (no tokens) so the caller can
	// re-POST with the chosen tenant_id.
	TenantID string
}

LoginRequest carries the inputs to realm.Auth.Login. Custom claims are NOT accepted on login (SPEC §4.1) — refresh-token identity only.

type LogoutFn

type LogoutFn func(ctx ctxpkg.Context, req *LogoutRequest) error

LogoutFn is the shape of AuthClient.Logout that TokensClient.RevokeOnLogout wraps. Decoupled as an alias so callers can also wrap their own logout helpers (e.g. ones that talk through a partner BFF).

type LogoutRequest

type LogoutRequest struct {
	RefreshToken string
	// AccessToken, when set, is pushed to the SDK's RevocationCache (if
	// configured) so the JWT's jti is rejected by Verify until natural
	// expiry. Bridges the gap between user logout and the access token's
	// stateless natural expiry per ADR-041 follow-up. The server-side
	// refresh revocation is independent and always happens.
	AccessToken string
}

LogoutRequest optionally targets a specific refresh token. If RefreshToken is empty, the server uses the cookie / current session.

type MFAChallengeRequest added in v0.7.0

type MFAChallengeRequest struct {
	AccessToken  string
	OnBehalfOfIP string
}

MFAChallengeRequest mints a step-up challenge for the user identified by AccessToken. OnBehalfOfIP, when set, is forwarded as X-On-Behalf-Of-IP for per-IP rate-limit attribution on /auth/mfa/challenge (ADR-050 plan §8.2).

type MFAEnrollResult

type MFAEnrollResult struct {
	Secret      string   `json:"secret,omitempty"`
	OtpauthURI  string   `json:"otpauth_uri,omitempty"`
	BackupCodes []string `json:"backup_codes,omitempty"`
}

MFAEnrollResult is the response from EnrollMFA.

type MFAGateReason

type MFAGateReason string

MFAGateReason mirrors the wire `reason` field on the 412 envelope.

const (
	MFAReasonNoMFA         MFAGateReason = "no_mfa"
	MFAReasonStaleMFA      MFAGateReason = "stale_mfa"
	MFAReasonFreshRequired MFAGateReason = "fresh_required"
)

type MFARule

type MFARule struct {
	Path         string
	MaxAge       time.Duration
	RequireFresh bool
}

MFARule is one entry in MiddlewareOptions.MFAProtectedPaths.

Per-route MFA freshness policy (SPEC §10.4):

  • MaxAge — accept any token whose mfa_at claim is at most that old. Zero means "use the realm-default freshness window" (MiddlewareOptions.MFADefaultMaxAge). Negative is treated as zero.
  • RequireFresh — require mfa_at within ~30s. Use for irreversible operations. Strict: a legacy amr/acr-only token (no mfa_at) cannot satisfy this — the gate has no way to prove freshness.

type MFAVerifyOTPRequest added in v0.10.0

type MFAVerifyOTPRequest struct {
	MFAToken     string
	Presented    string
	OnBehalfOfIP string
}

MFAVerifyOTPRequest is the input for AuthClient.MFAVerifyOTP (partner OTP proposal §3.2.2). The MFA challenge token comes from the prior /auth/login response; Presented is the OTP value the user typed.

type MFAVerifyRequest

type MFAVerifyRequest struct {
	ChallengeToken string
	Code           string
	Method         string // defaults to "totp"
	// OnBehalfOfIP forwards the end-user's IP to the issuer via
	// X-On-Behalf-Of-IP so per-IP rate limits on /auth/mfa/verify see the
	// SPA's IP rather than the BFF's egress (ADR-050 plan §8.2).
	OnBehalfOfIP string
}

MFAVerifyRequest carries an MFA challenge response (SPEC §4.3).

type MemRevocationCache

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

MemRevocationCache is a single-process implementation suitable for a single partner-API replica or for tests. Multi-replica deployments should wire a shared backend (Redis, etc.) by implementing the RevocationCache interface directly. Lazily evicts expired entries.

func NewMemRevocationCache

func NewMemRevocationCache(now func() time.Time) *MemRevocationCache

NewMemRevocationCache returns an empty MemRevocationCache. now is the clock; pass nil to default to time.Now.

func (*MemRevocationCache) IsRevoked

func (m *MemRevocationCache) IsRevoked(_ ctxpkg.Context, jti string) (bool, error)

IsRevoked implements RevocationCache.

func (*MemRevocationCache) Len

func (m *MemRevocationCache) Len() int

Len returns the current entry count. Useful for tests + instrumentation.

func (*MemRevocationCache) Revoke

func (m *MemRevocationCache) Revoke(_ ctxpkg.Context, jti string, expiresAt time.Time) error

Revoke implements RevocationCache.

type MiddlewareOptions

type MiddlewareOptions struct {
	// ExemptPaths is a list of glob patterns that bypass the
	// middleware entirely. Defaults to ["/health", "/public/*"].
	ExemptPaths []string

	// MFAProtectedPaths declares paths that require MFA. Each entry is
	// either a bare path string (sugar for {Path: s} — inherits the
	// realm-default freshness window) or a full MFARule for per-route
	// override. SPEC §10.4.
	MFAProtectedPaths []MFARule

	// MFADefaultMaxAge is the realm-wide default freshness window
	// applied to MFARule entries that omit MaxAge. Default 15 min.
	// Mirrors realms.config.mfa_session_ttl_seconds server-side.
	MFADefaultMaxAge time.Duration

	// LoginPath, LogoutPath, RefreshPath, MFAVerifyPath are the routes
	// the middleware handles directly (POST). Empty strings disable
	// the route.
	LoginPath     string // default "/login"
	LogoutPath    string // default "/logout"
	RefreshPath   string // default "/token"
	MFAVerifyPath string // default "/mfa/verify"

	// TokenDelivery is "cookie" (default) or "body". Cookie mode sets
	// a HttpOnly cookie carrying the refresh token; body mode returns
	// it inline in the JSON response.
	TokenDelivery string

	// CookieName, CookieDomain, CookieSecure, CookieSameSite control
	// the refresh-token cookie when TokenDelivery == "cookie".
	CookieName     string        // default "realmid_refresh"
	CookieDomain   string        // optional
	CookieSecure   bool          // default true
	CookieSameSite http.SameSite // default http.SameSiteLaxMode

	// OnAuthFailure overrides the default 401/412 response.
	OnAuthFailure func(http.ResponseWriter, *http.Request, *RealmError)
}

MiddlewareOptions configures Realm.Middleware (SPEC §10).

type MintResult

type MintResult struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	ExpiresIn    int    `json:"expires_in"`
	TenantID     string `json:"tenant_id"`
	Role         string `json:"role"`
}

MintResult is realm.Auth.Token's response.

type OTPClient added in v0.10.0

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

OTPClient implements the partner OTP primitive surface.

func (*OTPClient) Issue added in v0.10.0

func (c *OTPClient) Issue(ctx ctxpkg.Context, req IssueRequest) (*IssueResponse, error)

Issue mints a fresh OTP. The plaintext Value is returned exactly once — partners deliver it out-of-band (manager UI display, SMS, email).

func (*OTPClient) Verify added in v0.10.0

func (c *OTPClient) Verify(ctx ctxpkg.Context, req VerifyRequest) (*VerifyResponse, error)

Verify hash-matches a presented value against active OTP rows in (tenant, subject_ref, purpose). On success the matching row is consumed atomically — concurrent verifies can't double-spend.

The response carries IssuerUserID + IssuedAt so the partner backend can attribute the action to the human who minted the code without a follow-up RealmID query.

func (*OTPClient) View added in v0.10.0

func (c *OTPClient) View(ctx ctxpkg.Context, otpID string, opts ViewOptions) (*ViewResponse, error)

View returns the plaintext value of an OTP — issuer-scoped: only the user who minted the OTP can fetch it (the manager who issued the code, not a colleague). Cross-issuer / cross-tenant attempts return 404 with no info leak.

type OTPLoginRequest added in v0.10.0

type OTPLoginRequest struct {
	Identifier string
	Presented  string
	Origin     string
	TenantID   string
}

OTPLoginRequest is the input for AuthClient.OTPLogin (partner OTP proposal §3.2.1). RealmID is overridden via the Realm config; pass Identifier (email or E.164 phone) + Presented (the OTP value the user typed). Returns Session on success; an enumeration-safe invalid_credentials on miss.

type Origin

type Origin struct {
	ID             string  `json:"id"`
	Domain         string  `json:"domain"`
	EntityType     string  `json:"entity_type"`
	EntityID       string  `json:"entity_id"`
	VerificationID *string `json:"verification_id,omitempty"`
	CreatedAt      string  `json:"created_at,omitempty"`
	DetachedAt     *string `json:"detached_at,omitempty"`
}

Origin is one row in the per-realm origin allowlist (ADR-049 §A.7.2). A live row (DetachedAt == nil) means the bare hostname routes to the referenced realm or tenant entity — and, by extension, is permitted to make unauthenticated proxy calls to the partner backend that fronts platform-token-gated RealmID routes.

type OriginClaim added in v0.10.0

type OriginClaim struct {
	Domain         string `json:"domain"`
	Status         string `json:"status"`
	Method         string `json:"method"` // always "dns_txt"
	DNSRecordName  string `json:"dns_record_name"`
	DNSRecordValue string `json:"dns_record_value"`
}

OriginClaim is the response from OriginsClient.Claim. Realm-origin claims are DNS-TXT only — the html_file method is not exposed at the platform surface (see ADR-049 §"Method options").

type OriginsClient

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

OriginsClient implements ADR-047 §1.1 — partner-side origin allowlist enforcement. Concurrent-safe.

func (*OriginsClient) Claim added in v0.10.0

func (c *OriginsClient) Claim(ctx ctxpkg.Context, realmID, hostname string) (*OriginClaim, error)

Claim begins a realm-owned origin claim. The DV row is owner-scoped to the realm — any admin in this realm can complete the verify step and add the bind. Re-claiming returns the existing pending token (idempotent).

func (*OriginsClient) Invalidate

func (c *OriginsClient) Invalidate(realmID string)

Invalidate drops the cached allowlist for one realm (or all if "").

func (*OriginsClient) List

List returns a paginated iterator over the realm's live origin mappings. Wraps GET /platforms/{realmId}/origins (ADR-049 §A.7.2).

func (*OriginsClient) Validate

func (c *OriginsClient) Validate(ctx ctxpkg.Context, opts ValidateOriginOptions) (bool, error)

Validate reports whether the inbound origin is on the realm's allowlist. The cached allowlist is refreshed on cache miss + TTL expiry. On a 401 from the underlying list call, the platform token is invalidated and the call is retried once.

func (*OriginsClient) Verify added in v0.10.0

func (c *OriginsClient) Verify(ctx ctxpkg.Context, realmID, hostname string) (*DomainVerifyResult, error)

Verify drives the DNS check on the realm's pending DV row. Does NOT bind — call Origins.Bind (POST /platforms/{id}/origins) afterwards so a single verified apex can serve as the trust anchor for trusted-by-parent subdomain origins.

type Page

type Page[T any] struct {
	Items      []T    `json:"items"`
	NextCursor string `json:"next_cursor,omitempty"`
	Total      *int   `json:"total,omitempty"`
}

Page is one page of results in the locked wire shape (SPEC §7).

type PageOpts

type PageOpts struct {
	Cursor string
	Limit  int
}

PageOpts is the per-page input to a list endpoint's manual pager.

type Paginated

type Paginated[T any] struct {
	// contains filtered or unexported fields
}

Paginated wraps a list endpoint, exposing both an iterator and a manual .Page accessor so callers can choose either style.

func (*Paginated[T]) All

func (p *Paginated[T]) All(ctx context.Context) iter.Seq2[T, error]

All returns an iter.Seq2[T, error] that walks every page lazily.

for item, err := range list.All(ctx) { ... }

func (*Paginated[T]) Page

func (p *Paginated[T]) Page(ctx context.Context, opts *PageOpts) (*Page[T], error)

Page fetches a single page given the supplied cursor/limit.

type PassthroughOptions added in v0.8.0

type PassthroughOptions struct {
	// OnBehalfOfUser, when non-empty, rides as `X-On-Behalf-Of-User`
	// alongside the platform-token bearer. Use this from a BFF / partner
	// backend to attribute the call to a specific end-user (per ADR-041
	// §7 + ADR-050 plan §8.2).
	OnBehalfOfUser string

	// OnBehalfOfIP, when non-empty, rides as `X-On-Behalf-Of-IP`. Lets
	// the issuer's per-IP rate limits attribute to the SPA's IP rather
	// than the BFF's egress.
	OnBehalfOfIP string

	// UserBearer, when non-empty, REPLACES the platform-token bearer
	// with the supplied bearer (typically a user's access JWT or a
	// scoped one-shot token like a revocation_token). The platform
	// token is still minted (so the cache stays warm + token-mint
	// errors propagate), but the wire bearer is the user's. This is
	// the auth model needed for the session-limit-modal flow where the
	// auth server validates a one-shot revocation_token bearer.
	UserBearer string

	// Header carries any additional request headers to forward
	// verbatim (e.g. `Idempotency-Key`). Authorization is always
	// overwritten; do not set it here.
	Header http.Header
}

PassthroughOptions configures a single Realm.Do call.

type PlatformOwner added in v0.10.0

type PlatformOwner struct {
	UserID string `json:"user_id"`
	Name   string `json:"name"`
	Email  string `json:"email"`
}

PlatformOwner is the embedded owner block on a PlatformSummary.

type PlatformSummary added in v0.10.0

type PlatformSummary struct {
	ID             string        `json:"id"`
	DisplayName    string        `json:"display_name"`
	Slug           string        `json:"slug"`
	Status         string        `json:"status"`
	SignupMode     string        `json:"signup_mode"`
	Domains        []string      `json:"domains"`
	Owner          PlatformOwner `json:"owner"`
	TenantsCount   int           `json:"tenants_count"`
	UsersCount     int           `json:"users_count"`
	LastActivityAt int64         `json:"last_activity_at"`
	CreatedAt      int64         `json:"created_at"`
}

PlatformSummary is one row in AdminPlatformsResponse.Items.

type Realm

type Realm struct {
	Auth    *AuthClient
	Tenants *TenantsClient
	Domains *DomainsClient
	APIKeys *APIKeysClient
	Config  *ConfigClient
	Roles   *RolesClient
	Origins *OriginsClient
	Tokens  *TokensClient
	Admin   *AdminClient
	// OTP exposes the partner OTP primitive (issue / view / verify) —
	// see docs/proposals/partner-otp-primitive.md in the auth repo.
	OTP *OTPClient
	// contains filtered or unexported fields
}

Realm is the SDK handle. Construct with NewRealm; safe for concurrent use across goroutines.

func NewRealm

func NewRealm(cfg Config) (*Realm, error)

NewRealm constructs a *Realm from cfg.

func (*Realm) BaseURL

func (r *Realm) BaseURL() string

BaseURL returns the configured issuer host.

func (*Realm) Do added in v0.8.0

func (r *Realm) Do(ctx ctxpkg.Context, method, path string, body io.Reader, opts *PassthroughOptions) (*http.Response, error)

Do issues an authenticated request to the realm's API and returns the raw *http.Response. The platform token is minted (and cached) behind the scenes; callers must close resp.Body.

This is the escape hatch for BFF / proxy consumers that need to forward arbitrary admin-API calls without re-implementing the dual-token dance. Typed methods (Tenants, Roles, Origins, …) remain the recommended surface for application code.

`path` is joined to the realm's base URL (`/foo/bar?x=1`); a leading slash is added if missing. `body` may be nil. Non-2xx responses are returned with the status intact — the caller decides whether to map them through *RealmError.

func (*Realm) Info

func (r *Realm) Info(ctx context.Context) (*RealmInfo, error)

Info returns cached realm metadata. First call hits the network; subsequent calls reuse the cached value for the lifetime of the handle.

func (*Realm) Middleware

func (r *Realm) Middleware(opts MiddlewareOptions) func(http.Handler) http.Handler

Middleware returns an http.Handler middleware implementing SPEC §10.

func (*Realm) RealmID

func (r *Realm) RealmID() string

RealmID returns the configured realm id.

func (*Realm) Revocation

func (r *Realm) Revocation() RevocationCache

Revocation returns the configured shared revocation cache, or nil when the SDK was constructed without one. Partner code can push directly to the cache (e.g., on detected token theft outside the normal Logout path) by calling cache.Revoke(ctx, jti, exp).

func (*Realm) Verify

func (r *Realm) Verify(ctx context.Context, token string, opts *VerifyOptions) (*Claims, error)

Verify parses, signature-verifies, and claim-checks an access token. Audience is auto-discovered via realm.Info() unless opts.Audience is set. JWKS are cached for 10 minutes per realm, with unknown-kid forcing a refetch.

type RealmError

type RealmError struct {
	Code       ErrorCode
	Message    string
	HTTPStatus int
	Details    map[string]any
	Cause      error
}

RealmError is the unified error returned from every SDK operation.

func (*RealmError) Error

func (e *RealmError) Error() string

Error implements the error interface.

func (*RealmError) Unwrap

func (e *RealmError) Unwrap() error

Unwrap exposes the underlying cause for errors.Is / errors.As.

type RealmInfo

type RealmInfo struct {
	ID          string         `json:"id"`
	Audience    string         `json:"audience,omitempty"`
	Domain      string         `json:"domain,omitempty"`
	DisplayName string         `json:"display_name,omitempty"`
	Extra       map[string]any `json:"-"`
}

RealmInfo is realm metadata returned by realm.Info(). Audience is the canonical aud value for this realm — used for verifier auto-discovery (SPEC §1) and as the default Origin on auth calls.

type RevocationCache

type RevocationCache interface {
	// Revoke marks jti as revoked. expiresAt is the JWT's exp, used as
	// the cache entry TTL — partners' implementations should evict on
	// expiry so the cache never grows unboundedly.
	Revoke(ctx ctxpkg.Context, jti string, expiresAt time.Time) error
	// IsRevoked returns true when jti has been revoked and the TTL has
	// not elapsed. Errors propagate to the verifier which fails closed
	// (request rejected).
	IsRevoked(ctx ctxpkg.Context, jti string) (bool, error)
}

RevocationCache is the partner-pluggable JTI denylist. Cheap reads matter — IsRevoked is on the hot path of every authenticated request.

type RevokeSessionRequest added in v0.7.0

type RevokeSessionRequest struct {
	SessionID    string
	UserID       string
	UserBearer   string
	OnBehalfOfIP string
}

RevokeSessionRequest names the session to revoke and how to attest the caller. Auth shape is identical to ListSessionsRequest.

type Role

type Role = string

Role is the wire form of a role name. Stays a string alias — see ADR-040 decision §3 (no fixed enum).

const (
	RoleOwner  Role = "owner"
	RoleMember Role = "member"
)

System role names. Per ADR-040 §Decision, only `owner` and `member` are genuine system roles; the previous `admin` and `viewer` are now regular custom roles partners can edit/delete.

type RoleCreate

type RoleCreate struct {
	Name        string   `json:"name"`
	DisplayName string   `json:"display_name,omitempty"`
	Permissions []string `json:"permissions,omitempty"`
}

RoleCreate is the POST body.

type RoleDeleteResult

type RoleDeleteResult struct {
	Status string `json:"status"`
}

RoleDeleteResult is the DELETE acknowledgment.

type RoleListOpts

type RoleListOpts struct {
	Cursor string
	Limit  int
}

RoleListOpts are the optional pagination inputs.

type RoleListPage

type RoleListPage struct {
	Items      []RoleObject `json:"items"`
	NextCursor string       `json:"next_cursor,omitempty"`
	Total      *int         `json:"total,omitempty"`
}

RoleListPage is one page of `/platforms/{id}/roles` in the locked SPEC §7 envelope shape.

type RoleObject

type RoleObject struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	DisplayName string   `json:"display_name,omitempty"`
	Permissions []string `json:"permissions"`
	IsSystem    bool     `json:"is_system"`
	CreatedAt   int64    `json:"created_at"`
	UpdatedAt   int64    `json:"updated_at"`
}

RoleObject is one realm-defined role, as returned by the `/platforms/{id}/roles` endpoints.

type RolePatch

type RolePatch struct {
	DisplayName *string   `json:"display_name,omitempty"`
	Permissions *[]string `json:"permissions,omitempty"`
}

RolePatch is the PATCH body. Pointer fields signal "include in payload"; nil means "don't touch".

type RolesClient

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

RolesClient is realm.Roles.

func (*RolesClient) Create

func (c *RolesClient) Create(ctx ctxpkg.Context, body RoleCreate) (*RoleObject, error)

Create creates a custom role. Returns ErrRoleExists if the name is already taken in the realm.

func (*RolesClient) Delete

func (c *RolesClient) Delete(ctx ctxpkg.Context, roleID string) (*RoleDeleteResult, error)

Delete removes a custom role. Returns ErrRoleInUse (409) when the role is still attached to users/invitations, ErrSystemRoleImmutable (400) for `owner`/`member`.

func (*RolesClient) List

func (c *RolesClient) List(ctx ctxpkg.Context, opts *RoleListOpts) (*RoleListPage, error)

List returns one page of `/platforms/{id}/roles`. Unlike the typed iterators on Tenants etc., this surface returns the raw envelope so callers can drive their own paging UI directly.

func (*RolesClient) Rename

func (c *RolesClient) Rename(ctx ctxpkg.Context, roleID string, to string) (*RoleObject, error)

Rename rewrites a role's name in `realm_roles`, `users.role`, and `invitations.role` in one transaction (server-side).

func (*RolesClient) Update

func (c *RolesClient) Update(ctx ctxpkg.Context, roleID string, patch RolePatch) (*RoleObject, error)

Update patches display_name and/or permissions on an existing role. Returns ErrSystemRoleImmutable when called on `owner` or `member`.

type SearchHit added in v0.10.0

type SearchHit struct {
	Type       string `json:"type"`
	ID         string `json:"id"`
	Label      string `json:"label"`
	Sublabel   string `json:"sublabel,omitempty"`
	PlatformID string `json:"platform_id,omitempty"`
}

SearchHit is one row in AdminSearchResponse.Items.

type Session

type Session struct {
	AccessToken  string      `json:"access_token"`
	RefreshToken string      `json:"refresh_token"`
	ExpiresIn    int         `json:"expires_in"`
	ExpiresAt    string      `json:"expires_at,omitempty"`
	TenantID     string      `json:"tenant_id,omitempty"`
	Role         string      `json:"role,omitempty"`
	User         UserSummary `json:"user"`
	Tenants      []TenantRef `json:"tenants"`
}

Session is the result of a successful Login or MFA verify.

Login currently returns flat top-level `tenant_id` + `role` (the user's pinned tenant after the login resolved); these surface here so callers don't have to parse Tenants[]. `User` is populated from the access JWT's claims (sub/email) when the wire response omits it.

type SessionInfo

type SessionInfo struct {
	ID         string `json:"id"`
	CreatedAt  string `json:"created_at,omitempty"`
	LastUsedAt string `json:"last_used_at,omitempty"`
	UserAgent  string `json:"user_agent,omitempty"`
	IP         string `json:"ip,omitempty"`
}

SessionInfo is one entry in realm.Auth.ListSessions.

type SignupMode

type SignupMode string

SignupMode is the per-tenant signup policy (SPEC §6.1, ADR-045).

`closed` (default) is invitation-only; `allowlist` auto-provisions users whose verified email domain is in `allowed_domains`; `open` auto-provisions every authenticated user and is reserved for the base admin tenant — partner tenants cannot set this mode.

const (
	SignupModeClosed    SignupMode = "closed"
	SignupModeAllowlist SignupMode = "allowlist"
	SignupModeOpen      SignupMode = "open"
)

type Tenant

type Tenant struct {
	ID          string         `json:"id"`
	DisplayName string         `json:"display_name,omitempty"`
	OwnerUserID string         `json:"owner_user_id,omitempty"`
	Config      map[string]any `json:"config,omitempty"`
	CreatedAt   string         `json:"created_at,omitempty"`
	UpdatedAt   string         `json:"updated_at,omitempty"`
}

Tenant is one entry returned from realm.Tenants.* (SPEC §6.1).

type TenantCreate

type TenantCreate struct {
	DisplayName    string     `json:"display_name"`
	AllowedDomains []string   `json:"allowed_domains,omitempty"`
	SignupMode     SignupMode `json:"signup_mode,omitempty"`
}

TenantCreate is the create payload (SPEC §6.1).

type TenantDomainClaim added in v0.10.0

type TenantDomainClaim struct {
	Domain         string `json:"domain"`
	Status         string `json:"status"`
	Method         string `json:"method"`
	DNSRecordName  string `json:"dns_record_name,omitempty"`
	DNSRecordValue string `json:"dns_record_value,omitempty"`
	FilePath       string `json:"file_path,omitempty"`
	FileContent    string `json:"file_content,omitempty"`
}

TenantDomainClaim is the response from Tenants.ClaimDomain. Exactly one of (DNSRecordName/DNSRecordValue) and (FilePath/FileContent) is populated, determined by the verification method.

type TenantDomainClaimRequest added in v0.10.0

type TenantDomainClaimRequest struct {
	PlatformID string
	TenantID   string
	Domain     string
	Method     string // "dns_txt" (default) | "html_file"
}

TenantDomainClaimRequest parameterises Tenants.ClaimDomain. Method is optional (defaults to "dns_txt"); accepted values are "dns_txt" and "html_file".

type TenantPatch

type TenantPatch struct {
	DisplayName string `json:"display_name,omitempty"`
}

TenantPatch patches mutable tenant fields.

type TenantRef

type TenantRef struct {
	ID          string `json:"tenant_id"`
	IDLegacy    string `json:"id,omitempty"`
	Role        string `json:"role"`
	DisplayName string `json:"display_name,omitempty"`
}

TenantRef is the abbreviated tenant info embedded in Session.Tenants.

The wire shape uses `tenant_id` (api/internal/authsvc.TenantMembership); `id` is accepted as a fallback for older / mocked issuers in tests.

type TenantsClient

type TenantsClient struct {
	Invitations *InvitationsClient
	Users       *UsersClient
	// contains filtered or unexported fields
}

TenantsClient is realm.Tenants.

func (*TenantsClient) ClaimDomain added in v0.10.0

ClaimDomain initiates a tenant-owned domain claim. The DV row is owner-scoped to the tenant: any platform admin can complete a claim another admin started, and the row survives the claimer's user being removed. Re-claiming with the same method is idempotent.

func (*TenantsClient) Create

func (c *TenantsClient) Create(ctx context.Context, body TenantCreate) (*Tenant, error)

Create creates a tenant.

func (*TenantsClient) Delete

func (c *TenantsClient) Delete(ctx context.Context, id string) error

Delete removes a tenant.

func (*TenantsClient) Get

func (c *TenantsClient) Get(ctx context.Context, id string) (*Tenant, error)

Get returns a tenant by id.

func (*TenantsClient) List

func (c *TenantsClient) List(ctx context.Context) *Paginated[Tenant]

List paginates tenants (SPEC §6.1).

func (*TenantsClient) TransferOwner

func (c *TenantsClient) TransferOwner(ctx context.Context, id, newOwnerUserID string) (*Tenant, error)

TransferOwner reassigns tenant ownership.

func (*TenantsClient) Update

func (c *TenantsClient) Update(ctx context.Context, id string, patch TenantPatch) (*Tenant, error)

Update patches an existing tenant.

func (*TenantsClient) UpdateConfig

func (c *TenantsClient) UpdateConfig(ctx context.Context, id string, patch map[string]any) (*Tenant, error)

UpdateConfig patches the per-tenant config blob.

func (*TenantsClient) UpdateUserRole

func (c *TenantsClient) UpdateUserRole(ctx ctxpkg.Context, tenantID, userID, role string) (*UpdateUserRoleResult, error)

UpdateUserRole sets a user's role within a tenant. The role name must exist in the realm's role catalog (see RolesClient.Create). Setting role=owner is rejected — use TransferOwner for the explicit handover. Demoting the last owner returns RealmError(last_owner).

Wraps PATCH /tenants/{id}/users/{uid}/role. Test coverage: TestTenants_UpdateUserRole.

func (*TenantsClient) VerifyDomain added in v0.10.0

func (c *TenantsClient) VerifyDomain(ctx ctxpkg.Context, platformID, tenantID, domain string) (*DomainVerifyResult, error)

VerifyDomain drives the verification check on the tenant's pending DV row and (on success) inserts the domain_mappings binding in the same call. Method is read off the persisted row, so callers don't pass it again here.

type TokenRequest

type TokenRequest struct {
	RefreshToken string
	TenantID     string
	CustomClaims map[string]any
}

TokenRequest is realm.Auth.Token's input — refresh + tenant_id + optional access-token customClaims (SPEC §4.2).

type TokensClient

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

TokensClient is the access-token revocation cache surface. Concurrent-safe.

func (*TokensClient) Evict

func (t *TokensClient) Evict(jti string)

Evict drops a single jti, or all entries when jti == "".

func (*TokensClient) GateRequest

func (t *TokensClient) GateRequest(accessToken string) error

GateRequest is the per-request gate partner middleware calls before forwarding upstream. Returns ErrTokenRevoked (wrapped in a *RealmError with code "unauthorized" + details.revoked=true) when accessToken's jti is in the cache. Returns nil otherwise (including for malformed tokens — let the verifier surface that).

func (*TokensClient) IsRevoked

func (t *TokensClient) IsRevoked(accessToken string) bool

IsRevoked returns true iff accessToken's jti is cached and the entry has not expired. Lazy GC: stale entries are evicted on read. Returns false on malformed input.

func (*TokensClient) Len

func (t *TokensClient) Len() int

Len returns the current entry count. Useful for tests + instrumentation.

func (*TokensClient) MarkRevoked

func (t *TokensClient) MarkRevoked(accessToken string)

MarkRevoked extracts jti+exp from accessToken and caches the jti with TTL = exp - now(). No-op when the token is malformed, the jti/exp claims are missing, or exp is already in the past.

func (*TokensClient) RevokeOnLogout

func (t *TokensClient) RevokeOnLogout(logoutFn LogoutFn) func(ctx ctxpkg.Context, accessToken string, req *LogoutRequest) error

RevokeOnLogout wraps a LogoutFn so that the access token's jti is marked revoked on **either success or transport failure**. Rationale: the partner backend should fail closed — if RealmID is unreachable, the access token still gets blackholed locally so the user is logged out from the partner's perspective. Returns the wrapped function; the original logoutFn is called once per invocation.

type UpdateUserRoleResult

type UpdateUserRoleResult struct {
	ID        string `json:"id"`
	Role      string `json:"role"`
	TenantID  string `json:"tenant_id"`
	UpdatedAt int64  `json:"updated_at"`
}

UpdateUserRoleResult is the response shape returned by Tenants.UpdateUserRole.

type User

type User struct {
	ID          string `json:"id"`
	Email       string `json:"email,omitempty"`
	DisplayName string `json:"display_name,omitempty"`
	Status      string `json:"status,omitempty"`
	MFAEnabled  bool   `json:"mfa_enabled,omitempty"`
	Role        string `json:"role,omitempty"`
}

User is one entry in realm.Tenants.Users.* (SPEC §6.3).

type UserStatus

type UserStatus string

UserStatus is the discrete status field on a user record.

const (
	UserStatusActive      UserStatus = "active"
	UserStatusSuspended   UserStatus = "suspended"
	UserStatusDeactivated UserStatus = "deactivated"
)

type UserSummary

type UserSummary struct {
	ID          string `json:"id"`
	Email       string `json:"email,omitempty"`
	DisplayName string `json:"display_name,omitempty"`
}

UserSummary is the verified-user payload returned from Login / MFAVerify.

type UsersClient

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

UsersClient is realm.Tenants.Users.

func (*UsersClient) ConfirmMFA

func (c *UsersClient) ConfirmMFA(ctx context.Context, tenantID, userID, code string) error

func (*UsersClient) EnrollMFA

func (c *UsersClient) EnrollMFA(ctx context.Context, tenantID, userID string) (*MFAEnrollResult, error)

func (*UsersClient) Get

func (c *UsersClient) Get(ctx context.Context, tenantID, userID string) (*User, error)

func (*UsersClient) List

func (c *UsersClient) List(ctx context.Context, tenantID string) *Paginated[User]

func (*UsersClient) ResetMFA

func (c *UsersClient) ResetMFA(ctx context.Context, tenantID, userID string) error

func (*UsersClient) UpdateStatus

func (c *UsersClient) UpdateStatus(ctx context.Context, tenantID, userID string, status UserStatus) (*User, error)

type ValidateOriginOptions

type ValidateOriginOptions struct {
	RealmID string
	Origin  string
}

ValidateOriginOptions parameterises Origins.Validate.

type VerifyOptions

type VerifyOptions struct {
	// Audience overrides the auto-discovered audience for this call only.
	Audience string
}

VerifyOptions is the per-call override surface for Verify.

type VerifyRequest added in v0.10.0

type VerifyRequest struct {
	SubjectRef string
	Purpose    string
	Presented  string

	UserID       string
	UserBearer   string
	OnBehalfOfIP string
}

VerifyRequest names the entity to match against and the value typed by the end-user (or the delivery agent, for delivery-confirmation flows).

type VerifyResponse added in v0.10.0

type VerifyResponse struct {
	OTPID        string    `json:"otp_id"`
	IssuerUserID string    `json:"issuer_user_id"`
	IssuedAt     time.Time `json:"-"`
	IssuedAtS    string    `json:"issued_at"`
	SubjectRef   string    `json:"subject_ref"`
	Purpose      string    `json:"purpose"`
}

VerifyResponse mirrors api/internal/httpapi otpVerifyResp.

type ViewOptions added in v0.10.0

type ViewOptions struct {
	UserID       string
	UserBearer   string
	OnBehalfOfIP string
}

ViewOptions selects the caller identity for OTPClient.View.

type ViewResponse added in v0.10.0

type ViewResponse struct {
	ID           string    `json:"id"`
	Value        string    `json:"value"`
	ExpiresAt    time.Time `json:"-"`
	ExpiresAtS   string    `json:"expires_at"`
	Purpose      string    `json:"purpose"`
	SubjectRef   string    `json:"subject_ref"`
	IssuerUserID string    `json:"issuer_user_id"`
}

ViewResponse mirrors api/internal/httpapi otpViewResp.

Jump to

Keyboard shortcuts

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