credbound

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: Apache-2.0 Imports: 34 Imported by: 0

README

Credbound

CI Go Reference Go Report Card License

Credbound is a Go authentication and authorization library, built at Deepteams and open to everyone. It centralizes security invariants that would otherwise be reimplemented in every project:

  • local password authentication;
  • WebAuthn passkeys, including usernameless sign-in over discoverable credentials;
  • TOTP second factor and recovery codes;
  • password reset, magic-link, and email OTP sign-in through single-use, expiring email proofs with enumeration-resistant initiation;
  • an optional password policy port for breached-password (HIBP) vetting;
  • built-in account lockout after consecutive password or TOTP failures;
  • multiple verified email addresses per user, with one primary address, plus an updatable display name (self-service with a recent sign-in, any account by an instance administrator);
  • transactional tracking of the latest authentication (last_seen_at);
  • optional SSO per SaaS application (Google, GitHub, Microsoft, OIDC, and SAML);
  • freshness checks for step-up operations;
  • Personal Access Tokens (PATs) displayed only once, whose scopes are enforced ceilings on workspace permissions, not labels;
  • workspace isolation and extensible RBAC (admin, member, and application roles);
  • workspace invitations whose invitee chooses their own password, and an optional per-workspace MFA requirement;
  • optional self-service signup that atomically creates the user, their workspace, and their admin membership, with enumeration-resistant handling of already-registered addresses;
  • optional server-side sessions behind single-display cbs_ tokens, with device listings, an absolute TTL and an optional idle timeout, and a session-revocation cascade on reset, password change, disable, and credential revocation;
  • an optional per-address cooldown on the unauthenticated email-issuing flows (reset, magic-link, email OTP, verification resend) to blunt mail bombing;
  • optional verified workspace email domains (DNS-challenge capture) with per-domain SSO enforcement and just-in-time provisioning of passwordless members from a trusted SSO provider — a claim left unverified past Config.DomainClaimTTL loses its name reservation, so a squatter cannot permanently deny the domain's real owner;
  • atomic revocation of every PAT and OAuth grant of a user;
  • optional SCIM 2.0 provisioning per workspace (Users, Groups, .search);
  • optional OAuth 2.0/OIDC authorization-server capabilities for remote MCP resources, with pre-registration, independent CIMD and DCR policies, PKCE, opaque rotating tokens, pairwise subjects, and revocation cascades;
  • instance administration (root, developer, support, marketing, sales);
  • an append-only, hash-chained audit log — carrying the client IP address and user agent supplied by the host — that the host service can extend without spoofing the actor or timestamp, and whose integrity VerifyAuditChain recomputes end to end;
  • transactional hooks for atomic host-service business writes;
  • typed post-commit events for analytics, notifications, and external integrations;
  • structured logs, traces, and OpenTelemetry metrics.

The project follows a Specs First approach. The product contract is defined in specs/PRD.md, and the Go API in specs/API.md.

Status

The core, in-memory store, PostgreSQL store and migrations, and security adapters are implemented. Version v0 may still introduce breaking changes before the first stable release. CI applies the PostgreSQL migrations into the dedicated credbound schema and exercises lifecycle, OAuth, pagination, transactional hooks, and append-only audit behavior against a real PostgreSQL service.

Security principles

  • No password, TOTP secret, PAT, or recovery code is stored in plaintext.
  • A raw PAT is returned only when it is created.
  • Sensitive mutations and their audit records are atomic at repository level.
  • A secondary address cannot authenticate until its time-limited proof has been verified.
  • An SSO identity is explicitly linked by issuer and subject; the IdP email address never triggers an automatic account merge.
  • SSO and WebAuthn ceremony continuations are single use: the success commit consumes them, so a captured browser response can never be replayed — passkey signature counters alone cannot guarantee that, since many authenticators legitimately report a constant zero.
  • A PAT can never satisfy an interactive step-up request. Be aware that a workspace-unbound PAT holding the * scope is an instance credential: when its owner is an instance administrator it can perform the administration reads (listing every user and workspace, reading the instance audit log), though never the mutations, which stay interactive-only. Mint * PATs for automation deliberately.
  • Completing a password reset atomically revokes the account's PATs and OAuth grants and clears its lockout; a locked account still performs the same password derivation and answers with the same public error as a wrong password, so it never confirms whether an address exists.
  • Every audit event that identifies an actor is hash-chained to its predecessor inside the commit transaction; tampering is detectable with VerifyAuditChain. The rejection of a credential matching no record is the one exception: it names nobody, so it is recorded and listed but stays out of the chain — chaining it would let an unauthenticated caller take the instance-wide chain lock once per request.
  • Access to application resources must always provide a workspace_id.
  • All entity IDs created by Credbound are canonical UUIDv7 values that are monotonic within the process.

Integration

