service

package
v1.0.0-alpha.21 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: AGPL-3.0 Imports: 27 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultUserTeamsLimit = 10

DefaultUserTeamsLimit caps an embedded membership list. Embedded collections are not paginated, so without a cap one user on thousands of teams would dominate a page whose limit promised twenty users. Past the cap the user is flagged truncated and the full list is read from ListUserTeams.

Variables

View Source
var ErrListFilterRequired = errors.New("authz list filter missing")

ErrListFilterRequired means a management list ran without AuthzListFilter (or an unrestricted / one-shot skip marker) on the context. HTTP maps this to a 500 with message "authz list filter missing" so it is not triaged as a DB outage.

Functions

func AuthzListSkipOncePending

func AuthzListSkipOncePending(ctx context.Context) bool

AuthzListSkipOncePending reports whether a one-shot Allow skip is still available (not yet consumed).

func AuthzListUnrestricted

func AuthzListUnrestricted(ctx context.Context) bool

AuthzListUnrestricted reports whether WithAuthzListUnrestricted is set.

func ConsumeAuthzListSkipOnce

func ConsumeAuthzListSkipOnce(ctx context.Context) bool

ConsumeAuthzListSkipOnce returns true once for a WithAuthzListSkipOnce context, then false. compileList / ListUsers call this from RequireManagementListFilter.

func SyncUserTeamMembershipEdge

func SyncUserTeamMembershipEdge(ctx context.Context, edges AuthzMembershipEdgeStatements, projectID, teamID, userID string, status domain.MembershipStatus) error

SyncUserTeamMembershipEdge projects a team_memberships status change onto authz_membership_edges: authz-active statuses upsert the user→team edge; otherwise the edge is deleted.

func WithAuthzListFilter

func WithAuthzListFilter(ctx context.Context, filter AuthzListFilter) context.Context

WithAuthzListFilter attaches a list authz conjunct for dialect list SQL.

func WithAuthzListSkipOnce

func WithAuthzListSkipOnce(ctx context.Context) context.Context