Credbound requires Go 1.26 or newer (it relies on the standard library's crypto/hkdf).

Credbound is a library, not a server. The host service remains responsible for cookies, CSRF, rate limiting, its TLS/H2/H3 reverse proxy, and its UI. WebAuthn ceremonies remain transport-agnostic: the JSON produced by the library is sent to the browser, whose response is then passed back to the library.

For host-side rate limiting beyond the built-in per-account lockout and per-address email cooldown, the authentication events carry the RequestMetadata (client IP and user agent) the host supplied — listen for AuthenticationFailureEvent and UserLockedEvent to feed an IP-level throttle at the edge.

Sessions and the Authentication capability

Every successful sign-in (AuthenticatePassword, CompleteEmailAuthentication, CompleteEmailOTP, FinishPasskeyAuthentication, FinishSSO, VerifyTOTP) returns an Authentication value. It is a security capability, not a lookup result: Level, Method, and AuthenticatedAt directly drive step-up checks and per-workspace MFA enforcement. The host owns sessions and must:

  • store the Authentication server-side (or in a tamper-proof, signed and encrypted cookie) and reconstruct it verbatim on each request;
  • never rebuild one from client-supplied fields and never upgrade Level itself — only VerifyTOTP, a passkey, or SSO reauthentication may produce AAL2;
  • treat SecondFactorRequired: true as a pending session: send the user to VerifyTOTP and store the AAL2 Authentication that it returns — Authorize and AuthorizePermission reject a pending context with ErrStepUpRequired as a backstop, but keeping it out of the host's own authorized paths remains the host's job;
  • terminate its own sessions for a user when CompletePasswordReset, ChangePassword, DisableUser, or RevokeUserCredentials fires — the library revokes PATs and OAuth grants but cannot see host sessions.
Workspace-scoped keys

A PAT bound to a workspace (CreatePATInput.WorkspaceID) is administered by that workspace, not only by its owner: WorkspacePATs lists the bound tokens of every member joined with the owner's profile, and RevokeWorkspacePAT disables one — the page that turns off a departed colleague's key. Both are gated by workspace.credentials.manage, which the admin role holds by default and which conveys no power over memberships, so no instance-administration role is involved.

A token bound to no workspace is its owner's across the whole instance: no workspace administrator can see it, revoke it, or learn that it exists — a mismatched binding reports ErrNotFound. Reading one's own tokens (PATs) needs an interactive authentication and nothing more, since the listing carries names, prefixes and timestamps but never a secret; issuing and revoking follow Config.StepUp.

Machine traffic

Every chained audit write takes one instance-wide row lock (audit_chain) held until the commit, so it serializes with every other mutation of every tenant. That is invisible for a control plane and a hard ceiling for a hot path — and AuthenticatePAT is a hot path, since a PAT is how machines authenticate. Two settings keep it off that lock:

  • Config.PATTouchInterval coarsens the write a successful AuthenticatePAT performs (last-used, owner last-seen, audit event) to at most one per token per interval, exactly as SessionTouchInterval does for sessions. Every store-backed check — revocation, expiry, disablement, workspace access — still runs on every call, so a revoked token is refused on the very next request. The trade-offs are last-used granularity and an audit log that records a token's activity once per interval.
  • The rejection of a token matching no record is audited outside the chain (see above), so a caller holding no credential cannot serialize the instance by replaying garbage tokens.

A workload authenticating machines should set PATTouchInterval (a minute is a reasonable start); the default zero keeps the historical per-request write.

RequireStepUp accepts only interactive AAL2 authentications newer than Config.StepUpMaxAge; a PAT can never satisfy it.

A deployment whose users enroll no second factor cannot reach the operations that step-up guards at all — creating a workspace, inviting a member, granting a role. Config.StepUp therefore chooses, per family of operations, which of three modes gates them:

StepUpPolicy Accepted authentication
StepUpPolicyRequired (default, zero value) fresh interactive AAL2, exactly RequireStepUp
StepUpPolicyUserCapable the same step-up from users holding an active TOTP factor or a passkey; any interactive authentication from users holding neither
StepUpPolicyNone any interactive authentication, at any assurance level and any age

StepUp.Default governs every family; PAT, Workspace, Account and Admin override it where one deserves a different answer, and a zero field inherits Default:

cfg.StepUp = credbound.StepUpPolicies{
    Default: credbound.StepUpPolicyUserCapable, // tenant, account, PAT, admin
    PAT:     credbound.StepUpPolicyRequired,    // ...except token issuance
}

The scopes are StepUpScopeWorkspace (workspaces, memberships, roles, invitations, domains, SCIM, workspace OAuth resources, administrative user creation, workspace audit reads), StepUpScopeAccount (passkey deletion, TOTP disabling, recovery codes, email selection and removal, SSO unlinking, sessions, credential revocation, data export), StepUpScopePAT, and StepUpScopeAdmin (instance administration, whose loopback TrustedRequest waiver is unaffected). RequireStepUpFor(ctx, authn, scope) answers what a family demands of an actor, so a host can raise the second-factor prompt before the operation rather than after its refusal.

No mode goes below an interactive context of an enabled account whose factors are all verified: a PAT can never satisfy any of them, a workspace with RequireMFA still holds its interactive sessions to AAL2, and a refusal still emits step_up.denied. StepUpPolicyUserCapable is the mode to reach for first — a user who enrolls a factor is immediately held to the full step-up everywhere, including on the operations that could remove that factor, so relaxing a deployment for the users who have none never weakens the users who do.

Hosts that would rather not manage session persistence themselves can use the optional server-side session module (CreateSession, AuthenticateSession, Sessions, RevokeSession, RevokeUserSessions), available when the store implements SessionStore — both bundled stores, in-memory and PostgreSQL, do. It persists the Authentication snapshot behind a single-display cbs_ token (digest-only at rest, absolute Config.SessionTTL expiry plus an optional Config.SessionIdleTimeout), lists a user's devices, and extends the revocation cascade of CompletePasswordReset, ChangePassword, DisableUser, and RevokeUserCredentials to those sessions within the same transaction. The token's transport — cookies, CSRF, TLS — remains host-owned.

Minimal setup
import (
    "github.com/deepteams/credbound"
    "github.com/deepteams/credbound/password"
    "github.com/deepteams/credbound/sqlstore/postgresql" // driver: jackc/pgx/v5
    "github.com/deepteams/credbound/totpadapter"
    "github.com/deepteams/credbound/webauthnadapter"
)

store, err := postgresql.New(database, pool)
if err != nil {
    return err
}

passkeys, err := webauthnadapter.New(webauthnadapter.Config{
    RPID:          "app.example.com",
    RPDisplayName: "My application",
    RPOrigins:     []string{"https://app.example.com"},
    UserHandleKey: userHandleKey,
})
if err != nil {
    return err
}

passwords, err := password.New(password.DefaultParams())
if err != nil {
    return err
}

totpProvider, err := totpadapter.New(totpadapter.Config{Issuer: "My application"})
if err != nil {
    return err
}

auth, err := credbound.New(credbound.Config{
    Store:          store,
    Passwords:      passwords,
    TOTP:           totpProvider,  // optional second factor
    Passkeys:       passkeys,      // optional AAL2 first factor
    SecretKey:      encryptionKey, // exactly 32 bytes
    PATPepper:      patPepper,     // at least 32 bytes
    RecoveryPepper: recoveryPepper,// at least 32 bytes
    EmailVerificationTTL: 24 * time.Hour,
    SSOProviders: []credbound.SSOProvider{
        googleProvider,
        oidcProvider,
    },
    WorkspaceRoles: []credbound.RoleDefinition{
        {Role: "viewer", Permissions: []credbound.WorkspacePermission{"documents.read"}},
        {Role: "editor", Permissions: []credbound.WorkspacePermission{"documents.write"}, Inherits: []credbound.Role{"viewer"}},
    },
    TransactionHooks: []credbound.TransactionHook{billingHook},
    EventListeners:   []credbound.EventListener{segmentListener},
})

The snippet assumes the embedded migrations have already been applied to database (see migrations.PostgreSQL() below). For a first experiment, memory.New() (package github.com/deepteams/credbound/memory) is a full-featured store that needs no database or migrations at all. Only Store, Passwords, and the three secrets are required: the TOTP and Passkeys providers are optional, and their flows return ErrNotSupported until the host wires them. A complete, runnable integration — PostgreSQL, migration application, first-run bootstrap, and a cookie-session HTTP layer following the sessions contract above — lives in examples/minimal.

Each SSO provider has a UUIDv7 configuration identifier. The bundled ssoadapter (any spec-compliant OIDC issuer, including Google and Microsoft Entra ID), githubadapter (GitHub's OAuth 2.0 + REST flow), and samladapter (SAML 2.0) packages implement the SSOProvider port; the host service may also implement it for other IdPs and then remains responsible for network exchanges, client secrets, and cryptographic callback validation. Credbound handles sealed continuations, explicit linking, AAL2, step-up with forced IdP reauthentication, persistence, and auditing.

The admin and member roles are always available. The workspace role catalog is frozen when the Manager is constructed; application roles may inherit from one another, and the application may add permissions to admin or member without removing their built-in guarantees. Access is authorized by permission through AuthorizePermission. This extension never adds instance-administration roles or permissions.

Optional SCIM provisioning

The in-memory and PostgreSQL stores implement SCIMStore. A SaaS application offering SCIM creates a workspace configuration, returns the raw credential to the directory once, and then mounts the adapter:

scim, err := scimhttp.New(auth)
if err != nil {
    return err
}
router.Handle("/scim/v2/", http.StripPrefix("/scim/v2", scim))

The SaaS application retains the commercial decision to enable SCIM. Credbound manages the service credential, configuration isolation, passwordless users, groups, mappings to registered roles, membership suspension, auditing, and events. Deprovisioning never disables the global user account.

Optional OAuth, OIDC, and MCP authorization

Config.OAuth enables the authorization-server module. The host creates an issuer and workspace-bound protected resource, then selects pre-registration, CIMD, and DCR policies independently. The optional oauthhttp.Handler exposes discovery, authorization, token, revocation, registration, UserInfo, and JWKS routes when mounted by the host. oauthhttp.Protect validates a bearer token again at the resource boundary before calling an MCP handler.

Machine-to-machine access uses the client_credentials grant, deliberately narrow: only an administratively pre-registered confidential client may hold it, registration requires both a scope list and an explicit allowlist of the resource URIs the client may target, and its tokens are individually revocable through the revocation endpoint.

Use oauthclientadapter.JWTAssertionVerifier for hardened private_key_jwt verification and oauthhttp.MetadataFetcher for CIMD loading. The latter blocks special/private destinations, redirects, oversized documents, and unbounded concurrency. oidcadapter.NewES256KeyRing publishes one active signing key and any verification-only retiring keys during rotation.

The adapter contract is summarized in specs/oauthhttp.openapi.yaml. The host still owns login and consent UI, sessions, CSRF, rate limits, TLS, and route-level commercial policy.

A TransactionHook extends the Credbound transaction before its audit record and commit. This is the intended extension point for atomically creating a freemium credit ledger, quota, or outbox row in the host-service database. The PostgreSQL store provides a typed TxFrom; the handle is valid only during the callback. Any hook error or panic cancels the entire mutation.

An EventListener then receives the committed fact, such as user.created or workspace.created. It may call Segment on a best-effort basis. Its error is observed but never returned to the caller, and it does not prevent subsequent listeners from running. For guaranteed delivery, the hook writes to a transactional outbox and a host-service worker dispatches it after commit.

The two interfaces embed UnimplementedTransactionHook and UnimplementedEventListener, respectively, so that an integration only needs to implement useful callbacks. Their full contract and payload list are defined in specs/API.md; the architecture decision is documented in ADR-008.

To add a business fact to the audit log, the service calls RecordAudit with an AuditInput. Credbound always constructs the UUIDv7 identifier, actor, and timestamp.

Embedded Goose migrations are available through migrations.PostgreSQL(). Versions are timestamps so they interleave with the host service's own migrations, and every table lives in the dedicated credbound schema. Hosts without a migration tool call migrations.ApplyPostgreSQL(ctx, db) instead — idempotent, one transaction per migration; pick goose or the helpers for a given database, never both. The first Bootstrap call atomically creates the first user, their workspace, their admin membership, and their instance-level root role.

Operational guidance is in specs/OPERATIONS.md, the release process in specs/RELEASING.md, and vulnerability reporting in SECURITY.md.

Testing your integration

The credboundtest package builds a fully wired Manager for host-service tests: in-memory store, fast fake password hasher, deterministic clock and randomness, and TOTP/passkey fakes whose ceremonies succeed with fixed inputs. DiscoverablePasskeys replaces the passkey fake when the test covers usernameless sign-in, and WithStore points the manager at a migration-applied PostgreSQL store when persistence itself is under test. Nothing in it is safe for production.

func TestSignIn(t *testing.T) {
    clock := credboundtest.NewClock(credboundtest.DefaultStartTime)
    manager := credboundtest.NewManager(t, credboundtest.WithClock(clock))
    authn, workspace := credboundtest.Bootstrap(t, manager)

    issued, err := manager.CreatePAT(context.Background(),
        credboundtest.AAL2(authn.UserID, clock.Now()), // test-only step-up
        credbound.CreatePATInput{Name: "ci", WorkspaceID: workspace.ID, Scopes: []string{"read"}})
    if err != nil {
        t.Fatal(err)
    }
    _ = issued.Token // raw token, returned exactly once
}

Verification

make test
make coverage
make verify

make coverage enforces consolidated coverage of at least 89.5% for maintained code — the floor the suite sustains, tracked in scripts/coverage.sh. Generated sqlc code and the generated PostgreSQL store are excluded from this measurement; their reproducibility is checked by make generate.

Contributing

Credbound is specs-first: behavior changes update specs/PRD.md and specs/API.md alongside the code, and make verify must pass. See CONTRIBUTING.md and the code of conduct. Vulnerabilities are reported privately per SECURITY.md, never through public issues.

License

Licensed under the Apache License, Version 2.0.

Documentation

Overview

Package credbound provides transport-independent authentication, workspace authorization and instance-administration primitives for Go SaaS services: local accounts, self-service signup, TOTP and passkey factors, magic links, email OTP, password reset, PATs, server-side sessions, SSO linking with verified workspace domains and JIT provisioning, workspace RBAC, invitations, SCIM provisioning, an OAuth 2.1/OIDC authorization server for MCP resources, and a hash-chained audit log.

Credbound starts no HTTP server and issues no cookies or JWTs. The host service owns TLS, sessions, CSRF, throttling and UI; the optional oauthhttp and scimhttp packages provide mountable protocol handlers. The full contract lives in specs/API.md and specs/PRD.md in the module source.

Construction

A Manager is built once from a validated Config:

auth, err := credbound.New(credbound.Config{
	Store:          store,     // required persistence port
	Passwords:      passwords, // required Argon2id-style hasher
	SecretKey:      key,       // exactly 32 bytes
	PATPepper:      patPepper, // at least 32 bytes
	RecoveryPepper: recPepper, // at least 32 bytes
})

New rejects every invalid configuration invariant; cryptographic values have no weak fallback. Zero durations and limits fall back to safe defaults (10 minute step-up window, 12 character minimum password, 10 failed logins before a 15 minute lockout, and so on).

API map

The Manager exposes every operation as a flat method set; this map groups the entry points by domain so the one you need is a search away:

  • Sign-in: AuthenticatePassword, VerifyTOTP, Begin/FinishPasskeyAuthentication, Begin/FinishDiscoverablePasskeyAuthentication (usernameless), Begin/CompleteEmailAuthentication (magic link), Begin/CompleteEmailOTP, Begin/FinishSSO, AuthenticatePAT, SignUp.
  • Passwords: ChangePassword, Begin/CompletePasswordReset.
  • Second factor: Begin/ConfirmTOTPEnrollment, DisableTOTP, RegenerateRecoveryCodes, TOTPStatus; Begin/FinishPasskeyRegistration, DeletePasskey, Passkeys; AdminResetSecondFactor for total loss.
  • Email addresses: BeginEmailAddition, ConfirmEmail, ResendEmailVerification, SetPrimaryEmail, RemoveEmail, Emails.
  • Server-side sessions: CreateSession, AuthenticateSession, SignOut, Sessions, RevokeSession, RevokeUserSessions.
  • SSO linking and domains: BeginSSOLink, BeginSSOStepUp, UnlinkSSO, SSOIdentities; CreateWorkspaceDomain, ConfirmWorkspaceDomain, UpdateWorkspaceDomainPolicy, RemoveWorkspaceDomain, WorkspaceDomains.
  • PATs and revocation: CreatePAT, RevokePAT, PATs, WorkspacePATs, RevokeWorkspacePAT; RevokeUserCredentials.
  • Tenant authorization: AuthorizePermission (canonical), Authorize, GrantRole, RequireStepUp.
  • Lifecycle: Bootstrap, CreateUser, UpdateUser, Disable/EnableUser, CreateWorkspace, UpdateWorkspace, Disable/EnableWorkspace, AddMembership, SetMembershipStatus, RemoveMembership, the User, Workspace, Membership getters, and the Users, Workspaces, UserWorkspaces, Memberships listings.
  • Invitations: InviteToWorkspace, AcceptInvitation, RegisterFromInvitation, RevokeInvitation, WorkspaceInvitations.
  • Privacy (data-subject requests): ExportUserData, AnonymizeUser.
  • Instance administration: AuthorizeAdmin, RequireAdminMutation, SetInstanceRole, RemoveInstanceRole, and the Admin* variants of user, workspace and profile mutations.
  • SCIM provisioning: CreateSCIMConfiguration and the SCIM* resource operations (see scim.go).
  • OAuth 2.1/OIDC server: the OAuth* operations (see oauth.go and the mountable oauthhttp package).
  • Audit and extension: RecordAudit, AuditEvents, InstanceAuditEvents, VerifyAuditChain, VerifyAuditChainFrom, AddTransactionHook, AddEventListener.

Naming

Multi-step flows follow one convention: Begin.../Finish... frame a ceremony whose opaque state round-trips through the caller (WebAuthn, SSO); Begin.../Complete... frame a flow finished by presenting a token or code (reset, magic link, email OTP, OAuth authorization); Confirm... proves possession to activate a pending resource (email addition, TOTP enrollment, workspace domains).

Operations exist at up to three privilege scopes, and the signature tells them apart: self-service methods take only the actor; workspace-scoped mutations authorize through workspace RBAC (AuthorizePermission); and instance-scoped administrative mutations — the Admin* methods plus Disable/EnableUser, SetInstanceRole, RemoveInstanceRole, RevokeUserSessions and RevokeUserCredentials on another account — take a TrustedRequest and demand an admin mutation (a fresh AAL2 step-up, or a TrustedRequest verified as loopback by the server adapter).

Authentication and sessions

Authentication is a server-side capability describing who authenticated, how (Method), at what assurance level (AAL1 or AAL2) and when. It is returned by AuthenticatePassword, VerifyTOTP, FinishPasskeyAuthentication, FinishSSO, CompleteEmailAuthentication, CompleteEmailOTP and AuthenticatePAT. The host stores it in its own session and passes it back as the actor of later calls; it must never be rebuilt from fields supplied by a client, because every authorization decision trusts it.

A password or email login yields AAL1 and reports through SecondFactorRequired whether an active TOTP factor still has to be verified; VerifyTOTP upgrades the context to AAL2. A pending context is a first factor only: until VerifyTOTP completes it, every operation that requires a recent interactive authentication — registering a passkey, linking an SSO identity, re-enrolling TOTP, changing the password, adding an email address, and the other self-service operations — refuses it with ErrStepUpRequired, so a stolen password alone can never enroll a replacement second factor. Passkey authentication produces AAL2 directly. SSO yields AAL2 only when the provider carries a Config.SSOAssurance policy the asserted context satisfies (or that trusts the provider unverified); otherwise it is AAL1, because SSO never mints AAL2 on the IdP's unverified word. Sensitive operations demand a fresh interactive AAL2 context (RequireStepUp) and fail with ErrStepUpRequired otherwise. Config.StepUp relaxes that demand per family of operations — PAT, tenant mutations, account credentials, instance administration — for deployments whose users enroll no second factor; RequireStepUpFor answers what a given family demands of a given actor, so a host can prompt before the refusal. See StepUpPolicies. Administrative mutations use RequireAdminMutation, which may waive the step-up only for a TrustedRequest built from an actually observed loopback peer (see TrustedRequestFromAddr) — never from client-supplied headers.

Pagination

List operations return iter.Seq2[PageEvent[T], error] with opaque cursors and a default limit of 50. Each page streams item events followed by a final page_end event, matching the NDJSON transport contract:

{"type":"item","data":{"id":"..."}}
{"type":"page_end","next_cursor":"opaque","has_more":true}

CollectPage drains one page into ([]T, PageEnd, error) for callers that want a slice and a cursor; streaming callers range over the sequence and forward each PageEvent.

Errors

Failures map to sentinel errors compared with errors.Is: ErrInvalidCredentials, ErrUnauthorized, ErrForbidden, ErrStepUpRequired, ErrConflict, ErrNotFound, ErrNotSupported, ErrInvalidInput, ErrExpired, ErrLocked, ErrSSORequired, ErrDomainVerification, ErrAuditUnavailable, ErrAuditCompromised and ErrTransactionRejected. Two more are contracts between a security provider and the manager rather than manager-to-host results: ErrNoPasskey (a passkey provider reporting the user has none) and ErrPasskeyCloneDetected (a passkey provider rejecting a cloned authenticator; the caller still sees ErrInvalidCredentials). User-input validation failures additionally carry a *ValidationError{Field, Rule, Message} retrievable with errors.As; every ValidationError also satisfies errors.Is(err, ErrInvalidInput). HTTPStatus maps every sentinel to its canonical HTTP status code, so an HTTP adapter shares one table instead of maintaining its own errors.Is ladder. Public errors never contain secrets, and enumeration-sensitive flows (AuthenticatePassword, BeginPasswordReset, BeginEmailAuthentication, BeginEmailOTP) answer identically whether or not the account exists.

Extension points

TransactionHook lets the host append its own writes to a Credbound mutation: hooks run inside the store transaction, after the mutation and before the audit write, so a hook error aborts the whole commit (ErrTransactionRejected). EventListener observes committed facts; listener errors are recorded for observability and never propagate. Both are registered in Config or later with AddTransactionHook and AddEventListener, and implementations embed UnimplementedTransactionHook or UnimplementedEventListener to stay compatible. A listener that also implements AnyEventListener additionally receives every event through the single OnAnyEvent method — the natural shape for an analytics feed or a webhook dispatcher. PasswordPolicy vets candidate passwords beyond the built-in length rules (for example against a breached-password corpus), and SSOProvider injects the network adapter for each registered identity provider.

Optional capabilities

Config.TOTP and Config.Passkeys are optional providers; a Manager built without one reports ErrNotSupported from the corresponding enrollment, verification and ceremony operations. Store capabilities are detected by type assertion: SCIM provisioning requires SCIMStore, the OAuth server requires Config.OAuth plus OAuthStore, self-service signup requires Config.SignUp plus SignupStore, server-side sessions (CreateSession, AuthenticateSession, SignOut, device listing and the revocation cascade) require SessionStore, and verified workspace domains with JIT provisioning and domain-enforced SSO require DomainStore. Absent capabilities report ErrNotSupported without affecting anything else; the bundled memory, The PostgreSQL store implements all of them.

Audit

Sensitive mutations and their audit events are committed atomically by the Store contract and fail closed when the audit cannot be persisted (ErrAuditUnavailable). Every audit event that identifies an actor is hash-chained to its predecessor with ComputeAuditHash; VerifyAuditChain recomputes the chain and reports tampering as ErrAuditCompromised. The chain write takes one instance-wide lock, so the rejection of a credential matching no record is deliberately left unchained (Commit.Unchained) and Config.PATTouchInterval coarsens the write of the machine-authentication path; both keep a hot path from serializing every tenant's mutations. WithRequestMetadata attaches the sanitized client IP address and user agent that audit events record.

Index

Examples

Constants

View Source
const ProvisioningSourceLocal = "local"

ProvisioningSourceLocal marks a membership managed by local operations. A SCIM-managed membership instead carries the UUIDv7 of its configuration, and ordinary local mutations cannot overwrite it.

Variables

View Source
var (
	ErrInvalidCredentials   = errors.New("credbound: invalid credentials")
	ErrUnauthorized         = errors.New("credbound: authentication required")
	ErrForbidden            = errors.New("credbound: access forbidden")
	ErrStepUpRequired       = errors.New("credbound: recent interactive AAL2 authentication required")
	ErrConflict             = errors.New("credbound: resource already exists")
	ErrNotFound             = errors.New("credbound: resource not found")
	ErrInvalidInput         = errors.New("credbound: invalid input")
	ErrExpired              = errors.New("credbound: credential expired")
	ErrLocked               = errors.New("credbound: account temporarily locked")
	ErrSSORequired          = errors.New("credbound: single sign-on required by domain policy")
	ErrDomainVerification   = errors.New("credbound: domain ownership not verified")
	ErrNoPasskey            = errors.New("credbound: user has no passkey")
	ErrPasskeyCloneDetected = errors.New("credbound: passkey authenticator clone detected")
	ErrAuditUnavailable     = errors.New("credbound: audit unavailable")
	ErrAuditCompromised     = errors.New("credbound: audit chain verification failed")
	ErrTransactionRejected  = errors.New("credbound: transaction rejected by hook")
	ErrNotSupported         = errors.New("credbound: capability not enabled")
)

Sentinel errors compared with errors.Is. An application HTTP adapter maps them to its transport error contract (for example RFC 9457 problem documents) — HTTPStatus provides the canonical status-code table so the adapter only owns the body; the messages themselves are not part of the API.

Functions

func ComputeAuditHash

func ComputeAuditHash(previous []byte, event AuditEvent) []byte

ComputeAuditHash returns the SHA-256 hash that chains an audit event to its predecessor. Every field is length-prefixed so no two distinct events share a canonical encoding. Stores call it inside the commit transaction; hosts and auditors can recompute it to verify exported audit logs.

func CredentialFingerprint

func CredentialFingerprint(hash string) []byte

CredentialFingerprint condenses a stored password hash into the opaque, unkeyed digest carried by Authentication.CredentialDigest. Stores implement the SessionStore currency guard by recomputing it from the credential row inside the session-creation transaction and comparing it byte-for-byte with the fingerprint the sign-in observed. It reveals nothing useful about the hash (itself already a one-way derivation), so an Authentication remains safe to hold in host session state.

func HTTPRequestMetadata

func HTTPRequestMetadata(next http.Handler, trustedProxies ...netip.Prefix) http.Handler

HTTPRequestMetadata wraps next so that every request context carries the client's RequestMetadata — IP address and User-Agent — which audit events recorded while serving the request then pick up automatically.

Without trusted proxies the client address is the transport peer from RemoteAddr and no header is read, so a client can never influence what the audit chain records. When the peer belongs to one of the trusted prefixes, the rightmost X-Forwarded-For address outside every trusted prefix is used instead: entries appended by the host's own proxies are skipped, and entries a client forged beyond them are never reached. A malformed entry stops the walk and the peer address is recorded, failing toward an address the client cannot choose.

func HTTPStatus

func HTTPStatus(err error) int

HTTPStatus maps an error returned by any Manager operation to the HTTP status code a host transport would conventionally answer with, so every integrator does not re-write the same errors.Is ladder. nil maps to 200 and an unrecognized error to 500, so infrastructure failures never leak detail.

The mapping is intentionally coarse — a body format such as an RFC 9457 problem document, localized copy, and headers like Retry-After or WWW-Authenticate remain the host's contract. Hosts needing a different convention (401 versus 403 for step-up, 423 versus 429 for lockout) keep writing their own switch; this helper is the documented default:

401 ErrInvalidCredentials, ErrUnauthorized, ErrExpired,
    ErrPasskeyCloneDetected, ErrNoPasskey (folded with invalid
    credentials so passkey presence never leaks)
403 ErrForbidden, ErrStepUpRequired, ErrSSORequired
404 ErrNotFound
409 ErrConflict
400 ErrInvalidInput (including every ValidationError)
422 ErrDomainVerification, ErrTransactionRejected
429 ErrLocked
501 ErrNotSupported
503 ErrAuditUnavailable
500 ErrAuditCompromised and anything unrecognized

func WithRequestMetadata

func WithRequestMetadata(ctx context.Context, metadata RequestMetadata) context.Context

WithRequestMetadata returns a context that carries the client network context for every audit event recorded while serving the request. The host service is responsible for resolving the real client address from its trusted proxy configuration before attaching it.

Types

type ActorKind

type ActorKind string

ActorKind classifies the audit actor: an authenticated user, a service credential (SCIM, OAuth client), or the system itself.

const (
	ActorUser    ActorKind = "user"
	ActorService ActorKind = "service"
	ActorSystem  ActorKind = "system"
)

type AnyEventListener

type AnyEventListener interface {
	OnAnyEvent(ctx context.Context, name EventName, event any) error
}

AnyEventListener is an optional extension an EventListener may also implement to receive every event through a single method — the natural shape for an analytics feed, a host-owned outbox relay, or a webhook dispatcher that would otherwise implement every typed method. For each emitted event the registry first invokes the typed method, then OnAnyEvent on the same listener, under the same post-commit, best-effort delivery: errors and panics are observed and never propagate. The event value is the same typed struct the typed method received (a PasskeyRegisteredEvent, an OAuthEvent, …), so a dispatcher can type-switch on it or marshal it directly, and every one embeds EventMeta for the idempotency key.

type AssuranceLevel

type AssuranceLevel uint8

AssuranceLevel is the authenticator assurance level of an Authentication, after NIST 800-63B: AAL1 for a single factor, AAL2 once a second factor (TOTP, recovery code) or a strong single ceremony (passkey, SSO) has been verified.

const (
	AAL1 AssuranceLevel = 1
	AAL2 AssuranceLevel = 2
)

type AuditChainCheckpoint

type AuditChainCheckpoint struct {
	Sequence int64
	Hash     []byte
}

AuditChainCheckpoint is a previously verified chain position: the HeadSequence and HeadHash of an earlier report, kept by the caller in a trusted place. The zero value means the genesis.

type AuditChainReport

type AuditChainReport struct {
	Events       int64
	HeadSequence int64
	HeadHash     []byte
}

AuditChainReport summarizes a successful audit chain verification. Its HeadSequence and HeadHash form the AuditChainCheckpoint a later VerifyAuditChainFrom resumes from.

type AuditEvent

type AuditEvent struct {
	ID           UUID
	OccurredAt   time.Time
	ActorKind    ActorKind
	ActorID      UUID
	Action       string
	ResourceType string
	ResourceID   string
	WorkspaceID  UUID
	Outcome      AuditOutcome
	Reason       string
	IPAddress    string
	UserAgent    string
	// Sequence, PreviousHash and Hash are assigned by the store inside the
	// commit transaction and chain every event to its predecessor. A zero
	// Sequence marks an event recorded before the chain existed.
	Sequence     int64
	PreviousHash []byte
	Hash         []byte
}

AuditEvent is one immutable entry of the append-only audit log. ID, ActorID and OccurredAt are always derived by Credbound so a consuming service cannot impersonate an actor or backdate an entry.

type AuditInput

type AuditInput struct {
	Action       string
	ResourceType string
	ResourceID   string
	WorkspaceID  UUID
	Outcome      AuditOutcome
	Reason       string
}

AuditInput is a host-supplied audit entry recorded through RecordAudit. Credbound derives the actor, identifier and timestamp itself.

type AuditOutcome

type AuditOutcome string

AuditOutcome records whether the audited action succeeded or failed.

const (
	AuditSucceeded AuditOutcome = "succeeded"
	AuditFailed    AuditOutcome = "failed"
)

type AuditStore

type AuditStore interface {
	AppendAudit(context.Context, Commit) error
	AuditEvents(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[AuditEvent], error]
	InstanceAuditEvents(context.Context, PageRequest) iter.Seq2[PageEvent[AuditEvent], error]
	// AuditChainHead returns the sequence and hash of the latest chained
	// audit event; sequence 0 with a 32-zero-byte hash for an empty chain.
	AuditChainHead(context.Context) (int64, []byte, error)
	// ChainedAuditEvents streams every chained audit event with a sequence
	// strictly greater than afterSequence, in ascending order, so the chain
	// can be recomputed from the genesis (0) or from a checkpoint.
	ChainedAuditEvents(ctx context.Context, afterSequence int64) iter.Seq2[AuditEvent, error]
}

AuditStore persists the append-only audit trail and its hash chain.

type AuditUnavailableEvent

type AuditUnavailableEvent struct {
	EventMeta
	FailedOperation string
}

type AuthMethod

type AuthMethod string

AuthMethod identifies how an Authentication was produced.

const (
	MethodPassword AuthMethod = "password"
	MethodTOTP     AuthMethod = "totp"
	MethodPasskey  AuthMethod = "passkey"
	MethodPAT      AuthMethod = "pat"
	MethodSSO      AuthMethod = "sso"
	MethodEmail    AuthMethod = "email"
)

type Authentication

type Authentication struct {
	UserID UUID
	// Method records the factor that produced this context (password, totp,
	// passkey, sso, email, or pat). Non-interactive methods (PAT) are
	// rejected by step-up checks regardless of age.
	Method AuthMethod
	// Level is the assurance reached so far. AAL1 contexts are denied
	// sensitive operations until VerifyTOTP, a passkey or an SSO step-up
	// promotes the session to AAL2.
	Level AssuranceLevel
	// AuthenticatedAt is when the factor was verified. RequireStepUp accepts
	// only interactive AAL2 contexts whose AuthenticatedAt falls within
	// Config.StepUpMaxAge, so the host must preserve this timestamp rather
	// than refresh it.
	AuthenticatedAt time.Time
	// SecondFactorRequired reports that the account has an active TOTP
	// factor which has not been verified yet. The host should defer creating
	// the final session until VerifyTOTP upgrades the context; Authorize and
	// AuthorizePermission reject a pending context with ErrStepUpRequired, so
	// the first factor alone never authorizes workspace operations.
	SecondFactorRequired bool
	// WorkspaceID restricts the context to one workspace. It is set for
	// workspace-bound PATs; authorization in any other workspace fails.
	WorkspaceID UUID
	// Scopes limits what the credential may do (PATs). Empty means the
	// context carries no scope restriction of its own.
	Scopes []string
	// CredentialDigest fingerprints the password credential this context was
	// verified against (CredentialFingerprint of the stored hash); it is empty
	// for contexts that no password produced. CreateSession refuses, inside
	// the session transaction, a context whose fingerprint no longer matches
	// the stored credential, so an authentication that verified a password
	// concurrently replaced by ChangePassword or CompletePasswordReset can
	// never mint a session the replacement should have killed.
	CredentialDigest []byte
}

Authentication is the server-side capability returned by every successful authentication. The host stores it in its own session and passes it back as the actor of later operations; because every authorization decision trusts its fields, it must never be reconstructed from data a client supplies. Credbound issues no cookies or JWTs — the session strategy belongs to the host.

func (Authentication) HasScope

func (a Authentication) HasScope(required string) bool

HasScope reports whether the context carries the required scope. An empty requirement always passes and the literal "*" scope matches everything. AuthorizePermission consults it for every scoped authentication, so hosts only need it for checks outside the workspace RBAC model.

func (Authentication) Interactive

func (a Authentication) Interactive() bool

Interactive reports whether the context was produced by a user-present ceremony rather than a stored credential such as a PAT. Only interactive contexts can satisfy step-up requirements.

type AuthenticationEvent

type AuthenticationEvent struct {
	EventMeta
	Authentication Authentication
	// Request carries the client network context supplied by the host through
	// WithRequestMetadata, so listeners can throttle or alert by address
	// without re-reading the audit log.
	Request RequestMetadata
}

AuthenticationEvent reports every successful authentication, whatever the method.

type AuthenticationFailureEvent

type AuthenticationFailureEvent struct {
	EventMeta
	Method AuthMethod
	UserID UUID
	Reason string
	// Request carries the client network context supplied by the host through
	// WithRequestMetadata, so listeners can throttle or alert by address
	// without re-reading the audit log.
	Request RequestMetadata
}

AuthenticationFailureEvent reports a failed authentication attempt. UserID is empty when the failure cannot be attributed to an existing account.

type AuthorizationDeniedEvent

type AuthorizationDeniedEvent struct {
	EventMeta
	UserID       UUID
	RequiredRole Role
}

AuthorizationDeniedEvent is an advisory signal that a workspace authorization failed; it carries no audit of its own. RequiredRole is empty for permission-based checks.

type BeginOAuthAuthorizationInput

type BeginOAuthAuthorizationInput struct {
	Issuer              string
	ClientID            string
	RedirectURI         string
	Resource            string
	Scopes              []string
	State               string
	CodeChallenge       string
	CodeChallengeMethod string
	Nonce               string
	// MaxAge is the OIDC max_age request parameter: the maximum age the
	// end-user's authentication may have. When positive and the actor's
	// authentication is older, the consent reports RequiresStepUp and
	// CompleteOAuthAuthorization refuses with ErrStepUpRequired until the host
	// re-authenticates the user. Zero leaves the decision to the per-scope
	// server policy alone.
	MaxAge time.Duration
}

BeginOAuthAuthorizationInput is a parsed authorization request. Resource and State are mandatory, and only PKCE S256 is accepted.

type BootstrapCompletedEvent

type BootstrapCompletedEvent struct {
	EventMeta
	User      User
	Workspace Workspace
}

Listener event payloads mirror the committed fact they announce: the embedded EventMeta identifies the event, the remaining fields are a scrubbed snapshot of the affected records. Like transaction payloads, they never carry passwords, hashes, raw tokens, secrets, or digests; fields that are not otherwise documented are exactly the persisted values.

type BootstrapInput

type BootstrapInput struct {
	Email         string
	DisplayName   string
	Password      string
	WorkspaceName string
}

BootstrapInput describes the first account and workspace of an empty instance.

type CeremonyConsumption

type CeremonyConsumption struct {
	ID        UUID
	ExpiresAt time.Time
}

CeremonyConsumption identifies a single-use ceremony continuation being consumed by a Commit. ID is the UUIDv7 minted when the ceremony began and ExpiresAt bounds how long the store must remember it.

type ClientAuditRecord

type ClientAuditRecord struct {
	EventMeta
	Audit AuditEvent
}

ClientAuditRecord carries a host-supplied audit entry recorded through RecordAudit, with the derived actor and timestamp already enforced.

type ClientAuditRecordedEvent

type ClientAuditRecordedEvent struct {
	EventMeta
	Audit AuditEvent
}

type Commit

type Commit struct {
	Audit         AuditEvent
	Transactional func(context.Context, Tx) error
	// Ceremony, when set, marks the single-use ceremony that authorized
	// this mutation as consumed in the same transaction: the store records
	// the ceremony id and fails the whole commit with ErrConflict when it
	// was already recorded, so a replayed ceremony can never commit twice.
	// Records may be pruned once ExpiresAt has passed.
	Ceremony *CeremonyConsumption
	// Unchained appends the audit event without extending the hash chain:
	// no sequence, no predecessor hash, and no chain head to advance. It is
	// reserved for the high-frequency events that carry no identified actor
	// and therefore nothing to protect from tampering — today, the rejection
	// of a token that matches no record at all. Chaining those would make an
	// unauthenticated caller take the instance-wide chain lock once per
	// request, which is a throughput ceiling and an amplification vector;
	// leaving them out of the chain costs nothing, because an event about
	// nobody cannot be tampered with in anyone's favour. The event is still
	// persisted, still listed, and still fails the operation when it cannot
	// be written (AUDIT-002); it is simply invisible to VerifyAuditChain.
	//
	// A store that ignores the flag and chains the event anyway stays
	// correct — it only keeps the contention.
	Unchained bool
}

Commit couples the mandatory audit with an optional extension of the same store transaction. Store implementations must commit all three stages (mutation, Transactional callback and audit) or none of them.

type Config

type Config struct {
	// Store is the required persistence port. A store that additionally
	// implements SCIMStore, OAuthStore, SignupStore, SessionStore, or
	// DomainStore unlocks the corresponding optional capability.
	Store Store
	// Passwords derives and verifies password hashes (Argon2id is the
	// intended algorithm). Required.
	Passwords PasswordHasher
	// TOTP is the optional TOTP provider; without it the TOTP enrollment and
	// verification operations return ErrNotSupported.
	TOTP TOTPProvider
	// Passkeys is the optional WebAuthn provider; without it the passkey
	// ceremonies return ErrNotSupported.
	Passkeys PasskeyProvider
	// DomainVerifier proves control of a workspace domain inside
	// ConfirmWorkspaceDomain; see DomainVerifier. Without one,
	// ConfirmWorkspaceDomain refuses with ErrNotSupported unless
	// TrustActorDomainVerification opts into trusting the actor instead.
	DomainVerifier DomainVerifier
	// TrustActorDomainVerification lets ConfirmWorkspaceDomain succeed
	// without a DomainVerifier, treating the call as the administrator's
	// assertion that DNS verification completed out of band. Dangerous
	// wherever ConfirmWorkspaceDomain is reachable from self-serve actors: a
	// confirmed domain governs SSO enforcement and JIT provisioning for every
	// address on it. Ignored when a DomainVerifier is registered.
	TrustActorDomainVerification bool
	// SecretKey is the 32-byte root key. Distinct AEAD and HMAC keys are
	// derived from it with HKDF to seal ceremony continuations and TOTP
	// secrets and to digest single-use tokens.
	SecretKey []byte
	// PATPepper keys the HMAC digests of PAT and SCIM credentials. At least
	// 32 bytes.
	PATPepper []byte
	// RecoveryPepper keys the HMAC digests of TOTP recovery codes. At least
	// 32 bytes.
	RecoveryPepper []byte
	// RetiredSecretKeys lists previously active SecretKeys that reads still
	// accept: sealed TOTP secrets and passkey credentials keep decrypting
	// and outstanding token digests (sessions included) keep matching after
	// a rotation, while everything new uses SecretKey. Each retired key is
	// exactly 32 bytes. Remove a retired key once nothing sealed or
	// digested under it remains.
	RetiredSecretKeys [][]byte
	// RetiredPATPeppers lists previously active PATPeppers that reads
	// still accept, so outstanding PATs and SCIM credentials survive a
	// pepper rotation; new tokens always digest under PATPepper.
	RetiredPATPeppers [][]byte
	// RetiredRecoveryPeppers lists previously active RecoveryPeppers that
	// reads still accept, so outstanding recovery codes survive a pepper
	// rotation; new codes always digest under RecoveryPepper.
	RetiredRecoveryPeppers [][]byte
	// PATPrefix is the marker a PAT carries, ahead of its indexed prefix and
	// secret: PATPrefix_<12 hex>_<43 characters>. Zero keeps "cbp". Give each
	// deployment its own value when several of them issue PATs, so a token can
	// be attributed — and matched by a secret scanner — from its text alone.
	// One to sixteen lowercase letters or digits, no underscore: that is the
	// separator the parser splits on.
	//
	// Choose it once, before the first PAT is issued: the digest covers the
	// whole token, marker included, and only the digest is stored, so a later
	// change makes every outstanding PAT fail to authenticate with no raw
	// token left to re-digest. Peppers rotate (RetiredPATPeppers); this does
	// not. It leaves SCIM credentials, sessions and OAuth tokens untouched,
	// which keep their own markers.
	PATPrefix string
	// PATTouchInterval coarsens the write AuthenticatePAT performs: a
	// successful authentication whose token was already touched within the
	// interval skips the last-used refresh, the owner's last-seen refresh and
	// the audit event, turning the per-request write transaction into at most
	// one write per token per interval. It matters more than its session
	// sibling, because a PAT is how machines authenticate: every write goes
	// through the instance-wide audit-chain lock, so a per-request write makes
	// one token's traffic contend with every mutation of every tenant.
	// Revocation, expiry, disablement and workspace-access checks still run
	// against the store on every call, so a revoked token is refused on the
	// very next request; the explicit trade-offs are last-used granularity, an
	// audit log that records a token's activity at most once per interval, and
	// the authentication events (EventPATAuthenticated,
	// EventAuthenticationSucceeded) firing at that same reduced rate. Zero
	// keeps the per-request write.
	PATTouchInterval time.Duration
	// StepUpMaxAge bounds how old an interactive AAL2 authentication may be
	// to satisfy RequireStepUp. Zero keeps the default of 10 minutes.
	StepUpMaxAge time.Duration
	// StepUp selects, per family of guarded operations, the authentication a
	// step-up-gated call demands; see StepUpPolicies and StepUpScope. The
	// zero value keeps the strict historical behaviour everywhere: only a
	// fresh interactive AAL2 context may perform them. Relaxing a family
	// trades assurance for reachability in a deployment whose users hold no
	// second factor; no setting here ever lets a non-interactive credential
	// (a PAT) through, and none of them touch AuthenticatePAT or the
	// workspace RequireMFA policy.
	StepUp StepUpPolicies
	// CeremonyTTL bounds the validity of sealed ceremony continuations
	// (WebAuthn, SSO, OAuth consent). Email OTP continuations follow
	// EmailAuthenticationTTL plus a one-minute audit grace instead. Zero
	// keeps the default of 5 minutes.
	CeremonyTTL time.Duration
	// MinPasswordLen is the minimum accepted password length in runes. Zero
	// keeps the default of 12; values below 10 are rejected.
	MinPasswordLen int
	// PasswordPolicy optionally vets candidate passwords beyond the built-in
	// length rules (for example against a breached-password corpus). Nil
	// keeps only the built-in validation.
	PasswordPolicy PasswordPolicy
	// MaxFailedLogins locks an account after that many consecutive password
	// or TOTP failures. Zero keeps the default of 10; a negative value
	// disables the built-in lockout for hosts that throttle upstream.
	MaxFailedLogins int
	// LockoutDuration is how long a locked account rejects authentication.
	// Zero keeps the default of 15 minutes.
	LockoutDuration time.Duration
	// Clock supplies the current time; nil uses time.Now. Injectable for
	// tests only — persisted timestamps are always converted to UTC.
	Clock func() time.Time
	// Random supplies cryptographic randomness; nil uses crypto/rand.
	// Injectable for tests only.
	Random io.Reader
	// Observer receives one Operation record per API call, transaction hook
	// and event listener invocation, for metrics and tracing. Nil disables
	// observation.
	Observer Observer
	// AdminPermissions restricts the default instance-role permission
	// matrix. A role may only be narrowed: granting a permission outside its
	// default set fails, and no role but root may hold instance-role write.
	AdminPermissions map[InstanceRole][]Permission
	// WorkspaceRoles registers additional workspace roles and permissions in
	// the immutable RBAC catalog. Definitions are validated during New; see
	// RoleDefinition.
	WorkspaceRoles []RoleDefinition
	// EmailVerificationTTL bounds the validity of an email addition token.
	// Zero keeps the default of 24 hours.
	EmailVerificationTTL time.Duration
	// PasswordResetTTL bounds the validity of a password reset token. Zero
	// keeps the default of 1 hour.
	PasswordResetTTL time.Duration
	// EmailAuthenticationTTL bounds the validity of magic-link tokens and
	// email OTP codes. Zero keeps the default of 15 minutes.
	EmailAuthenticationTTL time.Duration
	// InvitationTTL bounds the validity of a workspace invitation token.
	// Zero keeps the default of 7 days.
	InvitationTTL time.Duration
	// DomainClaimTTL bounds how long an unconfirmed workspace-domain claim
	// reserves its globally unique name. Once the window passes without
	// ConfirmWorkspaceDomain, a new CreateWorkspaceDomain for the same name —
	// from any workspace — replaces the stale pending claim, so an
	// unverified claim can never permanently deny the domain's real owner.
	// Confirmed domains never expire. Zero keeps the default of 7 days.
	DomainClaimTTL time.Duration
	// SessionTTL bounds the absolute lifetime of a server-side session issued
	// by CreateSession (ExpiresAt = CreatedAt + SessionTTL); activity never
	// extends it. Zero keeps the default of 30 days. Sessions additionally
	// require a SessionStore-capable store.
	SessionTTL time.Duration
	// SessionIdleTimeout expires a server-side session after this much
	// inactivity: AuthenticateSession refuses a session whose last-seen
	// timestamp is older than the timeout, in addition to the absolute
	// SessionTTL. Zero disables the idle check (absolute expiry only). Every
	// successful AuthenticateSession refreshes last-seen, so the window slides
	// with activity.
	SessionIdleTimeout time.Duration
	// SessionTouchInterval coarsens the write AuthenticateSession performs:
	// a successful validation whose session was already touched within the
	// interval skips the last-seen refresh and its audit event, turning the
	// per-request write transaction into at most one write per session per
	// interval. Revocation, expiry, idle and disabled-user checks still run
	// against the store on every call, so — unlike a host-side result cache —
	// a revoked session is refused on the very next request; the explicit
	// trade-offs are last-seen granularity, an audit log that records session
	// activity at most once per interval per session, and a revocation racing
	// one in-flight validation no longer being caught by the touch's
	// conflict check. Zero keeps the per-request write. When
	// SessionIdleTimeout is set, the interval must be shorter than it, or a
	// continuously active session could idle-expire; New rejects the
	// combination.
	SessionTouchInterval time.Duration
	// EmailIssuanceCooldown is the minimum interval between two token-issuing
	// emails to the same address for the same purpose (password reset,
	// magic-link, email OTP, verification resend). Within the window the flow
	// answers with its usual enumeration-safe decoy and issues no token, so a
	// user cannot be bombarded with mails. Zero disables the cooldown. It
	// requires an EmailThrottleStore-capable store; New fails construction
	// otherwise, so a configured protection can never be silently inert.
	EmailIssuanceCooldown time.Duration
	// SSOProviders registers the identity providers the host enables. Each
	// must expose a unique UUIDv7 configuration ID and a known kind.
	SSOProviders []SSOProvider
	// SSOAssurance sets, per provider configuration ID, the authentication
	// context a provider must assert before FinishSSO grants AAL2; see
	// SSOAssurancePolicy. A provider with no entry here grants only AAL1 —
	// SSO never mints AAL2 on the IdP's unverified word — so a provider whose
	// sign-ins must satisfy a RequireMFA workspace or a step-up needs a
	// policy (use TrustUnverified to trust an IdP that asserts no ACR or
	// AMR). Every key must name a registered provider and every policy must
	// require something.
	SSOAssurance map[UUID]SSOAssurancePolicy
	// TransactionHooks run inside every mutation's store transaction, after
	// the mutation and before the audit write. A hook error aborts the
	// commit. More hooks can be added later with AddTransactionHook.
	TransactionHooks []TransactionHook
	// EventListeners observe committed facts. Listener errors are recorded
	// for observability and never propagate. More listeners can be added
	// later with AddEventListener.
	EventListeners []EventListener
	// OAuth enables the OAuth/OIDC authorization server module when the
	// store also implements OAuthStore. Nil leaves the module disabled.
	OAuth *OAuthConfig
	// SignUp enables self-service registration when the store also
	// implements SignupStore. Nil leaves the operation disabled.
	SignUp *SignUpConfig
}

Config assembles the ports, keys and policies of a Manager. Store and Passwords are required; New validates every invariant and applies safe defaults to zero durations and limits. Cryptographic values have no weak fallback.

type CreateOAuthInitialAccessTokenInput

type CreateOAuthInitialAccessTokenInput struct {
	ExpiresAt        time.Time
	MaxRegistrations int
}

CreateOAuthInitialAccessTokenInput describes a protected-DCR bootstrap token: an expiry within 30 days and a registration limit between 1 and 100 (zero means 1).

type CreateOAuthIssuerInput

type CreateOAuthIssuerInput struct {
	Issuer                   string
	OIDCEnabled              bool
	CIMDMode                 OAuthCIMDMode
	CIMDAllowedOrigins       []string
	DCRMode                  OAuthDCRMode
	DCRAllowClientSecrets    bool
	DCROpenRegistrationLimit int
	CodeTTL                  time.Duration
	AccessTokenTTL           time.Duration
	RefreshTokenTTL          time.Duration
}

CreateOAuthIssuerInput describes a new issuer. Zero TTLs fall back to the defaults (5 minute codes, 15 minute access tokens, 30 day refresh tokens), each bounded by a hard maximum.

type CreateOAuthProtectedResourceInput

type CreateOAuthProtectedResourceInput struct {
	IssuerID UUID
	Resource string
	Scopes   []OAuthScopeDefinition
}

CreateOAuthProtectedResourceInput describes a new MCP resource: its issuer, HTTPS resource URI and scope definitions.

type CreatePATInput

type CreatePATInput struct {
	Name        string
	WorkspaceID UUID
	Scopes      []string
	ExpiresAt   *time.Time
}

CreatePATInput describes a new personal access token. An empty WorkspaceID leaves the token unbound; at least one scope is required and a nil ExpiresAt issues a non-expiring token.

type CreateSCIMConfigurationInput

type CreateSCIMConfigurationInput struct {
	DefaultRole          Role
	TrustDirectoryEmails bool
	GroupRoleMappings    []SCIMGroupRoleMapping
	CredentialExpiresAt  *time.Time
}

CreateSCIMConfigurationInput configures a new provisioning domain and its first credential. A zero DefaultRole means member; a nil CredentialExpiresAt issues a non-expiring credential.

type CreateSessionInput

type CreateSessionInput struct{}

CreateSessionInput reserves room for future per-session options. Device metadata is not part of it: CreateSession reads the sanitized RequestMetadata attached to the context with WithRequestMetadata.

type CreateUserInput

type CreateUserInput struct {
	Email       string
	DisplayName string
	Password    string
	Role        Role
}

CreateUserInput describes an administratively created account. The address becomes the verified primary email and Role is the membership role in the target workspace.

type CreateWorkspaceInput

type CreateWorkspaceInput struct {
	Name       string
	RequireMFA bool
}

CreateWorkspaceInput describes a new workspace. RequireMFA enables the workspace MFA policy from the start.

type DiscoverablePasskeyProvider

type DiscoverablePasskeyProvider interface {
	// BeginDiscoverableAuthentication starts an assertion ceremony bound to
	// no account; the authenticator offers its resident credentials.
	BeginDiscoverableAuthentication(context.Context) (json.RawMessage, []byte, error)
	// FinishDiscoverableAuthentication validates the browser response,
	// resolving the account through the lookup and verifying the asserted
	// user handle belongs to it.
	FinishDiscoverableAuthentication(ctx context.Context, session, response []byte, lookup PasskeyUserLookup) (credentialID, credentialJSON []byte, err error)
}

DiscoverablePasskeyProvider is an optional extension of PasskeyProvider enabling usernameless (discoverable-credential) authentication: the ceremony starts with an empty allowCredentials list, so no per-address challenge exists to enumerate — the full fix for the residual allowCredentials-count signal the per-address decoy cannot close.

type DomainStore

type DomainStore interface {
	CreateWorkspaceDomain(ctx context.Context, domain WorkspaceDomain, staleBefore time.Time, commit Commit) error
	WorkspaceDomainByID(ctx context.Context, id UUID) (WorkspaceDomain, error)
	// ConfirmedWorkspaceDomainByName is the hot lookup behind SSO enforcement
	// and JIT provisioning: it resolves a normalized domain name to its
	// confirmed record and returns ErrNotFound when the domain is absent or
	// not yet confirmed.
	ConfirmedWorkspaceDomainByName(ctx context.Context, domain string) (WorkspaceDomain, error)
	ConfirmWorkspaceDomain(ctx context.Context, id UUID, at time.Time, commit Commit) error
	UpdateWorkspaceDomainPolicy(ctx context.Context, id UUID, policy WorkspaceDomainPolicyInput, at time.Time, commit Commit) error
	DeleteWorkspaceDomain(ctx context.Context, id UUID, commit Commit) error
	WorkspaceDomains(ctx context.Context, workspaceID UUID, page PageRequest) iter.Seq2[PageEvent[WorkspaceDomain], error]
	// JITProvisionSSOUser atomically creates a passwordless user, their
	// verified primary email, the auto-join membership and the SSO identity
	// link, or nothing at all. A concurrently claimed address or identity
	// fails with ErrConflict.
	JITProvisionSSOUser(ctx context.Context, user User, email EmailAddress, membership Membership, identity SSOIdentity, at time.Time, commit Commit) error
}

DomainStore is an optional persistence capability required by the workspace-domain operations (CreateWorkspaceDomain, ConfirmWorkspaceDomain, UpdateWorkspaceDomainPolicy, RemoveWorkspaceDomain, WorkspaceDomains), by domain-enforced SSO and by JIT provisioning; without it every domain operation returns ErrNotSupported and the authentication flows behave as if no domain existed.

A domain name is globally unique across workspaces: CreateWorkspaceDomain fails with ErrConflict for a taken name, except that it replaces — in the same transaction — a stale pending claim, one still unconfirmed and created before staleBefore, so an unverified claim can never permanently deny the domain's real owner (Config.DomainClaimTTL sets the window). Confirmed domains never expire. ConfirmWorkspaceDomain fails with ErrConflict when the domain was already confirmed, and UpdateWorkspaceDomainPolicy fails with ErrConflict on an unconfirmed domain, so the pending state never carries policy.

type DomainVerifier

type DomainVerifier interface {
	VerifyDomain(ctx context.Context, domain, challenge string) error
}

DomainVerifier optionally proves control of a workspace domain before ConfirmWorkspaceDomain marks it verified. Registered in Config.DomainVerifier, its VerifyDomain runs inside ConfirmWorkspaceDomain with the domain name and the challenge token minted at creation; any non-nil error refuses the confirmation with ErrDomainVerification. A typical implementation resolves the domain's TXT records and checks the challenge is published. Without a verifier, ConfirmWorkspaceDomain refuses with ErrNotSupported unless Config.TrustActorDomainVerification explicitly opts into trusting the actor's out-of-band check — a dangerous setting wherever the operation is reachable from self-serve actors, because a confirmed domain governs SSO enforcement and JIT provisioning for every address on it, instance-wide.

type EmailAddedEvent

type EmailAddedEvent struct {
	EventMeta
	Email EmailAddress
}

type EmailAddition

type EmailAddition struct {
	EventMeta
	Email EmailAddress
}

type EmailAddress

type EmailAddress struct {
	ID         UUID
	UserID     UUID
	Address    string
	Primary    bool
	VerifiedAt *time.Time
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

EmailAddress is one of a user's globally unique, normalized addresses. Exactly one address per user is primary; an address becomes usable for sign-in only once VerifiedAt is set.

type EmailAuthenticationCredential

type EmailAuthenticationCredential struct {
	ID        UUID
	UserID    UUID
	EmailID   UUID
	Digest    []byte
	CreatedAt time.Time
	ExpiresAt time.Time
	UsedAt    *time.Time
}

EmailAuthenticationCredential is the persisted single-use proof behind a magic link or email OTP. Only the HMAC digest of the token or code is stored.

type EmailAuthenticationRequestedEvent

type EmailAuthenticationRequestedEvent struct {
	EventMeta
	UserID    UUID
	EmailID   UUID
	ExpiresAt time.Time
}

type EmailAuthenticationStore

type EmailAuthenticationStore interface {
	CreateEmailAuthentication(context.Context, EmailAuthenticationCredential, Commit) error
	EmailAuthenticationByID(context.Context, UUID) (EmailAuthenticationCredential, error)
	// ConsumeEmailAuthentication atomically marks the single-use magic-link
	// or email OTP token as used. When completesLogin is true it also
	// updates last_seen_at and clears the login throttle; a consumption
	// that leaves a second factor pending passes false so the completing
	// factor clears them on success. It returns ErrConflict when the token
	// was already consumed.
	ConsumeEmailAuthentication(ctx context.Context, tokenID, userID UUID, at time.Time, completesLogin bool, commit Commit) error
}

EmailAuthenticationStore persists the single-use magic-link and email OTP credentials.

type EmailConfirmation

type EmailConfirmation struct {
	EventMeta
	Email EmailAddress
}

type EmailConfirmedEvent

type EmailConfirmedEvent struct {
	EventMeta
	Email EmailAddress
}

type EmailRemoval

type EmailRemoval struct {
	EventMeta
	UserID  UUID
	EmailID UUID
}

type EmailRemovedEvent

type EmailRemovedEvent struct {
	EventMeta
	UserID  UUID
	EmailID UUID
}

type EmailStore

type EmailStore interface {
	SaveEmail(context.Context, EmailAddress, EmailVerificationCredential, Commit) error
	EmailVerificationByID(context.Context, UUID) (EmailAddress, EmailVerificationCredential, error)
	// EmailByAddress resolves an address to its record without the verification
	// credential; ErrNotFound when no address matches. ResendEmailVerification
	// uses it to find the pending address to re-issue.
	EmailByAddress(context.Context, string) (EmailAddress, error)
	// ReissueEmailVerification replaces the pending verification credential of
	// an unverified address; an already-verified or missing address reports
	// ErrConflict.
	ReissueEmailVerification(context.Context, UUID, EmailVerificationCredential, Commit) error
	VerifyEmail(context.Context, UUID, time.Time, Commit) error
	SetPrimaryEmail(context.Context, UUID, UUID, Commit) error
	RemoveEmail(context.Context, UUID, UUID, Commit) error
	Emails(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[EmailAddress], error]
}

EmailStore persists a user's email addresses and their verification credentials.

type EmailThrottleStore

type EmailThrottleStore interface {
	// ClaimEmailIssuance atomically records an issuance for (address, purpose)
	// at time `at` and reports whether it was allowed: it claims only when no
	// prior issuance for that pair is newer than notBefore. The address the
	// manager passes is an opaque, fixed-size HMAC key derived from the
	// normalized address — never the address itself — so rows stay bounded
	// and the store learns nothing about the addresses tried. Entries with
	// last_issued_at at or before notBefore no longer throttle anything, and
	// implementations prune them on each claim so anonymous traffic cannot
	// grow the bookkeeping beyond the current cooldown window. It is keyed
	// regardless of account existence, so it opens no enumeration oracle,
	// and it needs no audit commit — it is rate-limit bookkeeping.
	ClaimEmailIssuance(ctx context.Context, address, purpose string, at, notBefore time.Time) (bool, error)
}

EmailThrottleStore is an optional persistence capability that backs the per-address cooldown on unauthenticated email-issuing flows (BeginPasswordReset, BeginEmailAuthentication, BeginEmailOTP, ResendEmailVerification). Without it, or with Config.EmailIssuanceCooldown left at zero, those flows are not throttled and the host is responsible for its own rate limiting.

type EmailVerificationCredential

type EmailVerificationCredential struct {
	EmailID   UUID
	Digest    []byte
	ExpiresAt time.Time
}

EmailVerificationCredential is the persisted proof for a pending email addition. Only the HMAC digest of the token is stored.

type EmailVerificationResentEvent

type EmailVerificationResentEvent struct {
	EventMeta
	Email EmailAddress
}

type EventListener

type EventListener interface {
	OnBootstrapCompleted(context.Context, BootstrapCompletedEvent) error
	OnSignUpCompleted(context.Context, SignUpCompletedEvent) error
	OnUserCreated(context.Context, UserCreatedEvent) error
	OnWorkspaceCreated(context.Context, WorkspaceCreatedEvent) error
	OnUserStatusChanged(context.Context, UserStatusEvent) error
	OnUserProfileUpdated(context.Context, UserProfileUpdatedEvent) error
	OnWorkspaceChanged(context.Context, WorkspaceChangedEvent) error
	OnMembershipChanged(context.Context, MembershipChangedEvent) error
	OnWorkspaceInvitationCreated(context.Context, WorkspaceInvitationEvent) error
	OnWorkspaceInvitationAccepted(context.Context, WorkspaceInvitationEvent) error
	OnWorkspaceInvitationRevoked(context.Context, WorkspaceInvitationEvent) error
	OnWorkspaceDomainCreated(context.Context, WorkspaceDomainEvent) error
	OnWorkspaceDomainConfirmed(context.Context, WorkspaceDomainEvent) error
	OnWorkspaceDomainPolicyUpdated(context.Context, WorkspaceDomainEvent) error
	OnWorkspaceDomainRemoved(context.Context, WorkspaceDomainEvent) error
	OnPasswordChanged(context.Context, PasswordChangedEvent) error
	OnPasswordRehashed(context.Context, PasswordRehashedEvent) error
	OnAuthenticationSucceeded(context.Context, AuthenticationEvent) error
	OnAuthenticationFailed(context.Context, AuthenticationFailureEvent) error
	OnStepUpDenied(context.Context, StepUpDeniedEvent) error
	OnAuthorizationDenied(context.Context, AuthorizationDeniedEvent) error
	OnEmailAdded(context.Context, EmailAddedEvent) error
	OnEmailConfirmed(context.Context, EmailConfirmedEvent) error
	OnEmailVerificationResent(context.Context, EmailVerificationResentEvent) error
	OnPrimaryEmailChanged(context.Context, PrimaryEmailChangedEvent) error
	OnEmailRemoved(context.Context, EmailRemovedEvent) error
	OnTOTPEnrollmentStarted(context.Context, TOTPEnrollmentStartedEvent) error
	OnTOTPActivated(context.Context, TOTPActivatedEvent) error
	OnTOTPDisabled(context.Context, TOTPDisabledEvent) error
	OnTOTPVerified(context.Context, TOTPVerifiedEvent) error
	OnTOTPReplayRejected(context.Context, TOTPReplayRejectedEvent) error
	OnRecoveryCodeConsumed(context.Context, RecoveryCodeConsumedEvent) error
	OnPasskeyRegistered(context.Context, PasskeyRegisteredEvent) error
	OnPasskeyDeleted(context.Context, PasskeyDeletedEvent) error
	OnPasskeyAuthenticated(context.Context, PasskeyAuthenticatedEvent) error
	OnPATCreated(context.Context, PATCreatedEvent) error
	OnPATRevoked(context.Context, PATRevokedEvent) error
	OnUserCredentialsRevoked(context.Context, UserCredentialsRevokedEvent) error
	OnSecondFactorReset(context.Context, SecondFactorResetEvent) error
	OnUserAnonymized(context.Context, UserAnonymizedEvent) error
	OnRecoveryCodesRegenerated(context.Context, RecoveryCodesRegeneratedEvent) error
	OnSessionCreated(context.Context, SessionCreatedEvent) error
	OnSessionRevoked(context.Context, SessionRevokedEvent) error
	OnUserSessionsRevoked(context.Context, UserSessionsRevokedEvent) error
	OnUserLocked(context.Context, UserLockedEvent) error
	OnPasswordResetRequested(context.Context, PasswordResetRequestedEvent) error
	OnPasswordResetCompleted(context.Context, PasswordResetCompletedEvent) error
	OnEmailAuthenticationRequested(context.Context, EmailAuthenticationRequestedEvent) error
	OnPATAuthenticated(context.Context, PATAuthenticatedEvent) error
	OnPATRejected(context.Context, PATRejectedEvent) error
	OnSSOChallengeIssued(context.Context, SSOChallengeIssuedEvent) error
	OnSSOLinked(context.Context, SSOLinkedEvent) error
	OnSSOUnlinked(context.Context, SSOUnlinkedEvent) error
	OnSSOAuthenticated(context.Context, SSOAuthenticatedEvent) error
	OnSSOJITProvisioned(context.Context, SSOJITProvisionedEvent) error
	OnRoleGranted(context.Context, RoleGrantedEvent) error
	OnInstanceRoleChanged(context.Context, InstanceRoleChangedEvent) error
	OnInstanceRoleRemoved(context.Context, InstanceRoleRemovedEvent) error
	OnClientAuditRecorded(context.Context, ClientAuditRecordedEvent) error
	OnAuditUnavailable(context.Context, AuditUnavailableEvent) error
	OnSCIMConfigurationCreated(context.Context, SCIMConfigurationCreatedEvent) error
	OnSCIMUserProvisioned(context.Context, SCIMUserEvent) error
	OnSCIMUserUpdated(context.Context, SCIMUserEvent) error
	OnSCIMUserActivated(context.Context, SCIMUserEvent) error
	OnSCIMUserSuspended(context.Context, SCIMUserEvent) error
	OnSCIMUserDeprovisioned(context.Context, SCIMUserEvent) error
	OnSCIMGroupCreated(context.Context, SCIMGroupEvent) error
	OnSCIMGroupUpdated(context.Context, SCIMGroupEvent) error
	OnSCIMGroupDeleted(context.Context, SCIMGroupEvent) error
	OnSCIMGroupMembersChanged(context.Context, SCIMGroupEvent) error
	OnOAuthEvent(context.Context, OAuthEvent) error
	// contains filtered or unexported methods
}

EventListener observes committed facts. Listeners run synchronously after the commit; their errors and panics are recorded through the Observer for observability only and never propagate or interrupt other listeners. Delivery is best effort — guaranteed delivery belongs in a host-owned outbox written from a TransactionHook, with EventMeta.ID as the idempotency key. Implementations embed UnimplementedEventListener to stay compatible as methods are added. Events never carry secrets.

type EventMeta

type EventMeta struct {
	ID          UUID
	Name        EventName
	Operation   string
	OccurredAt  time.Time
	ActorID     UUID
	WorkspaceID UUID
	AuditID     UUID
}

EventMeta is embedded by every hook payload and event. ID is a UUIDv7 suitable as an idempotency key or outbox messageId; AuditID references the audit event committed with the change (empty for advisory events that have no audit of their own).

type EventName

type EventName string

EventName is the stable, unversioned name of an event, such as "user.created" or "workspace.created". Payload shapes follow the library version; names do not change.

const (
	EventBootstrapCompleted           EventName = "bootstrap.completed"
	EventSignUpCompleted              EventName = "signup.completed"
	EventUserCreated                  EventName = "user.created"
	EventUserDisabled                 EventName = "user.disabled"
	EventUserEnabled                  EventName = "user.enabled"
	EventWorkspaceCreated             EventName = "workspace.created"
	EventWorkspaceUpdated             EventName = "workspace.updated"
	EventWorkspaceDisabled            EventName = "workspace.disabled"
	EventWorkspaceEnabled             EventName = "workspace.enabled"
	EventMembershipAdded              EventName = "membership.added"
	EventWorkspaceInvitationCreated   EventName = "workspace.invitation_created"
	EventWorkspaceInvitationAccepted  EventName = "workspace.invitation_accepted"
	EventWorkspaceInvitationRevoked   EventName = "workspace.invitation_revoked"
	EventWorkspaceDomainCreated       EventName = "workspace.domain.created"
	EventWorkspaceDomainConfirmed     EventName = "workspace.domain.confirmed"
	EventWorkspaceDomainPolicyUpdated EventName = "workspace.domain.policy_updated"
	EventWorkspaceDomainRemoved       EventName = "workspace.domain.removed"
	EventMembershipStatusChanged      EventName = "membership.status_changed"
	EventMembershipRemoved            EventName = "membership.removed"
	EventPasswordChanged              EventName = "password.changed"
	EventPasswordRehashed             EventName = "password.rehashed"
	EventPasswordResetRequested       EventName = "password.reset_requested"
	EventPasswordResetCompleted       EventName = "password.reset_completed"
	EventEmailAuthenticationRequested EventName = "email_authentication.requested"
	EventAuthenticationSucceeded      EventName = "authentication.succeeded"
	EventAuthenticationFailed         EventName = "authentication.failed"
	EventStepUpDenied                 EventName = "step_up.denied"
	EventAuthorizationDenied          EventName = "authorization.denied"
	EventEmailAdded                   EventName = "email.added"
	EventEmailConfirmed               EventName = "email.confirmed"
	EventEmailVerificationResent      EventName = "email.verification_resent"
	EventPrimaryEmailChanged          EventName = "email.primary_changed"
	EventEmailRemoved                 EventName = "email.removed"
	EventTOTPEnrollmentStarted        EventName = "totp.enrollment_started"
	EventTOTPActivated                EventName = "totp.activated"
	EventTOTPDisabled                 EventName = "totp.disabled"
	EventTOTPVerified                 EventName = "totp.verified"
	EventTOTPReplayRejected           EventName = "totp.replay_rejected"
	EventRecoveryCodeConsumed         EventName = "recovery_code.consumed"
	EventRecoveryCodesRegenerated     EventName = "recovery_codes.regenerated"
	EventPasskeyRegistered            EventName = "passkey.registered"
	EventPasskeyDeleted               EventName = "passkey.deleted"
	EventPasskeyAuthenticated         EventName = "passkey.authenticated"
	EventUserCredentialsRevoked       EventName = "user.credentials_revoked"
	EventSecondFactorReset            EventName = "user.second_factor_reset"
	EventUserAnonymized               EventName = "user.anonymized"
	EventUserLocked                   EventName = "user.locked"
	EventUserProfileUpdated           EventName = "user.profile_updated"
	EventSessionCreated               EventName = "session.created"
	EventSessionRevoked               EventName = "session.revoked"
	EventUserSessionsRevoked          EventName = "session.user_revoked"
	EventPATCreated                   EventName = "pat.created"
	EventPATRevoked                   EventName = "pat.revoked"
	EventPATAuthenticated             EventName = "pat.authenticated"
	EventPATRejected                  EventName = "pat.rejected"
	EventSSOChallengeIssued           EventName = "sso.challenge_issued"
	EventSSOLinked                    EventName = "sso.linked"
	EventSSOUnlinked                  EventName = "sso.unlinked"
	EventSSOAuthenticated             EventName = "sso.authenticated"
	EventSSOJITProvisioned            EventName = "sso.jit_provisioned"
	EventRoleGranted                  EventName = "role.granted"
	EventInstanceRoleChanged          EventName = "instance_role.changed"
	EventInstanceRoleRemoved          EventName = "instance_role.removed"
	EventClientAuditRecorded          EventName = "client_audit.recorded"
	EventAuditUnavailable             EventName = "audit.unavailable"
	EventSCIMConfigurationCreated     EventName = "scim.configuration.created"
	EventSCIMUserProvisioned          EventName = "scim.user.provisioned"
	EventSCIMUserUpdated              EventName = "scim.user.updated"
	EventSCIMUserActivated            EventName = "scim.user.activated"
	EventSCIMUserSuspended            EventName = "scim.user.suspended"
	EventSCIMUserDeprovisioned        EventName = "scim.user.deprovisioned"
	EventSCIMGroupCreated             EventName = "scim.group.created"
	EventSCIMGroupUpdated             EventName = "scim.group.updated"
	EventSCIMGroupDeleted             EventName = "scim.group.deleted"
	EventSCIMGroupMembersChanged      EventName = "scim.group.members_changed"
	EventOAuthClientRegistered        EventName = "oauth.client.registered"
	EventOAuthClientDisabled          EventName = "oauth.client.disabled"
	EventOAuthClientEnabled           EventName = "oauth.client.enabled"
	EventOAuthClientSecretRotated     EventName = "oauth.client.secret_rotated"
	EventOAuthClientJWKSReplaced      EventName = "oauth.client.jwks_replaced"
	EventOAuthIssuerDisabled          EventName = "oauth.issuer.disabled"
	EventOAuthIssuerEnabled           EventName = "oauth.issuer.enabled"
	EventOAuthResourceDisabled        EventName = "oauth.resource.disabled"
	EventOAuthResourceEnabled         EventName = "oauth.resource.enabled"
	EventOAuthCIMDResolved            EventName = "oauth.cimd.resolved"
	EventOAuthCIMDRejected            EventName = "oauth.cimd.rejected"
	EventOAuthCIMDChanged             EventName = "oauth.cimd.changed"
	EventOAuthAuthorizationGranted    EventName = "oauth.authorization.granted"
	EventOAuthAuthorizationDenied     EventName = "oauth.authorization.denied"
	EventOAuthTokenIssued             EventName = "oauth.token.issued"
	EventOAuthTokenRefreshed          EventName = "oauth.token.refreshed"
	EventOAuthTokenRevoked            EventName = "oauth.token.revoked"
	EventOAuthRefreshReuseDetected    EventName = "oauth.refresh_token.reuse_detected"
	EventOAuthCodeReuseDetected       EventName = "oauth.authorization_code.reuse_detected"
	EventOAuthConsentRevoked          EventName = "oauth.consent.revoked"
)

type ExchangeOAuthAuthorizationCodeInput

type ExchangeOAuthAuthorizationCodeInput struct {
	Issuer       string
	ClientID     string
	ClientSecret string
	// ClientSecretInBody reports that ClientSecret arrived as a
	// client_secret form field rather than an Authorization: Basic header.
	// The registered client_secret_basic method accepts only the header
	// (RFC 6749 section 2.3.1 discourages the form transport and the
	// discovery document does not announce client_secret_post), so a
	// body-borne secret fails authentication.
	ClientSecretInBody  bool
	ClientAssertion     string
	ClientAssertionType string
	Code                string
	RedirectURI         string
	CodeVerifier        string
	Resource            string
}

ExchangeOAuthAuthorizationCodeInput is a parsed token-endpoint request for the authorization_code grant, including the client credentials or assertion and the PKCE verifier.

type IdentityStore

type IdentityStore interface {
	Bootstrap(context.Context, User, EmailAddress, PasswordCredential, Workspace, Membership, InstanceAdministrator, Commit) error
	CreateUser(context.Context, User, EmailAddress, PasswordCredential, Membership, Commit) error
	SetUserDisabled(context.Context, UUID, bool, time.Time, Commit) error
	// UpdateUser persists the user's mutable profile fields. It returns
	// ErrNotFound for an unknown identifier.
	UpdateUser(context.Context, User, Commit) error
	UserByEmail(context.Context, string) (User, error)
	UserByID(context.Context, UUID) (User, error)
	Users(context.Context, PageRequest) iter.Seq2[PageEvent[User], error]
	PasswordByUserID(context.Context, UUID) (PasswordCredential, error)
	// RehashPassword installs a stronger hash of the password that just
	// verified — the transparent-rehash path — but only while previousHash,
	// the stored hash the verification ran against, is still in place. When
	// a concurrent change or reset already replaced the credential it
	// returns ErrConflict and leaves the store untouched: an unconditional
	// swap here would let an in-flight sign-in resurrect the very password
	// the user just rotated away from.
	RehashPassword(ctx context.Context, password PasswordCredential, previousHash string, commit Commit) error
	// ChangePassword installs the user's new password for an authenticated
	// password change. Unlike RehashPassword, a SessionStore-capable store
	// must stamp RevokedAt on the user's active sessions in the same
	// transaction, so a failure leaves both the password and the sessions
	// untouched.
	ChangePassword(ctx context.Context, password PasswordCredential, at time.Time, commit Commit) error
	// RecordAuthentication updates last_seen_at and clears the user's login
	// throttle in the same transaction as its audit event.
	RecordAuthentication(context.Context, UUID, time.Time, Commit) error
	// RecordPasswordAuthentication finalizes a password sign-in like
	// RecordAuthentication, but only while currentHash is still the user's
	// stored password credential; the comparison and the finalization happen
	// in the same transaction. When a concurrent change or reset replaced the
	// credential — or removed it — it returns ErrConflict and leaves the
	// store untouched, so a sign-in that verified a password can never
	// complete after that password stopped being current.
	RecordPasswordAuthentication(ctx context.Context, userID UUID, currentHash string, at time.Time, commit Commit) error
	LoginThrottleByUserID(context.Context, UUID) (LoginThrottle, error)
	// RecordLoginFailure atomically increments the failure counter and, once
	// the counter reaches the threshold, persists the lockout deadline. It
	// returns the updated throttle.
	RecordLoginFailure(ctx context.Context, userID UUID, at time.Time, threshold int64, lockedUntil time.Time, commit Commit) (LoginThrottle, error)
}

IdentityStore is the account backbone of the required Store: user records, the password credential with its currency guards, and the login throttle.

type InstanceAdministrator

type InstanceAdministrator struct {
	UserID    UUID
	Role      InstanceRole
	CreatedAt time.Time
	UpdatedAt time.Time
}

InstanceAdministrator records the instance role held by a user. The first account created by Bootstrap atomically receives root.

type InstanceRole

type InstanceRole string

InstanceRole is an instance-administration role, independent of workspace RBAC. The set is closed: only the five constants below exist, and only root may grant or remove instance roles.

const (
	InstanceRoleRoot      InstanceRole = "root"
	InstanceRoleDeveloper InstanceRole = "developer"
	InstanceRoleSupport   InstanceRole = "support"
	InstanceRoleMarketing InstanceRole = "marketing"
	InstanceRoleSales     InstanceRole = "sales"
)

type InstanceRoleChange

type InstanceRoleChange struct {
	EventMeta
	UserID       UUID
	Role         InstanceRole
	PreviousRole InstanceRole
}

type InstanceRoleChangedEvent

type InstanceRoleChangedEvent struct {
	EventMeta
	UserID       UUID
	Role         InstanceRole
	PreviousRole InstanceRole
}

type InstanceRoleRemoval

type InstanceRoleRemoval struct {
	EventMeta
	UserID       UUID
	PreviousRole InstanceRole
}

type InstanceRoleRemovedEvent

type InstanceRoleRemovedEvent struct {
	EventMeta
	UserID       UUID
	PreviousRole InstanceRole
}

type InvitationStore

type InvitationStore interface {
	CreateWorkspaceInvitation(context.Context, WorkspaceInvitation, Commit) error
	WorkspaceInvitationByID(context.Context, UUID) (WorkspaceInvitation, error)
	PendingWorkspaceInvitation(ctx context.Context, workspaceID UUID, email string) (WorkspaceInvitation, error)
	// AcceptWorkspaceInvitation atomically marks the pending invitation
	// accepted by the user and upserts the membership. It returns
	// ErrConflict when the invitation was already accepted or revoked.
	AcceptWorkspaceInvitation(ctx context.Context, invitationID, userID UUID, at time.Time, membership Membership, commit Commit) error
	// RegisterInvitedUser atomically creates the invited account (user,
	// verified email, password, membership) and marks the invitation
	// accepted. It returns ErrConflict when the invitation was already
	// accepted or revoked.
	RegisterInvitedUser(ctx context.Context, invitationID UUID, user User, email EmailAddress, password PasswordCredential, membership Membership, at time.Time, commit Commit) error
	RevokeWorkspaceInvitation(ctx context.Context, workspaceID, invitationID UUID, at time.Time, commit Commit) error
	WorkspaceInvitations(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[WorkspaceInvitation], error]
}

InvitationStore persists workspace invitations and their atomic acceptance paths.

type InviteToWorkspaceInput

type InviteToWorkspaceInput struct {
	Email string
	Role  Role
}

InviteToWorkspaceInput describes a workspace invitation: the address to invite and the role the invitee will receive on acceptance.

type IssuedEmailAuthentication

type IssuedEmailAuthentication struct {
	UserID    UUID
	EmailID   UUID
	Token     string
	ExpiresAt time.Time
	// Deliverable reports whether this is a real issuance to email. A zero
	// value (false, empty Token) is the enumeration-resistant decoy answer
	// for an ineligible address: send nothing, but answer the end user
	// exactly as if a message had been sent.
	Deliverable bool
}

IssuedEmailAuthentication carries the single-use magic-link token exactly once. The host service delivers it to the verified address and never stores it.

type IssuedEmailOTP

type IssuedEmailOTP struct {
	UserID       UUID
	EmailID      UUID
	Code         string
	Continuation string
	ExpiresAt    time.Time
	// Deliverable reports whether this is a real issuance to email. A zero
	// value (false, empty Code) is the enumeration-resistant decoy answer
	// for an ineligible address: send nothing, but answer the end user
	// exactly as if a message had been sent.
	Deliverable bool
}

IssuedEmailOTP carries the single-use numeric code exactly once, with the sealed continuation the host must hand back to CompleteEmailOTP. Code is empty when the address was not eligible; the host then sends no email but answers the end user identically.

type IssuedEmailVerification

type IssuedEmailVerification struct {
	Email EmailAddress
	Token string
	// Deliverable reports whether this is a real issuance to email. A zero
	// value (false, empty Token) is the enumeration-resistant decoy answer
	// of ResendEmailVerification: send nothing, but answer the end user
	// exactly as if a message had been sent.
	Deliverable bool
}

IssuedEmailVerification carries the raw verification token exactly once, from BeginEmailAddition or ResendEmailVerification. The host delivers it to the new address and never stores it.

type IssuedOAuthClient

type IssuedOAuthClient struct {
	Client       OAuthClient
	ClientSecret string
}

IssuedOAuthClient carries the client secret exactly once, when a registration requested client_secret_basic; ClientSecret is empty otherwise. The public Client value has no secret digest.

type IssuedOAuthInitialAccessToken

type IssuedOAuthInitialAccessToken struct {
	Credential OAuthInitialAccessToken
	Token      string
}

IssuedOAuthInitialAccessToken carries the raw DCR bootstrap token exactly once, from CreateOAuthInitialAccessToken.

type IssuedPAT

type IssuedPAT struct {
	PAT   PAT
	Token string
}

IssuedPAT carries the raw PAT exactly once, from CreatePAT. The host shows it to the user once and never stores it.

type IssuedPasswordReset

type IssuedPasswordReset struct {
	UserID    UUID
	Token     string
	ExpiresAt time.Time
	// Deliverable reports whether this is a real issuance to email. A zero
	// value (false, empty Token) is the enumeration-resistant decoy answer
	// for an unknown, disabled, or throttled address: send nothing, but
	// answer the end user exactly as if a message had been sent.
	Deliverable bool
}

IssuedPasswordReset carries the single-use reset token exactly once. The host service delivers it to the address that requested the reset and never stores it.

type IssuedSCIMCredential

type IssuedSCIMCredential struct {
	Configuration SCIMConfiguration
	Credential    SCIMCredential
	Token         string
}

IssuedSCIMCredential carries the raw SCIM bearer token exactly once, from CreateSCIMConfiguration or RotateSCIMCredential. The public Credential value has an empty Digest.

type IssuedSession

type IssuedSession struct {
	Session Session
	Token   string
}

IssuedSession carries the raw session token exactly once, from CreateSession. The host transports it (typically in a cookie) and never stores it; only the digest is persisted.

type IssuedWorkspaceDomain

type IssuedWorkspaceDomain struct {
	Domain    WorkspaceDomain
	Challenge string
}

IssuedWorkspaceDomain is the result of CreateWorkspaceDomain: the pending record and its DNS challenge value. Unlike single-use tokens the challenge is not secret (it is published in DNS) and also stays on the record.

type IssuedWorkspaceInvitation

type IssuedWorkspaceInvitation struct {
	Invitation WorkspaceInvitation
	Token      string
}

IssuedWorkspaceInvitation carries the raw invitation token exactly once, from InviteToWorkspace. The host delivers it to the invited address and never stores it.

type LoginThrottle

type LoginThrottle struct {
	UserID         UUID
	FailedAttempts int64
	LockedUntil    *time.Time
	UpdatedAt      time.Time
}

LoginThrottle tracks consecutive authentication failures for one user. It backs the built-in account lockout and is reset by any successful authentication.

type Manager

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

Manager is the façade over every Credbound capability. It is safe for concurrent use and is built once per process with New.

func New

func New(cfg Config) (*Manager, error)

New validates the configuration and builds a Manager. It rejects missing required ports, undersized keys and invalid role or provider definitions with errors matching ErrInvalidInput, and fills zero durations and limits with the documented defaults.

Example

ExampleNew shows the smallest working configuration: a store, the Argon2id password hasher, and the three secrets. TOTP and passkey providers are optional — without them the related flows return ErrNotSupported while everything else works. Production replaces memory.New() with a SQL store whose migrations have been applied, and loads the secrets from a secret manager instead of embedding them.

package main

import (
	"bytes"
	"fmt"
	"log"

	"github.com/deepteams/credbound"
	"github.com/deepteams/credbound/memory"
	"github.com/deepteams/credbound/password"
)

func main() {
	passwords, err := password.New(password.DefaultParams())
	if err != nil {
		log.Fatal(err)
	}
	manager, err := credbound.New(credbound.Config{
		Store:          memory.New(),
		Passwords:      passwords,
		SecretKey:      bytes.Repeat([]byte{0x11}, 32), // exactly 32 bytes
		PATPepper:      bytes.Repeat([]byte{0x22}, 32), // at least 32 bytes
		RecoveryPepper: bytes.Repeat([]byte{0x33}, 32), // at least 32 bytes
	})
	fmt.Println(manager != nil, err)
}
Output:
true <nil>

func (*Manager) AcceptInvitation

func (m *Manager) AcceptInvitation(ctx context.Context, actor Authentication, raw string) (_ Membership, err error)

AcceptInvitation lets an authenticated user who owns the invited address as a verified email join the workspace with the invited role.

func (*Manager) AddEventListener

func (m *Manager) AddEventListener(listener EventListener) Subscription

AddEventListener registers a listener after construction, in addition to Config.EventListeners. It returns the Subscription that removes it; a nil listener is ignored and yields a no-op Subscription.

func (*Manager) AddMembership

func (m *Manager) AddMembership(ctx context.Context, actor Authentication, workspaceID, userID UUID, role Role) (Membership, error)

AddMembership adds an existing user to the workspace with the given role, atomically with the audit event. The actor needs a fresh AAL2 step-up plus workspace users write and RBAC write. An existing membership — local or SCIM-managed — fails with ErrConflict.

func (*Manager) AddTransactionHook

func (m *Manager) AddTransactionHook(hook TransactionHook) Subscription

AddTransactionHook registers a hook after construction, in addition to Config.TransactionHooks. It returns the Subscription that removes it; a nil hook is ignored and yields a no-op Subscription.

func (*Manager) AdminDisableWorkspace

func (m *Manager) AdminDisableWorkspace(ctx context.Context, actor Authentication, request TrustedRequest, workspaceID UUID) error

AdminDisableWorkspace is the instance-administration variant of DisableWorkspace: it requires admin workspaces write and an admin mutation (fresh AAL2, or a trusted local request) instead of a membership.

func (*Manager) AdminEnableWorkspace

func (m *Manager) AdminEnableWorkspace(ctx context.Context, actor Authentication, request TrustedRequest, workspaceID UUID) error

AdminEnableWorkspace is the instance-administration variant of EnableWorkspace: it requires admin workspaces write and an admin mutation (fresh AAL2, or a trusted local request) instead of a membership.

func (*Manager) AdminResetSecondFactor

func (m *Manager) AdminResetSecondFactor(ctx context.Context, actor Authentication, request TrustedRequest, userID UUID) (err error)

AdminResetSecondFactor is the total-loss recovery path: when a user has lost every second factor (TOTP device, recovery codes, passkeys), an instance administrator removes them all and revokes the user's server-side sessions in one atomic operation, so the account falls back to its first factor and the user re-enrolls from a fresh sign-in. The actor needs admin users write and an admin mutation (fresh AAL2, or a trusted local request); the target must exist and cannot be the actor — an administrator's own factors are removed through DisableTOTP and DeletePasskey, which re-prove possession. Hosts should notify the affected user out of band through SecondFactorResetEvent.

func (*Manager) AdminUpdateUser

func (m *Manager) AdminUpdateUser(ctx context.Context, actor Authentication, request TrustedRequest, userID UUID, input UpdateUserInput) (user User, err error)

AdminUpdateUser changes any account's display name. The actor needs admin users write and an admin mutation (fresh AAL2, or a trusted local request), like every other administrative user lifecycle operation. It returns the updated user.

func (*Manager) AdminUpdateWorkspace

func (m *Manager) AdminUpdateWorkspace(ctx context.Context, actor Authentication, request TrustedRequest, workspaceID UUID, input UpdateWorkspaceInput) (_ Workspace, err error)

AdminUpdateWorkspace is the instance-administration variant of UpdateWorkspace: it requires admin workspaces write and an admin mutation (fresh AAL2, or a trusted local request) instead of a membership, and is audited like every administrative access.

func (*Manager) AdoptSCIMUser

func (m *Manager) AdoptSCIMUser(ctx context.Context, actor Authentication, configurationID, userID UUID, input SCIMUserInput) (_ SCIMUser, err error)

AdoptSCIMUser explicitly places an existing local membership under directory management, creating the SCIM link atomically with the audit event. Unlike the provisioning operations it is run by a workspace administrator: the actor needs a fresh AAL2 step-up and workspace RBAC write. A membership already managed by SCIM fails with ErrConflict.

func (*Manager) AnonymizeUser

func (m *Manager) AnonymizeUser(ctx context.Context, actor Authentication, request TrustedRequest, userID UUID) (err error)

AnonymizeUser is the right-to-erasure primitive: an instance administrator pseudonymizes a user by scrubbing their mutable personal data — display name, email addresses (replaced with unique tombstones), SSO and PAT names, session IP/User-Agent, the personal attributes of linked SCIM profiles (which are also marked deprovisioned) and the address on workspace invitations the user accepted — while disabling the account, revoking its PATs, sessions and OAuth grants, and removing its second factors, all in one transaction. The append-only, hash-chained audit log is deliberately preserved: it retains a pseudonymous user id and the request IP/User-Agent under the host's security-log retention basis, since scrubbing it would break VerifyAuditChain. The actor needs admin users write and an admin mutation (fresh AAL2, or a trusted local request); anonymizing the last enabled root or the sole admin of a workspace fails with ErrConflict, as disabling them would. Hosts erase their own application-owned data — and any business records referencing the user — separately, per ExportUserData and their retention policy. It is irreversible.

func (*Manager) AuditEvents

func (m *Manager) AuditEvents(ctx context.Context, actor Authentication, workspaceID UUID, page PageRequest) iter.Seq2[PageEvent[AuditEvent], error]

AuditEvents streams the audit log of one workspace. The actor needs a fresh AAL2 step-up and workspace audit read in that workspace.

func (*Manager) AuthenticateOAuthAccessToken

func (m *Manager) AuthenticateOAuthAccessToken(ctx context.Context, resourceURI, raw string) (_ OAuthAuthentication, err error)

AuthenticateOAuthAccessToken validates a bearer access token for one resource URI — the MCP middleware check run on every request. It verifies the token digest, expiry and revocation, then re-validates the grant, client, issuer, resource binding, user, workspace and the workspace permissions behind every scope, so a suspended membership or revoked consent takes effect immediately. Returns the OAuthAuthentication capability, or ErrInvalidCredentials (ErrForbidden when only a scope permission is missing).

func (*Manager) AuthenticatePAT

func (m *Manager) AuthenticatePAT(ctx context.Context, raw string) (_ Authentication, err error)

AuthenticatePAT validates a raw PAT, whose marker is Config.PATPrefix, in constant time against its stored digest and returns a non-interactive AAL1 authentication carrying the PAT's workspace binding and scopes; last_used_at is updated atomically with the audit event. Malformed, unknown, expired and revoked tokens, as well as tokens of disabled users or workspaces, all fail with ErrInvalidCredentials. The result never satisfies step-up checks.

func (*Manager) AuthenticatePassword

func (m *Manager) AuthenticatePassword(ctx context.Context, email, password string) (_ Authentication, err error)

AuthenticatePassword verifies an email and password and returns an AAL1 interactive authentication whose SecondFactorRequired flag reports an active TOTP factor. An unknown address, a wrong password, a disabled user, an account without a password credential and a locked account all perform the same hash derivation and fail identically with ErrInvalidCredentials, so the caller learns nothing about account existence — ErrLocked would be an existence oracle here and is never returned to this unauthenticated entry point. The lockout is still audited (reason "locked") and hosts observe it through UserLockedEvent and AuthenticationFailureEvent; flows that follow a proof of possession (VerifyTOTP, CompleteEmailOTP) keep reporting ErrLocked. Consecutive failures on an existing enabled account count toward the lockout.

Example

ExampleManager_AuthenticatePassword signs a user in with email and password. The deterministic clock, random source, and fast hasher come from the credboundtest package and are test-only.

package main

import (
	"bytes"
	"context"
	"fmt"
	"log"

	"github.com/deepteams/credbound"
	"github.com/deepteams/credbound/credboundtest"
	"github.com/deepteams/credbound/memory"
)

func main() {
	clock := credboundtest.NewClock(credboundtest.DefaultStartTime)
	manager, err := credbound.New(credbound.Config{
		Store:          memory.New(),
		Passwords:      credboundtest.Passwords{},
		SecretKey:      bytes.Repeat([]byte{0x11}, 32),
		PATPepper:      bytes.Repeat([]byte{0x22}, 32),
		RecoveryPepper: bytes.Repeat([]byte{0x33}, 32),
		Clock:          clock.Now,
		Random:         credboundtest.NewDeterministicRandom(),
	})
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()
	if _, _, err := manager.Bootstrap(ctx, credbound.BootstrapInput{
		Email: "root@example.com", DisplayName: "Root", Password: "correct horse battery staple", WorkspaceName: "Main",
	}); err != nil {
		log.Fatal(err)
	}
	// The returned Authentication is a security capability: the host stores
	// it server-side and reuses it verbatim on later requests. Level 1 is
	// AAL1; only VerifyTOTP, a passkey, or SSO reauthentication produce AAL2.
	authn, err := manager.AuthenticatePassword(ctx, "root@example.com", "correct horse battery staple")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(authn.Method, authn.Level, authn.SecondFactorRequired)
}
Output:
password 1 false

func (*Manager) AuthenticateSCIM

func (m *Manager) AuthenticateSCIM(ctx context.Context, raw string) (_ SCIMAuthentication, err error)

AuthenticateSCIM validates a raw cbs_ bearer token in constant time and returns the SCIMAuthentication service capability scoped to its configuration and workspace; the credential's last use is recorded atomically with the audit event. Malformed, unknown, expired and revoked tokens, disabled configurations and disabled workspaces all fail with ErrInvalidCredentials; ErrNotSupported without the SCIM capability.

func (*Manager) AuthenticateSession

func (m *Manager) AuthenticateSession(ctx context.Context, raw string) (_ Authentication, _ Session, err error)

AuthenticateSession validates a raw cbs_ session token in constant time against its stored digest, enforces expiry (ErrExpired) and revocation, re-checks that the user is still enabled, touches the session's last-seen timestamp atomically with the audit event, and returns the immutable Authentication snapshot together with the Session record (Digest scrubbed). Malformed and unknown tokens, revoked sessions and sessions of disabled users all fail with ErrInvalidCredentials.

The returned Authentication reproduces the snapshot verbatim — including AuthenticatedAt — so step-up freshness keeps measuring the original factor verification. Validation happens on every request, so it deliberately emits no authentication.succeeded (or any other) event; the audit log is the record of session activity.

Cost note: by default every successful validation performs one write transaction (the last-seen touch committed with its audit event). High-traffic hosts set Config.SessionTouchInterval to coarsen that to at most one write per session per interval — revocation, expiry, idle and disabled-user checks still run against the store on every call, so a revoked session is refused on the very next request, unlike with a host-side result cache. Within the interval the session's LastSeenAt is returned as last persisted.

func (*Manager) Authorize

func (m *Manager) Authorize(ctx context.Context, authn Authentication, workspaceID UUID, minimumRole Role) error

Authorize checks that the authentication belongs to an enabled user with an active membership whose role includes the minimum role in that workspace. Missing or insufficient memberships, disabled users or workspaces, and workspace-bound credentials used elsewhere fail with ErrForbidden; a workspace requiring MFA rejects interactive AAL1 contexts with ErrStepUpRequired, and a TOTP-pending context (SecondFactorRequired) is rejected the same way in every workspace — the first factor alone never authorizes anything. A scoped credential (a PAT) passes this coarse role check only with the "*" wildcard scope — a narrowed token has no role-shaped privilege, so route it through AuthorizePermission, the canonical, finer check.

func (*Manager) AuthorizeAdmin

func (m *Manager) AuthorizeAdmin(ctx context.Context, actor Authentication, permission Permission) error

AuthorizeAdmin checks that the actor is an enabled user holding an instance role that maps to the permission. Every check — allowed or denied — appends an audit event and fails with ErrAuditUnavailable when that audit cannot be persisted; a missing role or permission fails with ErrForbidden. Instance roles never grant workspace data access by themselves, and a scope-narrowed or workspace-bound credential (a PAT or OAuth token) is refused outright — instance administration is not a delegable, workspace-scoped capability.

Deliberate exception: a workspace-unbound PAT holding the "*" scope is an unrestricted credential of its owner and passes this check, so when the owner is an instance administrator such a PAT can perform administration reads — listing every user and workspace, reading the instance audit log. Mutations stay out of reach: RequireAdminMutation additionally demands an interactive fresh AAL2 authentication (or a trusted local request), which no PAT satisfies. Mint "*" PATs for automation deliberately.

func (*Manager) AuthorizePermission

func (m *Manager) AuthorizePermission(ctx context.Context, authn Authentication, workspaceID UUID, permission WorkspacePermission) error

AuthorizePermission is the canonical tenant authorization: it checks that the authentication belongs to an enabled user with an active membership whose role carries the workspace permission. A scoped credential (a PAT) must additionally carry the permission itself — or the "*" wildcard — as a scope: scopes are the least privilege the owner chose at creation, and without this check the role lookup would silently widen a narrow token back to the member's full permission set. Failures behave exactly like Authorize — ErrForbidden fails closed, a workspace requiring MFA rejects interactive AAL1 contexts with ErrStepUpRequired while non-interactive credentials such as PATs are unaffected, and a TOTP-pending context (SecondFactorRequired) is rejected with ErrStepUpRequired in every workspace.

func (*Manager) BeginDiscoverablePasskeyAuthentication

func (m *Manager) BeginDiscoverablePasskeyAuthentication(ctx context.Context) (_ PasskeyChallenge, err error)

BeginDiscoverablePasskeyAuthentication starts a usernameless WebAuthn ceremony: no address is asked and the challenge carries an empty allowCredentials list, so the authenticator offers its discoverable credentials. Because the challenge is bound to no account, there is no per-address answer left to probe — this closes the residual enumeration signal of the per-address decoy, whose fabricated allowCredentials list holds one entry while a real account may show several. It requires a provider implementing DiscoverablePasskeyProvider and a PasskeyCredentialStore-capable store; otherwise it returns ErrNotSupported.

func (*Manager) BeginEmailAddition

func (m *Manager) BeginEmailAddition(ctx context.Context, actor Authentication, address string) (_ IssuedEmailVerification, err error)

BeginEmailAddition attaches a new, unverified address to the actor's account and returns the raw verification token exactly once for the host to deliver to that address; only its HMAC is persisted. It requires a recent interactive authentication, and a globally taken address fails with ErrConflict. The address becomes usable for sign-in only after ConfirmEmail.

func (*Manager) BeginEmailAuthentication

func (m *Manager) BeginEmailAuthentication(ctx context.Context, email string) (_ IssuedEmailAuthentication, err error)

BeginEmailAuthentication issues a single-use, short-lived magic-link token for the account owning the verified address. The host delivers the token to that address and answers the end user identically whether or not the account exists. When the address does not belong to a verified email of an enabled account, the call succeeds with a zero IssuedEmailAuthentication so the host's error path never becomes an enumeration oracle: send the email only when Deliverable is true.

func (*Manager) BeginEmailOTP

func (m *Manager) BeginEmailOTP(ctx context.Context, email string) (_ IssuedEmailOTP, err error)

BeginEmailOTP issues a single-use, short-lived numeric code for the account owning the verified address, together with a sealed continuation that the host passes back to CompleteEmailOTP with the code the user typed. The continuation is AEAD-sealed and tamper-proof, so it may safely round-trip through the client (a cookie or hidden form field); what matters is that it come back with the code, not where it was kept. Binding the code to the continuation keeps its short length safe: a code is only ever compared against the single credential it was issued for, and failed attempts count toward the account lockout.

The call answers identically whether or not the address is eligible: for an unknown, disabled, or unverified address it still returns a well-formed continuation with an empty Code and Deliverable false, so the host sends no email but responds to the end user exactly as in the success case, and the later completion fails like any wrong code. Send the email only when Deliverable is true.

func (*Manager) BeginOAuthAuthorization

func (m *Manager) BeginOAuthAuthorization(ctx context.Context, actor Authentication, input BeginOAuthAuthorizationInput) (_ OAuthConsent, err error)

BeginOAuthAuthorization validates an authorization request — client, exact redirect URI, PKCE S256, state, resource, scopes and the actor's membership and per-scope permissions — and returns a sealed OAuthConsent for the host's consent UI. Nothing is persisted yet: only CompleteOAuthAuthorization can turn the continuation into a grant and code. The actor must be interactive; RequiresStepUp on the result signals that a scope demands a stronger or fresher authentication.

func (*Manager) BeginPasskeyAuthentication

func (m *Manager) BeginPasskeyAuthentication(ctx context.Context, email string) (_ PasskeyChallenge, err error)

BeginPasskeyAuthentication starts a WebAuthn authentication ceremony for the account owning the address. No actor is required. An address that cannot authenticate — unknown, disabled, or with no passkey — is answered with a decoy challenge indistinguishable from a real one, so the response never reveals whether the account exists or holds a passkey; the decoy fails at FinishPasskeyAuthentication like any wrong credential. Returns ErrNotSupported without Config.Passkeys.

func (*Manager) BeginPasskeyRegistration

func (m *Manager) BeginPasskeyRegistration(ctx context.Context, actor Authentication, name string) (_ PasskeyChallenge, err error)

BeginPasskeyRegistration starts a WebAuthn registration ceremony for the actor and returns the browser options with a sealed continuation bound to the user, operation and expiry. It requires a recent interactive authentication and returns ErrNotSupported without Config.Passkeys.

func (*Manager) BeginPasswordReset

func (m *Manager) BeginPasswordReset(ctx context.Context, email string) (_ IssuedPasswordReset, err error)

BeginPasswordReset issues a single-use, expiring reset token for the account owning the address. The host delivers the token to that address and answers the end user identically whether or not the account exists. When the address does not belong to an enabled account, the call succeeds with a zero IssuedPasswordReset so the host's error path never becomes an enumeration oracle: send the email only when Deliverable is true. The library performs the same cryptographic work and a comparable store write in both cases so timing does not reveal the difference either.

func (*Manager) BeginSSO

func (m *Manager) BeginSSO(ctx context.Context, providerConfigurationID UUID) (SSOChallenge, error)

BeginSSO starts a sign-in ceremony with a registered provider and returns the redirect URL with a sealed continuation for FinishSSO. No actor is required; an unregistered configuration fails with ErrNotFound. Sign-in only succeeds for an identity previously linked with BeginSSOLink — Credbound never matches accounts by IdP email.

func (m *Manager) BeginSSOLink(ctx context.Context, actor Authentication, providerConfigurationID UUID) (SSOChallenge, error)

BeginSSOLink starts a ceremony that links the provider identity to the actor's existing account when finished. It requires a recent interactive authentication, per the explicit-linking policy.

func (*Manager) BeginSSOStepUp

func (m *Manager) BeginSSOStepUp(ctx context.Context, actor Authentication, providerConfigurationID UUID) (SSOChallenge, error)

BeginSSOStepUp starts a step-up ceremony for the actor: the provider is asked to force reauthentication and its own MFA, and the finished ceremony must resolve to an identity already linked to this actor. It requires a recent interactive authentication.

func (*Manager) BeginTOTPEnrollment

func (m *Manager) BeginTOTPEnrollment(ctx context.Context, actor Authentication) (_ TOTPEnrollment, err error)

BeginTOTPEnrollment creates or replaces the actor's inactive TOTP factor and returns the otpauth URI once for the host to render; the secret is persisted sealed. It requires a recent interactive authentication and returns ErrNotSupported without Config.TOTP. The factor gates nothing until ConfirmTOTPEnrollment activates it.

func (*Manager) Bootstrap

func (m *Manager) Bootstrap(ctx context.Context, input BootstrapInput) (_ Authentication, _ Workspace, err error)

Bootstrap creates the first account of an empty instance: the user, its verified primary email, the initial workspace, an admin membership and the instance-level root role, all in one transaction with the audit event. It requires no actor, returns an AAL1 password authentication, and every call after the first fails with ErrConflict.

func (*Manager) ChangePassword

func (m *Manager) ChangePassword(ctx context.Context, actor Authentication, currentPassword, newPassword string) (err error)

ChangePassword replaces the actor's password after re-verifying the current one. A wrong current password counts toward the account lockout exactly like a failed sign-in, and a locked account is refused with ErrLocked before any verification. It requires a recent interactive authentication (any AAL, within the step-up window) and validates the new password against the built-in rules and Config.PasswordPolicy. A wrong current password is audited and returns ErrInvalidCredentials.

A change revokes the user's server-side sessions (when the store is SessionStore-capable) in the same transaction that installs the new password, so a leaked session token cannot outlive it and a failure leaves both untouched; this includes the actor's current session, so the host re-establishes one afterwards by re-authenticating with the new password — a pre-change password Authentication can no longer mint sessions (see CreateSession). PATs and OAuth grants are deliberately preserved: they are integration credentials, not interactive sessions, and a routine change should not break machine-to-machine access. A host managing its own sessions must terminate them itself, and one treating the change as a full compromise response can still call RevokeUserCredentials alongside it.

func (*Manager) CompleteEmailAuthentication

func (m *Manager) CompleteEmailAuthentication(ctx context.Context, raw string) (_ Authentication, err error)

CompleteEmailAuthentication consumes a magic-link token and returns an AAL1 interactive authentication. Like a password login, it reports whether an active TOTP factor still has to be verified before AAL2 operations.

func (*Manager) CompleteEmailOTP

func (m *Manager) CompleteEmailOTP(ctx context.Context, continuation, code string) (_ Authentication, err error)

CompleteEmailOTP consumes an email OTP and returns an AAL1 interactive authentication. Like a password login, it reports whether an active TOTP factor still has to be verified before AAL2 operations. A wrong code counts toward the account lockout exactly like a wrong password.

func (*Manager) CompleteOAuthAuthorization

func (m *Manager) CompleteOAuthAuthorization(ctx context.Context, actor Authentication, rawContinuation string, approved bool) (_ OAuthAuthorizationResult, err error)

CompleteOAuthAuthorization resolves a consent continuation: approval re-validates everything, creates the grant and the single-use authorization code atomically with the audit event, and returns the code exactly once; denial audits the refusal and returns an access_denied result for the redirect. The continuation must belong to the same interactive actor as BeginOAuthAuthorization, and scopes demanding a stronger or fresher authentication fail with ErrStepUpRequired.

func (*Manager) CompletePasswordReset

func (m *Manager) CompletePasswordReset(ctx context.Context, raw, newPassword string) (_ User, err error)

CompletePasswordReset consumes a reset token and installs the new password. For a passwordless member provisioned by SSO JIT or SCIM this installs their first password: the verified email proof is the same authority every reset rests on. An address under a confirmed EnforceSSO domain is refused by BeginPasswordReset up front, and the account's primary address is re-checked here so an in-flight token does not outlive an EnforceSSO confirmation — consuming it would install an unusable password yet still revoke the account's sessions, PATs and grants. As required by the recovery policy, it atomically revokes every PAT and OAuth grant of the account and clears its login throttle. When the store supports sessions (SessionStore) the user's server-side sessions are revoked in the same transaction; hosts managing their own sessions must still terminate those themselves. The user then signs in again with the new password.

func (*Manager) ConfirmEmail

func (m *Manager) ConfirmEmail(ctx context.Context, raw string) (_ EmailAddress, err error)

ConfirmEmail marks the pending address verified by proving possession of the verification token. Possession of the token is the authorization — no actor is required. An unknown or mismatched token fails with ErrInvalidCredentials, a stale one with ErrExpired, and an already verified address with ErrConflict.

func (*Manager) ConfirmTOTPEnrollment

func (m *Manager) ConfirmTOTPEnrollment(ctx context.Context, actor Authentication, code string) (_ []string, err error)

ConfirmTOTPEnrollment activates the pending factor after proving a valid code and returns the single-use recovery codes exactly once; only their peppered digests are persisted. It requires a recent interactive authentication and returns ErrNotSupported without Config.TOTP, ErrNotFound without a pending enrollment, and ErrInvalidCredentials for a wrong code.

func (*Manager) ConfirmWorkspaceDomain

func (m *Manager) ConfirmWorkspaceDomain(ctx context.Context, actor Authentication, domainID UUID) (err error)

ConfirmWorkspaceDomain marks the pending domain verified. When a Config.DomainVerifier is registered it proves control here — resolving the challenge against the domain's DNS — and fails with ErrDomainVerification when the challenge is not published. Without one it refuses with ErrNotSupported unless Config.TrustActorDomainVerification explicitly opts into treating the call as the actor's assertion that DNS verification completed; Credbound never queries DNS itself. It requires a fresh AAL2 step-up and workspace settings write in the owning workspace, and fails with ErrConflict when the domain was already confirmed. Only from this point on does the domain's policy apply.

func (*Manager) CreateOAuthInitialAccessToken

func (m *Manager) CreateOAuthInitialAccessToken(ctx context.Context, actor Authentication, request TrustedRequest, issuerID UUID, input CreateOAuthInitialAccessTokenInput) (_ IssuedOAuthInitialAccessToken, err error)

CreateOAuthInitialAccessToken issues the expiring, registration-limited bootstrap credential for protected DCR and returns the raw token exactly once; only its HMAC is persisted, atomically with the audit event. The actor needs admin settings write and an admin mutation (fresh AAL2, or a trusted local request); an issuer whose DCR mode is not protected fails with ErrNotSupported. The token grants no authority over any resource.

func (*Manager) CreateOAuthIssuer

func (m *Manager) CreateOAuthIssuer(ctx context.Context, actor Authentication, request TrustedRequest, input CreateOAuthIssuerInput) (_ OAuthIssuer, err error)

CreateOAuthIssuer registers an authorization-server issuer with its CIMD, DCR, OIDC and token-lifetime policy, atomically with the audit event. The actor needs admin settings write and an admin mutation (fresh AAL2, or a trusted local request). Returns ErrNotSupported unless both Config.OAuth and the OAuthStore capability exist.

func (*Manager) CreateOAuthProtectedResource

func (m *Manager) CreateOAuthProtectedResource(ctx context.Context, actor Authentication, workspaceID UUID, input CreateOAuthProtectedResourceInput) (_ OAuthProtectedResource, err error)

CreateOAuthProtectedResource registers an MCP resource of the workspace under an issuer, with scope definitions that map onto registered workspace permissions, atomically with the audit event. The actor needs a fresh AAL2 step-up and the oauth.resource.manage workspace permission; access tokens are later bound to this resource URI and workspace. Returns ErrNotSupported without the OAuth capability.

func (*Manager) CreatePAT

func (m *Manager) CreatePAT(ctx context.Context, actor Authentication, input CreatePATInput) (_ IssuedPAT, err error)

CreatePAT issues a personal access token with at least 256 bits of entropy and returns the raw token exactly once; only its HMAC digest is persisted, atomically with the audit event. The authentication it demands follows Config.StepUp's PAT scope — a fresh AAL2 step-up by default — and binding the token to a workspace additionally requires access to that workspace. Each scope is either the "*" wildcard or a workspace permission string: AuthorizePermission denies a scoped authentication any permission outside its scopes, and the coarse role-based Authorize requires the wildcard, so the scopes chosen here are the ceiling of what the token can ever do.

Example

ExampleManager_CreatePAT issues a personal access token. Creation requires a fresh interactive AAL2 authentication (a step-up); the raw token is returned exactly once and only its digest is persisted.

package main

import (
	"bytes"
	"context"
	"fmt"
	"log"

	"github.com/deepteams/credbound"
	"github.com/deepteams/credbound/credboundtest"
	"github.com/deepteams/credbound/memory"
)

func main() {
	clock := credboundtest.NewClock(credboundtest.DefaultStartTime)
	manager, err := credbound.New(credbound.Config{
		Store:          memory.New(),
		Passwords:      credboundtest.Passwords{},
		SecretKey:      bytes.Repeat([]byte{0x11}, 32),
		PATPepper:      bytes.Repeat([]byte{0x22}, 32),
		RecoveryPepper: bytes.Repeat([]byte{0x33}, 32),
		Clock:          clock.Now,
		Random:         credboundtest.NewDeterministicRandom(),
	})
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()
	authn, workspace, err := manager.Bootstrap(ctx, credbound.BootstrapInput{
		Email: "root@example.com", DisplayName: "Root", Password: "correct horse battery staple", WorkspaceName: "Main",
	})
	if err != nil {
		log.Fatal(err)
	}
	stepUp := credboundtest.AAL2(authn.UserID, clock.Now()) // test-only step-up
	issued, err := manager.CreatePAT(ctx, stepUp, credbound.CreatePATInput{
		Name: "ci", WorkspaceID: workspace.ID, Scopes: []string{"read"},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(issued.PAT.Name, issued.PAT.Scopes, issued.Token != "", issued.PAT.Digest == nil)
}
Output:
ci [read] true true

func (*Manager) CreateSCIMConfiguration

func (m *Manager) CreateSCIMConfiguration(ctx context.Context, actor Authentication, workspaceID UUID, input CreateSCIMConfigurationInput) (_ IssuedSCIMCredential, err error)

CreateSCIMConfiguration creates the provisioning domain of a workspace with its first bearer credential and returns the raw token exactly once; only its HMAC is persisted, atomically with the audit event. The actor needs a fresh AAL2 step-up and workspace RBAC write. Returns ErrNotSupported when the store lacks the SCIM capability.

func (*Manager) CreateSession

func (m *Manager) CreateSession(ctx context.Context, actor Authentication, _ CreateSessionInput) (_ IssuedSession, err error)

CreateSession persists a server-side session for the actor: an immutable snapshot of the Authentication (method, level, authenticated-at, pending second factor) plus the device metadata attached to the context with WithRequestMetadata, behind an opaque cbs_ token returned exactly once. Only the HMAC digest of the token is stored, atomically with the audit event. It requires a SessionStore-capable store (ErrNotSupported otherwise), an interactive actor — a PAT-backed Authentication is non-interactive and fails with ErrForbidden — and an enabled user.

Sessions never change assurance level in place: after VerifyTOTP or any other AAL transition the host calls CreateSession again with the promoted Authentication and revokes the previous session, which doubles as fixation protection. Expiry is absolute (CreatedAt plus Config.SessionTTL) and is never extended by activity.

A password-derived Authentication carries a fingerprint of the credential it verified, and the store re-checks it inside the session transaction: an Authentication whose password was replaced in the meantime — by ChangePassword or CompletePasswordReset — fails with ErrInvalidCredentials instead of minting a session the replacement's revocation sweep can no longer reach. After a password change the host therefore re-authenticates with the new password before creating the follow-up session.

func (*Manager) CreateUser

func (m *Manager) CreateUser(ctx context.Context, actor Authentication, workspaceID UUID, input CreateUserInput) (_ User, err error)

CreateUser administratively creates an account with a verified primary email and an active membership in the workspace, atomically with its audit. The actor needs a fresh AAL2 step-up and workspace users write in that workspace; a taken address fails with ErrConflict.

func (*Manager) CreateWorkspace

func (m *Manager) CreateWorkspace(ctx context.Context, actor Authentication, input CreateWorkspaceInput) (_ Workspace, err error)

CreateWorkspace creates a workspace and makes the actor its admin member, atomically with the audit event. It requires a fresh AAL2 step-up from an enabled user.

func (*Manager) CreateWorkspaceDomain

func (m *Manager) CreateWorkspaceDomain(ctx context.Context, actor Authentication, workspaceID UUID, domain string) (_ IssuedWorkspaceDomain, err error)

CreateWorkspaceDomain registers an email domain for the workspace in a pending state and returns the DNS challenge value the host publishes as a TXT record. Credbound performs no network I/O: the host proves control of the domain out of band and then calls ConfirmWorkspaceDomain. The domain name is normalized to lowercase and must be a registrable DNS name, unique across all workspaces (ErrConflict) — except that a stale pending claim, one left unconfirmed past Config.DomainClaimTTL, is replaced rather than defended, so an unverified claim can never permanently deny the domain's real owner. It requires a DomainStore-capable store (ErrNotSupported otherwise), a fresh AAL2 step-up and workspace settings write, exactly like UpdateWorkspace. An unconfirmed domain has no effect on any flow.

func (*Manager) DeletePasskey

func (m *Manager) DeletePasskey(ctx context.Context, actor Authentication, passkeyID UUID) (err error)

DeletePasskey removes one of the actor's passkeys, atomically with the audit event. It requires a fresh AAL2 step-up and works even without Config.Passkeys, so stale credentials remain removable.

func (*Manager) DeleteSCIMGroup

func (m *Manager) DeleteSCIMGroup(ctx context.Context, principal SCIMAuthentication, id UUID) (err error)

DeleteSCIMGroup logically deletes a directory group and recomputes the roles of its former members, atomically with the transactional hook and audit. Deleting an unknown group is a no-op.

func (*Manager) DeprovisionSCIMUser

func (m *Manager) DeprovisionSCIMUser(ctx context.Context, principal SCIMAuthentication, id UUID) (err error)

DeprovisionSCIMUser logically deprovisions a managed user: it suspends the membership and revokes the user's workspace-scoped PATs while keeping the global account and the SCIM link for auditing and restoration, atomically with the transactional hook and audit. Deprovisioning an unknown or already deprovisioned user is a no-op.

Server-side sessions are global, not workspace-scoped, so they survive a SCIM deprovisioning: the suspended membership already denies every tenant-scoped authorization on the next check. A host that wants IdP offboarding to also end sessions everywhere calls DisableUser or RevokeUserSessions from its own directory-event handling.

func (*Manager) DisableOAuthClient

func (m *Manager) DisableOAuthClient(ctx context.Context, actor Authentication, request TrustedRequest, clientRecordID UUID) error

DisableOAuthClient disables a client record so authorization, token, and bearer-validation operations refuse it — already-issued access tokens stop authenticating immediately — atomically with the audit event. The actor needs admin settings write and an admin mutation (fresh AAL2, or a trusted local request); disabling an already disabled client is a no-op.

func (*Manager) DisableOAuthIssuer

func (m *Manager) DisableOAuthIssuer(ctx context.Context, actor Authentication, request TrustedRequest, issuerID UUID) error

DisableOAuthIssuer disables an issuer so all its discovery, authorization and token operations are refused, atomically with the audit event. The actor needs admin settings write and an admin mutation (fresh AAL2, or a trusted local request); disabling an already disabled issuer is a no-op. Returns ErrNotSupported without the OAuth capability.

func (*Manager) DisableOAuthProtectedResource

func (m *Manager) DisableOAuthProtectedResource(ctx context.Context, actor Authentication, workspaceID UUID, resourceID UUID) error

DisableOAuthProtectedResource disables an MCP resource of the workspace so its bearer validation and metadata are refused, atomically with the audit event. The actor needs a fresh AAL2 step-up and the oauth.resource.manage permission in that workspace; a resource of another workspace fails with ErrForbidden.

func (*Manager) DisableSCIMConfiguration

func (m *Manager) DisableSCIMConfiguration(ctx context.Context, actor Authentication, configurationID UUID) (err error)

DisableSCIMConfiguration turns provisioning off for the workspace and revokes all the configuration's active credentials, atomically with the audit event. The actor needs a fresh AAL2 step-up and workspace RBAC write; ErrNotSupported without the SCIM capability.

func (*Manager) DisableTOTP

func (m *Manager) DisableTOTP(ctx context.Context, actor Authentication, code string) (err error)

DisableTOTP removes the actor's active TOTP factor and its recovery codes after re-proving possession of a valid code, atomically with the audit event. The verification itself yields the fresh AAL2 step-up the removal requires. Returns ErrNotSupported without Config.TOTP.

func (*Manager) DisableUser

func (m *Manager) DisableUser(ctx context.Context, actor Authentication, request TrustedRequest, userID UUID) error

DisableUser disables a global account so it can no longer authenticate or authorize anywhere, atomically with the audit event. The actor needs admin users write and an admin mutation (fresh AAL2, or a trusted local request); the store protects the last enabled root administrator. The store cascade revokes the user's credentials and, when the store supports sessions (SessionStore), their server-side sessions in the same transaction; re-enabling never restores them. Disabling an already disabled user is a no-op.

func (*Manager) DisableWorkspace

func (m *Manager) DisableWorkspace(ctx context.Context, actor Authentication, workspaceID UUID) error

DisableWorkspace disables the workspace so every tenant-scoped capability is denied until it is re-enabled; the store cascade also revokes the workspace-bound PAT and OAuth credentials. The actor needs a fresh AAL2 step-up and workspace settings write. Disabling an already disabled workspace is a no-op.

func (*Manager) Emails

func (m *Manager) Emails(ctx context.Context, actor Authentication, userID UUID, page PageRequest) iter.Seq2[PageEvent[EmailAddress], error]

Emails streams a user's email addresses. An empty userID means the actor, which requires a recent interactive authentication; reading another user requires admin users read.

func (*Manager) EnableOAuthClient

func (m *Manager) EnableOAuthClient(ctx context.Context, actor Authentication, request TrustedRequest, clientRecordID UUID) error

EnableOAuthClient re-enables a disabled client under the same authorization as DisableOAuthClient.

func (*Manager) EnableOAuthIssuer

func (m *Manager) EnableOAuthIssuer(ctx context.Context, actor Authentication, request TrustedRequest, issuerID UUID) error

EnableOAuthIssuer re-enables a disabled issuer under the same authorization as DisableOAuthIssuer.

func (*Manager) EnableOAuthProtectedResource

func (m *Manager) EnableOAuthProtectedResource(ctx context.Context, actor Authentication, workspaceID UUID, resourceID UUID) error

EnableOAuthProtectedResource re-enables a disabled resource under the same authorization as DisableOAuthProtectedResource.

func (*Manager) EnableUser

func (m *Manager) EnableUser(ctx context.Context, actor Authentication, request TrustedRequest, userID UUID) error

EnableUser re-enables a disabled global account under the same authorization as DisableUser.

func (*Manager) EnableWorkspace

func (m *Manager) EnableWorkspace(ctx context.Context, actor Authentication, workspaceID UUID) error

EnableWorkspace restores a disabled workspace. The actor must still be an active member holding workspace settings write with a fresh AAL2 step-up — the only tenant mutation a disabled workspace accepts.

func (*Manager) ExchangeOAuthAuthorizationCode

func (m *Manager) ExchangeOAuthAuthorizationCode(ctx context.Context, input ExchangeOAuthAuthorizationCodeInput) (_ OAuthTokenResponse, err error)

ExchangeOAuthAuthorizationCode implements the token endpoint's authorization_code grant: it authenticates the client, verifies the single-use code, exact redirect URI and PKCE verifier, and consumes the code atomically with the issued tokens and audit. Opaque tokens are returned exactly once; a refresh token is added only for offline_access grants of refresh-capable clients and an ID Token only for openid grants of OIDC issuers. Every mismatch fails with ErrInvalidCredentials.

func (*Manager) ExportUserData

func (m *Manager) ExportUserData(ctx context.Context, actor Authentication, userID UUID) (_ UserDataExport, err error)

ExportUserData gathers every record Credbound holds about a user into one document for a data-subject access request. An empty userID exports the actor's own data and needs only a recent interactive authentication; exporting another user requires a fresh AAL2 step-up and admin users read. Sessions are included only on a SessionStore-capable store, SCIM profiles and accepted workspace invitations only on a PrivacyStore-capable one, and OAuth grants only with the OAuth capability. Token digests, sealed passkey credentials and the audit log are never included; the audit log stays available through AuditEvents under its own retention policy.

func (*Manager) FinishDiscoverablePasskeyAuthentication

func (m *Manager) FinishDiscoverablePasskeyAuthentication(ctx context.Context, continuation string, response []byte) (_ Authentication, err error)

FinishDiscoverablePasskeyAuthentication validates the browser response of a discoverable ceremony, resolves the account from the asserted credential, and returns an AAL2 interactive authentication with the same single-use ceremony consumption as FinishPasskeyAuthentication. A disabled account and a confirmed EnforceSSO domain are refused — the domain policy, checked by address at Begin in the email-first flow, is enforced here against the resolved account's primary address.

func (*Manager) FinishPasskeyAuthentication

func (m *Manager) FinishPasskeyAuthentication(ctx context.Context, continuation string, response []byte) (_ Authentication, err error)

FinishPasskeyAuthentication validates the browser response against the sealed continuation and returns an AAL2 interactive authentication — a user-verified passkey ceremony needs no second factor. The passkey's last-use timestamp is updated atomically with the audit event, and the same commit consumes the single-use ceremony, so a captured response can never be replayed — WebAuthn signature counters alone cannot guarantee that, since many authenticators legitimately report a constant zero. A failed or replayed ceremony is audited and returns ErrInvalidCredentials.

func (*Manager) FinishPasskeyRegistration

func (m *Manager) FinishPasskeyRegistration(ctx context.Context, actor Authentication, continuation string, response []byte) (_ Passkey, err error)

FinishPasskeyRegistration validates the browser response against the sealed continuation and persists the new passkey, atomically with the audit event. The continuation must belong to the same actor, who still needs a recent interactive authentication; a failed ceremony is audited and returns ErrInvalidCredentials. The returned Passkey carries no credential material.

func (*Manager) FinishSSO

func (m *Manager) FinishSSO(ctx context.Context, continuation string, response []byte) (_ Authentication, err error)

FinishSSO completes any SSO ceremony (sign-in, link or step-up) by validating the provider response against the sealed continuation. The authentication is AAL2 only when the provider carries a Config.SSOAssurance policy the asserted context satisfies (or that trusts the provider unverified); otherwise it is AAL1, because SSO never mints AAL2 on the IdP's unverified word. Link ceremonies persist the new identity atomically with the audit event; sign-in and step-up resolve the stable issuer/subject pair and update its last use. Failed or mismatched ceremonies return ErrInvalidCredentials, stale continuations ErrExpired.

On a DomainStore-capable store, a sign-in whose identity is unknown may JIT-provision an account: when the IdP-verified email belongs to a confirmed auto-join workspace domain that trusts this provider configuration and no existing account owns the address, one transaction creates a passwordless user, its verified primary email, the configured membership, and the identity link. An address owned by an existing account is never auto-linked and the sign-in fails as an unknown identity.

func (*Manager) GrantRole

func (m *Manager) GrantRole(ctx context.Context, actor Authentication, workspaceID, userID UUID, role Role) (err error)

GrantRole sets a user's workspace role, creating the local membership when none exists, atomically with the audit event. The actor needs a fresh AAL2 step-up and workspace RBAC write; SCIM-managed memberships fail with ErrConflict and unknown roles are rejected. The store protects the last active workspace administrator from demotion.

func (*Manager) InstanceAdministrator

func (m *Manager) InstanceAdministrator(ctx context.Context, actor Authentication, userID UUID) (InstanceAdministrator, error)

InstanceAdministrator returns one user's instance-administration role. It requires admin instance-roles read; an unknown or role-less user reports ErrNotFound.

func (*Manager) InstanceAdministrators

func (m *Manager) InstanceAdministrators(ctx context.Context, actor Authentication) iter.Seq2[InstanceAdministrator, error]

InstanceAdministrators streams every instance role assignment, oldest first, so an administration interface can render the governance roster. It requires admin instance-roles read.

func (*Manager) InstanceAuditEvents

func (m *Manager) InstanceAuditEvents(ctx context.Context, actor Authentication, page PageRequest) iter.Seq2[PageEvent[AuditEvent], error]

InstanceAuditEvents streams the audit log of the whole instance. It requires admin audit read.

func (*Manager) InviteToWorkspace

func (m *Manager) InviteToWorkspace(ctx context.Context, actor Authentication, workspaceID UUID, input InviteToWorkspaceInput) (_ IssuedWorkspaceInvitation, err error)

InviteToWorkspace invites an email address into a workspace with a pre-assigned role and returns the single-use token once. The invitee either accepts it from an existing authenticated account owning that address, or registers a new account with it.

func (*Manager) IssueOAuthClientCredentials

func (m *Manager) IssueOAuthClientCredentials(ctx context.Context, input OAuthClientCredentialsInput) (_ OAuthTokenResponse, err error)

IssueOAuthClientCredentials authenticates a confidential client and issues a machine-to-machine access token bound to a protected resource, with no user subject and no refresh token (RFC 6749 §4.4). The client must authenticate (client_secret or private_key_jwt, never a public client) and be registered for the client_credentials grant; the requested scopes must be non-reserved scopes the resource defines and the client is allowed. Revocation is implicit when the client, resource or issuer is disabled.

func (*Manager) Membership

func (m *Manager) Membership(ctx context.Context, actor Authentication, workspaceID, userID UUID) (Membership, error)

Membership returns one membership by workspace and user. An empty userID means the actor's own membership, which needs workspace access; reading another member requires workspace users read in that workspace.

func (*Manager) Memberships

func (m *Manager) Memberships(ctx context.Context, actor Authentication, workspaceID UUID, page PageRequest) iter.Seq2[PageEvent[Membership], error]

Memberships streams the memberships of a workspace. The actor needs workspace users read in that workspace.

func (*Manager) OAuthAuthorizationServerMetadata

func (m *Manager) OAuthAuthorizationServerMetadata(ctx context.Context, issuerURL string) (OAuthAuthorizationServerMetadata, error)

OAuthAuthorizationServerMetadata returns the RFC 8414 discovery document of an enabled issuer, reflecting its actual DCR, CIMD and OIDC policy. No authentication is required; unknown or disabled issuers fail with ErrNotFound.

func (*Manager) OAuthClients

func (m *Manager) OAuthClients(ctx context.Context, actor Authentication, issuerID UUID, page PageRequest) iter.Seq2[PageEvent[OAuthClient], error]

OAuthClients streams the client records of an issuer. It requires admin settings read.

func (*Manager) OAuthGrants

func (m *Manager) OAuthGrants(ctx context.Context, actor Authentication, workspaceID UUID, page PageRequest) iter.Seq2[PageEvent[OAuthGrant], error]

OAuthGrants streams delegations. With a workspaceID it lists the workspace's grants and requires the oauth.resource.manage permission there; with an empty workspaceID it lists the actor's own grants and requires a recent interactive authentication.

func (*Manager) OAuthInitialAccessTokens

func (m *Manager) OAuthInitialAccessTokens(ctx context.Context, actor Authentication, issuerID UUID) iter.Seq2[OAuthInitialAccessToken, error]

OAuthInitialAccessTokens streams the issuer's DCR bootstrap credentials, oldest first, revoked ones included and digests omitted, so an administration interface can inventory and revoke them. The actor needs admin settings read; ErrNotSupported without the OAuth capability.

func (*Manager) OAuthIssuers

func (m *Manager) OAuthIssuers(ctx context.Context, actor Authentication, page PageRequest) iter.Seq2[PageEvent[OAuthIssuer], error]

OAuthIssuers streams every registered issuer. It requires admin settings read; ErrNotSupported without the OAuth capability.

func (*Manager) OAuthJWKS

func (m *Manager) OAuthJWKS(ctx context.Context, issuerURL string) ([]byte, error)

OAuthJWKS returns the JSON Web Key Set of an OIDC-enabled issuer, as published by the configured OIDCSigner. No authentication is required; issuers without OIDC or a signer fail with ErrNotSupported.

func (*Manager) OAuthProtectedResourceMetadata

func (m *Manager) OAuthProtectedResourceMetadata(ctx context.Context, resourceURI string) (OAuthProtectedResourceMetadata, error)

OAuthProtectedResourceMetadata returns the RFC 9728 metadata of an enabled protected resource. No authentication is required; unknown or disabled resources and issuers fail with ErrNotFound.

func (*Manager) OAuthProtectedResources

func (m *Manager) OAuthProtectedResources(ctx context.Context, actor Authentication, workspaceID UUID, page PageRequest) iter.Seq2[PageEvent[OAuthProtectedResource], error]

OAuthProtectedResources streams the MCP resources of a workspace. The actor needs the oauth.resource.manage permission in that workspace.

func (*Manager) OAuthUserInfo

func (m *Manager) OAuthUserInfo(ctx context.Context, issuerURL, rawAccessToken string) (OIDCUserInfo, error)

OAuthUserInfo implements the OIDC UserInfo endpoint for an issuer. The access token must carry the openid scope; the subject is pairwise and never exposes the user's global UUID, and email claims require the email scope. Returns ErrNotSupported when the issuer has OIDC disabled.

func (*Manager) PATs

func (m *Manager) PATs(ctx context.Context, actor Authentication, userID UUID, page PageRequest) iter.Seq2[PageEvent[PAT], error]

PATs streams a user's tokens — metadata, prefix and timestamps, never the secret. An empty userID means the actor, which requires an interactive authentication and nothing more: what it returns are names, prefixes and timestamps, not credentials, and a token-management screen that had to re-prompt for a password every StepUpMaxAge would train users to re-authenticate on sight. Issuing and revoking stay gated by Config.StepUp's PAT scope. Reading another user requires admin users read — the same scoping as Sessions, Emails and Passkeys.

func (*Manager) Passkeys

func (m *Manager) Passkeys(ctx context.Context, actor Authentication, userID UUID) iter.Seq2[Passkey, error]

Passkeys streams the metadata of a user's registered passkeys so the host can render a credential-management page. The sealed credential material is never exposed. Reading another user requires admin users read permission.

func (*Manager) PreRegisterOAuthClient

func (m *Manager) PreRegisterOAuthClient(ctx context.Context, actor Authentication, request TrustedRequest, issuerID UUID, input OAuthClientRegistrationInput) (_ IssuedOAuthClient, err error)

PreRegisterOAuthClient administratively registers a client under an issuer, atomically with the audit event, and returns the generated client secret exactly once when client_secret_basic is requested. The actor needs admin settings write and an admin mutation (fresh AAL2, or a trusted local request); only pre-registered clients may be marked Trusted.

func (*Manager) ProvisionSCIMUser

func (m *Manager) ProvisionSCIMUser(ctx context.Context, principal SCIMAuthentication, input SCIMUserInput) (_ SCIMUser, err error)

ProvisionSCIMUser creates a passwordless global account, its primary email, a directory-owned membership with the configuration's default role and the SCIM link, all atomically with the transactional hook and audit. The principal is a SCIMAuthentication from AuthenticateSCIM; the primary address is marked verified only under TrustDirectoryEmails. A taken address or userName fails with ErrConflict.

func (*Manager) RecordAudit

func (m *Manager) RecordAudit(ctx context.Context, actor Authentication, input AuditInput) (err error)

RecordAudit appends a host-supplied event to the audit log. Credbound derives the actor, UUIDv7 and timestamp itself so a consuming service can neither impersonate an actor nor backdate an entry. A workspace-scoped event requires workspace access in that workspace; a global event requires admin access. The entry commits atomically with the ApplyClientAudit hook and fails closed with ErrAuditUnavailable.

func (*Manager) RefreshOAuthToken

func (m *Manager) RefreshOAuthToken(ctx context.Context, input RefreshOAuthTokenInput) (_ OAuthTokenResponse, err error)

RefreshOAuthToken rotates a refresh token: it authenticates the client, re-validates the grant, and atomically retires the presented token while issuing a new access/refresh pair, optionally narrowed to a subset of the granted scopes. Reuse of an already rotated or revoked refresh token revokes its whole family and fails with ErrInvalidCredentials; an expired token fails with ErrExpired.

func (*Manager) RegenerateRecoveryCodes

func (m *Manager) RegenerateRecoveryCodes(ctx context.Context, actor Authentication) (_ []string, err error)

RegenerateRecoveryCodes replaces the actor's recovery codes with a fresh set returned exactly once; the previous codes stop working in the same transaction, so a user who suspects their codes leaked — or has consumed most of them — rotates the set without re-enrolling TOTP. It requires an active TOTP factor and a fresh interactive AAL2 authentication. Returns ErrNotSupported without Config.TOTP and ErrNotFound without an active factor.

func (*Manager) RegisterFromInvitation

func (m *Manager) RegisterFromInvitation(ctx context.Context, raw string, input RegisterFromInvitationInput) (_ Authentication, _ User, err error)

RegisterFromInvitation creates the invited account in one atomic operation: the invitee chooses their own password, the invited address is the verified primary email (delivery of the token proved control of the mailbox), and the invited role becomes their membership.

func (*Manager) RegisterOAuthClient

func (m *Manager) RegisterOAuthClient(ctx context.Context, issuerURL, initialAccessToken string, input OAuthClientRegistrationInput) (_ IssuedOAuthClient, err error)

RegisterOAuthClient performs dynamic client registration against an issuer. In protected DCR mode it consumes one registration of a valid initial access token; in open mode no credential is accepted. Registered clients are never trusted, and a client secret is returned exactly once when the issuer allows it. Returns ErrNotSupported when the issuer's DCR mode is disabled and ErrInvalidCredentials for an unusable initial access token.

func (*Manager) RemoveEmail

func (m *Manager) RemoveEmail(ctx context.Context, actor Authentication, emailID UUID) (err error)

RemoveEmail deletes one of the actor's addresses, atomically with the audit event. It requires a fresh AAL2 step-up; the primary address and the last verified address cannot be removed.

func (*Manager) RemoveInstanceRole

func (m *Manager) RemoveInstanceRole(ctx context.Context, actor Authentication, request TrustedRequest, userID UUID) (err error)

RemoveInstanceRole withdraws a user's instance-administration role, atomically with the audit event, under the same authorization as SetInstanceRole. An administrator cannot remove its own role, and the store protects the last root.

func (*Manager) RemoveMembership

func (m *Manager) RemoveMembership(ctx context.Context, actor Authentication, workspaceID, userID UUID) (err error)

RemoveMembership removes a local membership, atomically with the audit event; the store cascade revokes the member's workspace-bound credentials. The actor needs a fresh AAL2 step-up and workspace users write. SCIM-managed memberships fail with ErrConflict, and the store protects the last active workspace administrator.

func (*Manager) RemoveWorkspaceDomain

func (m *Manager) RemoveWorkspaceDomain(ctx context.Context, actor Authentication, domainID UUID) (err error)

RemoveWorkspaceDomain deletes the domain and its policy, atomically with the audit event; addresses under it immediately authenticate like any other. It requires a fresh AAL2 step-up and workspace settings write in the owning workspace.

func (*Manager) ReplaceOAuthClientJWKS

func (m *Manager) ReplaceOAuthClientJWKS(ctx context.Context, actor Authentication, request TrustedRequest, clientRecordID UUID, jwks []byte) (err error)

ReplaceOAuthClientJWKS atomically replaces the inline JWKS of a pre-registered or DCR private_key_jwt client, so a compromised signing key rotates without re-registering the client. A client publishing a jwks_uri rotates by republishing its own document instead and fails here with ErrConflict, like a CIMD client, whose keys follow its published metadata. Same authorization and capability requirements as RotateOAuthClientSecret.

func (*Manager) ReplaceSCIMUser

func (m *Manager) ReplaceSCIMUser(ctx context.Context, principal SCIMAuthentication, id UUID, input SCIMUserInput) (_ SCIMUser, err error)

ReplaceSCIMUser replaces the SCIM representation of a managed user and synchronizes the membership status from Active — false suspends, true reactivates — atomically with the transactional hook and audit. The membership must be managed by the principal's configuration (ErrConflict otherwise); the global account is never disabled.

func (*Manager) RequireAdminMutation

func (m *Manager) RequireAdminMutation(actor Authentication, request TrustedRequest) error

RequireAdminMutation gates administrative writes: the actor must be interactive, and either the request was verified as loopback by the server adapter (TrustedRequest.Local) or RequireStepUp must pass. It complements AuthorizeAdmin, which checks the permission itself.

func (*Manager) RequireStepUp

func (m *Manager) RequireStepUp(authn Authentication) error

RequireStepUp accepts only an interactive AAL2 authentication whose AuthenticatedAt falls within Config.StepUpMaxAge. Anything else — a PAT regardless of age, an AAL1 context, or a stale AAL2 context — fails with ErrStepUpRequired (ErrUnauthorized when there is no actor at all), and the host should prompt for the second factor.

func (*Manager) RequireStepUpFor added in v0.0.2

func (m *Manager) RequireStepUpFor(ctx context.Context, authn Authentication, scope StepUpScope) error

RequireStepUpFor answers what the operations of one scope demand under Config.StepUp, for an actor and as of now: nil when authn may perform them, ErrStepUpRequired when the host should prompt for a second factor, ErrUnauthorized without an actor, ErrForbidden when the account is disabled or unreadable. It is the check the Manager runs itself before every gated call, so a host can raise the prompt ahead of the refusal — an admin screen that greys out its mutations, for instance — instead of discovering it from the error. Unlike RequireStepUp it needs a context, because deciding StepUpPolicyUserCapable means reading what the user has enrolled.

func (*Manager) ResendEmailVerification

func (m *Manager) ResendEmailVerification(ctx context.Context, address string) (_ IssuedEmailVerification, err error)

ResendEmailVerification re-issues a verification token for an unverified address without requiring authentication, so a user whose signup token expired or was lost is not locked out of their own account. The host answers the end user identically whether or not the address exists or is already verified: the call succeeds with a zero IssuedEmailVerification in those cases, so the error path is not an enumeration oracle — send the email only when Deliverable is true. It performs the same cryptographic work and a comparable store write in every case so timing does not reveal the difference. Re-issuing a token invalidates the previous one (the stored digest is replaced). An address under a confirmed EnforceSSO domain is refused before any lookup, exactly like signup.

func (*Manager) RevokeInvitation

func (m *Manager) RevokeInvitation(ctx context.Context, actor Authentication, workspaceID, invitationID UUID) (err error)

RevokeInvitation withdraws a pending invitation so its token can no longer be accepted.

func (*Manager) RevokeOAuthGrant

func (m *Manager) RevokeOAuthGrant(ctx context.Context, actor Authentication, grantID UUID) (err error)

RevokeOAuthGrant revokes a delegation and its tokens, atomically with the audit event. The grant's own user needs a fresh AAL2 step-up; revoking another user's grant requires the oauth.resource.manage permission in the grant's workspace with a fresh AAL2 step-up.

func (*Manager) RevokeOAuthInitialAccessToken

func (m *Manager) RevokeOAuthInitialAccessToken(ctx context.Context, actor Authentication, request TrustedRequest, tokenID UUID) (err error)

RevokeOAuthInitialAccessToken withdraws a DCR bootstrap credential, atomically with the audit event, under the same authorization as CreateOAuthInitialAccessToken.

func (*Manager) RevokeOAuthToken

func (m *Manager) RevokeOAuthToken(ctx context.Context, input RevokeOAuthTokenInput) (err error)

RevokeOAuthToken implements RFC 7009 revocation for the authenticated client's own tokens: an access token is revoked individually, a refresh token revokes its whole family. As the RFC requires, unknown or foreign tokens are silently ignored; only a failed client authentication returns an error.

func (*Manager) RevokePAT

func (m *Manager) RevokePAT(ctx context.Context, actor Authentication, patID UUID) (err error)

RevokePAT revokes one of the actor's own tokens, atomically with the audit event. It demands the same authentication as CreatePAT (the PAT scope of Config.StepUp, a fresh AAL2 step-up by default), so a token can never become harder to revoke than it was to issue; a token belonging to another user is reported as ErrNotFound by the store.

func (*Manager) RevokeSCIMCredential

func (m *Manager) RevokeSCIMCredential(ctx context.Context, actor Authentication, configurationID UUID, credentialID UUID) (err error)

RevokeSCIMCredential revokes one bearer credential of the configuration, atomically with the audit event. The actor needs a fresh AAL2 step-up and workspace RBAC write; ErrNotSupported without the SCIM capability.

func (*Manager) RevokeSession

func (m *Manager) RevokeSession(ctx context.Context, actor Authentication, sessionID UUID) (err error)

RevokeSession revokes one of the actor's own sessions, atomically with the audit event. Like PAT revocation it requires a fresh AAL2 step-up, and a session belonging to another user is reported as ErrNotFound. Bulk and administrative revocation go through RevokeUserSessions instead.

func (*Manager) RevokeUserCredentials

func (m *Manager) RevokeUserCredentials(ctx context.Context, actor Authentication, request TrustedRequest, userID UUID) (err error)

RevokeUserCredentials revokes every active PAT of a user and, when the store has the OAuth capability, every OAuth grant with its tokens, in one atomic operation. When the store supports sessions (SessionStore) the user's server-side sessions are revoked in the same transaction. A user runs it on their own account after a suspected compromise; revoking another account requires an instance administrator. Sessions the host service manages itself remain host-owned and must be invalidated by the host alongside this call.

func (*Manager) RevokeUserSessions

func (m *Manager) RevokeUserSessions(ctx context.Context, actor Authentication, request TrustedRequest, userID UUID) (err error)

RevokeUserSessions revokes every active session of a user in one atomic operation ("log out everywhere"). Its authorization mirrors RevokeUserCredentials: a user runs it on their own account with a fresh AAL2 step-up; revoking another user's sessions requires an instance administrator with admin users write and an admin mutation (fresh AAL2, or a trusted local request).

func (*Manager) RevokeWorkspacePAT added in v0.0.4

func (m *Manager) RevokeWorkspacePAT(ctx context.Context, actor Authentication, workspaceID, patID UUID) (err error)

RevokeWorkspacePAT revokes a token bound to the workspace, whoever owns it, atomically with the audit event. It is how a tenant disables the key of a departed member without instance administration: the actor needs workspace credentials manage and the step-up its workspace scope demands (Config.StepUp). A token that is not bound to this workspace — another tenant's, or an unbound instance-wide one — reports ErrNotFound, so the permission can never reach beyond the tenant that granted it.

func (*Manager) RotateOAuthClientSecret

func (m *Manager) RotateOAuthClientSecret(ctx context.Context, actor Authentication, request TrustedRequest, clientRecordID UUID) (_ IssuedOAuthClient, err error)

RotateOAuthClientSecret replaces the secret of a pre-registered or DCR client that authenticates with client_secret_basic and returns the new secret exactly once; the previous secret stops authenticating immediately. The client keeps its client_id, so deployed configurations only change the secret, and rotation works on a disabled client, so the compromise runbook — disable, rotate, re-enable — has no window where the old secret is live. The actor needs admin settings write and an admin mutation (fresh AAL2, or a trusted local request). A CIMD client fails with ErrConflict (its credentials follow its published metadata) and a client without client_secret_basic with ErrInvalidInput. Returns ErrNotSupported without the OAuth capability.

func (*Manager) RotateSCIMCredential

func (m *Manager) RotateSCIMCredential(ctx context.Context, actor Authentication, configurationID UUID, expiresAt *time.Time) (_ IssuedSCIMCredential, err error)

RotateSCIMCredential issues an additional bearer credential for the configuration and returns the raw token exactly once; existing credentials stay valid until individually revoked. The actor needs a fresh AAL2 step-up and workspace RBAC write; ErrNotSupported without the SCIM capability.

func (*Manager) SCIMConfigurations

func (m *Manager) SCIMConfigurations(ctx context.Context, actor Authentication, workspaceID UUID) iter.Seq2[SCIMConfiguration, error]

SCIMConfigurations streams the workspace's provisioning domains, oldest first, so an administration interface can inventory them. The actor needs workspace RBAC write in that workspace — the permission governing every SCIM administration operation — but no step-up, since nothing mutates. ErrNotSupported without the SCIM capability.

func (*Manager) SCIMCredentials

func (m *Manager) SCIMCredentials(ctx context.Context, actor Authentication, configurationID UUID) iter.Seq2[SCIMCredential, error]

SCIMCredentials streams the configuration's bearer credentials, oldest first, with digests omitted, so an administration interface can inventory and rotate them. The actor needs workspace RBAC write in the configuration's workspace but no step-up, since nothing mutates. ErrNotSupported without the SCIM capability.

func (*Manager) SCIMGroup

func (m *Manager) SCIMGroup(ctx context.Context, principal SCIMAuthentication, id UUID) (SCIMGroup, error)

SCIMGroup reads one directory group of the principal's configuration.

func (*Manager) SCIMGroups

func (m *Manager) SCIMGroups(ctx context.Context, principal SCIMAuthentication, filter SCIMFilter, page PageRequest) iter.Seq2[PageEvent[SCIMGroup], error]

SCIMGroups streams the directory groups of the principal's configuration, optionally narrowed by a supported equality filter (id, externalId, displayName); unsupported filters fail with ErrInvalidInput.

func (*Manager) SCIMUser

func (m *Manager) SCIMUser(ctx context.Context, principal SCIMAuthentication, id UUID) (SCIMUser, error)

SCIMUser reads one managed user of the principal's configuration.

func (*Manager) SCIMUsers

func (m *Manager) SCIMUsers(ctx context.Context, principal SCIMAuthentication, filter SCIMFilter, page PageRequest) iter.Seq2[PageEvent[SCIMUser], error]

SCIMUsers streams the managed users of the principal's configuration, optionally narrowed by a supported equality filter (id, externalId, userName, emails.value, active); unsupported filters fail with ErrInvalidInput.

func (*Manager) SSOIdentities

func (m *Manager) SSOIdentities(ctx context.Context, actor Authentication, userID UUID, page PageRequest) iter.Seq2[PageEvent[SSOIdentity], error]

SSOIdentities streams a user's linked external identities with their latest uses. An empty userID means the actor, which requires a recent interactive authentication; reading another user requires admin users read — the same scoping as Sessions, Emails and Passkeys.

func (*Manager) Sessions

func (m *Manager) Sessions(ctx context.Context, actor Authentication, userID UUID, page PageRequest) iter.Seq2[PageEvent[Session], error]

Sessions streams a user's sessions — snapshot, device metadata and timestamps, never the token digest. The actor lists their own sessions (userID empty or equal to the actor) with a recent interactive authentication; listing another user's sessions additionally requires a fresh AAL2 step-up and admin users read.

func (*Manager) SetInstanceRole

func (m *Manager) SetInstanceRole(ctx context.Context, actor Authentication, request TrustedRequest, userID UUID, role InstanceRole) (err error)

SetInstanceRole grants or changes a user's instance-administration role, atomically with the audit event. The actor needs admin instance-roles write (root only by default) and an admin mutation (fresh AAL2, or a trusted local request). A root cannot downgrade itself, and only the five built-in roles are accepted.

func (*Manager) SetMembershipStatus

func (m *Manager) SetMembershipStatus(ctx context.Context, actor Authentication, workspaceID, userID UUID, status MembershipStatus) (Membership, error)

SetMembershipStatus suspends or reactivates a local membership, atomically with the audit event; the store cascade revokes workspace-bound credentials on suspension. The actor needs a fresh AAL2 step-up and workspace users write. SCIM-managed memberships fail with ErrConflict, and the store protects the last active workspace administrator.

func (*Manager) SetPrimaryEmail

func (m *Manager) SetPrimaryEmail(ctx context.Context, actor Authentication, emailID UUID) (err error)

SetPrimaryEmail makes one of the actor's verified addresses the primary address, atomically with the audit event. It requires a fresh AAL2 step-up; an unverified address is rejected by the store.

func (*Manager) SignOut

func (m *Manager) SignOut(ctx context.Context, raw string) (err error)

SignOut revokes the session identified by possession of its raw token — the ordinary logout. Unlike RevokeSession it needs no step-up and no actor: holding the single-display token proves ownership exactly as it does for AuthenticateSession, so even a password-only (AAL1) deployment can sign out immediately. Signing out an already-revoked session succeeds silently (logout is idempotent); an expired session is still revoked so its record reads as closed. Malformed, unknown, and forged tokens fail with ErrInvalidCredentials.

func (*Manager) SignUp

func (m *Manager) SignUp(ctx context.Context, input SignUpInput) (_ SignUpResult, err error)

SignUp registers an anonymous visitor: one store transaction creates the user, their primary email address, their password credential, their workspace and their admin membership, with no instance role. It requires Config.SignUp and a SignupStore-capable store; otherwise it returns ErrNotSupported. The primary address starts unverified and the result carries the IssuedEmailVerification token the host delivers — the account cannot authenticate by email address until ConfirmEmail proves it. With Config.SignUp.AutoVerifyEmail the address is verified immediately and the result instead carries an AAL1 password Authentication.

When the address already belongs to an account the call performs the same hashing and identifier generation, audits the collision, and returns SignUpResult{ExistingAccount: true} with no error, so the host answers the end user identically and may deliver an "already registered" notice to the address instead of a verification token.

func (*Manager) TOTPStatus

func (m *Manager) TOTPStatus(ctx context.Context, actor Authentication, userID UUID) (_ TOTPStatus, err error)

TOTPStatus reports whether a user has a TOTP factor, whether it is active, and how many recovery codes remain unused. It never exposes the secret. Reading another user requires admin users read permission.

func (*Manager) UnlinkSSO

func (m *Manager) UnlinkSSO(ctx context.Context, actor Authentication, identityID UUID) (err error)

UnlinkSSO removes one of the actor's linked external identities, atomically with the audit event. It requires a fresh AAL2 step-up, and fails with ErrConflict when the identity is the actor's last remaining authentication method (no password, no passkey, no other SSO identity), so a JIT-provisioned passwordless member cannot lock themselves out.

func (*Manager) UpdateOAuthIssuer

func (m *Manager) UpdateOAuthIssuer(ctx context.Context, actor Authentication, request TrustedRequest, issuerID UUID, input UpdateOAuthIssuerInput) (_ OAuthIssuer, err error)

UpdateOAuthIssuer replaces the policy of an issuer (its URL is immutable), atomically with the audit event, under the same authorization as CreateOAuthIssuer.

func (*Manager) UpdateSCIMConfiguration

func (m *Manager) UpdateSCIMConfiguration(ctx context.Context, actor Authentication, configurationID UUID, input UpdateSCIMConfigurationInput) (_ SCIMConfiguration, err error)

UpdateSCIMConfiguration replaces the role policy of the configuration and immediately recomputes the roles of every membership it manages; the configuration change, the recomputed memberships and the audit record commit atomically. The actor needs a fresh AAL2 step-up and workspace RBAC write in the configuration's workspace. An ambiguous group mapping fails with ErrConflict; ErrNotSupported without the SCIM capability.

func (*Manager) UpdateUser

func (m *Manager) UpdateUser(ctx context.Context, actor Authentication, input UpdateUserInput) (user User, err error)

UpdateUser changes the actor's own display name. It requires a recent interactive authentication (any assurance level within the step-up window), mirroring ChangePassword. Updating another account is an administrative mutation, see AdminUpdateUser. It returns the updated user.

func (*Manager) UpdateWorkspace

func (m *Manager) UpdateWorkspace(ctx context.Context, actor Authentication, workspaceID UUID, input UpdateWorkspaceInput) (_ Workspace, err error)

UpdateWorkspace renames the workspace and optionally toggles its MFA policy, atomically with the audit event. The actor needs a fresh AAL2 step-up and workspace settings write as an active member.

func (*Manager) UpdateWorkspaceDomainPolicy

func (m *Manager) UpdateWorkspaceDomainPolicy(ctx context.Context, actor Authentication, domainID UUID, input WorkspaceDomainPolicyInput) (err error)

UpdateWorkspaceDomainPolicy replaces the policy of a confirmed domain: the auto-join flag with its target role, the SSO provider configuration the domain trusts, and the SSO enforcement flag. An unconfirmed domain fails with ErrConflict. A zero AutoJoinRole means member and the role must exist in the workspace role catalog; when AutoJoin or EnforceSSO is set the provider configuration must be registered with the Manager. It requires a fresh AAL2 step-up and workspace settings write in the owning workspace.

func (*Manager) UpsertSCIMGroup

func (m *Manager) UpsertSCIMGroup(ctx context.Context, principal SCIMAuthentication, id UUID, input SCIMGroupInput) (_ SCIMGroup, err error)

UpsertSCIMGroup creates (empty id) or replaces a directory group and recomputes the roles of every membership the change affects through the configured group-role mappings, atomically with the transactional hook and audit. An unknown or deprovisioned member fails with ErrInvalidInput and an ambiguous mapping fails closed with ErrConflict.

func (*Manager) User

func (m *Manager) User(ctx context.Context, actor Authentication, userID UUID) (User, error)

User returns one account by ID. An empty userID means the actor, which requires a recent interactive authentication; reading another user requires admin users read — the same scoping as Emails and Passkeys.

func (*Manager) UserWorkspaces

func (m *Manager) UserWorkspaces(ctx context.Context, actor Authentication, page PageRequest) iter.Seq2[PageEvent[Workspace], error]

UserWorkspaces streams the workspaces the actor belongs to. It only requires an authenticated, enabled actor.

func (*Manager) Users

func (m *Manager) Users(ctx context.Context, actor Authentication, page PageRequest) iter.Seq2[PageEvent[User], error]

Users streams every global account. It requires admin users read; the authorization itself is audited like every administrative access.

func (*Manager) ValidateOAuthAuthorizationRedirect

func (m *Manager) ValidateOAuthAuthorizationRedirect(ctx context.Context, issuerURL, clientID, redirectURI string) error

ValidateOAuthAuthorizationRedirect resolves the client and validates an exact redirect URI. HTTP adapters use it before deciding whether an OAuth authorization error may safely be redirected to the client.

func (*Manager) VerifyAuditChain

func (m *Manager) VerifyAuditChain(ctx context.Context, actor Authentication) (_ AuditChainReport, err error)

VerifyAuditChain recomputes the whole audit hash chain from the genesis and compares it with the persisted chain head. Any edited, deleted or reordered chained event yields ErrAuditCompromised. It requires admin audit read; VerifyAuditChainFrom verifies only the delta after a trusted checkpoint when the full scan grows too expensive.

func (*Manager) VerifyAuditChainFrom

func (m *Manager) VerifyAuditChainFrom(ctx context.Context, actor Authentication, checkpoint AuditChainCheckpoint) (_ AuditChainReport, err error)

VerifyAuditChainFrom recomputes the chain from a previously verified checkpoint — the HeadSequence and HeadHash of an earlier report — to the current head, so periodic verification costs the delta instead of a full scan. The zero checkpoint verifies from the genesis. The checkpoint must come from the caller's own trusted record (the previous run's report, ideally anchored outside the database as OPERATIONS.md recommends): a checkpoint read back from compromised storage would vouch for a rewritten prefix, since events at or below its sequence are not re-read.

func (*Manager) VerifyTOTP

func (m *Manager) VerifyTOTP(ctx context.Context, actor Authentication, code string) (_ Authentication, err error)

VerifyTOTP validates a TOTP or single-use recovery code for an interactive actor and returns a fresh AAL2 authentication — the second step of the password-then-TOTP flow the host stores in place of the AAL1 context. Replays of an already accepted time step and wrong codes fail with ErrInvalidCredentials; wrong codes count toward the account lockout (ErrLocked while it lasts). Returns ErrNotSupported without Config.TOTP.

func (*Manager) Workspace

func (m *Manager) Workspace(ctx context.Context, actor Authentication, workspaceID UUID) (Workspace, error)

Workspace returns one workspace by ID. The actor needs workspace access in it; admin workspaces read reaches any workspace of the instance.

func (*Manager) WorkspaceDomains

func (m *Manager) WorkspaceDomains(ctx context.Context, actor Authentication, workspaceID UUID, page PageRequest) iter.Seq2[PageEvent[WorkspaceDomain], error]

WorkspaceDomains streams the workspace's domains with their confirmation state and policy. The listing requires an active membership holding workspace access, like the other tenant read operations; the challenge is included because it is published in public DNS and is not a secret.

func (*Manager) WorkspaceInvitations

func (m *Manager) WorkspaceInvitations(ctx context.Context, actor Authentication, workspaceID UUID, page PageRequest) iter.Seq2[PageEvent[WorkspaceInvitation], error]

WorkspaceInvitations streams the workspace's invitations without their digests.

func (*Manager) WorkspaceMembers

func (m *Manager) WorkspaceMembers(ctx context.Context, actor Authentication, workspaceID UUID, page PageRequest) iter.Seq2[PageEvent[WorkspaceMember], error]

WorkspaceMembers streams the memberships of a workspace joined with each member's account profile (display name, primary email), so a workspace administrator can render a member list without the instance-wide admin users read permission. Like Memberships, it requires workspace users read in that workspace; a membership whose account vanished mid-stream is skipped rather than failing the page.

func (*Manager) WorkspacePATs added in v0.0.4

func (m *Manager) WorkspacePATs(ctx context.Context, actor Authentication, workspaceID UUID, page PageRequest) iter.Seq2[PageEvent[WorkspacePAT], error]

WorkspacePATs streams the tokens bound to one workspace joined with each owner's account profile, so a workspace administrator can render the keys page of their tenant — including the key of a colleague who has left — without the instance-wide admin users read permission. It requires workspace credentials manage in that workspace. Digests are never exposed, and a token whose owner vanished mid-stream is skipped rather than failing the page, exactly like WorkspaceMembers.

Only workspace-bound tokens appear: a token created without a WorkspaceID belongs to its user across the whole instance, not to a tenant, so no workspace administrator can see or revoke it.

func (*Manager) Workspaces

func (m *Manager) Workspaces(ctx context.Context, actor Authentication, page PageRequest) iter.Seq2[PageEvent[Workspace], error]

Workspaces streams every workspace of the instance. It requires admin workspaces read.

type Membership

type Membership struct {
	WorkspaceID UUID
	UserID      UUID
	Role        Role
	Status      MembershipStatus
	// ProvisioningSource identifies who owns the membership: the literal
	// "local", or the UUIDv7 of the SCIM configuration that manages it.
	// SCIM-managed memberships reject ordinary local mutations.
	ProvisioningSource string
	CreatedAt          time.Time
	UpdatedAt          time.Time
}

Membership binds a user to a workspace with a role and lifecycle status.

type MembershipChange

type MembershipChange struct {
	EventMeta
	Membership Membership
	Previous   *Membership
	Removed    bool
}

MembershipChange covers membership addition, status change and removal. Previous is nil for an addition, and Removed marks a removal whose Membership field holds the final state.

type MembershipChangedEvent

type MembershipChangedEvent struct {
	EventMeta
	Membership Membership
	Previous   *Membership
	Removed    bool
}

MembershipChangedEvent reports a membership addition, status change or removal. Previous is nil for an addition and Removed marks a removal.

type MembershipStatus

type MembershipStatus string

MembershipStatus is the lifecycle state of a workspace membership. A suspended membership fails every authorization but retains its role.

const (
	MembershipActive    MembershipStatus = "active"
	MembershipSuspended MembershipStatus = "suspended"
)

type OAuthAccessToken

type OAuthAccessToken struct {
	ID             UUID
	Prefix         string
	Digest         []byte
	GrantID        UUID
	ClientRecordID UUID
	UserID         UUID
	WorkspaceID    UUID
	ResourceID     UUID
	Scopes         []string
	CreatedAt      time.Time
	ExpiresAt      time.Time
	RevokedAt      *time.Time
}

OAuthAccessToken is the persisted metadata of an opaque access token, bound to one grant, resource and workspace. Only the HMAC Digest of the raw token is stored.

type OAuthApplicationType

type OAuthApplicationType string

OAuthApplicationType distinguishes web clients (HTTPS redirect URIs only) from native clients (HTTPS, or plain HTTP on a loopback host).

const (
	OAuthApplicationWeb    OAuthApplicationType = "web"
	OAuthApplicationNative OAuthApplicationType = "native"
)

type OAuthAuthentication

type OAuthAuthentication struct {
	TokenID         UUID
	GrantID         UUID
	ClientRecordID  UUID
	ClientID        string
	UserID          UUID
	WorkspaceID     UUID
	Resource        string
	Scopes          []string
	AuthenticatedAt time.Time
}

OAuthAuthentication is the validated bearer capability returned by AuthenticateOAuthAccessToken: the token, grant, client, user, workspace, resource and scopes a request may act with. Like Authentication, it must never be rebuilt from client-supplied data.

func (OAuthAuthentication) HasScope

func (a OAuthAuthentication) HasScope(required string) bool

HasScope reports whether the token carries the required scope; an empty requirement always passes.

type OAuthAuthorizationCode

type OAuthAuthorizationCode struct {
	ID             UUID
	Prefix         string
	Digest         []byte
	GrantID        UUID
	ClientRecordID UUID
	RedirectURI    string
	ResourceID     UUID
	Scopes         []string
	CodeChallenge  string
	Nonce          string
	CreatedAt      time.Time
	ExpiresAt      time.Time
	UsedAt         *time.Time
}

OAuthAuthorizationCode is the persisted single-use code of an approved authorization, stored as an HMAC digest with its PKCE challenge and exact redirect URI.

type OAuthAuthorizationResult

type OAuthAuthorizationResult struct {
	RedirectURI string
	Code        string
	State       string
	Issuer      string
	Error       string
}

OAuthAuthorizationResult is the outcome of a completed authorization: the redirect target with either the single-use Code (returned only once) or an OAuth error code such as access_denied.

type OAuthAuthorizationServerMetadata

type OAuthAuthorizationServerMetadata struct {
	Issuer                                      string   `json:"issuer"`
	AuthorizationEndpoint                       string   `json:"authorization_endpoint"`
	TokenEndpoint                               string   `json:"token_endpoint"`
	RevocationEndpoint                          string   `json:"revocation_endpoint"`
	RegistrationEndpoint                        string   `json:"registration_endpoint,omitempty"`
	JWKSURI                                     string   `json:"jwks_uri,omitempty"`
	UserInfoEndpoint                            string   `json:"userinfo_endpoint,omitempty"`
	ScopesSupported                             []string `json:"scopes_supported,omitempty"`
	ResponseTypesSupported                      []string `json:"response_types_supported"`
	GrantTypesSupported                         []string `json:"grant_types_supported"`
	TokenEndpointAuthMethodsSupported           []string `json:"token_endpoint_auth_methods_supported"`
	CodeChallengeMethodsSupported               []string `json:"code_challenge_methods_supported"`
	AuthorizationResponseIssuerParameterSupport bool     `json:"authorization_response_iss_parameter_supported"`
	ClientIDMetadataDocumentSupported           bool     `json:"client_id_metadata_document_supported"`
	SubjectTypesSupported                       []string `json:"subject_types_supported,omitempty"`
	IDTokenSigningAlgValuesSupported            []string `json:"id_token_signing_alg_values_supported,omitempty"`
}

OAuthAuthorizationServerMetadata is the RFC 8414 discovery document of an issuer.

type OAuthCIMDMode

type OAuthCIMDMode string

OAuthCIMDMode is an issuer's policy for Client Identifier Metadata Document clients: disabled, restricted to an origin allowlist, or open to any HTTPS Client Identifier URL on the public web.

const (
	OAuthCIMDDisabled  OAuthCIMDMode = "disabled"
	OAuthCIMDAllowlist OAuthCIMDMode = "allowlist"
	OAuthCIMDPublicWeb OAuthCIMDMode = "public_web"
)

type OAuthChange

type OAuthChange struct {
	EventMeta
	IssuerID UUID
	// ClientRecordID is Credbound's client record, like the other identifiers
	// here. The protocol client_id is a different value — text, and a Client
	// Identifier URL for CIMD clients — and is not carried by this payload.
	ClientRecordID UUID
	ClientSource   OAuthClientSource
	GrantID        UUID
	TokenID        UUID
	ResourceID     UUID
	Scopes         []string
}

OAuthChange is the shared payload of every OAuth hook call and event. Only the identifiers that apply to the specific change are set; raw codes, tokens and secrets never appear.

type OAuthClient

type OAuthClient struct {
	ID              UUID
	IssuerID        UUID
	ClientID        string
	Source          OAuthClientSource
	Name            string
	ApplicationType OAuthApplicationType
	RedirectURIs    []string
	// SectorIdentifier is the single redirect-URI host used to derive
	// pairwise OIDC subjects.
	SectorIdentifier string
	GrantTypes       []string
	ResponseTypes    []string
	Scopes           []string
	// ClientCredentialsResources is the explicit allowlist of protected
	// resource URIs the client may target with the client_credentials grant.
	// Resolving a client never authorizes it: a machine-to-machine token is
	// only minted for a resource named here, and only for a pre-registered
	// client with a non-empty registered scope list.
	ClientCredentialsResources []string
	TokenEndpointAuthMethod    OAuthTokenEndpointAuthMethod
	JWKSURI                    string
	JWKS                       json.RawMessage
	// Trusted may only be set on pre-registered clients; CIMD and DCR
	// clients are never trusted.
	Trusted      bool
	SecretDigest []byte
	// MetadataHash fingerprints the client metadata. Grants pin it, so a
	// changed CIMD document invalidates the delegations approved under the
	// previous metadata.
	MetadataHash []byte
	// MetadataExpiresAt bounds a cached CIMD document; a stale record is
	// re-fetched on next resolution.
	MetadataExpiresAt *time.Time
	CreatedAt         time.Time
	UpdatedAt         time.Time
	DisabledAt        *time.Time
}

OAuthClient is a persisted application identity under an issuer. ID is Credbound's record identifier while ClientID is the protocol client_id (equal to ID for pre-registered and DCR clients, the Client Identifier URL for CIMD clients).

type OAuthClientAccessToken

type OAuthClientAccessToken struct {
	ID             UUID
	Prefix         string
	Digest         []byte
	ClientRecordID UUID
	IssuerID       UUID
	ResourceID     UUID
	WorkspaceID    UUID
	Scopes         []string
	CreatedAt      time.Time
	ExpiresAt      time.Time
	RevokedAt      *time.Time
}

OAuthClientAccessToken is the persisted metadata of a client-credentials access token: a machine-to-machine bearer with no user subject, bound to a client, resource and workspace. It has no refresh token (RFC 6749 §4.4.3); the owning client revokes it individually through RevokeOAuthToken, and disabling the client, resource or issuer retires it implicitly.

type OAuthClientAssertionVerifier

type OAuthClientAssertionVerifier interface {
	Verify(context.Context, OAuthClient, string, string, time.Time) error
}

OAuthClientAssertionVerifier validates a private_key_jwt client assertion against the client's registered keys for the given token-endpoint audience at the given time. Implementations must provide atomic jti replay protection; oauthclientadapter.NewJWTAssertionVerifier is the bundled implementation.

type OAuthClientCredentialsInput

type OAuthClientCredentialsInput struct {
	Issuer       string
	ClientID     string
	ClientSecret string
	// ClientSecretInBody reports that ClientSecret arrived as a
	// client_secret form field rather than an Authorization: Basic header.
	// The registered client_secret_basic method accepts only the header
	// (RFC 6749 section 2.3.1 discourages the form transport and the
	// discovery document does not announce client_secret_post), so a
	// body-borne secret fails authentication.
	ClientSecretInBody  bool
	ClientAssertion     string
	ClientAssertionType string
	Resource            string
	Scopes              []string
}

OAuthClientCredentialsInput authenticates a confidential client and requests a machine-to-machine access token bound to a protected resource. The resource must appear in the client's ClientCredentialsResources allowlist. Scopes may be empty to receive every non-reserved scope the client is registered for that the resource defines.

type OAuthClientMetadataDocument

type OAuthClientMetadataDocument struct {
	ClientID                string
	ClientName              string
	ApplicationType         OAuthApplicationType
	RedirectURIs            []string
	GrantTypes              []string
	ResponseTypes           []string
	Scope                   string
	TokenEndpointAuthMethod OAuthTokenEndpointAuthMethod
	JWKSURI                 string
	JWKS                    json.RawMessage
	FetchedAt               time.Time
	ExpiresAt               time.Time
}

OAuthClientMetadataDocument is a validated CIMD document as returned by an OAuthClientMetadataFetcher, with the fetch time and cache expiry that bound how long the resolved client may be reused.

type OAuthClientMetadataFetcher

type OAuthClientMetadataFetcher interface {
	Fetch(context.Context, string) (OAuthClientMetadataDocument, error)
}

OAuthClientMetadataFetcher retrieves the CIMD document behind a Client Identifier URL. Implementations must defend against SSRF, DNS rebinding, redirects and oversized responses; oauthhttp.NewMetadataFetcher provides a hardened implementation.

type OAuthClientRegistrationInput

type OAuthClientRegistrationInput struct {
	Name            string
	ApplicationType OAuthApplicationType
	RedirectURIs    []string
	GrantTypes      []string
	ResponseTypes   []string
	Scopes          []string
	// ClientCredentialsResources allowlists the protected resource URIs a
	// client_credentials client may target. The grant is pre-registration
	// only and requires both this list and Scopes to be non-empty; the field
	// is rejected without the grant.
	ClientCredentialsResources []string
	TokenEndpointAuthMethod    OAuthTokenEndpointAuthMethod
	JWKSURI                    string
	JWKS                       json.RawMessage
	Trusted                    bool
}

OAuthClientRegistrationInput is the client metadata accepted by pre-registration and DCR. Empty grant and response types default to authorization_code with code; Trusted is honored only for pre-registration.

type OAuthClientSource

type OAuthClientSource string

OAuthClientSource records how a client record came to exist: administrative pre-registration, CIMD resolution, or DCR.

const (
	OAuthClientPreRegistered OAuthClientSource = "pre_registered"
	OAuthClientCIMD          OAuthClientSource = "cimd"
	OAuthClientDCR           OAuthClientSource = "dcr"
)

type OAuthConfig

type OAuthConfig struct {
	// Pepper keys the HMAC digests of OAuth codes, tokens and client
	// secrets, and the pairwise OIDC subjects. At least 32 bytes.
	Pepper []byte
	// MetadataFetcher resolves Client Identifier Metadata Documents; it is
	// required for CIMD client policies. oauthhttp.NewMetadataFetcher
	// provides a hardened implementation.
	MetadataFetcher OAuthClientMetadataFetcher
	// ClientAssertions verifies private_key_jwt client assertions; required
	// for that authentication method.
	ClientAssertions OAuthClientAssertionVerifier
	// OIDCSigner signs ID Tokens and publishes the JWKS; required for any
	// issuer with OIDC enabled.
	OIDCSigner OIDCSigner
}

OAuthConfig configures the optional OAuth/OIDC module.

type OAuthConsent

type OAuthConsent struct {
	Continuation string
	ClientID     string
	ClientName   string
	ClientHost   string
	RedirectURI  string
	RedirectHost string
	Resource     string
	WorkspaceID  UUID
	Scopes       []OAuthConsentScope
	// RequiresStepUp signals that a requested scope demands a stronger or
	// fresher authentication than the current session; the host should
	// re-authenticate before completing.
	RequiresStepUp bool
	// LocalhostRedirect flags a localhost redirect target so the UI can warn
	// that the code will be delivered to a program on the user's machine.
	LocalhostRedirect bool
}

OAuthConsent is a validated, sealed authorization awaiting the user's decision in the host UI. Only CompleteOAuthAuthorization can turn the Continuation into an approval or denial; the display fields let the host render an honest consent page.

type OAuthConsentScope

type OAuthConsentScope struct {
	Name        string
	Description string
}

OAuthConsentScope is one scope of a pending consent with its human-readable description for the consent page.

type OAuthDCRMode

type OAuthDCRMode string

OAuthDCRMode is an issuer's dynamic client registration policy: disabled, protected by an initial access token, or open with a registration limit. It is independent of the CIMD policy.

const (
	OAuthDCRDisabled  OAuthDCRMode = "disabled"
	OAuthDCRProtected OAuthDCRMode = "protected"
	OAuthDCROpen      OAuthDCRMode = "open"
)

type OAuthEvent

type OAuthEvent struct {
	OAuthChange
}

OAuthEvent is the shared payload of every OAuth event, distinguished by the EventMeta name of its OAuthChange.

type OAuthGrant

type OAuthGrant struct {
	ID             UUID
	IssuerID       UUID
	ClientRecordID UUID
	UserID         UUID
	WorkspaceID    UUID
	ResourceID     UUID
	Scopes         []string
	MetadataHash   []byte
	AuthTime       time.Time
	AuthMethod     AuthMethod
	AAL            AssuranceLevel
	CreatedAt      time.Time
	UpdatedAt      time.Time
	RevokedAt      *time.Time
}

OAuthGrant is a persisted delegation from a user to a client for one resource and scope set. AuthTime, AuthMethod and AAL snapshot the authorizing session for OIDC claims, and MetadataHash pins the client metadata that was consented to. Revoking the grant kills its tokens.

type OAuthInitialAccessToken

type OAuthInitialAccessToken struct {
	ID                UUID
	IssuerID          UUID
	Prefix            string
	Digest            []byte
	MaxRegistrations  int
	RegistrationCount int
	CreatedAt         time.Time
	ExpiresAt         time.Time
	RevokedAt         *time.Time
}

OAuthInitialAccessToken is the persisted metadata of a protected-DCR bootstrap credential: expiring, revocable, limited to MaxRegistrations registrations, and granting no authority over any resource. Only the HMAC Digest of the raw token is stored.

type OAuthIssuer

type OAuthIssuer struct {
	ID                       UUID
	Issuer                   string
	OIDCEnabled              bool
	CIMDMode                 OAuthCIMDMode
	CIMDAllowedOrigins       []string
	DCRMode                  OAuthDCRMode
	DCRAllowClientSecrets    bool
	DCROpenRegistrationLimit int
	CodeTTL                  time.Duration
	AccessTokenTTL           time.Duration
	RefreshTokenTTL          time.Duration
	CreatedAt                time.Time
	UpdatedAt                time.Time
	DisabledAt               *time.Time
}

OAuthIssuer is one authorization server: its HTTPS issuer URL, CIMD, DCR and OIDC policy, and token lifetimes. A disabled issuer refuses every discovery, authorization and token operation.

type OAuthProtectedResource

type OAuthProtectedResource struct {
	ID          UUID
	IssuerID    UUID
	WorkspaceID UUID
	Resource    string
	Scopes      []OAuthScopeDefinition
	CreatedAt   time.Time
	UpdatedAt   time.Time
	DisabledAt  *time.Time
}

OAuthProtectedResource is a tenant-scoped MCP resource under an issuer. Access tokens are bound to its Resource URI and workspace and cannot be replayed elsewhere.

type OAuthProtectedResourceMetadata

type OAuthProtectedResourceMetadata struct {
	Resource             string   `json:"resource"`
	AuthorizationServers []string `json:"authorization_servers"`
	ScopesSupported      []string `json:"scopes_supported,omitempty"`
	BearerMethods        []string `json:"bearer_methods_supported"`
}

OAuthProtectedResourceMetadata is the RFC 9728 metadata document of a protected resource.

type OAuthRefreshToken

type OAuthRefreshToken struct {
	ID             UUID
	FamilyID       UUID
	Prefix         string
	Digest         []byte
	GrantID        UUID
	ClientRecordID UUID
	UserID         UUID
	WorkspaceID    UUID
	ResourceID     UUID
	Scopes         []string
	CreatedAt      time.Time
	ExpiresAt      time.Time
	UsedAt         *time.Time
	ReplacedByID   UUID
	RevokedAt      *time.Time
}

OAuthRefreshToken is the persisted metadata of a rotating refresh token. Tokens descending from the same initial issuance share a FamilyID; reuse of a rotated token revokes the whole family.

type OAuthScopeDefinition

type OAuthScopeDefinition struct {
	Name        string
	Description string
	// Permissions are the registered workspace permissions the grant's user
	// must hold for this scope. At least one is required.
	Permissions []WorkspacePermission
	// MinimumAAL is the assurance the authorizing session must have reached;
	// zero means AAL1.
	MinimumAAL AssuranceLevel
	// MaxAuthAge bounds how old the authorizing authentication may be; zero
	// disables the freshness requirement.
	MaxAuthAge time.Duration
}

OAuthScopeDefinition declares one scope of a protected resource and its assurance policy. The workspace permissions behind the scope are re-evaluated on every bearer validation, not only at consent time.

type OAuthStore

type OAuthStore interface {
	CreateOAuthIssuer(context.Context, OAuthIssuer, Commit) error
	UpdateOAuthIssuer(context.Context, OAuthIssuer, Commit) error
	SetOAuthIssuerDisabled(context.Context, UUID, bool, time.Time, Commit) error
	OAuthIssuerByID(context.Context, UUID) (OAuthIssuer, error)
	OAuthIssuerByURL(context.Context, string) (OAuthIssuer, error)
	OAuthIssuers(context.Context, PageRequest) iter.Seq2[PageEvent[OAuthIssuer], error]
	CreateOAuthProtectedResource(context.Context, OAuthProtectedResource, Commit) error
	SetOAuthProtectedResourceDisabled(context.Context, UUID, bool, time.Time, Commit) error
	OAuthProtectedResourceByID(context.Context, UUID) (OAuthProtectedResource, error)
	OAuthProtectedResourceByURI(context.Context, string) (OAuthProtectedResource, error)
	OAuthProtectedResources(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[OAuthProtectedResource], error]

	CreateOAuthClient(context.Context, OAuthClient, UUID, time.Time, Commit) error
	UpsertOAuthCIMDClient(context.Context, OAuthClient, Commit) error
	SetOAuthClientDisabled(context.Context, UUID, bool, time.Time, Commit) error
	// RotateOAuthClientCredentials atomically replaces the client's secret
	// digest and/or inline JWKS (with its recomputed metadata hash) after an
	// administrative credential rotation; a nil secretDigest keeps the
	// current secret and a nil jwks keeps the current key set.
	RotateOAuthClientCredentials(ctx context.Context, id UUID, secretDigest, jwks, metadataHash []byte, at time.Time, commit Commit) error
	OAuthClientByID(context.Context, UUID) (OAuthClient, error)
	OAuthClientByClientID(context.Context, UUID, string) (OAuthClient, error)
	OAuthClients(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[OAuthClient], error]
	CreateOAuthInitialAccessToken(context.Context, OAuthInitialAccessToken, Commit) error
	OAuthInitialAccessTokenByPrefix(context.Context, string) (OAuthInitialAccessToken, error)
	// OAuthInitialAccessTokens streams the issuer's DCR bootstrap
	// credentials, oldest first, revoked ones included and digests omitted.
	OAuthInitialAccessTokens(context.Context, UUID) iter.Seq2[OAuthInitialAccessToken, error]
	RevokeOAuthInitialAccessToken(context.Context, UUID, time.Time, Commit) error

	CreateOAuthGrantAndCode(context.Context, OAuthGrant, OAuthAuthorizationCode, Commit) error
	OAuthGrant(context.Context, UUID) (OAuthGrant, error)
	RevokeOAuthGrant(context.Context, UUID, time.Time, Commit) error
	OAuthGrants(context.Context, UUID, UUID, PageRequest) iter.Seq2[PageEvent[OAuthGrant], error]
	OAuthAuthorizationCodeByPrefix(context.Context, string) (OAuthAuthorizationCode, error)
	ConsumeOAuthAuthorizationCode(context.Context, UUID, time.Time, OAuthAccessToken, *OAuthRefreshToken, Commit) error
	OAuthAccessTokenByPrefix(context.Context, string) (OAuthAccessToken, error)
	// CreateOAuthClientAccessToken persists a client-credentials access token
	// (machine-to-machine, no user subject); OAuthClientAccessTokenByPrefix
	// resolves one for AuthenticateOAuthAccessToken, and
	// RevokeOAuthClientAccessToken stamps one revoked for RevokeOAuthToken.
	CreateOAuthClientAccessToken(context.Context, OAuthClientAccessToken, Commit) error
	OAuthClientAccessTokenByPrefix(context.Context, string) (OAuthClientAccessToken, error)
	RevokeOAuthClientAccessToken(context.Context, UUID, time.Time, Commit) error
	OAuthRefreshTokenByPrefix(context.Context, string) (OAuthRefreshToken, error)
	RotateOAuthRefreshToken(context.Context, UUID, time.Time, OAuthAccessToken, OAuthRefreshToken, Commit) error
	RevokeOAuthAccessToken(context.Context, UUID, time.Time, Commit) error
	// RevokeOAuthRefreshFamily stamps RevokedAt on every token of the
	// refresh-token family and on the access tokens of the grants the
	// family descends from, so a detected reuse (or an RFC 7009 refresh
	// revocation) leaves no derived bearer credential alive.
	RevokeOAuthRefreshFamily(context.Context, UUID, time.Time, Commit) error
}

OAuthStore is an optional persistence capability. OAuth operations return ErrNotSupported unless both Config.OAuth and this store capability exist.

type OAuthTokenEndpointAuthMethod

type OAuthTokenEndpointAuthMethod string

OAuthTokenEndpointAuthMethod is how a client authenticates at the token endpoint. private_key_jwt requires Config.OAuth.ClientAssertions; client_secret_basic is only granted where the issuer policy allows it.

const (
	OAuthAuthNone              OAuthTokenEndpointAuthMethod = "none"
	OAuthAuthPrivateKeyJWT     OAuthTokenEndpointAuthMethod = "private_key_jwt"
	OAuthAuthClientSecretBasic OAuthTokenEndpointAuthMethod = "client_secret_basic"
)

type OAuthTokenKind

type OAuthTokenKind string

OAuthTokenKind distinguishes the two opaque bearer token families.

const (
	OAuthAccessTokenKind  OAuthTokenKind = "access_token"
	OAuthRefreshTokenKind OAuthTokenKind = "refresh_token"
)

type OAuthTokenResponse

type OAuthTokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int64  `json:"expires_in"`
	RefreshToken string `json:"refresh_token,omitempty"`
	Scope        string `json:"scope"`
	IDToken      string `json:"id_token,omitempty"`
}

OAuthTokenResponse is the token-endpoint success payload. The tokens are opaque, returned only once, and never recoverable from persisted models.

type OIDCClaims

type OIDCClaims struct {
	Issuer        string   `json:"iss"`
	Subject       string   `json:"sub"`
	Audience      string   `json:"aud"`
	ExpiresAt     int64    `json:"exp"`
	IssuedAt      int64    `json:"iat"`
	AuthTime      int64    `json:"auth_time,omitempty"`
	Nonce         string   `json:"nonce,omitempty"`
	ACR           string   `json:"acr,omitempty"`
	AMR           []string `json:"amr,omitempty"`
	Email         string   `json:"email,omitempty"`
	EmailVerified *bool    `json:"email_verified,omitempty"`
}

OIDCClaims is the minimal ID Token claim set Credbound asks an OIDCSigner to sign. Subject is pairwise and never the user's global UUID.

type OIDCSigner

type OIDCSigner interface {
	SignIDToken(context.Context, OIDCClaims) (string, error)
	JWKS(context.Context) (json.RawMessage, error)
	Algorithms() []string
}

OIDCSigner signs ID Tokens and publishes the verification keys of an OIDC issuer. Algorithms advertises the actual signing algorithms for discovery; "none" is rejected. The bundled ES256 signer keeps one active signing key plus verification-only retiring keys.

type OIDCUserInfo

type OIDCUserInfo struct {
	Subject       string `json:"sub"`
	Email         string `json:"email,omitempty"`
	EmailVerified *bool  `json:"email_verified,omitempty"`
}

OIDCUserInfo is the minimal UserInfo response: the pairwise subject, and email claims only when the token carries the email scope.

type Observer

type Observer interface {
	Observe(context.Context, Operation)
}

Observer receives Operation records for metrics, logs and traces (OTEL is the intended sink). Implementations must be safe for concurrent use.

type Operation

type Operation struct {
	Name     string
	Outcome  string
	Duration time.Duration
}

Operation is one observed unit of work: an API call, a transaction hook or an event listener invocation. Outcome is "success", "error" or "panic". Names are low-cardinality and values never contain secrets.

type PAT

type PAT struct {
	ID          UUID
	UserID      UUID
	Name        string
	Prefix      string
	Digest      []byte
	WorkspaceID UUID
	Scopes      []string
	CreatedAt   time.Time
	ExpiresAt   *time.Time
	LastUsedAt  *time.Time
	RevokedAt   *time.Time
}

PAT is the persisted metadata of a personal access token. The raw token has the form <marker>_<prefix>_<secret>, where the marker is Config.PATPrefix ("cbp" by default); Prefix enables an indexed lookup and only the HMAC Digest of the full token is stored. A PAT bound to a WorkspaceID authenticates only within that workspace.

type PATAuthenticatedEvent

type PATAuthenticatedEvent struct {
	EventMeta
	PATID  UUID
	UserID UUID
}

type PATCreatedEvent

type PATCreatedEvent struct {
	EventMeta
	PATID            UUID
	UserID           UUID
	PATName          string
	BoundWorkspaceID UUID
	Scopes           []string
	ExpiresAt        *time.Time
}

type PATCreation

type PATCreation struct {
	EventMeta
	PATID            UUID
	UserID           UUID
	PATName          string
	BoundWorkspaceID UUID
	Scopes           []string
	ExpiresAt        *time.Time
}

type PATRejectedEvent

type PATRejectedEvent struct {
	EventMeta
	Reason string
}

PATRejectedEvent reports a rejected PAT authentication. The token owner, when identifiable, is only in the associated audit — malformed tokens have no attributable user.

type PATRevocation

type PATRevocation struct {
	EventMeta
	PATID  UUID
	UserID UUID
}

type PATRevokedEvent

type PATRevokedEvent struct {
	EventMeta
	PATID  UUID
	UserID UUID
}

type PATStore

type PATStore interface {
	CreatePAT(context.Context, PAT, Commit) error
	PATByPrefix(context.Context, string) (PAT, error)
	// PATByID returns one token by identifier, reporting ErrNotFound for an
	// unknown one. It backs the workspace-scoped administration, which
	// resolves a token's owner and binding before acting on it.
	PATByID(context.Context, UUID) (PAT, error)
	TouchPAT(context.Context, UUID, time.Time, Commit) error
	RevokePAT(context.Context, UUID, UUID, time.Time, Commit) error
	PATs(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[PAT], error]
	// WorkspacePATs streams the tokens bound to one workspace, newest first,
	// across every owner. Tokens that are not bound to a workspace belong to
	// no tenant and never appear here.
	WorkspacePATs(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[PAT], error]
}

PATStore persists personal access tokens.

type PageEnd

type PageEnd struct {
	NextCursor string `json:"next_cursor,omitempty"`
	HasMore    bool   `json:"has_more"`
}

PageEnd terminates a page: the opaque cursor of the next page and whether one exists.

func CollectPage

func CollectPage[T any](seq iter.Seq2[PageEvent[T], error]) ([]T, PageEnd, error)

CollectPage drains a paginated sequence into its items and final PageEnd — the common case when a caller wants one page and a cursor rather than a stream:

pats, page, err := credbound.CollectPage(manager.PATs(ctx, authn, "", credbound.PageRequest{Limit: 50}))

Streaming callers range over the sequence directly and forward each PageEvent (for example as NDJSON) instead.

Example

ExampleCollectPage drains a paginated listing into one page of items plus the cursor for the next call. Streaming callers range over the sequence directly instead.

package main

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"time"

	"github.com/deepteams/credbound"
	"github.com/deepteams/credbound/credboundtest"
	"github.com/deepteams/credbound/memory"
)

func main() {
	clock := credboundtest.NewClock(credboundtest.DefaultStartTime)
	manager, err := credbound.New(credbound.Config{
		Store:          memory.New(),
		Passwords:      credboundtest.Passwords{},
		SecretKey:      bytes.Repeat([]byte{0x11}, 32),
		PATPepper:      bytes.Repeat([]byte{0x22}, 32),
		RecoveryPepper: bytes.Repeat([]byte{0x33}, 32),
		Clock:          clock.Now,
		Random:         credboundtest.NewDeterministicRandom(),
	})
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()
	authn, workspace, err := manager.Bootstrap(ctx, credbound.BootstrapInput{
		Email: "root@example.com", DisplayName: "Root", Password: "correct horse battery staple", WorkspaceName: "Main",
	})
	if err != nil {
		log.Fatal(err)
	}
	stepUp := credboundtest.AAL2(authn.UserID, clock.Now()) // test-only step-up
	for _, name := range []string{"ci", "deploy", "backup"} {
		if _, err := manager.CreatePAT(ctx, stepUp, credbound.CreatePATInput{
			Name: name, WorkspaceID: workspace.ID, Scopes: []string{"read"},
		}); err != nil {
			log.Fatal(err)
		}
		clock.Advance(time.Second) // distinct creation instants keep the page order stable
	}
	pats, page, err := credbound.CollectPage(manager.PATs(ctx, authn, credbound.UUID{}, credbound.PageRequest{Limit: 2}))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(pats), page.HasMore)
	rest, page, err := credbound.CollectPage(manager.PATs(ctx, authn, credbound.UUID{}, credbound.PageRequest{Limit: 2, Cursor: page.NextCursor}))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(rest), page.HasMore)
}
Output:
2 true
1 false

type PageEvent

type PageEvent[T any] struct {
	Type string   `json:"type"`
	Data *T       `json:"data,omitempty"`
	End  *PageEnd `json:"page_end,omitempty"`
}

PageEvent is one element of a paginated stream: either an item (Data set) or the final page_end (End set). Its JSON encoding is the NDJSON transport contract documented in the package overview.

func EndEvent

func EndEvent[T any](end PageEnd) PageEvent[T]

EndEvent wraps a PageEnd as the terminal element of a paginated stream.

func ItemEvent

func ItemEvent[T any](value T) PageEvent[T]

ItemEvent wraps a value as the item element of a paginated stream. Store implementations use it to build PageEvent sequences.

type PageRequest

type PageRequest struct {
	Cursor string
	Limit  int
}

PageRequest selects one page of a list. Cursor is an opaque value from a previous PageEnd (empty for the first page); Limit defaults to 50 and is capped at 100.

type Passkey

type Passkey struct {
	ID             UUID
	UserID         UUID
	Name           string
	CredentialID   []byte
	CredentialJSON []byte
	CreatedAt      time.Time
	LastUsedAt     *time.Time
}

Passkey is a registered WebAuthn credential. CredentialJSON holds the provider's sealed credential state and is scrubbed from every value the Manager returns to callers.