WithAuthzListSkipOnce is the project-wide Allow skip: the next compileList or ListUsers consumes it and omits EXISTS. A later management list on the same request without a filter still fails closed (#838). Do not use this for nested uniqueness / resolve — those must use WithAuthzListUnrestricted.

func WithAuthzListUnrestricted

func WithAuthzListUnrestricted(ctx context.Context) context.Context

WithAuthzListUnrestricted marks a nested non-HTTP list (create uniqueness, login Resolve, branding GetLatest, stmttest). It skips the #838 tripwire and ignores an inherited AuthzListFilter so a nested query cannot see zero rows from the outer kind's EXISTS.

Types

type AuthAttemptService

type AuthAttemptService interface {
	// Create and store a new auth attempt.
	//
	// If input.SessionID is set, the existing session's verified checks are copied
	// into the attempt for step-up authentication against an existing session.
	//
	// If input.RequiredChecks are nil, the project's default required checks are used.
	//
	// errors: domain.ErrAuthAttemptInvalidRequest, domain.ErrInternal
	Create(ctx context.Context, input CreateAuthAttemptInput) (*domain.AuthAttempt, error)

	// GetByID retrieves an auth attempt including all factors and challenges by its ID.
	//
	// errors: domain.ErrAuthAttemptNotFound, domain.ErrInternal
	GetByID(ctx context.Context, projectID, attemptID string) (*domain.AuthAttempt, error)

	// IssueChallenge creates and returns a new challenge for the given check type.
	// An existing challenge of the same type will be overwritten.
	//
	// For passkey challenges, the user check must already be verified, so the user ID is known
	// for retrieving registered credentials.
	//
	// errors: domain.ErrAuthAttemptNotFound, domain.ErrAuthAttemptInvalidRequest, domain.ErrAuthAttemptInvalidState, domain.ErrAuthAttemptAlreadyHandedOff, domain.ErrInternal
	IssueChallenge(ctx context.Context, input IssueChallengeInput) (*domain.AuthAttempt, error)

	// VerifyProof verifies the submitted proof against the challenge identified by ChallengeID.
	//
	// On success, it persists the verification and marks the attempt complete if all
	// required checks are now satisfied.
	//
	// On failure, it records the failed attempt for rate-limiting purposes.
	//
	// The ChallengeID must match the ID returned by IssueChallenge to prevent use of
	// stale proofs after a challenge is re-issued.
	//
	// errors: domain.ErrAuthAttemptNotFound, domain.ErrAuthAttemptStaleChallenge, domain.ErrAuthAttemptInvalidRequest, domain.ErrAuthAttemptProofRejected, domain.ErrAuthAttemptInvalidState, domain.ErrAuthAttemptAlreadyHandedOff, domain.ErrInternal
	VerifyProof(ctx context.Context, input VerifyProofInput) (*domain.AuthAttempt, error)

	// Handoff mints a single-use handoff token for a completed attempt.
	//
	// The client exchanges the token at POST /sessions/exchange to create
	// a new session or add factors to an existing session.
	//
	// The attempt must be in a completed state and not expired.
	//
	// errors: domain.ErrAuthAttemptNotFound, domain.ErrAuthAttemptInvalidState, domain.ErrAuthAttemptNotCompleted, domain.ErrAuthAttemptAlreadyHandedOff, domain.ErrInternal
	Handoff(ctx context.Context, input HandoffInput) (*domain.AuthAttempt, error)

	// BeginPasskeyEnrollment starts a management-plane enrollment ceremony on
	// an internal attempt: the target user is pinned as a verified user
	// factor and a registration challenge is issued. RegistrationID is the
	// internal attempt's id, but the attempt is invisible on the attempt
	// surface — internal attempts cannot be read, handed off, or exchanged.
	//
	// errors: domain.ErrAuthAttemptInvalidRequest, domain.ErrInternal
	BeginPasskeyEnrollment(ctx context.Context, input BeginPasskeyEnrollmentInput) (*BeginPasskeyEnrollmentOutput, error)

	// FinishPasskeyEnrollment verifies the attestation against the ceremony
	// and persists the credential; the internal attempt is consumed in the
	// same transaction, so a completed ceremony cannot be replayed and no
	// best-effort cleanup is needed.
	//
	// errors: domain.ErrAuthAttemptNotFound, domain.ErrAuthAttemptStaleChallenge, domain.ErrAuthAttemptInvalidRequest, domain.ErrAuthAttemptProofRejected, domain.ErrInternal
	FinishPasskeyEnrollment(ctx context.Context, input FinishPasskeyEnrollmentInput) (*FinishPasskeyEnrollmentOutput, error)
}

AuthAttemptService is the primary port for auth attempt operations. REST handler, OIDC server, SAML server and the flow engine all depend on this interface.

func NewAuthAttemptService

func NewAuthAttemptService(
	stmts StatementPool,
	sessions SessionResolver,
	users UserLookup,
	passwordVerifier crypto.HashVerifier,
) AuthAttemptService

type AuthAttemptStatements

type AuthAttemptStatements interface {
	Statements
	CreateAuthAttempt(ctx context.Context, entity *domain.AuthAttempt) error
	GetAuthAttemptByID(ctx context.Context, projectID, authAttemptID string) (*domain.AuthAttempt, error)
	GetAuthAttemptByHandoffToken(ctx context.Context, projectID string, handoffToken []byte) (*domain.AuthAttempt, error)
	DeleteAuthAttemptByID(ctx context.Context, projectID, authAttemptID string) error
	HandoffAuthAttempt(ctx context.Context, attempt *domain.AuthAttempt) error
	SetAuthAttemptChallenge(ctx context.Context, projectID, authAttemptID string, challenge domain.AuthChallenge) error
	// SetAuthAttemptFactor upserts a verified factor directly, without a
	// challenge/proof cycle: the caller has already established the fact by
	// other means (e.g. the user row was just created in the same transaction).
	// An existing check row of the same type is overwritten and its challenge
	// state cleared. Returns the check row's id for audit emits.
	SetAuthAttemptFactor(ctx context.Context, projectID, authAttemptID string, factor domain.AuthFactor) (checkID string, err error)
	AuthAttemptChallengeSucceeded(ctx context.Context, projectID, authAttemptID string, factor domain.AuthFactor, challengeID string) error
	AuthAttemptChallengeFailed(ctx context.Context, projectID, authAttemptID string, challenge domain.AuthChallenge) error
}

type AuthzAssignmentStatements

type AuthzAssignmentStatements interface {
	Statements
	CreateAuthzAssignment(ctx context.Context, assignment *domain.AuthzAssignment) error
	GetAuthzAssignment(ctx context.Context, projectID, id string) (*domain.AuthzAssignment, error)
	ListAuthzAssignments(ctx context.Context, projectID string, principalType domain.AuthzPrincipalType, principalID string, includeRevoked bool) ([]*domain.AuthzAssignment, error)
	RevokeAuthzAssignment(ctx context.Context, projectID, id string) error
	// GetActiveOwningTeamGrant returns the project's active owning-team grant
	// (object project, relation team) or NoRowFoundError when the project is
	// unclaimed. The unique active grant is the claim source of truth
	// (ADR 046 / ADR 054 §2); at most one active row exists per project,
	// enforced by authz_assignments_one_owning_team.
	GetActiveOwningTeamGrant(ctx context.Context, projectID string) (*domain.AuthzAssignment, error)
	// ListClaimedProjectIDs returns project ids holding an active owning-team
	// grant (ADR 049 export visibility), ordered by project_id after afterID
	// (empty starts at the beginning).
	ListClaimedProjectIDs(ctx context.Context, afterID string, limit uint32) ([]string, error)
}

AuthzAssignmentStatements persists grants (principal → catalog relation at a scope).

Use cases: grant/revoke product APIs and resolver reads — not dual-write from CreateUser. Create/Revoke are the write path; Get/List support admin and check-time lookup.

type AuthzCatalogStatements

type AuthzCatalogStatements interface {
	Statements
	PersistCatalogVersion(ctx context.Context, meta domain.AuthzCatalogVersion, mutations compiler.CatalogMutations) error
	GetAuthzCatalog(ctx context.Context, catalogID string) (*domain.AuthzCatalog, error)
	LoadCatalogMutations(ctx context.Context, catalogID string) (compiler.PersistedCatalog, error)
}

AuthzCatalogStatements persists a compiled catalog version (#720 → Wave 1 tables).

PersistCatalogVersion inserts authz_catalogs plus relations, relation references, expression edges, and closure. It does not write RSI, assignments, membership edges, or bundles. Retires any previously active catalog for the same (catalog_kind, owner_id).

GetAuthzCatalog loads one catalog version and its projected child rows by id.

LoadCatalogMutations reads the child rows PersistCatalogVersion wrote as compiler.PersistedCatalog (stmttest round-trip and L4 oracle catalog load).

type AuthzListFilter

type AuthzListFilter = domain.AuthzListObjectsParams

AuthzListFilter is the portable authz conjunct injected into resource list SQL. Management compileList requires this on context; compileRead and nested lists omit it on purpose.

func AuthzListFilterFromContext

func AuthzListFilterFromContext(ctx context.Context) (AuthzListFilter, bool)

AuthzListFilterFromContext returns the list authz filter when present. An unrestricted wrap wins over an inherited filter so nested uniqueness / resolve / GetLatest queries are not EXISTS-narrowed to the outer kind.

type AuthzMembershipEdgeStatements

type AuthzMembershipEdgeStatements interface {
	Statements
	UpsertAuthzMembershipEdge(ctx context.Context, edge *domain.AuthzMembershipEdge) error
	GetAuthzMembershipEdge(ctx context.Context, key domain.AuthzMembershipEdgeKey) (*domain.AuthzMembershipEdge, error)
	ListAuthzMembershipEdgesByMember(ctx context.Context, projectID string, memberType domain.AuthzMemberType, memberID string) ([]*domain.AuthzMembershipEdge, error)
	DeleteAuthzMembershipEdges(ctx context.Context, filter database.Filter[domain.AuthzMembershipEdgeField]) error
	DeleteAuthzMembershipEdgesForTeamDeactivate(ctx context.Context, projectID, teamID string) error
}

AuthzMembershipEdgeStatements persists the authz projection of set membership. The resolver reads these edges, not team_memberships (membership/lifecycle stays separate).

Use cases:

  • Upsert: low-level row ops. Prefer SyncUserTeamMembershipEdge for membership status changes.
  • DeleteAuthzMembershipEdges: column-shaped deletes via Filter (single edge, by member, by set).
  • DeleteAuthzMembershipEdgesForTeamDeactivate: team deactivate (team set + lifecycle-owned users).
  • Get/ListByMember: resolver / dual-write test reads.

type AuthzResolverStatements

type AuthzResolverStatements interface {
	Statements
	ActiveSystemCatalogID(ctx context.Context) (string, error)
	HasAuthzProjectFoothold(ctx context.Context, projectID string, principalType domain.AuthzPrincipalType, principalID string) (bool, error)
	CheckAuthz(ctx context.Context, params domain.AuthzCheckParams) (allowed bool, foothold bool, err error)
	ListAuthzObjectIDs(ctx context.Context, params domain.AuthzListObjectsParams) ([]string, error)
}

AuthzResolverStatements is the storage surface for permission Check / list (#423).

Use cases:

  • ActiveSystemCatalogID: active system catalog (kind=system, owner=system).
  • HasAuthzProjectFoothold: any active assignment or membership edge in project.
  • CheckAuthz: allowed + foothold in one round-trip (assignments, closure, membership, TTU).
  • ListAuthzObjectIDs: L4/oracle helper materializing authorized RSI ids for one kind. List *endpoints* compose an injectable SQL predicate (ADR 033); that lands with HTTP wiring.

type BeginPasskeyEnrollmentInput

type BeginPasskeyEnrollmentInput struct {
	ProjectID   string
	UserID      string
	Username    string
	DisplayName string
	RPID        string
	RPOrigins   []url.URL
}

BeginPasskeyEnrollmentInput starts a management-plane enrollment ceremony for an existing user.

type BeginPasskeyEnrollmentOutput

type BeginPasskeyEnrollmentOutput struct {
	// RegistrationID identifies the ceremony on finish.
	RegistrationID string
	// Options is the PublicKeyCredentialCreationOptions JSON.
	Options []byte
}

type BrandingService

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

BrandingService publishes and resolves immutable branding revisions (ADR 040). Revisions are create-only; flow responses resolve the latest revision per project.

func NewBrandingService

func NewBrandingService(v2Pool *DB) *BrandingService

func (*BrandingService) Create

Create validates and publishes a new branding revision.

func (*BrandingService) Get

func (s *BrandingService) Get(ctx context.Context, projectID, id string) (*domain.Branding, error)

Get returns a single revision by id.

func (*BrandingService) GetLatest

func (s *BrandingService) GetLatest(ctx context.Context, projectID string) (*domain.Branding, error)

GetLatest returns the newest revision for the project, or nil (no error) when the project has none — callers fall back to built-in defaults.

func (*BrandingService) List

func (s *BrandingService) List(ctx context.Context, projectID string) ([]*domain.Branding, error)

List returns the newest revisions for the project, newest first, capped at maxBrandingListRevisions.

type BrandingStatements

type BrandingStatements interface {
	Statements
	CreateBranding(ctx context.Context, entity *domain.Branding) error
	GetBrandingByID(ctx context.Context, projectID, id string) (*domain.Branding, error)
	ListBrandings(ctx context.Context, filter *database.ListOptions[domain.BrandingField]) (*database.ListResult[*domain.Branding], error)
}

type BuiltinSchemaProvider

type BuiltinSchemaProvider interface {
	GetBuiltinSchema(uri string) (*jsonschema.Schema, error)
	LatestSchemaURI(kind domain.KnownSchemaKind) (string, error)
}

type Challenge

type Challenge interface {
	ChallengeCheckType() domain.AuthCheckType
}

Challenge is implemented by each check-type-specific challenge value.

type ClaimCompleteResult

type ClaimCompleteResult struct {
	ProjectID string
	TeamID    string
	ClaimedAt time.Time
}

type ClaimInitResult

type ClaimInitResult struct {
	// ClaimURL is the browser URL the developer opens; it embeds the plaintext
	// challenge token so the browser leg never needs the project secret.
	ClaimURL string
	// ChallengeID is the plaintext challenge token the CLI polls with.
	ChallengeID string
	ExpiresAt   time.Time
}

type ClaimService

type ClaimService interface {
	// Init mints a single-use challenge for an unclaimed project. secretHash
	// is the hex SHA-256 of the presented project-secret bearer, computed by
	// the handler; it is stored so Status can verify proof of possession.
	Init(ctx context.Context, projectID, secretHash string) (*ClaimInitResult, error)
	// Status reports the challenge state. challengeID is the plaintext token
	// from Init; secretHash must match the initiating secret's hash.
	Status(ctx context.Context, projectID, challengeID, secretHash string) (*ClaimStatusResult, error)
	// Complete spends the challenge and attaches the project to userID's
	// personal team in one transaction. userID comes from the platform-session
	// check (verifyClaimSession, C2).
	Complete(ctx context.Context, projectID, challengeID, userID string) (*ClaimCompleteResult, error)
}

ClaimService is the claim use-case surface (ADR 046): the server side of the init/poll/complete dance that attaches an unclaimed project to the claiming user's personal team. Handler wiring is Claim E2 (#612).

func NewClaimService

func NewClaimService(v2Pool *DB, consoleBaseURL, platformProjectID string) ClaimService

NewClaimService builds the claim service. platformProjectID is the project hosting the claiming humans' accounts and personal teams (ADR 046 §2).

type ClaimStatements

type ClaimStatements interface {
	Statements
	// CreateChallenge inserts a pending claim challenge; entity.ID is the
	// SHA-256 hash of the challenge token, minted by the caller.
	CreateChallenge(ctx context.Context, entity *domain.ClaimChallenge) error
	GetChallengeByID(ctx context.Context, projectID, id string) (*domain.ClaimChallenge, error)
	// MarkChallengeCompleted flips pending -> completed; a challenge that is
	// absent, in another project, or already completed returns NoRowFoundError.
	MarkChallengeCompleted(ctx context.Context, projectID, id string) error
	// GetPersonalTeamForUser resolves the user's earliest membership as the
	// personal team and returns NoRowFoundError when that membership or its team
	// is not active. It never falls back to a later membership: a deactivated
	// personal team is not silently replaced by another team the user belongs to.
	GetPersonalTeamForUser(ctx context.Context, projectID, userID string) (*domain.Team, error)
	// GetEarliestTeamMembership returns the same earliest membership
	// GetPersonalTeamForUser resolves from, but regardless of its status and
	// without joining the team. It exists to tell apart the two states
	// GetPersonalTeamForUser deliberately collapses into NoRowFoundError:
	// a user who holds no membership at all (NoRowFoundError here too) from one
	// whose personal team is deactivated (a row with a non-active status).
	// Provisioning must create a team for the first and leave the second alone.
	GetEarliestTeamMembership(ctx context.Context, projectID, userID string) (*domain.TeamMembership, error)
}

type ClaimStatusResult

type ClaimStatusResult struct {
	Status domain.ClaimChallengeStatus
	// TeamID, ClaimedAt, and DashboardURL are set only when Status is
	// completed, reconstructed from the grant (claim state lives in the
	// permission engine, ADR 046 §1).
	TeamID       string
	ClaimedAt    time.Time
	DashboardURL string
}

type CreateAuthAttemptInput

type CreateAuthAttemptInput struct {
	ProjectID string
	// SessionID is set for step-up auth against an existing session.
	SessionID *string
	// RequiredChecks overrides the project's default policy.
	// Used by the OIDC adapter (acr_values) and flow engine.
	// If nil the project's DefaultRequiredChecks are used.
	RequiredChecks []domain.AuthCheckType
	// Internal marks a server-orchestrated ceremony (see
	// [domain.AuthAttempt.Internal]). Never settable through the REST surface.
	Internal bool
}

type CreateBrandingInput

type CreateBrandingInput struct {
	ProjectID      string
	Layout         string
	LiquidTemplate string
	LogoURL        string
	FontURL        string
	HeroURL        string
}

type CreateGrantInput

type CreateGrantInput struct {
	ProjectID     string
	PrincipalType domain.AuthzPrincipalType
	PrincipalID   string
	Relation      string
	ExpiresAt     *time.Time
}

type CreateSchemaByURLInput

type CreateSchemaByURLInput struct {
	ProjectID string
	TeamID    string
	URL       url.URL
}

type CreateSchemaInput

type CreateSchemaInput struct {
	ProjectID string
	TeamID    string
	Schema    []byte
}

type CreateSessionInput

type CreateSessionInput struct {
	ProjectID string
	UserAgent *domain.UserAgent
}

type CreateTeamInput

type CreateTeamInput struct {
	ProjectID string
	Name      string
}

type CreateUserAction

type CreateUserAction struct {
	CreateUserInput

	CreateUser *domain.CreateUser
	// contains filtered or unexported fields
}

func NewCreateUserAction

func NewCreateUserAction(input CreateUserInput, schemaStore domain.JSONSchemaStore) *CreateUserAction

func (*CreateUserAction) Apply

func (o *CreateUserAction) Apply(ctx context.Context, stmts AllStatements) error

func (*CreateUserAction) Prepare

func (o *CreateUserAction) Prepare(ctx context.Context) error

type CreateUserInput

type CreateUserInput struct {
	ProjectID string
	TeamID    *string
	// SchemaURL names the schema Attributes is validated against.
	SchemaURL string
	// Attributes is the schema-defined content only, without envelope fields.
	Attributes map[string]any
	ID         string
}

type CryptoKeyStatements

type CryptoKeyStatements interface {
	Statements
	GetEncryptionKey(ctx context.Context, filter database.Filter[domain.EncryptionKeyField]) (*domain.EncryptionKey, error)
	ListEncryptionKeys(ctx context.Context, opts *database.ListOptions[domain.EncryptionKeyField]) (*database.ListResult[*domain.EncryptionKey], error)
	CreateEncryptionKey(ctx context.Context, key *domain.EncryptionKey) error
	UpdateKey(ctx context.Context, id string, key string) error
	GetSigningKey(ctx context.Context, filter database.Filter[domain.SigningKeyField]) (*domain.SigningKey, error)
	CreateSigningKey(ctx context.Context, key *domain.SigningKey) error
}

type DB

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

func NewPool

func NewPool(pool Pool) *DB

func (*DB) Statements

func (p *DB) Statements() AllStatements

Statements implements Pool.

func (*DB) Transaction

func (p *DB) Transaction(ctx context.Context, callback func(ctx context.Context, tx Statementer[AllStatements]) error) error

type DeleteSessionInput

type DeleteSessionInput struct {
	ProjectID string
	SessionID string
}

type DeleteUserAction

type DeleteUserAction struct {
	DeleteUserInput
}

func NewDeleteUserAction

func NewDeleteUserAction(input DeleteUserInput) *DeleteUserAction

func (*DeleteUserAction) Apply

func (o *DeleteUserAction) Apply(ctx context.Context, stmts AllStatements) error

func (*DeleteUserAction) Prepare

func (o *DeleteUserAction) Prepare(_ context.Context) error

type DeleteUserInput

type DeleteUserInput struct {
	ProjectID string
	UserID    string
}

type EnvironmentService

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

func NewEnvironmentService

func NewEnvironmentService(v2Pool *DB) *EnvironmentService

func (*EnvironmentService) GetByName

func (s *EnvironmentService) GetByName(ctx context.Context, projectID, name string) (*domain.Environment, error)

func (*EnvironmentService) List

func (*EnvironmentService) SeedDefaults

func (s *EnvironmentService) SeedDefaults(ctx context.Context, stmts AllStatements, projectID string) error

SeedDefaults creates domain.DefaultEnvironmentNames for the project, in order, on the statements of an already open transaction.

It takes stmts rather than opening its own transaction because project creation seeds environments inside the transaction that creates the project: a project that committed without its runtime slots would be a project nothing can ever be deployed to, and no later code path would repair it.

type EnvironmentStatements

type EnvironmentStatements interface {
	Statements
	CreateEnvironment(ctx context.Context, entity *domain.Environment) error
	GetEnvironmentByName(ctx context.Context, projectID, name string) (*domain.Environment, error)
	ListEnvironments(ctx context.Context, filter *database.ListOptions[domain.EnvironmentField]) (*database.ListResult[*domain.Environment], error)
}

type EventExportAdapter

type EventExportAdapter struct {
	Pool *DB
}

EventExportAdapter adapts AllStatements to audit retention/shipper interfaces.

func (EventExportAdapter) DeleteEventsOlderThan

func (a EventExportAdapter) DeleteEventsOlderThan(ctx context.Context, createdBefore time.Time) (int64, error)

func (EventExportAdapter) EnsureSink

func (a EventExportAdapter) EnsureSink(ctx context.Context, sink *domain.EventSink) error

func (EventExportAdapter) GetEventSinkCursor

func (a EventExportAdapter) GetEventSinkCursor(ctx context.Context, sinkID, projectID string) (*domain.EventSinkCursor, error)

func (EventExportAdapter) InsertEvents

func (a EventExportAdapter) InsertEvents(ctx context.Context, events []*domain.Event) error

InsertEvents persists a Path A batch in one transaction.

func (EventExportAdapter) ListClaimedProjectIDs

func (a EventExportAdapter) ListClaimedProjectIDs(ctx context.Context) ([]string, error)

ListClaimedProjectIDs returns project IDs that have completed claim (active owning-team grant, ADR 054 §2), matching ADR 049 export visibility.

func (EventExportAdapter) ListEventsAfterCursor

func (a EventExportAdapter) ListEventsAfterCursor(ctx context.Context, projectID string, afterCreatedAt time.Time, afterID string, limit int) ([]*domain.Event, error)

func (EventExportAdapter) ListProjectIDs

func (a EventExportAdapter) ListProjectIDs(ctx context.Context) ([]string, error)

func (EventExportAdapter) UpsertEventSinkCursor

func (a EventExportAdapter) UpsertEventSinkCursor(ctx context.Context, cursor *domain.EventSinkCursor) error

type EventService

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

EventService lists and loads audit events (ADR 049).

func NewEventService

func NewEventService(v2Pool *DB) *EventService

func (*EventService) Get

func (s *EventService) Get(ctx context.Context, projectID, id string) (*domain.Event, error)

Get loads one event by (project_id, id). Pre-claim projects and misses return not found (no existence leak).

func (*EventService) List

List returns events for a claimed project with optional filters and keyset pagination on (created_at, id). Pre-claim projects yield an empty page.

type EventStatements

type EventStatements interface {
	Statements
	InsertEvent(ctx context.Context, event *domain.Event) error
	GetEventByID(ctx context.Context, projectID, id string) (*domain.Event, error)
	ListEvents(ctx context.Context, filter *database.ListOptions[domain.EventField]) (*database.ListResult[*domain.Event], error)
	// DeleteEventsOlderThan removes events with created_at before cutoff across
	// all projects (retention job; includes orphaned project_id rows).
	DeleteEventsOlderThan(ctx context.Context, createdBefore time.Time) (int64, error)
	EnsureEventSink(ctx context.Context, sink *domain.EventSink) error
	// GetEventSinkCursor returns (nil, nil) when no cursor row exists.
	GetEventSinkCursor(ctx context.Context, sinkID, projectID string) (*domain.EventSinkCursor, error)
	UpsertEventSinkCursor(ctx context.Context, cursor *domain.EventSinkCursor) error
	ListEventsAfterCursor(ctx context.Context, projectID string, afterCreatedAt time.Time, afterID string, limit uint32) ([]*domain.Event, error)
}

EventStatements persists append-only wide events (ADR 048).

type ExchangeInput

type ExchangeInput struct {
	ProjectID      string
	HandoffToken   string
	IdempotencyKey *string
	// TTL is optional; nil uses the configured default.
	TTL *time.Duration
}

type Filter

type Filter struct {
	Field     string
	Operation string
	Value     any
}

Filter is a single field/operation/value predicate from a list request.

type FinishPasskeyEnrollmentInput

type FinishPasskeyEnrollmentInput struct {
	ProjectID      string
	RegistrationID string
	// UserID must match the user the ceremony was begun for.
	UserID      string
	Attestation []byte
	// Name optionally labels the credential; empty derives a name.
	Name string
}

type FinishPasskeyEnrollmentOutput

type FinishPasskeyEnrollmentOutput struct {
	PasskeyID string
	Name      string
	CreatedAt time.Time
}

type FlowAuthAttemptAdapter

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

FlowAuthAttemptAdapter adapts the broad AuthAttemptService surface to the narrow domain.FlowAuthAttemptService the flow state machine consumes. Each Submit* collapses the underlying issue-then-verify cycle so the state machine never deals with challenge IDs.

func NewFlowAuthAttemptAdapter

func NewFlowAuthAttemptAdapter(attempts AuthAttemptService, schemaStore domain.JSONSchemaStore) *FlowAuthAttemptAdapter

func (*FlowAuthAttemptAdapter) Handoff

func (*FlowAuthAttemptAdapter) IssuePasskeyChallenge

IssuePasskeyChallenge issues a WebAuthn assertion challenge and returns its id plus the PublicKeyCredentialRequestOptions the browser hands to navigator.credentials.get(). Unlike the other Submit* methods it does not verify — the assertion arrives on a later submit (see [SubmitPasskey]).

func (*FlowAuthAttemptAdapter) IssuePasskeyRegistrationChallenge

IssuePasskeyRegistrationChallenge issues a WebAuthn registration challenge on the attempt and returns its id, the (possibly minted) user handle, and the PublicKeyCredentialCreationOptions the browser hands to navigator.credentials.create().

func (*FlowAuthAttemptAdapter) Start

func (*FlowAuthAttemptAdapter) SubmitIdentifier

func (*FlowAuthAttemptAdapter) SubmitPasskey

SubmitPasskey verifies the assertion against the challenge identified by ChallengeID and returns the authenticated user id (resolved from the assertion for a discoverable login, or the already-pinned user otherwise).

func (*FlowAuthAttemptAdapter) SubmitPasskeyRegistration

SubmitPasskeyRegistration verifies the attestation against the challenge identified by ChallengeID. The create-user factory rides along; the attempt service invokes it with the persisted challenge's authoritative user handle, and only when that challenge is provisional.

func (*FlowAuthAttemptAdapter) SubmitPassword

type FlowCreateUserWithPasswordHandler

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

FlowCreateUserWithPasswordHandler implements the `create_user` on_success: persist a new user from validated identifier + password fields.

func NewFlowCreateUserHandler

func NewFlowCreateUserHandler(
	hasher crypto.Hasher,
	userService UserService,
	schemaStore domain.JSONSchemaStore,
	db StatementPool,
) *FlowCreateUserWithPasswordHandler

func (*FlowCreateUserWithPasswordHandler) Handle

type FlowDefinitionRequest

type FlowDefinitionRequest struct {
	FlowDefinitionID string
	ProjectID        string
	Name             string
	Status           string
	SchemaVersion    string // todo (grvijayan): currently empty as the request does not contain schema version
	FlowSchemaURI    string // todo (grvijayan): schema_version (semver) stored in the db vs schema_uri needed for validation
	UserSchema       string
	Purposes         map[string]string
	Audience         domain.FlowDefinitionAudience
	Steps            []domain.FlowDefinitionStep
}

type FlowDefinitionService

type FlowDefinitionService interface {
	Create(ctx context.Context, req FlowDefinitionRequest) (*domain.FlowDefinition, error)
	Update(ctx context.Context, req FlowDefinitionRequest) (*domain.FlowDefinition, error)
	Get(ctx context.Context, projectID, id string) (*domain.FlowDefinition, error)
	List(ctx context.Context, req ListFlowDefinitionsRequest) (*ListFlowDefinitionsResponse, error)
	Delete(ctx context.Context, projectID string, id string) error
}

func NewFlowDefinitionService

func NewFlowDefinitionService(
	v2Pool *DB,
	schemaGetter SchemaGetter,
	schemaProvider BuiltinSchemaProvider,
	flowDefinitionValidatorFn flowDefinitionValidatorFunc,
) FlowDefinitionService

type FlowDefinitionStatements

type FlowDefinitionStatements interface {
	Statements
	CreateFlowDefinition(ctx context.Context, entity *domain.FlowDefinition) error
	GetFlowDefinitionByID(ctx context.Context, projectID, id string) (*domain.FlowDefinition, error)
	UpdateFlowDefinition(ctx context.Context, entity *domain.FlowDefinition) error
	ListFlowDefinitions(ctx context.Context, filter *database.ListOptions[domain.FlowDefinitionField]) (*database.ListResult[*domain.FlowDefinition], error)
	DeleteFlowDefinitionByID(ctx context.Context, projectID, id string) error
}

type FlowService

type FlowService interface {
	// Resolve returns the [domain.FlowDefinition] to run. Direct lookup
	// when Name is set; audience match otherwise.
	Resolve(ctx context.Context, req ResolveFlowRequest) (*domain.FlowDefinition, error)
	// Start mints a fresh flow on the resolved definition.
	Start(ctx context.Context, req StartFlowRequest) (domain.FlowStepResult, error)
	// Submit advances the state machine. Re-fetches the definition
	// from FlowState.DefinitionID.
	Submit(ctx context.Context, req SubmitFlowRequest) (domain.FlowStepResult, error)
	// GetStep re-emits the current step without advancing.
	GetStep(ctx context.Context, req GetFlowStepRequest) (domain.FlowStepResult, error)
}

FlowService is the flow engine's use-case surface. The API handler depends only on this interface.

func NewFlowService

func NewFlowService(
	v2Pool *DB,
	stateMachine domain.FlowStateMachine,
) FlowService

type GetFlowStepRequest

type GetFlowStepRequest struct {
	State *domain.FlowState
}

type GetMyUserInput

type GetMyUserInput struct {
	// SessionToken is the parsed session token, already verified at the API
	// security boundary.
	SessionToken *domain.Token
}

type GetSessionInput

type GetSessionInput struct {
	ProjectID string
	SessionID string
	// WithUserIdentity resolves the linked user's reference (ADR 058 §3)
	// onto Session.User. Set by reads that render the signed-in identity.
	WithUserIdentity bool
}

type GetUserInput

type GetUserInput struct {
	ProjectID string
	TeamID    *string
	UserID    string
}

type GrantService

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

func NewGrantService

func NewGrantService(v2Pool *DB, platformProjectID string) *GrantService

func (*GrantService) Create

func (*GrantService) Get

func (s *GrantService) Get(ctx context.Context, projectID, id string) (*domain.AuthzAssignment, error)

func (*GrantService) Revoke

func (s *GrantService) Revoke(ctx context.Context, projectID, id string) error

type HandoffInput

type HandoffInput struct {
	ProjectID      string
	AttemptID      string
	IdempotencyKey *string
}

type IssueChallengeInput

type IssueChallengeInput struct {
	ProjectID string
	AttemptID string
	// Challenge is a discriminated union — use one of the Challenge* types below.
	Challenge Challenge
}

type JSONSchemaQueryOptions

type JSONSchemaQueryOptions struct {
	// LatestRevisionPerObjectType keeps only the newest revision of each
	// object_type. Rows without an object_type are revisions of nothing and
	// pass through untouched.
	LatestRevisionPerObjectType bool
}

JSONSchemaQueryOptions carries query modes for ListJSONSchemas that are not column predicates. Column predicates stay in Filter / ListOptions.

type JSONSchemaStatements

type JSONSchemaStatements interface {
	Statements
	CreateJSONSchema(ctx context.Context, entity *domain.JSONSchema) error
	GetJSONSchemaByID(ctx context.Context, projectID, schemaID string) (*domain.JSONSchema, error)
	ListJSONSchemas(ctx context.Context, filter *database.ListOptions[domain.JSONSchemaField], opts JSONSchemaQueryOptions) (*database.ListResult[*domain.JSONSchema], error)
	DeleteJSONSchemaByID(ctx context.Context, projectID, schemaID string) error
}

type KeyService

type KeyService interface {
	GetEncryptionKey(ctx context.Context, keyID string, algorithm jose.ContentEncryption) (*domain.EncryptionKey, error)
	GetCrypter(ctx context.Context, keyID string, algorithm jose.ContentEncryption) (op.Crypto, error)
	GetProjectEncryptionKey(ctx context.Context, projectID string, purpose domain.EncryptionKeyPurpose) (*domain.EncryptionKey, error)
	GetProjectCrypter(ctx context.Context, projectID string, purpose domain.EncryptionKeyPurpose) (op.Crypto, error)
	GetProjectSigningKey(ctx context.Context, projectID string, purpose domain.SigningKeyPurpose) (*domain.SigningKey, error)
	GetProjectSigner(ctx context.Context, projectID string, purpose domain.SigningKeyPurpose) (jose.Signer, error)
	GetMasterKeyCrypter(ctx context.Context) (op.Crypto, error)
	MigrateToLatestMasterKey(ctx context.Context) error
}

func NewKeyService

func NewKeyService(
	db *DB,
	masterKeys domain.MasterKeys,
) KeyService

type ListEnvironmentsInput

type ListEnvironmentsInput struct {
	ProjectID string
	PageToken string
	Limit     int
}

type ListEnvironmentsOutput

type ListEnvironmentsOutput struct {
	Items         []*domain.Environment
	NextPageToken string
}

type ListEventsRequest

type ListEventsRequest struct {
	ProjectID     string
	Categories    []string
	EventTypes    []string
	ActorID       string
	ClientID      string
	SessionID     string
	FlowID        string
	RequestID     string
	Fingerprint   string
	EntityType    string
	EntityID      string
	TeamID        string
	CreatedAfter  *time.Time
	CreatedBefore *time.Time
	Order         string
	Limit         int
	PageToken     string
}

ListEventsRequest is the input for listing project audit events (ADR 049).

type ListEventsResponse

type ListEventsResponse struct {
	Events        []*domain.Event
	NextPageToken string
}

ListEventsResponse is a cursor page of events.

type ListFlowDefinitionsRequest

type ListFlowDefinitionsRequest struct {
	ProjectID string
	Purpose   string
	Limit     int
	PageToken string
}

type ListFlowDefinitionsResponse

type ListFlowDefinitionsResponse struct {
	Items         []*domain.FlowDefinition
	NextPageToken string
}

type ListPasskeysInput

type ListPasskeysInput struct {
	ProjectID string
	UserID    string
	PageToken string
	Limit     int
}

type ListProjectsRequest

type ListProjectsRequest struct {
	// ProjectID restricts results to that single project. Handlers set it from
	// the caller's scope, and every credential today is bound to one project.
	// Required.
	ProjectID string
	Limit     int
	PageToken string
	Sorting   *Sorting // optional; defaults to createdAt asc
	Filters   []Filter
}

ListProjectsRequest is the input for listing projects.

type ListProjectsResponse

type ListProjectsResponse struct {
	Projects      []*domain.Project
	NextPageToken string
}

ListProjectsResponse is the output for listing projects.

type ListSchemasInput

type ListSchemasInput struct {
	ProjectID string
	// IDs narrows the list to the given schema ids. Empty means no id filter.
	// Not exposed on the wire: it serves in-process batch resolution, such as
	// the flow-definition expand hydrator.
	IDs        []string
	ObjectType string
	// Kind is nil when the caller did not filter by one. It is a pointer rather
	// than a zero value because JSONSchemaKindUnknown is a real stored kind, so
	// it cannot double as "no filter".
	Kind *domain.JSONSchemaKind
	// LatestRevisionPerObjectType asks for the newest revision of each object
	// type instead of the full revision history.
	LatestRevisionPerObjectType bool
	PageToken                   string
	Limit                       int
}

type ListSchemasOutput

type ListSchemasOutput struct {
	Items         []*domain.JSONSchema
	NextPageToken string
}

type ListSessionInput

type ListSessionInput struct {
	// ProjectID restricts results to that single project. Handlers set it from
	// the caller's scope. Required.
	ProjectID string
	Limit     int
	PageToken string
	Sorting   *Sorting // optional; defaults to createdAt desc (newest first)
	Filters   []Filter
}

type ListSessionsResponse

type ListSessionsResponse struct {
	Sessions      []*domain.Session
	NextPageToken string
}

ListSessionsResponse is the output for listing sessions.

type ListTeamsRequest

type ListTeamsRequest struct {
	// ProjectID restricts results to that project. Required: a team only has
	// meaning inside its project, so an unscoped team list is never valid.
	ProjectID string
	Limit     int
	PageToken string
	Sorting   *Sorting // optional; defaults to createdAt asc
	Filters   []Filter
}

ListTeamsRequest is the input for listing the teams of a project.

type ListTeamsResponse

type ListTeamsResponse struct {
	Teams         []*domain.Team
	NextPageToken string
}

ListTeamsResponse is the output for listing teams.

type ListUserTeamsInput

type ListUserTeamsInput struct {
	ProjectID string
	UserID    string
	PageToken string
	Limit     int
}

type ListUserTeamsOutput

type ListUserTeamsOutput struct {
	Items         []*domain.UserTeam
	NextPageToken string
}

type ListUsersInput

type ListUsersInput struct {
	ProjectID string
	PageToken string
	Limit     int
	Sorting   *Sorting // optional; defaults to createdAt desc
	Filters   []Filter
	// IncludeTeams embeds each user's team memberships (ADR 059).
	IncludeTeams bool
	// IncludeLifecycleOwnerTeam embeds the team that owns each user's
	// lifecycle (ADR 059).
	IncludeLifecycleOwnerTeam bool
}

type ListUsersOutput

type ListUsersOutput struct {
	Items         []*domain.User
	NextPageToken string
}

type ManagedIDGenerator

type ManagedIDGenerator interface {
	NewManagedID(prefix string) (string, error)
}

ManagedIDGenerator mints prefixed managed resource IDs.

type PasskeyChallenge

type PasskeyChallenge struct {
	UserVerification string
	RPID             string
	RPOrigins        []url.URL
}

PasskeyChallenge carries the raw WebAuthn assertion response bytes.

func (PasskeyChallenge) ChallengeCheckType

func (PasskeyChallenge) ChallengeCheckType() domain.AuthCheckType

type PasskeyProof

type PasskeyProof struct {
	AssertionResponse []byte
}

PasskeyProof carries the raw WebAuthn assertion response bytes.

type PasskeyRegistrationChallenge

type PasskeyRegistrationChallenge struct {
	UserID      string
	Username    string
	DisplayName string
	RPID        string
	RPOrigins   []url.URL
}

PasskeyRegistrationChallenge starts a WebAuthn credential-enrollment ceremony. UserID targets an existing user (must match a pinned user factor when one exists); empty UserID on an attempt without a user factor makes the ceremony provisional: a user handle is minted and the user row is created when the attestation is verified.

func (PasskeyRegistrationChallenge) ChallengeCheckType

func (PasskeyRegistrationChallenge) ChallengeCheckType() domain.AuthCheckType

type PasskeyRegistrationProof

type PasskeyRegistrationProof struct {
	AttestationResponse []byte
	// Name labels the credential in passkey management surfaces. Optional:
	// when empty, a name is derived from the credential itself
	// ([domain.GeneratePasskeyCredentialName]).
	Name string
	// CreateUser, when set, builds the actions that create the ceremony's
	// user inside the success transaction, so user creation, credential
	// persistence, and check success land atomically. The service invokes it
	// with the stored challenge's authoritative user handle — a caller cannot
	// create a row under a different id than the credential binds to. Only
	// consulted for provisional ceremonies.
	CreateUser func(userID string) []UserAction
}

PasskeyRegistrationProof carries the raw WebAuthn attestation response bytes.

type PasswordChallenge

type PasswordChallenge struct{}

PasswordChallenge carries the plaintext password for verification.

func (PasswordChallenge) ChallengeCheckType

func (PasswordChallenge) ChallengeCheckType() domain.AuthCheckType

type PasswordProof

type PasswordProof struct {
	Password string
}

PasswordProof carries the plaintext password for verification.

type PersonalTeamEnsurer

type PersonalTeamEnsurer interface {
	// EnsurePersonalTeam is a no-op outside the platform project and for users
	// that already hold an active membership. Otherwise it creates the team
	// and its membership in one transaction.
	EnsurePersonalTeam(ctx context.Context, projectID, userID string) error
}

PersonalTeamEnsurer guarantees a platform-project user a team — the team `claim/complete` attaches claimed projects to (ADR 046 §2, #527).

The requirement is only that a registered user *has* a team. The team is an ordinary team: it may later be renamed, shared, or end up carrying other members, and that is fine. "Personal team" is not a modeled concept — it resolves to the user's earliest active membership (`personalTeamForUserStmt`, shared with the claim service) — so a user who already holds any membership is already provisioned and the call is a no-op.

An idempotent *ensure* rather than a create: it runs on session exchange — the one credential-agnostic point every registration and every sign-in passes through before any authenticated call — best-effort, so users created before the effect existed converge on their next sign-in too.

func NewPersonalTeamService

func NewPersonalTeamService(
	v2Pool StatementPool,
	platformProjectID string,
) PersonalTeamEnsurer

NewPersonalTeamService wires the ensurer.

type Pool

type ProjectService

type ProjectService interface {
	// Create generates a new project with server-minted secrets and persists it.
	// If seedDefaults is true, server fallback schema and flow resources are
	// created with the project for non-CLI creation paths.
	// Returns the stored project including timestamps.
	Create(ctx context.Context, name string, previewOrigins []string, seedDefaults bool) (*domain.Project, error)

	// CreateWithID is Create under a caller-supplied id, for the one project
	// whose id the server owns rather than mints: the platform project
	// (domain.PlatformProjectID). Reports the same already-exists error as
	// Create when the id is taken.
	CreateWithID(ctx context.Context, id, name string, previewOrigins []string, seedDefaults bool) (*domain.Project, error)

	// Get retrieves a project by ID.
	// Returns [database.NoRowFoundError] when no project with the given ID exists.
	Get(ctx context.Context, id string) (*domain.Project, error)

	// DefaultProject resolves the transitional standalone default retained by
	// Console ADR 0004 §2's bootstrap cutover rule: the configured project when
	// cfgProjectID is set — which must exist; a missing configured project is a
	// configuration error — otherwise the deployment's first-created
	// project. Returns (nil, nil) while no project exists yet: the server
	// never creates the default project, the customer's integration
	// (`zitadel setup` → POST /projects) does.
	DefaultProject(ctx context.Context, cfgProjectID string) (*domain.Project, error)

	// Update updates the name of a project.
	// Returns domain.ErrProjectMissingID or domain.ErrProjectNameInvalid for validation failures.
	// Returns domain.ErrProjectNotFound when no project with the given ID exists; other failures return domain.ErrInternal.
	Update(ctx context.Context, id, name string) (*domain.Project, error)

	// List returns projects matching the request, ordered and paginated with an
	// opaque cursor token. The returned NextPageToken is empty when the last page
	// has been reached.
	// Returns domain.ErrProjectMissingID when the request carries no project.
	List(ctx context.Context, req ListProjectsRequest) (*ListProjectsResponse, error)

	// Delete hard-deletes a project, cascading to its child resources through the
	// storage delete. Deleting a project that does not exist is a no-op.
	Delete(ctx context.Context, id string) error
}

ProjectService is the project use-case surface.

func NewProjectService

func NewProjectService(
	v2Pool *DB,
	serverURL string,
	schemaValidator *domain.SchemaValidator,
	keyService KeyService,
) ProjectService

NewProjectService returns a ProjectService backed by the given repository.

type ProjectStatements

type ProjectStatements interface {
	Statements
	CreateProject(ctx context.Context, entity *domain.Project) error
	GetProjectByID(ctx context.Context, id string) (*domain.Project, error)
	UpdateProject(ctx context.Context, entity *domain.Project) error
	ListProjects(ctx context.Context, filter *database.ListOptions[domain.ProjectField]) (*database.ListResult[*domain.Project], error)
	// DeleteProjectByID removes the project. changed is false when no row matched.
	DeleteProjectByID(ctx context.Context, id string) (changed bool, err error)
}

type Proof

type Proof interface {
	// contains filtered or unexported methods
}

Proof is implemented by each check-type-specific proof value.

type ResolveFlowHint

type ResolveFlowHint struct {
	AppID        *string
	TeamID       *string
	UserSchemaID *string
}

type ResolveFlowRequest

type ResolveFlowRequest struct {
	ProjectID     string
	Purpose       domain.FlowDefinitionPurpose
	Name          *string // direct-lookup slug
	SchemaVersion *string // nil = latest active
	AuthRequestID *string
	// Hint scopes audience resolution (ignored when Name is set) — see
	// resolveByAudience for the scoring rules.
	Hint ResolveFlowHint
}

type ResourceScopeStatements

type ResourceScopeStatements interface {
	Statements
	UpsertResourceScope(ctx context.Context, scope *domain.ResourceScope) error
	GetResourceScope(ctx context.Context, resourceID string) (*domain.ResourceScope, error)
	GetResourceScopeInProject(ctx context.Context, kind domain.ResourceKind, projectID, resourceID string) (*domain.ResourceScope, error)
	GetResourceScopeByIDInProject(ctx context.Context, projectID, resourceID string) (*domain.ResourceScope, error)
	ExistsResourceScopeElsewhere(ctx context.Context, kind domain.ResourceKind, resourceID, excludeProjectID string) (bool, error)
	DeleteResourceScope(ctx context.Context, kind domain.ResourceKind, projectID, resourceID string) error
}

ResourceScopeStatements persists resource_scope_index rows (path.id → project/team).

Use cases:

  • UpsertResourceScope: dual-write on project/team/user/schema/branding/ flow_definition/session create (build via domain.New*ResourceScope).
  • GetResourceScope: tests/oracle only when resource_id is known globally unique; must not be used by the HTTP management gate (schema $id URLs are not).
  • GetResourceScopeInProject: gate lookup by kind + credential project + path.id.
  • GetResourceScopeByIDInProject: gate wrong-kind fallback (same project, any kind).
  • ExistsResourceScopeElsewhere: gate delete path — foreign presence without requiring a unique global resource_id.
  • DeleteResourceScope: explicit cleanup where FK cascade does not apply (user / schema / flow_definition / session delete today; branding relies on project cascade; project delete cascades RSI via project_id FK).

type SchemaGetter

type SchemaGetter interface {
	GetSchema(ctx context.Context, projectID string, teamID string, schemaID string) (*domain.JSONSchema, error)
}

type SchemaService

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

func NewSchemaService

func NewSchemaService(
	v2Pool *DB,
	schemaResolver *domain.JSONSchemaResolver,
	schemaValidator *domain.SchemaValidator,
) *SchemaService

func (*SchemaService) CreateSchema

func (s *SchemaService) CreateSchema(ctx context.Context, input CreateSchemaInput) (_ *domain.JSONSchema, err error)

func (*SchemaService) CreateSchemaByUrl

func (s *SchemaService) CreateSchemaByUrl(ctx context.Context, input CreateSchemaByURLInput) (*domain.JSONSchema, error)

func (*SchemaService) GetSchema

func (s *SchemaService) GetSchema(ctx context.Context, projectID string, teamID string, schemaID string) (*domain.JSONSchema, error)

func (*SchemaService) ListSchemas

func (s *SchemaService) ListSchemas(ctx context.Context, input ListSchemasInput) (*ListSchemasOutput, error)

type SessionConfig

type SessionConfig struct {
	DefaultTTL time.Duration `mapstructure:"default_ttl"`
	MaxTTL     time.Duration `mapstructure:"max_ttl"`
}

SessionConfig holds server-wide session TTL settings for exchange.

func (SessionConfig) Validate

func (c SessionConfig) Validate() error

Validate checks session TTL configuration at startup.

type SessionResolver

type SessionResolver interface {
	Get(ctx context.Context, projectID, sessionID string) (*domain.Session, error)
}

type SessionService

type SessionService interface {
	Create(ctx context.Context, input CreateSessionInput) (*domain.Session, error)
	Exchange(ctx context.Context, input ExchangeInput) (*domain.Session, error)
	// Get retrieves a session by ID within a project. When the input sets
	// WithUserIdentity, the linked user's resolved reference (ADR 058 §3) is
	// hydrated onto Session.User (absent users degrade to a session without
	// a ref). errors: domain.ErrSessionNotFound, domain.ErrInternal
	Get(ctx context.Context, input GetSessionInput) (*domain.Session, error)
	// List returns the project's sessions matching the input's filters,
	// errors: domain.ErrProjectMissingID, domain.ErrRequestInvalid, domain.ErrNotImplemented, domain.ErrInternal
	List(ctx context.Context, input ListSessionInput) (*ListSessionsResponse, error)
	Delete(ctx context.Context, input DeleteSessionInput) error
}

func NewSessionService

func NewSessionService(v2Pool StatementPool, refs UserRefResolver, cfg SessionConfig) SessionService

type SessionStatements

type SessionStatements interface {
	Statements
	// CreateSession inserts user_agent (optional), session, and session token.
	// It wraps the multi-write steps in withTransaction.
	CreateSession(ctx context.Context, entity *domain.Session) error
	// ExchangeSession promotes verified auth-attempt checks onto a session
	// (create or upgrade), rotates the session token, and deletes the attempt.
	// It wraps the multi-write steps in withTransaction.
	// idempotencyKey is accepted for API compatibility and currently ignored.
	ExchangeSession(ctx context.Context, projectID, handoffToken string, idempotencyKey *string, ttl time.Duration) (*domain.Session, error)
	GetSessionByID(ctx context.Context, projectID, sessionID string) (*domain.Session, error)
	ListSessions(ctx context.Context, filter *database.ListOptions[domain.SessionField]) (*database.ListResult[*domain.Session], error)
	DeleteSessionByID(ctx context.Context, projectID, sessionID string) error
}

type SessionStatementsResolver

type SessionStatementsResolver struct {
	Pool StatementPool
}

SessionStatementsResolver adapts StatementPool to SessionResolver.

func (SessionStatementsResolver) Get

func (r SessionStatementsResolver) Get(ctx context.Context, projectID, sessionID string) (*domain.Session, error)

type SetPasswordInput

type SetPasswordInput struct {
	ProjectID                string
	UserID                   string
	Password                 string
	IsPasswordChangeRequired bool
}

type SetPasswordUserAction

type SetPasswordUserAction struct {
	SetPasswordInput
	// contains filtered or unexported fields
}

func NewSetUserPasswordAction

func NewSetUserPasswordAction(input SetPasswordInput, hasher crypto.Hasher) *SetPasswordUserAction

func (*SetPasswordUserAction) Apply

func (*SetPasswordUserAction) Prepare

func (o *SetPasswordUserAction) Prepare(_ context.Context) (err error)

type Sorting

type Sorting struct {
	Field     string
	Direction string
}

Sorting selects the field and direction a list is ordered by.

type StartFlowRequest

type StartFlowRequest struct {
	Definition    *domain.FlowDefinition
	Purpose       domain.FlowDefinitionPurpose
	RedirectURI   *string
	AuthRequestID *string
	SessionID     *string
	UserAgent     *domain.UserAgent
}

type StatementPool

type StatementPool interface {
	Statementer[AllStatements]
	Transactioner[AllStatements]
}

type Statementer

type Statementer[S any] interface {
	Statements() S
}

type Statements

type Statements interface {
	IsStatements()
}

type StatementsUserRefResolver

type StatementsUserRefResolver struct {
	Pool StatementPool
}

StatementsUserRefResolver resolves refs against the statement surface.

func (StatementsUserRefResolver) ResolveRefsForUsers

func (r StatementsUserRefResolver) ResolveRefsForUsers(ctx context.Context, projectID string, users []*domain.User) (map[string]domain.UserRef, error)

ResolveRefsForUsers maps already-listed users through their own schema's designations (ADR 058 §3a). The callers' read paths hydrate the full attribute document, a superset of the designated keys, so resolution needs only the schema documents — one schema-list query per page, zero additional user queries (§4: list endpoints must not resolve per row). Users whose schema URL has no stored document resolve to a bare id ref.

func (StatementsUserRefResolver) ResolveUserRefs

func (r StatementsUserRefResolver) ResolveUserRefs(ctx context.Context, projectID string, userIDs []string) (map[string]domain.UserRef, error)

ResolveUserRefs loads the project's user-schema documents (every stored revision — users pin the schema URL they were created under, so each revision's own designations govern its users), lists the requested users in one call hydrating only the designated attribute keys, and maps each user through its schema's designations.

type SubmitFlowRequest

type SubmitFlowRequest struct {
	State         *domain.FlowState
	Action        string
	Fields        map[string]any
	GateProofs    map[string]string
	SSOProviderID *string
	// ChallengeResponse carries the client's answer to a pending ceremony
	// (e.g. a passkey assertion). Nil unless the step issued a challenge.
	ChallengeResponse *domain.FlowChallengeResponse
	// PasskeyRP carries the WebAuthn relying-party params derived from the
	// request, needed when issuing a passkey challenge.
	PasskeyRP *domain.FlowPasskeyRP
}

type TeamMembershipStatements

type TeamMembershipStatements interface {
	Statements
	CreateTeamMembership(ctx context.Context, membership *domain.TeamMembership) error
	GetTeamMembership(ctx context.Context, projectID, teamID, userID string) (*domain.TeamMembership, error)
	ListTeamMemberships(ctx context.Context, filter *database.ListOptions[domain.TeamMembershipField]) (*database.ListResult[*domain.TeamMembership], error)
	ListUserTeams(ctx context.Context, filter *database.ListOptions[domain.UserTeamField]) (*database.ListResult[*domain.UserTeam], error)
	UpdateTeamMembershipStatus(ctx context.Context, projectID, teamID, userID string, status domain.MembershipStatus) error
}

type TeamService

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

func NewTeamService

func NewTeamService(v2Pool *DB) *TeamService

func (*TeamService) Create

func (s *TeamService) Create(ctx context.Context, input CreateTeamInput) (*domain.Team, error)

func (*TeamService) Delete

func (s *TeamService) Delete(ctx context.Context, projectID, teamID string) error

Delete deactivates the team and cascades to its memberships and the users whose lifecycle it owns (ADR 024). The team is tombstoned, not erased, so it stays readable through TeamService.Get with status deactivated.

Delete is idempotent: DeactivateTeam only touches an active team, so an unknown or already-deactivated team reports success without persisting any changes.

func (*TeamService) Get

func (s *TeamService) Get(ctx context.Context, projectID string, teamID string) (*domain.Team, error)

func (*TeamService) List

List returns the teams of a project, ordered and paginated with an opaque cursor token. The returned NextPageToken is empty when the last page has been reached.

func (*TeamService) Update

func (s *TeamService) Update(ctx context.Context, input UpdateTeamInput) (*domain.Team, error)

type TeamStatements

type TeamStatements interface {
	Statements
	CreateTeam(ctx context.Context, entity *domain.Team) error
	GetTeamByID(ctx context.Context, projectID, id string) (*domain.Team, error)
	GetTeam(ctx context.Context, filter database.Filter[domain.TeamField]) (*domain.Team, error)
	UpdateTeam(ctx context.Context, entity *domain.Team) error
	ListTeams(ctx context.Context, filter *database.ListOptions[domain.TeamField]) (*database.ListResult[*domain.Team], error)
	// DeactivateTeam tombs the team and cascades membership/user lifecycle
	// updates. It wraps the multi-write steps in withTransaction (opens a tx
	// via Statements(), joins an outer pool.Transaction when already nested).
	//
	// Only an active team is deactivated: the team UPDATE is guarded on the status,
	// and a zero-row result skips the cascade and reports success with changed=false.
	// An unknown or already-deactivated team is therefore a no-op;
	// updated_at records when the team was first deactivated.
	DeactivateTeam(ctx context.Context, projectID, id string) (changed bool, err error)
}

type TokenService

type TokenService interface {
	GenerateJWE(ctx context.Context, data *domain.Token) (string, error)
	RevokeToken(ctx context.Context, projectID, tokenID string) error
	IntrospectToken(ctx context.Context, token string) (*domain.Token, error)
	GetActivePreviewToken(ctx context.Context, projectID string) (*domain.Token, error)
}

func NewTokenService

func NewTokenService(
	keys KeyService,
	v2Pool StatementPool,
) TokenService

type TokenStatements

type TokenStatements interface {
	Statements
	CreateToken(ctx context.Context, entity *domain.Token) error
	GetTokenByID(ctx context.Context, projectID, tokenID string) (*domain.Token, error)
	ListTokens(ctx context.Context, filter *database.ListOptions[domain.TokenField]) (*database.ListResult[*domain.Token], error)
	DeleteTokenByID(ctx context.Context, projectID, tokenID string) error
	DeleteTokensBySessionID(ctx context.Context, projectID, sessionID string) error
}

type Transactioner

type Transactioner[S any] interface {
	Transaction(ctx context.Context, fn func(ctx context.Context, tx Statementer[S]) error) error
}

type UpdateTeamInput

type UpdateTeamInput struct {
	ProjectID string
	TeamID    string
	Name      *string
}

type UserAction

type UserAction interface {
	Prepare(ctx context.Context) error
	Apply(ctx context.Context, stmts AllStatements) error
}

type UserChallenge

type UserChallenge struct{}

UserChallenge identifies the user by login name (email, username, phone).

func (UserChallenge) ChallengeCheckType

func (UserChallenge) ChallengeCheckType() domain.AuthCheckType

type UserLookup

type UserLookup interface {
	GetByAttributes(ctx context.Context, projectID string, attrs []domain.Attribute) (*domain.User, error)
}

type UserPasskeyStatements

type UserPasskeyStatements interface {
	Statements
	CreateUserPasskey(ctx context.Context, passkey *domain.CreateUserPasskey) error
	GetUserPasskey(ctx context.Context, filter database.Filter[domain.UserPasskeyField]) (*domain.UserPasskey, error)
	ListUserPasskeys(ctx context.Context, filter *database.ListOptions[domain.UserPasskeyField]) (*database.ListResult[*domain.UserPasskey], error)
	UpdateUserPasskey(ctx context.Context, filter database.Filter[domain.UserPasskeyField], updates ...domain.UserPasskeyUpdate) error
	DeleteUserPasskey(ctx context.Context, filter database.Filter[domain.UserPasskeyField]) error
}

type UserPasswordStatements

type UserPasswordStatements interface {
	Statements
	SetUserPassword(ctx context.Context, pw *domain.SetUserPassword) error
	GetUserPassword(ctx context.Context, filter database.Filter[domain.UserPasswordField]) (*domain.UserPassword, error)
	ListUserPasswords(ctx context.Context, filter *database.ListOptions[domain.UserPasswordField]) (*database.ListResult[*domain.UserPassword], error)
	UpdateUserPassword(ctx context.Context, filter database.Filter[domain.UserPasswordField], updates ...domain.UserPasswordUpdate) error
	DeleteUserPassword(ctx context.Context, filter database.Filter[domain.UserPasswordField]) error
}

type UserProof

type UserProof struct {
	AttributeName string
	LoginName     string
}

UserProof identifies the user by login name (email, username, phone).

type UserQueryOptions

type UserQueryOptions struct {
	// AttributeKeys limits hydrated EAV keys; empty means all attributes.
	AttributeKeys []string
	// Attributes, when non-empty, restricts to users matching all key/value pairs.
	Attributes []domain.Attribute
	// UniqueAttributesOnly restricts Attributes matching to values recorded
	// in the unique-attributes registry. Identifier lookups set it so an
	// equal value in a non-unique property of another user (for example a
	// notification address) cannot make the lookup ambiguous.
	UniqueAttributesOnly bool
	// MembershipTeamID, when set, requires an active team membership.
	MembershipTeamID *string
	// IncludeTeams hydrates each user's team memberships (ADR 059). The list is loaded
	// by a batched second query keyed on the page's ids, never by joining it
	// into the user query: a join multiplies rows, which would make LIMIT
	// count memberships instead of users and corrupt the keyset cursor.
	IncludeTeams bool
	// TeamsLimit caps how many membership entries each user carries when
	// IncludeTeams is set. Zero means [DefaultUserTeamsLimit].
	TeamsLimit int
	// IncludeLifecycleOwnerTeam hydrates the team that owns each user's
	// lifecycle (ADR 059). Like IncludeTeams it is a batched second query,
	// keyed on the distinct owner ids the page returned; self-owned users
	// contribute no id and stay nil.
	IncludeLifecycleOwnerTeam bool
}

UserQueryOptions carries EAV match/hydrate options for GetUser / ListUsers. Column predicates stay in Filter / ListOptions.

type UserRecoveryCodesStatements

type UserRecoveryCodesStatements interface {
	Statements
	CreateUserRecoveryCodes(ctx context.Context, codes *domain.CreateRecoveryCodes) error
	GetUserRecoveryCodes(ctx context.Context, filter database.Filter[domain.UserRecoveryCodesField]) (*domain.UserRecoveryCodes, error)
	ListUserRecoveryCodes(ctx context.Context, filter *database.ListOptions[domain.UserRecoveryCodesField]) (*database.ListResult[*domain.UserRecoveryCodes], error)
	UpdateUserRecoveryCodes(ctx context.Context, filter database.Filter[domain.UserRecoveryCodesField], updates ...domain.UserRecoveryCodesUpdate) error
	DeleteUserRecoveryCodes(ctx context.Context, filter database.Filter[domain.UserRecoveryCodesField]) error
}

type UserRefResolver

type UserRefResolver interface {
	ResolveUserRefs(ctx context.Context, projectID string, userIDs []string) (map[string]domain.UserRef, error)
	// ResolveRefsForUsers resolves refs for users the caller already holds
	// fully hydrated (ADR 058 §3a) — one schema-list query, no user query.
	ResolveRefsForUsers(ctx context.Context, projectID string, users []*domain.User) (map[string]domain.UserRef, error)
}

UserRefResolver is the batch resolution port of ADR 058 §4: it hydrates user references for a page of user IDs with one batched attribute query — list endpoints must not resolve per row. Missing users are simply absent from the result; their refs degrade to the user id at the caller.

type UserService

type UserService interface {
	ApplyActions(ctx context.Context, actions ...UserAction) (err error)
	CreateUser(ctx context.Context, input CreateUserInput) (*domain.User, error)
	DeleteUser(ctx context.Context, input DeleteUserInput) error
	ListUsers(ctx context.Context, input ListUsersInput) (*ListUsersOutput, error)
	ListPasskeys(ctx context.Context, input ListPasskeysInput) (passkeys []*domain.UserPasskey, nextPage string, err error)
	ListUserTeams(ctx context.Context, input ListUserTeamsInput) (*ListUserTeamsOutput, error)
	GetUserByID(ctx context.Context, input GetUserInput) (*domain.User, error)
	SetPassword(ctx context.Context, input SetPasswordInput) (err error)
	GetMyUser(ctx context.Context, input GetMyUserInput) (*domain.User, error)
}

func NewUserService

func NewUserService(
	v2Pool StatementPool,
	schemaStore domain.JSONSchemaStore,
	hasher crypto.Hasher,
	refs UserRefResolver,
) UserService

NewUserService returns the interface rather than *userService, deliberately against "accept interfaces, return structs". The OpenAPI error generator reflects the API handler's fields to find which services an operation calls, and only an interface-typed field gives it something to key on — a handler holding *userService drops this service out of the reflected floor that backstops the call-graph walk.

type UserStatements

type UserStatements interface {
	Statements
	CreateUser(ctx context.Context, user *domain.CreateUser) error
	GetUser(ctx context.Context, filter database.Filter[domain.UserField], opts UserQueryOptions) (*domain.User, error)
	ListUsers(ctx context.Context, filter *database.ListOptions[domain.UserField], opts UserQueryOptions) (*database.ListResult[*domain.User], error)
	DeactivateUser(ctx context.Context, projectID, userID string) error
	DeleteUserByID(ctx context.Context, projectID, userID string) error
	UserExists(ctx context.Context, projectID, userID string) (bool, error)
}

type UserStatementsLookup

type UserStatementsLookup struct {
	Pool StatementPool
}

UserStatementsLookup adapts UserStatements to UserLookup for AuthAttemptService.

func (UserStatementsLookup) GetByAttributes

func (l UserStatementsLookup) GetByAttributes(ctx context.Context, projectID string, attrs []domain.Attribute) (*domain.User, error)

GetByAttributes resolves a login identifier, so it matches only uniquely registered values: an equal value in a non-unique property of another user must not make the lookup ambiguous and reject the sign-in.

type UserTOTPStatements

type UserTOTPStatements interface {
	Statements
	CreateUserTOTP(ctx context.Context, totp *domain.CreateUserTOTP) error
	GetUserTOTP(ctx context.Context, filter database.Filter[domain.UserTOTPField]) (*domain.UserTOTP, error)
	ListUserTOTPs(ctx context.Context, filter *database.ListOptions[domain.UserTOTPField]) (*database.ListResult[*domain.UserTOTP], error)
	UpdateUserTOTP(ctx context.Context, filter database.Filter[domain.UserTOTPField], updates ...domain.UserTOTPUpdate) error
	DeleteUserTOTP(ctx context.Context, filter database.Filter[domain.UserTOTPField]) error
}

type VerifyProofInput

type VerifyProofInput struct {
	ProjectID string
	AttemptID string
	// ChallengeID must match the ID returned by IssueChallenge.
	// Prevents use of stale proofs after a challenge is re-issued.
	ChallengeID string
	// Proof is a discriminated union — use one of the Proof* types below.
	Proof Proof
}

Directories

Path Synopsis
Package mocks is a generated GoMock package.
Package mocks is a generated GoMock package.

Jump to

Keyboard shortcuts

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