type PasskeyAuthenticatedEvent

type PasskeyAuthenticatedEvent struct {
	EventMeta
	PasskeyID UUID
	UserID    UUID
}

type PasskeyChallenge

type PasskeyChallenge struct {
	Options      json.RawMessage `json:"options"`
	Continuation string          `json:"continuation"`
}

PasskeyChallenge is a started WebAuthn ceremony: the provider options the host forwards to the browser and the sealed continuation it passes back to the matching Finish call.

type PasskeyCredentialStore

type PasskeyCredentialStore interface {
	// PasskeyByCredentialID returns the passkey owning the credential ID,
	// or ErrNotFound.
	PasskeyByCredentialID(ctx context.Context, credentialID []byte) (Passkey, error)
}

PasskeyCredentialStore is an optional persistence capability required by discoverable passkey authentication: a global credential-ID lookup, which the per-user Passkeys stream cannot provide.

type PasskeyDeletedEvent

type PasskeyDeletedEvent struct {
	EventMeta
	PasskeyID UUID
	UserID    UUID
}

type PasskeyDeletion

type PasskeyDeletion struct {
	EventMeta
	PasskeyID UUID
	UserID    UUID
}

type PasskeyProvider

type PasskeyProvider interface {
	BeginRegistration(context.Context, PasskeyUser) (json.RawMessage, []byte, error)
	FinishRegistration(context.Context, PasskeyUser, []byte, []byte) (credentialID, credentialJSON []byte, err error)
	// BeginAuthentication starts an assertion ceremony over the user's stored
	// passkeys. It returns ErrNoPasskey when the user has none, which the
	// manager answers with a decoy so the response never reveals whether an
	// account has a passkey.
	BeginAuthentication(context.Context, PasskeyUser) (json.RawMessage, []byte, error)
	// BeginDecoyAuthentication produces an assertion challenge for an address
	// with no passkey (or no account), structurally indistinguishable from
	// BeginAuthentication. The seed makes the fabricated credential descriptors
	// stable for a given address, so repeated probes cannot tell a decoy from a
	// real challenge by its variation.
	BeginDecoyAuthentication(ctx context.Context, seed []byte) (json.RawMessage, []byte, error)
	FinishAuthentication(context.Context, PasskeyUser, []byte, []byte) (credentialID, credentialJSON []byte, err error)
}

PasskeyProvider is the optional WebAuthn ceremony port. Each Begin method returns the browser options and an opaque session that Credbound seals into the continuation; each Finish method validates the browser response against that session. Implementations must require user verification.

type PasskeyRegisteredEvent

type PasskeyRegisteredEvent struct {
	EventMeta
	PasskeyID   UUID
	UserID      UUID
	PasskeyName string
}

type PasskeyRegistration

type PasskeyRegistration struct {
	EventMeta
	PasskeyID   UUID
	UserID      UUID
	PasskeyName string
}

type PasskeyStore

type PasskeyStore interface {
	Passkeys(context.Context, UUID) iter.Seq2[Passkey, error]
	SavePasskey(context.Context, Passkey, Commit) error
	// TouchPasskey persists the credential's updated JSON and last-used time
	// after a successful assertion, updates last_seen_at and — the sign-in
	// completed (AUTH-009) — clears the login throttle.
	TouchPasskey(context.Context, UUID, []byte, []byte, time.Time, Commit) error
	DeletePasskey(context.Context, UUID, UUID, Commit) error
}

PasskeyStore persists WebAuthn credentials. The optional PasskeyCredentialStore capability extends it for the usernameless flow.

type PasskeyUser

type PasskeyUser struct {
	User        User
	Credentials func(yield func(Passkey, error) bool)
}

PasskeyUser is the view of a user handed to a PasskeyProvider: the account and a lazy sequence of its registered credentials with the credential state decrypted.

type PasskeyUserLookup

type PasskeyUserLookup func(ctx context.Context, credentialID []byte) (PasskeyUser, error)

PasskeyUserLookup resolves the account owning a credential ID during a discoverable ceremony. It reports ErrNotFound for an unknown credential.

type PasswordChange

type PasswordChange struct {
	EventMeta
	UserID UUID
}

type PasswordChangedEvent

type PasswordChangedEvent struct {
	EventMeta
	UserID UUID
}

type PasswordCredential

type PasswordCredential struct {
	UserID    UUID
	Hash      string
	UpdatedAt time.Time
}

PasswordCredential is the persisted password hash of a user. Hash is an encoded derivation (Argon2id by default) and never the password itself.

type PasswordHasher

type PasswordHasher interface {
	Hash(string) (string, error)
	Verify(string, string) (match bool, rehash bool, err error)
}

PasswordHasher derives and verifies password hashes. Verify additionally reports rehash when the stored hash uses outdated parameters, so a successful authentication can transparently renew it. Argon2id with versioned parameters is the intended implementation.

type PasswordPolicy

type PasswordPolicy interface {
	ValidatePassword(ctx context.Context, password string) error
}

PasswordPolicy lets the host reject candidate passwords beyond the built-in length rules — typically against a breached-password corpus such as Have I Been Pwned via k-anonymity, per NIST 800-63B. Return an error wrapping ErrInvalidInput to reject the password; any other error is treated as an infrastructure failure and aborts the operation. The policy runs on every password acceptance path (bootstrap, user creation, change, reset, and invitation registration) and always after the built-in length validation. The candidate password must never be logged or persisted by implementations.

type PasswordRehashedEvent

type PasswordRehashedEvent struct {
	EventMeta
	UserID UUID
}

PasswordRehashedEvent reports the transparent hash renewal performed after a successful authentication when the hashing parameters changed.

type PasswordResetCompletedEvent

type PasswordResetCompletedEvent struct {
	EventMeta
	UserID UUID
}

type PasswordResetCredential

type PasswordResetCredential struct {
	ID        UUID
	UserID    UUID
	Digest    []byte
	CreatedAt time.Time
	ExpiresAt time.Time
	UsedAt    *time.Time
}

PasswordResetCredential is the persisted single-use reset proof. Only the HMAC digest of the token is stored.

type PasswordResetRequestedEvent

type PasswordResetRequestedEvent struct {
	EventMeta
	UserID    UUID
	ResetID   UUID
	ExpiresAt time.Time
}

type PasswordResetStore

type PasswordResetStore interface {
	CreatePasswordReset(context.Context, PasswordResetCredential, Commit) error
	PasswordResetByID(context.Context, UUID) (PasswordResetCredential, error)
	// CompletePasswordReset atomically consumes the single-use reset,
	// installs the password — replacing the previous one, or creating the
	// account's first for a passwordless member provisioned by SSO JIT or
	// SCIM — deletes the user's other pending resets, revokes the user's
	// PATs and OAuth grants (and, for SessionStore-capable stores, their
	// sessions), and clears the login throttle. It returns ErrConflict when
	// the reset was already consumed.
	CompletePasswordReset(ctx context.Context, resetID UUID, password PasswordCredential, at time.Time, commit Commit) error
}

PasswordResetStore persists the single-use password-reset credentials and the atomic reset completion with its revocation sweep.

type Permission

type Permission string

Permission names an instance-administration capability checked by AuthorizeAdmin. Each InstanceRole maps to an explicit permission set; services authorize by permission, never by comparing role names.

const (
	PermissionAdminAccess        Permission = "admin.access"
	PermissionAuditRead          Permission = "admin.audit.read"
	PermissionSettingsRead       Permission = "admin.settings.read"
	PermissionSettingsWrite      Permission = "admin.settings.write"
	PermissionUsersRead          Permission = "admin.users.read"
	PermissionUsersWrite         Permission = "admin.users.write"
	PermissionWorkspacesRead     Permission = "admin.workspaces.read"
	PermissionWorkspacesWrite    Permission = "admin.workspaces.write"
	PermissionRBACRead           Permission = "admin.rbac.read"
	PermissionRBACWrite          Permission = "admin.rbac.write"
	PermissionInstanceRolesRead  Permission = "admin.instance_roles.read"
	PermissionInstanceRolesWrite Permission = "admin.instance_roles.write"
)

type PrimaryEmailChange

type PrimaryEmailChange struct {
	EventMeta
	UserID  UUID
	EmailID UUID
}

type PrimaryEmailChangedEvent

type PrimaryEmailChangedEvent struct {
	EventMeta
	UserID  UUID
	EmailID UUID
}

type PrivacyStore

type PrivacyStore interface {
	// SCIMUsersByUser streams every tenant-scoped SCIM profile linked to
	// the user, across configurations, oldest first.
	SCIMUsersByUser(context.Context, UUID) iter.Seq2[SCIMUser, error]
	// AcceptedWorkspaceInvitations streams every workspace invitation the
	// user accepted, oldest first, with digests included; readers exporting
	// them must scrub the Digest.
	AcceptedWorkspaceInvitations(context.Context, UUID) iter.Seq2[WorkspaceInvitation, error]
}

PrivacyStore is an optional persistence capability that extends the data-subject primitives beyond the core account records. ExportUserData includes SCIM profiles and accepted workspace invitations only on a PrivacyStore-capable store; the first-party stores all implement it. Custom stores that skip it keep every other feature.

type RecoveryCode

type RecoveryCode struct {
	UserID UUID
	Digest []byte
	UsedAt *time.Time
}

RecoveryCode is one single-use TOTP fallback code, persisted as a peppered HMAC digest only.

type RecoveryCodeConsumedEvent

type RecoveryCodeConsumedEvent struct {
	EventMeta
	UserID UUID
}

type RecoveryCodeRegeneration

type RecoveryCodeRegeneration struct {
	EventMeta
	UserID            UUID
	RecoveryCodeCount int
}

RecoveryCodeRegeneration reports the replacement of a user's recovery codes; payloads carry only the count, never code material.

type RecoveryCodesRegeneratedEvent

type RecoveryCodesRegeneratedEvent struct {
	EventMeta
	UserID            UUID
	RecoveryCodeCount int
}

RecoveryCodesRegeneratedEvent reports that the user replaced their recovery codes; the previous set stopped working in the same transaction.

type RefreshOAuthTokenInput

type RefreshOAuthTokenInput struct {
	Issuer       string
	ClientID     string
	ClientSecret string
	// ClientSecretInBody reports that ClientSecret arrived as a
	// client_secret form field rather than an Authorization: Basic header.
	// The registered client_secret_basic method accepts only the header
	// (RFC 6749 section 2.3.1 discourages the form transport and the
	// discovery document does not announce client_secret_post), so a
	// body-borne secret fails authentication.
	ClientSecretInBody  bool
	ClientAssertion     string
	ClientAssertionType string
	RefreshToken        string
	Resource            string
	Scopes              []string
}

RefreshOAuthTokenInput is a parsed token-endpoint request for the refresh_token grant. Scopes optionally narrows the issue to a subset of the granted scopes.

type RegisterFromInvitationInput

type RegisterFromInvitationInput struct {
	DisplayName string
	Password    string
}

RegisterFromInvitationInput carries the profile the invitee chooses when registering a new account from an invitation token. The invited address becomes the verified primary email.

type RequestMetadata

type RequestMetadata struct {
	IPAddress string
	UserAgent string
}

RequestMetadata carries the client network context of the request being served. The host service extracts it from its trusted proxy headers and attaches it with WithRequestMetadata; Credbound never reads transport headers itself. Both fields are sanitized and bounded before being audited.

type RevocationStore

type RevocationStore interface {
	// RevokeUserCredentials atomically revokes every active PAT of the user
	// and, when the store has the OAuth capability, every OAuth grant and its
	// tokens. A SessionStore-capable store also revokes the user's sessions
	// in the same transaction; sessions the host manages itself remain
	// host-owned and unaffected.
	RevokeUserCredentials(context.Context, UUID, time.Time, Commit) error
	// AnonymizeUser pseudonymizes a user in one transaction: it scrubs the
	// mutable personal data (display name, email addresses, SSO and PAT names,
	// session IP/User-Agent), disables the account, revokes its PATs, sessions
	// and (with the OAuth capability) grants, and removes its second factors.
	// A SCIMStore-capable store also scrubs the personal attributes of every
	// SCIM profile linked to the user (user name, display name, emails,
	// directory attributes) and marks it deprovisioned, and the email on
	// workspace invitations the user accepted is replaced with a tombstone.
	// The append-only audit chain is deliberately left intact. It reports
	// ErrConflict when the target is the last enabled root administrator or the
	// sole admin of a workspace, mirroring SetUserDisabled, and ErrNotFound for
	// an unknown user.
	AnonymizeUser(ctx context.Context, userID UUID, at time.Time, commit Commit) error
}

RevocationStore holds the cross-credential compromise-response and privacy operations.

type RevokeOAuthTokenInput

type RevokeOAuthTokenInput struct {
	Issuer       string
	ClientID     string
	ClientSecret string
	// ClientSecretInBody reports that ClientSecret arrived as a
	// client_secret form field rather than an Authorization: Basic header.
	// The registered client_secret_basic method accepts only the header
	// (RFC 6749 section 2.3.1 discourages the form transport and the
	// discovery document does not announce client_secret_post), so a
	// body-borne secret fails authentication.
	ClientSecretInBody  bool
	ClientAssertion     string
	ClientAssertionType string
	Token               string
}

RevokeOAuthTokenInput is a parsed RFC 7009 revocation request; Token may be an access or refresh token.

type Role

type Role string

Role names a workspace role. The built-in member and admin roles always exist; Config.WorkspaceRoles may register additional roles that implicitly inherit from member. An unknown role fails closed everywhere.

const (
	RoleMember Role = "member"
	RoleAdmin  Role = "admin"
)

type RoleDefinition

type RoleDefinition struct {
	Role        Role
	Permissions []WorkspacePermission
	// Inherits lists roles whose permissions this role also receives.
	Inherits []Role
}

RoleDefinition registers a workspace role in the immutable RBAC catalog validated by New. A definition named member or admin adds permissions to the built-in role without removing its guarantees; any other role implicitly inherits from member. Inheritance must be acyclic.

type RoleGrant

type RoleGrant struct {
	EventMeta
	UserID       UUID
	Role         Role
	PreviousRole Role
}

type RoleGrantedEvent

type RoleGrantedEvent struct {
	EventMeta
	UserID       UUID
	Role         Role
	PreviousRole Role
}

type SCIMAuthentication

type SCIMAuthentication struct {
	ConfigurationID UUID
	WorkspaceID     UUID
	CredentialID    UUID
	AuthenticatedAt time.Time
}

SCIMAuthentication is the service capability obtained through AuthenticateSCIM. It scopes every provisioning operation to one configuration and its workspace and must never be constructed from fields freely supplied by a client.

type SCIMConfiguration

type SCIMConfiguration struct {
	ID          UUID
	WorkspaceID UUID
	Enabled     bool
	DefaultRole Role
	// TrustDirectoryEmails marks the primary address of provisioned users as
	// verified, making it usable for sign-in. Without it even the primary
	// SCIM address stays unverified.
	TrustDirectoryEmails bool
	GroupRoleMappings    []SCIMGroupRoleMapping
	CreatedAt            time.Time
	UpdatedAt            time.Time
}

SCIMConfiguration is the provisioning domain of one workspace: the default role for provisioned users, the group-to-role mappings, and whether directory-asserted primary emails are trusted as verified.

type SCIMConfigurationChange

type SCIMConfigurationChange struct {
	EventMeta
	Configuration SCIMConfiguration
}

type SCIMConfigurationCreatedEvent

type SCIMConfigurationCreatedEvent struct {
	EventMeta
	Configuration SCIMConfiguration
}

type SCIMCredential

type SCIMCredential struct {
	ID              UUID
	ConfigurationID UUID
	Prefix          string
	Digest          []byte
	CreatedAt       time.Time
	ExpiresAt       *time.Time
	LastUsedAt      *time.Time
	RevokedAt       *time.Time
}

SCIMCredential is the persisted metadata of a SCIM bearer credential. The raw token has the form cbs_<prefix>_<secret> and only its HMAC Digest is stored. A credential is a service identity and never represents a user.

type SCIMEmail

type SCIMEmail struct {
	Value   string `json:"value"`
	Type    string `json:"type,omitempty"`
	Primary bool   `json:"primary,omitempty"`
}

SCIMEmail is one email attribute of a SCIM user profile. Only the primary address created with a new user joins the global identity model; the others remain tenant-scoped profile data.

type SCIMFilter

type SCIMFilter struct {
	Attribute string
	Value     string
}

SCIMFilter is a single equality filter over a supported SCIM attribute. A zero value matches everything.

type SCIMGroup

type SCIMGroup struct {
	ID              UUID
	ConfigurationID UUID
	ExternalID      string
	DisplayName     string
	MemberIDs       []UUID
	CreatedAt       time.Time
	UpdatedAt       time.Time
	DeletedAt       *time.Time
}

SCIMGroup is a persisted directory group. MemberIDs reference SCIMUser link identifiers, not global user IDs.

type SCIMGroupChange

type SCIMGroupChange struct {
	EventMeta
	Group SCIMGroup
}

type SCIMGroupEvent

type SCIMGroupEvent struct {
	EventMeta
	Group SCIMGroup
}

SCIMGroupEvent is the shared payload of every SCIM group lifecycle event (created, updated, deleted, members changed); the EventMeta name tells them apart.

type SCIMGroupInput

type SCIMGroupInput struct {
	ExternalID  string
	DisplayName string
	MemberIDs   []UUID
}

SCIMGroupInput is the SCIM group representation accepted by UpsertSCIMGroup. MemberIDs reference SCIMUser link identifiers.

type SCIMGroupRoleMapping

type SCIMGroupRoleMapping struct {
	ExternalID string
	Role       Role
	Priority   int
}

SCIMGroupRoleMapping maps a directory group (by its external identifier) to a workspace role from the immutable catalog. When a user belongs to several mapped groups the highest Priority wins; two mappings of equal priority resolving to different roles fail closed with ErrConflict.

type SCIMStore

type SCIMStore interface {
	CreateSCIMConfiguration(context.Context, SCIMConfiguration, SCIMCredential, Commit) error
	SCIMConfiguration(context.Context, UUID) (SCIMConfiguration, error)
	// SCIMConfigurations streams the workspace's provisioning domains,
	// oldest first. A workspace holds few configurations, so the stream is
	// not paginated.
	SCIMConfigurations(context.Context, UUID) iter.Seq2[SCIMConfiguration, error]
	UpdateSCIMConfiguration(context.Context, SCIMConfiguration, []Membership, Commit) error
	SCIMConfigurationByCredentialPrefix(context.Context, string) (SCIMConfiguration, SCIMCredential, error)
	// SCIMCredentials streams the configuration's bearer credentials, oldest
	// first, with digests omitted.
	SCIMCredentials(context.Context, UUID) iter.Seq2[SCIMCredential, error]
	SaveSCIMCredential(context.Context, SCIMCredential, Commit) error
	RevokeSCIMCredential(context.Context, UUID, UUID, time.Time, Commit) error
	TouchSCIMCredential(context.Context, UUID, time.Time, Commit) error
	DisableSCIMConfiguration(context.Context, UUID, time.Time, Commit) error

	CreateSCIMUser(context.Context, User, EmailAddress, Membership, SCIMUser, Commit) error
	AdoptSCIMUser(context.Context, Membership, SCIMUser, Commit) error
	SCIMUser(context.Context, UUID, UUID) (SCIMUser, error)
	SCIMUserByExternalID(context.Context, UUID, string) (SCIMUser, error)
	SCIMUserByUserName(context.Context, UUID, string) (SCIMUser, error)
	UpdateSCIMUser(context.Context, SCIMUser, Membership, bool, Commit) error
	SCIMUsers(context.Context, UUID, SCIMFilter, PageRequest) iter.Seq2[PageEvent[SCIMUser], error]

	UpsertSCIMGroup(context.Context, SCIMGroup, []Membership, Commit) error
	SCIMGroup(context.Context, UUID, UUID) (SCIMGroup, error)
	SCIMGroupByExternalID(context.Context, UUID, string) (SCIMGroup, error)
	DeleteSCIMGroup(context.Context, SCIMGroup, []Membership, Commit) error
	SCIMGroups(context.Context, UUID, SCIMFilter, PageRequest) iter.Seq2[PageEvent[SCIMGroup], error]
}

SCIMStore is an optional persistence capability. Custom stores that do not implement it can continue to use every non-SCIM feature of Credbound.

type SCIMUser

type SCIMUser struct {
	ID              UUID
	ConfigurationID UUID
	UserID          UUID
	Schemas         []string
	ExternalID      string
	UserName        string
	DisplayName     string
	Emails          []SCIMEmail
	Attributes      map[string]json.RawMessage
	Active          bool
	CreatedAt       time.Time
	UpdatedAt       time.Time
	DeprovisionedAt *time.Time
}

SCIMUser is the tenant-scoped link between a SCIM configuration and a global account. Its ID is the SCIM resource identifier and is distinct from UserID; unknown directory attributes are retained in Attributes. Deprovisioning sets DeprovisionedAt without disabling the global account.

type SCIMUserChange

type SCIMUserChange struct {
	EventMeta
	User SCIMUser
}

type SCIMUserEvent

type SCIMUserEvent struct {
	EventMeta
	User SCIMUser
}

SCIMUserEvent is the shared payload of every SCIM user lifecycle event (provisioned, updated, activated, suspended, deprovisioned); the EventMeta name tells them apart.

type SCIMUserInput

type SCIMUserInput struct {
	Schemas     []string
	ExternalID  string
	UserName    string
	DisplayName string
	Emails      []SCIMEmail
	Attributes  map[string]json.RawMessage
	Active      bool
}

SCIMUserInput is the normalized SCIM user representation accepted by the provisioning operations. Active drives the membership status; false suspends the membership without touching the global account.

type SSOAssurancePolicy

type SSOAssurancePolicy struct {
	AcceptedACR     []string
	RequiredAMR     []string
	TrustUnverified bool
}

SSOAssurancePolicy makes the AAL2 an SSO sign-in produces verifiable instead of declared: FinishSSO completes only when the provider's asserted authentication context satisfies the policy, and fails with ErrStepUpRequired otherwise, so the host can send the user back to the IdP for its second factor. A policy with AcceptedACR requires the asserted ACR to be one of the listed values; RequiredAMR requires every listed method among the asserted AMR values; both together must both hold. Registered per provider configuration in Config.SSOAssurance.

A provider without a registered policy grants only AAL1: SSO cannot mint AAL2 on the IdP's unverified word. TrustUnverified is the explicit, auditable opt-out for a provider whose IdP asserts no ACR or AMR yet the host still consciously trusts to have authenticated at AAL2 — it grants AAL2 without inspecting the asserted context, and is the only way a policy may leave both AcceptedACR and RequiredAMR empty.

type SSOAuthenticatedEvent

type SSOAuthenticatedEvent struct {
	EventMeta
	IdentityID     UUID
	Authentication Authentication
}

type SSOChallenge

type SSOChallenge struct {
	RedirectURL  string
	Continuation string
}

SSOChallenge is a started SSO ceremony: the provider redirect URL for the browser and the sealed continuation the host passes back to FinishSSO.

type SSOChallengeIssuedEvent

type SSOChallengeIssuedEvent struct {
	EventMeta
	ProviderConfigurationID UUID
	ProviderKind            SSOProviderKind
	Purpose                 string
}

type SSOClaims

type SSOClaims struct {
	Issuer        string
	Subject       string
	Email         string
	EmailVerified bool
	// ACR is the authentication context class the provider asserted — the
	// OIDC acr claim or the SAML AuthnContextClassRef — or empty when the
	// provider asserted none. Config.SSOAssurance can require it before
	// the authentication is granted AAL2.
	ACR string
	// AMR lists the OIDC authentication method references (amr) the
	// provider asserted, such as "mfa", "otp" or "hwk".
	AMR []string
}

SSOClaims is the validated identity a provider returns from a finished ceremony. Issuer and Subject form the stable link key; Email is informational only and never triggers an automatic account match.

type SSOIdentity

type SSOIdentity struct {
	ID                      UUID
	UserID                  UUID
	ProviderConfigurationID UUID
	ProviderKind            SSOProviderKind
	Issuer                  string
	Subject                 string
	Email                   string
	CreatedAt               time.Time
	LastUsedAt              *time.Time
}

SSOIdentity is a persisted link between a user and an external identity. The (ProviderConfigurationID, Issuer, Subject) triplet is the stable key; Email is informational only.

type SSOJITProvisionedEvent

type SSOJITProvisionedEvent struct {
	EventMeta
	User       User
	Email      EmailAddress
	Membership Membership
	Identity   SSOIdentity
	// DomainID references the confirmed workspace domain whose auto-join
	// policy produced the account.
	DomainID UUID
}

SSOJITProvisionedEvent reports a just-in-time provisioned account: the passwordless user created inside FinishSSO from a verified IdP email under a confirmed auto-join domain, its verified primary email, the configured membership and the linked identity. It is emitted alongside user.created, sso.linked and authentication.succeeded for the same commit.

type SSOLink struct {
	EventMeta
	Identity SSOIdentity
}

type SSOLinkStore

type SSOLinkStore interface {
	SSOIdentity(context.Context, UUID, string, string) (SSOIdentity, error)
	// LinkSSO stores a new SSO identity link; an identity already linked to
	// any user reports ErrConflict. A link carrying LastUsedAt records a
	// completed sign-in, so it also updates last_seen_at and clears the
	// login throttle (AUTH-009).
	LinkSSO(context.Context, SSOIdentity, Commit) error
	// TouchSSO updates the identity's last-used time after a successful SSO
	// login, updates last_seen_at and — the sign-in completed (AUTH-009) —
	// clears the login throttle.
	TouchSSO(context.Context, UUID, UUID, time.Time, Commit) error
	// UnlinkSSO removes one linked identity, refusing with ErrConflict when
	// it is the user's last remaining authentication method (no password
	// credential, no passkey, no other SSO identity) — a JIT-provisioned,
	// passwordless member must not be able to lock themselves out.
	UnlinkSSO(context.Context, UUID, UUID, Commit) error
	SSOIdentities(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[SSOIdentity], error]
}

SSOLinkStore persists the links between accounts and external SSO identities.

type SSOLinkedEvent

type SSOLinkedEvent struct {
	EventMeta
	Identity SSOIdentity
}

type SSOProvider

type SSOProvider interface {
	ConfigurationID() UUID
	Kind() SSOProviderKind
	Begin(context.Context, SSORequest) (SSOProviderChallenge, error)
	Finish(context.Context, []byte, []byte) (SSOClaims, error)
}

SSOProvider is the network adapter for one registered identity provider. ConfigurationID returns the stable UUIDv7 the host addresses it by, Begin starts a ceremony (honoring SSORequest.ForceReauthentication for step-up), and Finish validates the provider response against the sealed session and returns the asserted claims.

type SSOProviderChallenge

type SSOProviderChallenge struct {
	RedirectURL string
	Session     []byte
}

SSOProviderChallenge is the provider's half of a started SSO ceremony: the URL to send the browser to and the opaque session state Credbound seals into the continuation.

type SSOProviderKind

type SSOProviderKind string

SSOProviderKind is the protocol family of a registered SSO provider.

const (
	SSOProviderGoogle    SSOProviderKind = "google"
	SSOProviderGitHub    SSOProviderKind = "github"
	SSOProviderMicrosoft SSOProviderKind = "microsoft"
	SSOProviderOIDC      SSOProviderKind = "oidc"
	SSOProviderSAML      SSOProviderKind = "saml"
)

type SSORequest

type SSORequest struct {
	ForceReauthentication bool
}

SSORequest carries Credbound's requirements to an SSOProvider when a ceremony begins. ForceReauthentication is set for step-up flows so the provider re-verifies the user and its own MFA instead of reusing an existing IdP session.

type SSOUnlink struct {
	EventMeta
	UserID     UUID
	IdentityID UUID
}

type SSOUnlinkedEvent

type SSOUnlinkedEvent struct {
	EventMeta
	UserID     UUID
	IdentityID UUID
}

type SecondFactorReset

type SecondFactorReset struct {
	EventMeta
	UserID UUID
}

SecondFactorReset reports the administrative removal of every second factor of a user: the TOTP factor with its recovery codes and all passkeys, with the user's sessions revoked in the same transaction.

type SecondFactorResetEvent

type SecondFactorResetEvent struct {
	EventMeta
	UserID UUID
}

SecondFactorResetEvent reports that an instance administrator removed every second factor of the user (TOTP, recovery codes, passkeys) and revoked their sessions — the total-loss recovery path. Hosts should notify the affected user out of band.

type Session

type Session struct {
	ID     UUID
	UserID UUID
	// Method and Level snapshot the creating Authentication verbatim; they
	// never change for the lifetime of the session.
	Method AuthMethod
	Level  AssuranceLevel
	// AuthenticatedAt is copied from the creating Authentication so step-up
	// freshness keeps measuring the factor verification, not session reuse.
	AuthenticatedAt      time.Time
	SecondFactorRequired bool
	// UserAgent and IPAddress are the sanitized RequestMetadata observed when
	// the session was created, for device listings.
	UserAgent string
	IPAddress string
	// Digest is the HMAC of the raw token under the derived key (domain
	// "session:"). It is scrubbed from listings and from every value the
	// Manager returns.
	Digest    []byte
	CreatedAt time.Time
	// LastSeenAt is telemetry only: expiry is absolute (CreatedAt plus
	// Config.SessionTTL) and is never extended by activity.
	LastSeenAt time.Time
	ExpiresAt  time.Time
	RevokedAt  *time.Time
}

Session is a persisted server-side session: an immutable snapshot of the Authentication that created it, plus the device metadata observed at creation. The raw cbs_ token is returned exactly once by CreateSession and only its HMAC digest is stored. A session never upgrades its assurance level in place — after VerifyTOTP or any other AAL transition the host mints a new session and revokes the previous one, which doubles as fixation protection.

type SessionCreatedEvent

type SessionCreatedEvent struct {
	EventMeta
	Session Session
	// Request carries the client network context supplied by the host through
	// WithRequestMetadata, so listeners can alert on new devices without
	// re-reading the audit log.
	Request RequestMetadata
}

SessionCreatedEvent reports a new server-side session. The Session carries a nil Digest and the raw token is never part of any event. AuthenticateSession emits no per-validation event — it runs on every request and would flood listeners; only the audit log records validations.

type SessionCreation

type SessionCreation struct {
	EventMeta
	Session Session
	// Request is the client network context observed at creation.
	Request RequestMetadata
}

SessionCreation carries the created session record; its Digest is always nil so hook payloads never see token material.

type SessionRevocation

type SessionRevocation struct {
	EventMeta
	SessionID UUID
	UserID    UUID
}

type SessionRevokedEvent

type SessionRevokedEvent struct {
	EventMeta
	SessionID UUID
	UserID    UUID
}

type SessionStore

type SessionStore interface {
	// CreateSession stores a server-side session. A non-empty
	// credentialDigest is the currency guard: inside the same transaction the
	// store recomputes CredentialFingerprint over the user's current password
	// credential hash and returns ErrConflict when it no longer matches (or
	// the credential vanished), so an Authentication whose password was
	// concurrently replaced cannot mint a session that would survive the
	// replacement's revocation sweep. An empty digest skips the guard — the
	// context was not produced by a password.
	CreateSession(ctx context.Context, session Session, credentialDigest []byte, commit Commit) error
	SessionByID(ctx context.Context, id UUID) (Session, error)
	// TouchSession refuses a session already revoked with ErrConflict, so an
	// authentication racing a revocation can neither record activity on nor
	// extend the idle window of a dead session; otherwise it
	// updates the session's last-seen timestamp (and the user's
	// last_seen_at) in the same transaction as its audit event.
	TouchSession(ctx context.Context, id UUID, at time.Time, commit Commit) error
	RevokeSession(ctx context.Context, id UUID, at time.Time, commit Commit) error
	// RevokeUserSessions stamps RevokedAt on every active session of the user.
	RevokeUserSessions(ctx context.Context, userID UUID, at time.Time, commit Commit) error
	Sessions(ctx context.Context, userID UUID, page PageRequest) iter.Seq2[PageEvent[Session], error]
}

SessionStore is an optional persistence capability required by the session operations (CreateSession, AuthenticateSession, Sessions, RevokeSession, RevokeUserSessions); without it every one of them returns ErrNotSupported.

Cascade contract: a store implementing SessionStore must extend its CompletePasswordReset, ChangePassword, SetUserDisabled (when disabling, not when re-enabling) and RevokeUserCredentials implementations to also stamp RevokedAt on the user's active sessions inside the same transaction, so a recovery or lockdown revokes interactive sessions atomically with the rest of the account's credentials. Stores without the capability keep their existing contracts; hosts then terminate their own sessions.

Sessions never returns the token digest: listed Session values carry a nil Digest. SessionByID returns the digest for constant-time validation by the Manager, which scrubs it before results leave the library.

type SignUpCompletedEvent

type SignUpCompletedEvent struct {
	EventMeta
	User      User
	Workspace Workspace
}

SignUpCompletedEvent reports a successful self-service registration: the created account and the workspace it administers. Collisions with an existing address emit no event so listeners cannot become an enumeration side channel.

type SignUpConfig

type SignUpConfig struct {
	// AutoVerifyEmail marks the primary address verified at creation instead
	// of issuing an email-verification token, and makes SignUp additionally
	// return an AAL1 password Authentication. Hosts enabling it accept that
	// mailbox control was never proven.
	AutoVerifyEmail bool
}

SignUpConfig configures the optional self-service signup operation.

type SignUpInput

type SignUpInput struct {
	Email         string
	DisplayName   string
	Password      string
	WorkspaceName string
}

SignUpInput describes a self-service registration: the visitor's address, profile, chosen password and the name of the workspace created for them.

type SignUpResult

type SignUpResult struct {
	User              User
	Workspace         Workspace
	Authentication    Authentication
	EmailVerification IssuedEmailVerification
	ExistingAccount   bool
}

SignUpResult is the outcome of a SignUp call. When ExistingAccount is true the address already belonged to an account: every other field is zero, the collision was reported only to the audit log, and the host must answer the end user exactly as if the registration had succeeded. Otherwise User and Workspace carry the created records; EmailVerification carries the single-use token proving the primary address (delivered by the host, never stored) unless Config.SignUp.AutoVerifyEmail is set, in which case Authentication instead carries an AAL1 password authentication.

type SignupStore

type SignupStore interface {
	CreateSignup(ctx context.Context, user User, email EmailAddress, verification *EmailVerificationCredential, password PasswordCredential, workspace Workspace, membership Membership, commit Commit) error
}

SignupStore is an optional persistence capability required by SignUp. CreateSignup atomically creates the user, their primary email address, the password credential, the workspace and the admin membership, or nothing at all. The verification credential is nil when the host auto-verifies the address (the email then carries VerifiedAt); otherwise the email starts unverified and the credential is persisted exactly as SaveEmail would, keyed by the email identifier, so ConfirmEmail completes the proof. A globally taken address fails with ErrConflict. Custom stores that do not implement it can continue to use every other feature of Credbound.

type StepUpDeniedEvent

type StepUpDeniedEvent struct {
	EventMeta
	UserID UUID
}

StepUpDeniedEvent is an advisory signal that an operation was refused for lack of a fresh AAL2 authentication; it carries no audit of its own.

type StepUpPolicies added in v0.0.2

type StepUpPolicies struct {
	// Default applies to every scope that names no policy of its own. Zero
	// keeps StepUpPolicyRequired.
	Default StepUpPolicy
	// PAT overrides Default for StepUpScopePAT. Zero inherits Default.
	PAT StepUpPolicy
	// Workspace overrides Default for StepUpScopeWorkspace. Zero inherits
	// Default.
	Workspace StepUpPolicy
	// Account overrides Default for StepUpScopeAccount. Zero inherits
	// Default.
	Account StepUpPolicy
	// Admin overrides Default for StepUpScopeAdmin. Zero inherits Default —
	// so a relaxed Default reaches instance administration too, which is the
	// one place worth naming explicitly when that is not what you want.
	Admin StepUpPolicy
}

StepUpPolicies assigns a StepUpPolicy to each StepUpScope. Default governs every family, and the per-family fields override it where one deserves a different answer; a zero field inherits Default, and a zero Default is StepUpPolicyRequired. The zero StepUpPolicies therefore means "every step-up-gated operation demands a fresh interactive AAL2 context", which is what Credbound did before the policies existed.

StepUpPolicyUserCapable deserves a special mention here: under it, a user who enrolls a second factor is immediately held to the full step-up on every family, including the operations that could remove that factor (DeletePasskey, DisableTOTP). Relaxing a deployment for the users who have no factor therefore does not weaken the users who do.

type StepUpPolicy

type StepUpPolicy string

StepUpPolicy selects how strictly an operation demands a fresh interactive AAL2 authentication. A step-up is only ever as available as the second factors a deployment actually enrolls: without a policy, a product whose users have no TOTP factor and no passkey is simply locked out of every operation the step-up guards. Config.StepUp assigns one policy per family of guarded operations; see StepUpPolicies.

const (
	// StepUpPolicyRequired is the default and the strict mode: the operation
	// accepts exactly what RequireStepUp accepts, so a user with no enrolled
	// second factor can never perform it.
	StepUpPolicyRequired StepUpPolicy = "required"
	// StepUpPolicyUserCapable demands the full step-up from every user who
	// could satisfy one — an active TOTP factor, or a passkey with a passkey
	// provider configured — and accepts a plain interactive authentication
	// from users who have neither. Enrolling a factor therefore tightens the
	// operation for that user with no configuration change, and nobody is
	// locked out meanwhile.
	StepUpPolicyUserCapable StepUpPolicy = "user_capable"
	// StepUpPolicyNone asks for no step-up: any interactive authentication of
	// an active user is accepted, at any assurance level and any age. The
	// non-interactive floor still holds — a PAT can never mint or revoke a
	// PAT — and a context whose second factor is still pending is refused.
	StepUpPolicyNone StepUpPolicy = "none"
)

type StepUpScope added in v0.0.2

type StepUpScope string

StepUpScope names one family of step-up-gated operations. The families are what Config.StepUp configures and what RequireStepUpFor evaluates, so a host can prompt for a second factor before an operation instead of after its refusal.

const (
	// StepUpScopePAT covers CreatePAT and RevokePAT.
	StepUpScopePAT StepUpScope = "pat"
	// StepUpScopeWorkspace covers the tenant mutations: creating, updating,
	// disabling and enabling a workspace, memberships and their roles,
	// invitations, workspace domains, SCIM configurations, workspace OAuth
	// resources, administrative user creation, and reading a workspace audit
	// log.
	StepUpScopeWorkspace StepUpScope = "workspace"
	// StepUpScopeAccount covers a user's own credentials and data: deleting a
	// passkey, disabling TOTP, regenerating recovery codes, selecting or
	// removing an email address, unlinking an SSO identity, listing and
	// revoking sessions, revoking a personal OAuth grant, the credential
	// revocation cascade, and the personal-data export.
	StepUpScopeAccount StepUpScope = "account"
	// StepUpScopeAdmin covers the instance-administration mutations behind
	// RequireAdminMutation — disabling a user, instance roles, the instance
	// audit log — whose loopback exception (TrustedRequest) is unaffected.
	StepUpScopeAdmin StepUpScope = "admin"
)

type Store

Store persists every mutation together with its transaction hooks and audit. Implementations must commit all three stages or none of them.

Store is the required persistence port. It is composed of the capability groups below purely for navigability — the method set is unchanged and a Store implements all of them; the optional capabilities (SignupStore, SessionStore, EmailThrottleStore, DomainStore, SCIMStore, PrivacyStore, OAuthStore, PasskeyCredentialStore) remain separate interfaces.

Compatibility contract: the required method set may grow in minor releases as features ship, so implementing Store from scratch means tracking every release. External stores that want to stay compile-compatible across upgrades should embed one of the shipped implementations (sqlstore/postgresql, memory) in a struct and override only the methods they need.

type StoreKind

type StoreKind string

StoreKind identifies the engine behind a Tx so a TransactionHook can recover the engine-specific handle (for example sqlstore's TxFrom).

const (
	StoreMemory     StoreKind = "memory"
	StorePostgreSQL StoreKind = "postgresql"
)

type Subscription

type Subscription interface {
	Remove()
}

Subscription undoes an AddTransactionHook or AddEventListener registration. Remove is idempotent.

type TOTPActivatedEvent

type TOTPActivatedEvent struct {
	EventMeta
	UserID            UUID
	RecoveryCodeCount int
}

type TOTPActivation

type TOTPActivation struct {
	EventMeta
	UserID            UUID
	RecoveryCodeCount int
}

type TOTPDisable

type TOTPDisable struct {
	EventMeta
	UserID UUID
}

type TOTPDisabledEvent

type TOTPDisabledEvent struct {
	EventMeta
	UserID UUID
}

type TOTPEnrollment

type TOTPEnrollment struct {
	URI string
}

TOTPEnrollment is a started TOTP enrollment. URI is the otpauth:// URI the host renders as a QR code; it contains the secret and must not be persisted or logged.

type TOTPEnrollmentChange

type TOTPEnrollmentChange struct {
	EventMeta
	UserID UUID
}

type TOTPEnrollmentStartedEvent

type TOTPEnrollmentStartedEvent struct {
	EventMeta
	UserID UUID
}

type TOTPFactor

type TOTPFactor struct {
	UserID          UUID
	EncryptedSecret []byte
	// Active becomes true only after ConfirmTOTPEnrollment proved a valid
	// code; an inactive enrollment never gates authentication.
	Active       bool
	LastUsedStep int64
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

TOTPFactor is a user's persisted TOTP enrollment. The secret is sealed with the Manager's AEAD key, and LastUsedStep prevents replay of an already accepted time step.

type TOTPProvider

type TOTPProvider interface {
	Generate(accountName string) (secret string, uri string, err error)
	Validate(code, secret string, at time.Time) (step int64, valid bool)
}

TOTPProvider is the optional time-based OTP algorithm port. Generate returns the shared secret and its otpauth:// URI; Validate reports the accepted time step so the store can reject replays of the same step.

type TOTPReplayRejectedEvent

type TOTPReplayRejectedEvent struct {
	EventMeta
	UserID UUID
}

type TOTPStatus

type TOTPStatus struct {
	Enrolled            bool
	Active              bool
	UnusedRecoveryCodes int
	CreatedAt           time.Time
	UpdatedAt           time.Time
}

TOTPStatus is the read-only state of a user's TOTP factor. It never contains the secret, the otpauth URI or any recovery code material.

type TOTPStore

type TOTPStore interface {
	TOTPByUserID(context.Context, UUID) (TOTPFactor, error)
	SaveTOTPEnrollment(context.Context, TOTPFactor, Commit) error
	ActivateTOTP(context.Context, TOTPFactor, []RecoveryCode, Commit) error
	UseTOTP(context.Context, UUID, int64, Commit) (bool, error)
	ConsumeRecoveryCode(context.Context, UUID, []byte, time.Time, Commit) (bool, error)
	CountUnusedRecoveryCodes(context.Context, UUID) (int64, error)
	DisableTOTP(context.Context, UUID, Commit) error
	// ReplaceRecoveryCodes atomically deletes the user's recovery codes and
	// inserts the replacement set. It returns ErrNotFound without an active
	// TOTP factor.
	ReplaceRecoveryCodes(ctx context.Context, userID UUID, codes []RecoveryCode, commit Commit) error
	// ResetSecondFactor atomically removes the user's TOTP factor with its
	// recovery codes and every passkey and, for SessionStore-capable
	// stores, revokes the user's sessions in the same transaction. It
	// succeeds even when the user has no second factor; an unknown user
	// reports ErrNotFound.
	ResetSecondFactor(ctx context.Context, userID UUID, at time.Time, commit Commit) error
}

TOTPStore persists the TOTP factor, its recovery codes, and the atomic second-factor reset.

type TOTPVerifiedEvent

type TOTPVerifiedEvent struct {
	EventMeta
	UserID UUID
}

type TransactionHook

type TransactionHook interface {
	ApplyUserCreate(context.Context, Tx, UserCreateChange) error
	ApplyWorkspaceCreate(context.Context, Tx, WorkspaceCreateChange) error
	ApplyUserStatusChange(context.Context, Tx, UserStatusChange) error
	ApplyUserProfileChange(context.Context, Tx, UserProfileChange) error
	ApplyWorkspaceChange(context.Context, Tx, WorkspaceChange) error
	ApplyMembershipChange(context.Context, Tx, MembershipChange) error
	ApplyWorkspaceInvitationChange(context.Context, Tx, WorkspaceInvitationChange) error
	ApplyWorkspaceDomainChange(context.Context, Tx, WorkspaceDomainChange) error
	ApplyPasswordChange(context.Context, Tx, PasswordChange) error
	ApplyEmailAddition(context.Context, Tx, EmailAddition) error
	ApplyEmailConfirmation(context.Context, Tx, EmailConfirmation) error
	ApplyPrimaryEmailChange(context.Context, Tx, PrimaryEmailChange) error
	ApplyEmailRemoval(context.Context, Tx, EmailRemoval) error
	ApplyTOTPEnrollment(context.Context, Tx, TOTPEnrollmentChange) error
	ApplyTOTPActivation(context.Context, Tx, TOTPActivation) error
	ApplyTOTPDisable(context.Context, Tx, TOTPDisable) error
	ApplyPasskeyRegistration(context.Context, Tx, PasskeyRegistration) error
	ApplyPasskeyDeletion(context.Context, Tx, PasskeyDeletion) error
	ApplyPATCreation(context.Context, Tx, PATCreation) error
	ApplyPATRevocation(context.Context, Tx, PATRevocation) error
	ApplyUserCredentialRevocation(context.Context, Tx, UserCredentialRevocation) error
	ApplySecondFactorReset(context.Context, Tx, SecondFactorReset) error
	ApplyUserAnonymization(context.Context, Tx, UserAnonymization) error
	ApplyRecoveryCodeRegeneration(context.Context, Tx, RecoveryCodeRegeneration) error
	ApplySessionCreation(context.Context, Tx, SessionCreation) error
	ApplySessionRevocation(context.Context, Tx, SessionRevocation) error
	ApplyUserSessionRevocation(context.Context, Tx, UserSessionRevocation) error
	ApplySSOLink(context.Context, Tx, SSOLink) error
	ApplySSOUnlink(context.Context, Tx, SSOUnlink) error
	ApplyRoleGrant(context.Context, Tx, RoleGrant) error
	ApplyInstanceRoleChange(context.Context, Tx, InstanceRoleChange) error
	ApplyInstanceRoleRemoval(context.Context, Tx, InstanceRoleRemoval) error
	ApplyClientAudit(context.Context, Tx, ClientAuditRecord) error
	ApplySCIMConfigurationCreate(context.Context, Tx, SCIMConfigurationChange) error
	ApplySCIMUserProvision(context.Context, Tx, SCIMUserChange) error
	ApplySCIMUserUpdate(context.Context, Tx, SCIMUserChange) error
	ApplySCIMUserDeprovision(context.Context, Tx, SCIMUserChange) error
	ApplySCIMGroupUpsert(context.Context, Tx, SCIMGroupChange) error
	ApplySCIMGroupDelete(context.Context, Tx, SCIMGroupChange) error
	ApplyOAuthChange(context.Context, Tx, OAuthChange) error
	// contains filtered or unexported methods
}

TransactionHook lets the host add its own writes to a Credbound mutation. Hooks run sequentially inside the store transaction, after the mutation and before the audit write, so returning an error aborts the whole commit; errors that are not sentinel errors surface as ErrTransactionRejected. Hooks must not perform external I/O, invoke another Manager mutation, retain the Tx, or use it from another goroutine. Implementations embed UnimplementedTransactionHook to stay compatible as methods are added.

type TrustedRequest

type TrustedRequest struct {
	// Local reports that the transport peer is a loopback address. When set,
	// RequireAdminMutation waives the AAL2 step-up for administrative
	// mutations, so it must never be copied from a request parameter,
	// header or body.
	Local bool
}

TrustedRequest is constructed by a trusted server adapter, never from client-controlled URL, Host, Origin or forwarding headers. TrustedRequestFromAddr derives it correctly from the observed network peer.

func TrustedRequestFromAddr

func TrustedRequestFromAddr(remoteAddr string) TrustedRequest

TrustedRequestFromAddr derives a TrustedRequest from the transport peer address (typically http.Request.RemoteAddr): Local is set only when the peer is a loopback address. Always call it with the actual network peer — never with a value copied from a request parameter, header, or body, which a client controls. Requests arriving through a reverse proxy have the proxy as their peer and are correctly reported as non-local.

type Tx

type Tx interface {
	Kind() StoreKind
	Audit() AuditEvent
}

Tx represents the store transaction that is open during a TransactionHook. It is only valid for the duration of the callback and must not be retained or used by another goroutine.

type UUID

type UUID = uuid.UUID

UUID identifies every Credbound record: the 16 raw bytes PostgreSQL stores in a uuid column. It is comparable, so it works with == and as a map key, and it costs 16 bytes rather than the 36 of the canonical text.

Every identifier the library mints is a UUIDv7 (RFC 9562), so identifiers sort by creation time — which is what cursor pagination and the audit ordering rely on.

The zero value means "no identifier": it is what an absent optional reference reads as, and it maps to SQL NULL in both directions. A zero UUID carries neither version 7 nor the RFC variant, so it never passes the checks at the API boundary nor the CHECK constraints in the schema.

This is an alias, not a new type, so it stays interchangeable with the package it comes from. That package is the one accepted for the Go standard library (golang/go#62026), vendored verbatim under internal/uuid until it ships; when it does, this alias is repointed at the standard library and the vendored copy is deleted, with no other change. Nothing depends on methods the standard library does not provide: the store binds pgtype.UUID for its rows and converts at the boundary.

func MustParseUUID

func MustParseUUID(value string) UUID

MustParseUUID is ParseUUID for constants and tests; it panics on a malformed value and must never be handed anything a caller supplied.

func ParseUUID

func ParseUUID(value string) (UUID, error)

ParseUUID reads an identifier in its canonical 8-4-4-4-12 form. A malformed value reports ErrInvalidInput, so a caller can classify it like any other rejected input.

Only the canonical form is accepted. The underlying parser also takes the compact, braced and urn:uuid: spellings, but Credbound does not: those would give one record several spellings that String never renders back, so a client comparing or caching what it received would see identifiers that differ from the ones the library returns. Hexadecimal case is the one exception, since String renders it back canonically; a token parser, whose input the library itself issued, is stricter still and takes the rendered form alone.

type UnimplementedEventListener

type UnimplementedEventListener struct{}

func (UnimplementedEventListener) OnAuditUnavailable

func (UnimplementedEventListener) OnAuthenticationFailed

func (UnimplementedEventListener) OnAuthenticationSucceeded

func (UnimplementedEventListener) OnAuthorizationDenied

func (UnimplementedEventListener) OnBootstrapCompleted

func (UnimplementedEventListener) OnClientAuditRecorded

func (UnimplementedEventListener) OnEmailAdded

func (UnimplementedEventListener) OnEmailAuthenticationRequested

func (UnimplementedEventListener) OnEmailConfirmed

func (UnimplementedEventListener) OnEmailRemoved

func (UnimplementedEventListener) OnEmailVerificationResent

func (UnimplementedEventListener) OnInstanceRoleChanged

func (UnimplementedEventListener) OnInstanceRoleRemoved

func (UnimplementedEventListener) OnMembershipChanged

func (UnimplementedEventListener) OnOAuthEvent

func (UnimplementedEventListener) OnPATAuthenticated

func (UnimplementedEventListener) OnPATCreated

func (UnimplementedEventListener) OnPATRejected

func (UnimplementedEventListener) OnPATRevoked

func (UnimplementedEventListener) OnPasskeyAuthenticated

func (UnimplementedEventListener) OnPasskeyDeleted

func (UnimplementedEventListener) OnPasskeyRegistered

func (UnimplementedEventListener) OnPasswordChanged

func (UnimplementedEventListener) OnPasswordRehashed

func (UnimplementedEventListener) OnPasswordResetCompleted

func (UnimplementedEventListener) OnPasswordResetRequested

func (UnimplementedEventListener) OnPrimaryEmailChanged

func (UnimplementedEventListener) OnRecoveryCodeConsumed

func (UnimplementedEventListener) OnRecoveryCodesRegenerated

func (UnimplementedEventListener) OnRoleGranted

func (UnimplementedEventListener) OnSCIMConfigurationCreated

func (UnimplementedEventListener) OnSCIMGroupCreated

func (UnimplementedEventListener) OnSCIMGroupDeleted

func (UnimplementedEventListener) OnSCIMGroupMembersChanged

func (UnimplementedEventListener) OnSCIMGroupMembersChanged(context.Context, SCIMGroupEvent) error

func (UnimplementedEventListener) OnSCIMGroupUpdated

func (UnimplementedEventListener) OnSCIMUserActivated

func (UnimplementedEventListener) OnSCIMUserDeprovisioned

func (UnimplementedEventListener) OnSCIMUserDeprovisioned(context.Context, SCIMUserEvent) error

func (UnimplementedEventListener) OnSCIMUserProvisioned

func (UnimplementedEventListener) OnSCIMUserSuspended

func (UnimplementedEventListener) OnSCIMUserUpdated

func (UnimplementedEventListener) OnSSOAuthenticated

func (UnimplementedEventListener) OnSSOChallengeIssued

func (UnimplementedEventListener) OnSSOJITProvisioned

func (UnimplementedEventListener) OnSSOLinked

func (UnimplementedEventListener) OnSSOUnlinked

func (UnimplementedEventListener) OnSecondFactorReset

func (UnimplementedEventListener) OnSessionCreated

func (UnimplementedEventListener) OnSessionRevoked

func (UnimplementedEventListener) OnSignUpCompleted

func (UnimplementedEventListener) OnStepUpDenied

func (UnimplementedEventListener) OnTOTPActivated

func (UnimplementedEventListener) OnTOTPDisabled

func (UnimplementedEventListener) OnTOTPEnrollmentStarted

func (UnimplementedEventListener) OnTOTPReplayRejected

func (UnimplementedEventListener) OnTOTPVerified

func (UnimplementedEventListener) OnUserAnonymized

func (UnimplementedEventListener) OnUserCreated

func (UnimplementedEventListener) OnUserCredentialsRevoked

func (UnimplementedEventListener) OnUserLocked

func (UnimplementedEventListener) OnUserProfileUpdated

func (UnimplementedEventListener) OnUserSessionsRevoked

func (UnimplementedEventListener) OnUserStatusChanged

func (UnimplementedEventListener) OnWorkspaceChanged

func (UnimplementedEventListener) OnWorkspaceCreated

func (UnimplementedEventListener) OnWorkspaceDomainConfirmed

func (UnimplementedEventListener) OnWorkspaceDomainCreated

func (UnimplementedEventListener) OnWorkspaceDomainPolicyUpdated

func (UnimplementedEventListener) OnWorkspaceDomainPolicyUpdated(context.Context, WorkspaceDomainEvent) error

func (UnimplementedEventListener) OnWorkspaceDomainRemoved

func (UnimplementedEventListener) OnWorkspaceInvitationAccepted

func (UnimplementedEventListener) OnWorkspaceInvitationCreated

func (UnimplementedEventListener) OnWorkspaceInvitationRevoked

type UnimplementedTransactionHook

type UnimplementedTransactionHook struct{}

func (UnimplementedTransactionHook) ApplyClientAudit

func (UnimplementedTransactionHook) ApplyEmailAddition

func (UnimplementedTransactionHook) ApplyEmailConfirmation

func (UnimplementedTransactionHook) ApplyEmailRemoval

func (UnimplementedTransactionHook) ApplyInstanceRoleChange

func (UnimplementedTransactionHook) ApplyInstanceRoleRemoval

func (UnimplementedTransactionHook) ApplyMembershipChange

func (UnimplementedTransactionHook) ApplyOAuthChange

func (UnimplementedTransactionHook) ApplyPATCreation

func (UnimplementedTransactionHook) ApplyPATRevocation

func (UnimplementedTransactionHook) ApplyPasskeyDeletion

func (UnimplementedTransactionHook) ApplyPasskeyRegistration

func (UnimplementedTransactionHook) ApplyPasswordChange

func (UnimplementedTransactionHook) ApplyPrimaryEmailChange

func (UnimplementedTransactionHook) ApplyRecoveryCodeRegeneration

func (UnimplementedTransactionHook) ApplyRoleGrant

func (UnimplementedTransactionHook) ApplySCIMConfigurationCreate

func (UnimplementedTransactionHook) ApplySCIMGroupDelete

func (UnimplementedTransactionHook) ApplySCIMGroupUpsert

func (UnimplementedTransactionHook) ApplySCIMUserDeprovision

func (UnimplementedTransactionHook) ApplySCIMUserDeprovision(context.Context, Tx, SCIMUserChange) error

func (UnimplementedTransactionHook) ApplySCIMUserProvision

func (UnimplementedTransactionHook) ApplySCIMUserUpdate

func (UnimplementedTransactionHook) ApplySecondFactorReset

func (UnimplementedTransactionHook) ApplySessionCreation

func (UnimplementedTransactionHook) ApplySessionRevocation

func (UnimplementedTransactionHook) ApplyTOTPActivation

func (UnimplementedTransactionHook) ApplyTOTPDisable

func (UnimplementedTransactionHook) ApplyTOTPEnrollment

func (UnimplementedTransactionHook) ApplyUserAnonymization

func (UnimplementedTransactionHook) ApplyUserCreate

func (UnimplementedTransactionHook) ApplyUserCredentialRevocation

func (UnimplementedTransactionHook) ApplyUserProfileChange

func (UnimplementedTransactionHook) ApplyUserSessionRevocation

func (UnimplementedTransactionHook) ApplyUserStatusChange

func (UnimplementedTransactionHook) ApplyWorkspaceChange

func (UnimplementedTransactionHook) ApplyWorkspaceCreate

func (UnimplementedTransactionHook) ApplyWorkspaceDomainChange

func (UnimplementedTransactionHook) ApplyWorkspaceInvitationChange

type UpdateOAuthIssuerInput

type UpdateOAuthIssuerInput struct {
	OIDCEnabled              bool
	CIMDMode                 OAuthCIMDMode
	CIMDAllowedOrigins       []string
	DCRMode                  OAuthDCRMode
	DCRAllowClientSecrets    bool
	DCROpenRegistrationLimit int
	CodeTTL                  time.Duration
	AccessTokenTTL           time.Duration
	RefreshTokenTTL          time.Duration
}

UpdateOAuthIssuerInput replaces an issuer's policy; the issuer URL itself is immutable. Zero TTLs fall back to the defaults.

type UpdateSCIMConfigurationInput

type UpdateSCIMConfigurationInput struct {
	DefaultRole          Role
	TrustDirectoryEmails bool
	GroupRoleMappings    []SCIMGroupRoleMapping
}

UpdateSCIMConfigurationInput replaces the role policy of a provisioning domain. Applying it recomputes the roles of every managed membership.

type UpdateUserInput

type UpdateUserInput struct {
	// DisplayName is the new 1-200 character profile name, trimmed.
	DisplayName string
}

UpdateUserInput describes a user profile update. Emails are managed by the dedicated add/primary/remove operations and the disabled flag by the administrative lifecycle, so the only mutable profile field is the display name.

type UpdateWorkspaceInput

type UpdateWorkspaceInput struct {
	Name string
	// RequireMFA toggles the workspace MFA policy; nil leaves it unchanged.
	RequireMFA *bool
}

UpdateWorkspaceInput carries a workspace update. Name is always applied.

type User

type User struct {
	ID          UUID
	Email       string
	DisplayName string
	Disabled    bool
	LastSeenAt  *time.Time
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

User is a global account. Email mirrors the primary EmailAddress; LastSeenAt reflects the latest successful authentication across all factors and is updated atomically with the authentication audit.

type UserAnonymization

type UserAnonymization struct {
	EventMeta
	UserID UUID
}

UserAnonymization is the transactional-hook change for AnonymizeUser: the user's mutable personal data is scrubbed and their credentials revoked in the same transaction, while the append-only audit chain is preserved.

type UserAnonymizedEvent

type UserAnonymizedEvent struct {
	EventMeta
	UserID UUID
}

UserAnonymizedEvent reports that an instance administrator pseudonymized a user: their mutable personal data (display name, email addresses, SSO and credential names) was scrubbed and their credentials revoked, while the append-only audit chain was preserved. It is the library's answer to a right-to-erasure request; hosts erase their own application-owned data separately.

type UserCreateChange

type UserCreateChange struct {
	EventMeta
	User       User
	Email      EmailAddress
	Membership Membership
}

UserCreateChange carries a created account with its primary email and initial membership.

type UserCreatedEvent

type UserCreatedEvent struct {
	EventMeta
	User       User
	Email      EmailAddress
	Membership Membership
}

type UserCredentialRevocation

type UserCredentialRevocation struct {
	EventMeta
	UserID UUID
}

type UserCredentialsRevokedEvent

type UserCredentialsRevokedEvent struct {
	EventMeta
	UserID UUID
}

type UserDataExport

type UserDataExport struct {
	User          User
	Emails        []EmailAddress
	Workspaces    []WorkspaceMembership
	SSOIdentities []SSOIdentity
	Passkeys      []Passkey
	PATs          []PAT
	Sessions      []Session
	// SCIMUsers are the tenant-scoped directory profiles provisioned for the
	// user, with the directory's own attributes included.
	SCIMUsers []SCIMUser
	// Invitations are the workspace invitations the user accepted; they carry
	// the address the user was invited under, which may predate a rename.
	Invitations []WorkspaceInvitation
	// OAuthGrants are the user's OAuth consents, revoked ones included.
	OAuthGrants []OAuthGrant
}

UserDataExport is the structured copy of everything Credbound holds about one user, assembled for a data-subject access request (GDPR Article 15/20). It deliberately omits secrets — token digests and sealed passkey material are scrubbed — and the append-only audit log, which is retained under the host's security-log policy and read separately through AuditEvents. Sessions need a SessionStore-capable store, SCIMUsers and Invitations a PrivacyStore-capable one, and OAuthGrants the OAuth capability; the sections are empty otherwise.

type UserLockedEvent

type UserLockedEvent struct {
	EventMeta
	UserID      UUID
	LockedUntil time.Time
	// Request carries the client network context supplied by the host through
	// WithRequestMetadata, so listeners can throttle or alert by address
	// without re-reading the audit log.
	Request RequestMetadata
}

UserLockedEvent is emitted once when consecutive failures reach the lockout threshold, so listeners can alert without counting failures themselves.

type UserProfileChange

type UserProfileChange struct {
	EventMeta
	User            User
	PreviousProfile string
}

UserProfileChange carries a profile display-name update and the value it replaced.

type UserProfileUpdatedEvent

type UserProfileUpdatedEvent struct {
	EventMeta
	UserID          UUID
	DisplayName     string
	PreviousProfile string
}

UserProfileUpdatedEvent reports a profile display-name update.

type UserSessionRevocation

type UserSessionRevocation struct {
	EventMeta
	UserID UUID
}

UserSessionRevocation reports a bulk "log out everywhere" for one user.

type UserSessionsRevokedEvent

type UserSessionsRevokedEvent struct {
	EventMeta
	UserID UUID
}

UserSessionsRevokedEvent reports that every active session of the user was revoked in one operation ("log out everywhere").

type UserStatusChange

type UserStatusChange struct {
	EventMeta
	UserID   UUID
	Disabled bool
}

UserStatusChange covers both directions of the user lifecycle; Disabled tells them apart, as does the EventMeta name.

type UserStatusEvent

type UserStatusEvent struct {
	EventMeta
	UserID   UUID
	Disabled bool
}

UserStatusEvent reports a user being disabled or re-enabled; Disabled tells the directions apart.

type ValidationError

type ValidationError struct {
	// Field names the offending input in lower_snake_case, e.g. "email",
	// "password", "display_name", "workspace_name", "role".
	Field string
	// Rule is a stable machine-readable identifier of the violated rule,
	// e.g. "required", "format", "too_short", "too_long", "unknown".
	Rule string
	// Message is a human-readable English description for logs; hosts
	// localize their own user-facing copy from Field and Rule.
	Message string
}

ValidationError reports which input field failed validation and why, so a host can answer with structured, per-field feedback instead of parsing the error text. It always matches errors.Is(err, ErrInvalidInput); retrieve it with errors.As. User-input validation failures (addresses, passwords, names, roles) carry one; protocol-level rejections may still return a plain ErrInvalidInput.

func (*ValidationError) Error

func (e *ValidationError) Error() string

func (*ValidationError) Unwrap

func (e *ValidationError) Unwrap() error

Unwrap makes every ValidationError satisfy errors.Is(err, ErrInvalidInput).

type Workspace

type Workspace struct {
	ID   UUID
	Name string
	// RequireMFA rejects interactive access below AAL2 for every member of
	// the workspace. Non-interactive credentials such as PATs, whose
	// creation already required a step-up, are not affected.
	RequireMFA bool
	CreatedAt  time.Time
	UpdatedAt  time.Time
	DisabledAt *time.Time
}

Workspace is a tenant. A workspace with DisabledAt set denies every tenant-scoped capability until it is re-enabled.

type WorkspaceChange

type WorkspaceChange struct {
	EventMeta
	Workspace Workspace
	Previous  Workspace
}

WorkspaceChange carries a workspace update, disable or enable together with the state it replaced.

type WorkspaceChangedEvent

type WorkspaceChangedEvent struct {
	EventMeta
	Workspace Workspace
	Previous  Workspace
}

WorkspaceChangedEvent reports a workspace update, disable or enable together with the state it replaced.

type WorkspaceCreateChange

type WorkspaceCreateChange struct {
	EventMeta
	Workspace Workspace
	Owner     Membership
}

type WorkspaceCreatedEvent

type WorkspaceCreatedEvent struct {
	EventMeta
	Workspace Workspace
	Owner     Membership
}

type WorkspaceDomain

type WorkspaceDomain struct {
	ID          UUID
	WorkspaceID UUID
	// Domain is the normalized, lowercase registrable DNS name
	// ("corp.example.com").
	Domain string
	// Challenge is the DNS TXT value proving control of the domain. It is
	// deliberately not a secret credential — the host publishes it in public
	// DNS — so it is stored in plaintext and remains visible on the record so
	// the host can re-display it until the domain is confirmed.
	Challenge string
	// ConfirmedAt is set once the host asserted that DNS verification
	// completed. An unconfirmed domain carries no policy effect.
	ConfirmedAt *time.Time
	// AutoJoin enables JIT provisioning: an unknown SSO identity whose
	// verified email is under this domain and arrives through the trusted
	// provider configuration is provisioned as a passwordless member.
	AutoJoin bool
	// AutoJoinRole is the workspace role granted to JIT-provisioned users.
	AutoJoinRole Role
	// SSOProviderConfigurationID names the registered SSO provider
	// configuration this domain trusts for JIT provisioning.
	SSOProviderConfigurationID UUID
	// EnforceSSO rejects password, magic-link and email-OTP authentication
	// for addresses under the domain with ErrSSORequired.
	EnforceSSO bool
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

WorkspaceDomain is a workspace-owned email domain. It is created pending with a DNS challenge value; the host proves control of the domain (by convention a TXT record carrying Challenge) out of band and confirms it with ConfirmWorkspaceDomain. Only a confirmed domain carries policy: auto-join (JIT provisioning through the trusted SSO provider configuration) and SSO enforcement. A domain name is globally unique across workspaces.

type WorkspaceDomainChange

type WorkspaceDomainChange struct {
	EventMeta
	Domain  WorkspaceDomain
	Removed bool
}

WorkspaceDomainChange covers workspace-domain creation, confirmation, policy update and removal; the EventMeta name tells them apart and Removed marks a removal whose Domain field holds the final state. The Challenge is deliberately included: it is published in public DNS and is not a secret.

type WorkspaceDomainEvent

type WorkspaceDomainEvent struct {
	EventMeta
	Domain WorkspaceDomain
}

WorkspaceDomainEvent is the shared payload of the workspace-domain lifecycle events (created, confirmed, policy updated, removed); the EventMeta name tells them apart. The Challenge is deliberately included: it is published in public DNS and is not a secret.

type WorkspaceDomainPolicyInput

type WorkspaceDomainPolicyInput struct {
	AutoJoin                   bool
	AutoJoinRole               Role
	SSOProviderConfigurationID UUID
	EnforceSSO                 bool
}

WorkspaceDomainPolicyInput replaces the policy of a confirmed workspace domain: the auto-join flag with its target role, the SSO provider configuration the domain trusts, and the SSO enforcement flag. A zero AutoJoinRole means member. When AutoJoin or EnforceSSO is set the provider configuration must be registered with the Manager.

type WorkspaceInvitation

type WorkspaceInvitation struct {
	ID             UUID
	WorkspaceID    UUID
	Email          string
	Role           Role
	InvitedBy      UUID
	Digest         []byte
	CreatedAt      time.Time
	ExpiresAt      time.Time
	AcceptedAt     *time.Time
	AcceptedUserID UUID
	RevokedAt      *time.Time
}

WorkspaceInvitation invites an email address into a workspace with a pre-assigned role. The digest is stored server-side only; the single-use token is returned once at creation for the host to deliver.

type WorkspaceInvitationChange

type WorkspaceInvitationChange struct {
	EventMeta
	Invitation WorkspaceInvitation
}

WorkspaceInvitationChange never carries the invitation digest.

type WorkspaceInvitationEvent

type WorkspaceInvitationEvent struct {
	EventMeta
	Invitation WorkspaceInvitation
}

WorkspaceInvitationEvent never carries the invitation digest.

type WorkspaceMember

type WorkspaceMember struct {
	User       User
	Membership Membership
}

WorkspaceMember pairs a workspace membership with the member's account profile. It is the row a workspace administrator's member list renders.

type WorkspaceMembership

type WorkspaceMembership struct {
	Workspace  Workspace
	Membership Membership
}

WorkspaceMembership pairs a workspace with the exported user's membership in it. It is the workspace-affiliation entry of a UserDataExport.

type WorkspacePAT added in v0.0.4

type WorkspacePAT struct {
	PAT  PAT
	User User
}

WorkspacePAT is one workspace-bound token joined with its owner's account profile. It is the row a workspace administrator's key list renders: the token never carries its digest, and the owner is resolved so the page can name whose key it is without the instance-wide admin users read permission.

type WorkspacePermission

type WorkspacePermission string

WorkspacePermission names a tenant-scoped capability checked by AuthorizePermission. The admin role always holds every registered workspace permission, including host-defined ones.

const (
	PermissionWorkspaceAccess        WorkspacePermission = "workspace.access"
	PermissionWorkspaceUsersRead     WorkspacePermission = "workspace.users.read"
	PermissionWorkspaceUsersWrite    WorkspacePermission = "workspace.users.write"
	PermissionWorkspaceSettingsWrite WorkspacePermission = "workspace.settings.write"
	PermissionWorkspaceRBACWrite     WorkspacePermission = "workspace.rbac.write"
	PermissionWorkspaceAuditRead     WorkspacePermission = "workspace.audit.read"
	// PermissionWorkspaceCredentialsManage administers the workspace-bound
	// credentials of every member — listing and revoking the PATs bound to
	// the workspace — without any power over the members themselves. It is
	// deliberately separate from workspace users write: the page that
	// disables a departed colleague's key is not the page that manages
	// memberships, and a machine-operations role may need the first without
	// the second.
	PermissionWorkspaceCredentialsManage WorkspacePermission = "workspace.credentials.manage"
	PermissionOAuthResourceManage        WorkspacePermission = "oauth.resource.manage"
)

type WorkspaceStore

type WorkspaceStore interface {
	CreateWorkspace(context.Context, Workspace, Membership, Commit) error
	WorkspaceByID(context.Context, UUID) (Workspace, error)
	UpdateWorkspace(context.Context, Workspace, Commit) error
	SetWorkspaceDisabled(context.Context, UUID, bool, time.Time, Commit) error
	Workspaces(context.Context, PageRequest) iter.Seq2[PageEvent[Workspace], error]
	UserWorkspaces(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[Workspace], error]
	Membership(context.Context, UUID, UUID) (Membership, error)
	UpsertMembership(context.Context, Membership, Commit) error
	RemoveMembership(context.Context, UUID, UUID, time.Time, Commit) error
	Memberships(context.Context, UUID, PageRequest) iter.Seq2[PageEvent[Membership], error]
	InstanceAdministrator(context.Context, UUID) (InstanceAdministrator, error)
	// InstanceAdministrators streams every instance role assignment, oldest
	// first. The set is bounded by governance, not by end users, so it is
	// not paginated.
	InstanceAdministrators(context.Context) iter.Seq2[InstanceAdministrator, error]
	SetInstanceRole(context.Context, InstanceAdministrator, Commit) error
	RemoveInstanceRole(context.Context, UUID, Commit) error
}

WorkspaceStore persists workspaces, memberships and instance role assignments.

Directories

Path Synopsis
Package credboundtest provides deterministic test doubles and constructors for host services that integrate github.com/deepteams/credbound.
Package credboundtest provides deterministic test doubles and constructors for host services that integrate github.com/deepteams/credbound.
examples
minimal command
Command minimal is a runnable, end-to-end Credbound integration: PostgreSQL persistence through pgx, the embedded migrations, Argon2id password hashing, and a net/http layer backed by Credbound's server-side session module (CreateSession, AuthenticateSession, SignOut), following the "Sessions and the Authentication capability" contract from the README.
Command minimal is a runnable, end-to-end Credbound integration: PostgreSQL persistence through pgx, the embedded migrations, Argon2id password hashing, and a net/http layer backed by Credbound's server-side session module (CreateSession, AuthenticateSession, SignOut), following the "Sessions and the Authentication capability" contract from the README.
Package githubadapter provides a hardened GitHub implementation of the credbound.SSOProvider port.
Package githubadapter provides a hardened GitHub implementation of the credbound.SSOProvider port.
internal
cmd/genevents command
Command genevents generates the forward-compatible no-op implementations for Credbound's typed hook and event interfaces.
Command genevents generates the forward-compatible no-op implementations for Credbound's typed hook and event interfaces.
dbtype
Package dbtype names the types the generated queries bind to.
Package dbtype names the types the generated queries bind to.
ssrf
Package ssrf holds the outbound-fetch address policy shared by the adapters that dereference configured URLs on behalf of a host: OAuth Client Identifier Metadata Documents (oauthhttp), private_key_jwt JWKS documents (oauthclientadapter), and SAML IdP metadata (samladapter).
Package ssrf holds the outbound-fetch address policy shared by the adapters that dereference configured URLs on behalf of a host: OAuth Client Identifier Metadata Documents (oauthhttp), private_key_jwt JWKS documents (oauthclientadapter), and SAML IdP metadata (samladapter).
storetest
Package storetest runs the store-independent behavioral conformance suite of Credbound: the same Manager-level flows executed against every store implementation, so a divergence between the in-memory and PostgreSQL backends fails a test instead of reaching production.
Package storetest runs the store-independent behavioral conformance suite of Credbound: the same Manager-level flows executed against every store implementation, so a divergence between the in-memory and PostgreSQL backends fails a test instead of reaching production.
uuid
Package uuid provides support for generating and manipulating UUIDs.
Package uuid provides support for generating and manipulating UUIDs.
Package memory provides an in-memory implementation of every Credbound persistence port — Store plus the optional SessionStore, SignupStore, DomainStore, SCIMStore, EmailThrottleStore and OAuthStore capabilities — including the hash-chained audit log and its atomic commit semantics.
Package memory provides an in-memory implementation of every Credbound persistence port — Store plus the optional SessionStore, SignupStore, DomainStore, SCIMStore, EmailThrottleStore and OAuthStore capabilities — including the hash-chained audit log and its atomic commit semantics.
Package migrations embeds Credbound's Goose-compatible PostgreSQL migrations.
Package migrations embeds Credbound's Goose-compatible PostgreSQL migrations.
Package oauthclientadapter provides hardened OAuth client authentication adapters without coupling Credbound to an HTTP server: JWTAssertionVerifier validates private_key_jwt client assertions (RFC 7523) against a client's registered JWKS with single-use JWT ID enforcement, and MemoryReplayStore supplies the replay protection for single-process hosts.
Package oauthclientadapter provides hardened OAuth client authentication adapters without coupling Credbound to an HTTP server: JWTAssertionVerifier validates private_key_jwt client assertions (RFC 7523) against a client's registered JWKS with single-use JWT ID enforcement, and MemoryReplayStore supplies the replay protection for single-process hosts.
Package oauthhttp provides the optional, mountable HTTP adapters for Credbound's OAuth 2.1/OIDC authorization server: Handler serves the protocol endpoints (discovery, JWKS, authorize, token, revoke, register, userinfo), Protect wraps an MCP or API resource with bearer-token authentication, and MetadataFetcher resolves Client Identifier Metadata Documents over SSRF-hardened HTTPS.
Package oauthhttp provides the optional, mountable HTTP adapters for Credbound's OAuth 2.1/OIDC authorization server: Handler serves the protocol endpoints (discovery, JWKS, authorize, token, revoke, register, userinfo), Protect wraps an MCP or API resource with bearer-token authentication, and MetadataFetcher resolves Client Identifier Metadata Documents over SSRF-hardened HTTPS.
Package oidcadapter provides standard-library OIDC signing adapters.
Package oidcadapter provides standard-library OIDC signing adapters.
Package otelobserver implements the credbound.Observer port with OpenTelemetry: every observed operation becomes a span, a "credbound.operations" counter increment, a "credbound.operation.duration" histogram sample and a log record, all attributed with the operation name and outcome.
Package otelobserver implements the credbound.Observer port with OpenTelemetry: every observed operation becomes a span, a "credbound.operations" counter increment, a "credbound.operation.duration" histogram sample and a log record, all attributed with the operation name and outcome.
Package password implements the credbound.PasswordHasher port with Argon2id (RFC 9106).
Package password implements the credbound.PasswordHasher port with Argon2id (RFC 9106).
Package samladapter provides a hardened SAML 2.0 service-provider implementation of the credbound.SSOProvider port, so hosts never hand-roll XML signature validation — historically the most dangerous part of SAML.
Package samladapter provides a hardened SAML 2.0 service-provider implementation of the credbound.SSOProvider port, so hosts never hand-roll XML signature validation — historically the most dangerous part of SAML.
Package scimhttp exposes Credbound's optional SCIM 2.0 provisioning adapter (RFC 7643/7644): Users, Groups, /.search, PATCH, discovery endpoints and SCIM-shaped errors, all delegated to a Manager whose store implements credbound.SCIMStore.
Package scimhttp exposes Credbound's optional SCIM 2.0 provisioning adapter (RFC 7643/7644): Users, Groups, /.search, PATCH, discovery endpoints and SCIM-shaped errors, all delegated to a Manager whose store implements credbound.SCIMStore.
sqlstore
postgresql
Package postgresql implements every Credbound persistence port — Store plus the optional SessionStore, SignupStore, DomainStore, SCIMStore, EmailThrottleStore and OAuthStore capabilities — on PostgreSQL, pairing sqlc-generated database/sql queries for transactional mutations with pgx streaming for paginated reads, and committing each mutation's hash-chained audit event atomically with the change.
Package postgresql implements every Credbound persistence port — Store plus the optional SessionStore, SignupStore, DomainStore, SCIMStore, EmailThrottleStore and OAuthStore capabilities — on PostgreSQL, pairing sqlc-generated database/sql queries for transactional mutations with pgx streaming for paginated reads, and committing each mutation's hash-chained audit event atomically with the change.
Package ssoadapter provides a hardened, generic OpenID Connect implementation of the credbound.SSOProvider port, so hosts do not have to hand-roll the network side of SSO.
Package ssoadapter provides a hardened, generic OpenID Connect implementation of the credbound.SSOProvider port, so hosts do not have to hand-roll the network side of SSO.
Package totpadapter implements the credbound.TOTPProvider port with RFC 6238 six-digit SHA-1 codes (the interoperable authenticator-app profile) on top of github.com/pquerna/otp.
Package totpadapter implements the credbound.TOTPProvider port with RFC 6238 six-digit SHA-1 codes (the interoperable authenticator-app profile) on top of github.com/pquerna/otp.
Package webauthnadapter implements the credbound.PasskeyProvider port on top of github.com/go-webauthn/webauthn.
Package webauthnadapter implements the credbound.PasskeyProvider port on top of github.com/go-webauthn/webauthn.

Jump to

Keyboard shortcuts

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