identity

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package identity converges the CONTENT of one Zitadel instance to a declared Spec: the project, its roles, the OIDC applications (web, SPA, native and the native-sign-in broker app), machine users and their keys, the login / password-complexity / lockout policies, SMTP, declared role members and (dev and preview only) seed users.

It is the one library behind both halves of managed identity. `forge env up` runs it against the dev instance with a bootstrap PAT; the control plane runs it against each customer instance with that instance's owner machine key. The same code path in both is what makes dev parity real.

What it deliberately is not

It does not create or delete INSTANCES (that is the System API, the control plane's job) and it speaks plain JSON over HTTP. It imports no Zitadel Go package: Zitadel is AGPL-3.0 and this library must stay on the permissive side of that boundary. Stdlib plus forge packages only.

Idempotent by construction

Every step reads first, creates only what is missing and repairs drift only where that is safe. A second run against a converged instance performs no write at all (Report.Changed is false), which is the property the reconciler's periodic sweep depends on. Objects are matched by their natural key — project name, role key, application name, username, email — never by an internal id, because the ids are generated and a declaration cannot name them.

Adoption

An instance that already holds content (one created by hand) is converged in place. The natural-key match above is what keeps its objects: nothing is recreated because its internal id is not the one a fresh instance would have produced. Where another system holds an id (the project id is the token audience; the client id is baked into a deployed server's config), pin it with ProjectSpec.AdoptID / Application.AdoptClientID: a pin turns "the match diverged from what is deployed" into an error instead of a silent outage, and finds the object under another name (a console rename) and renames it back. Options.Adopt additionally forbids creating the project or an application at all. Options.DryRun reports the writes a pass would make, so an adoption can be reviewed before it runs.

Seams

KeyStore is where credentials the library GENERATES live — machine keys, the broker's access token, client secrets — and is also its memory: a key is kept only while the store still holds it. Outputs is the write-only publication of what consumers need (issuer, audience, client ids, the JWKS snapshot, the broker token). Dev wires directories and files; the control plane wires OpenBao and its delivery path.

Index

Constants

View Source
const RolesScope = "urn:zitadel:iam:org:projects:roles"

RolesScope is the reserved scope that asks for project roles to be asserted into a token. It is one of THREE things a roles claim needs in an ACCESS token: the project's projectRoleAssertion, this scope in the authorize request, and accessTokenRoleAssertion on the application. Omit any one and the token is well-formed, passes signature, issuer and audience checks, and carries no roles — so every operator reads as unprivileged.

View Source
const SignInCodeLifetime = time.Hour

SignInCodeLifetime is how long a minted sign-in code stays redeemable: Zitadel's default PasswordVerificationCode expiry (cmd/defaults.yaml, SecretGenerators.PasswordVerificationCode.Expiry). An instance configured otherwise makes SignInCode.ExpiresAt an estimate.

Variables

This section is empty.

Functions

func AudienceScope

func AudienceScope(projectID string) string

AudienceScope is the reserved scope that puts the project id into a token's `aud`. Zitadel has no arbitrary "API resource indicator": the audience IS the project's generated id, so a client must request this scope or its token names its own client id instead of the API.

func BrokerTokenEntry

func BrokerTokenEntry(username string) string

BrokerTokenEntry is the KeyStore name of a broker account's token record.

func ClientSecretEntry

func ClientSecretEntry(app string) string

ClientSecretEntry is the KeyStore name of a web application's client secret.

func MachineKeyEntry

func MachineKeyEntry(username string) string

MachineKeyEntry is the KeyStore name of a machine user's key document.

func ProjectRolesClaim

func ProjectRolesClaim(projectID string) string

ProjectRolesClaim is the claim the project's roles are asserted under. Zitadel does not mint a flat `roles` list: the claim is namespaced by project id and its value is a MAP of role key to {org id: org domain}.

func ValidMachineUsername

func ValidMachineUsername(name string) error

ValidMachineUsername reports why name cannot be a machine user's username. It is deliberately narrower than Zitadel: the name becomes part of a secret name, must be a DNS-label fragment, and starting with a letter keeps it from ever equalling one of Zitadel's numeric ids.

Types

type APIError

type APIError struct {
	Method  string
	Path    string
	Status  int
	Code    int
	ID      string
	Message string
	Body    string
}

APIError is a non-2xx answer from the instance. Message is Zitadel's own text and ID its stable error id (COMMAND-1m88i): branch on the id, never on the localized message.

func (*APIError) Error

func (e *APIError) Error() string

type Action

type Action string

Action is what a pass did (or, in a dry run, would do) to an object.

const (
	Created Action = "created"
	Updated Action = "updated"
	Granted Action = "granted"
	Revoked Action = "revoked"
	Issued  Action = "issued"
	Pruned  Action = "pruned"
)

type AppKind

type AppKind string

AppKind selects the OIDC application shape.

const (
	// AppSPA is a browser single-page app: a public client using PKCE.
	AppSPA AppKind = "spa"
	// AppWeb is a confidential server-side web app with a client secret,
	// which is generated once and held in the KeyStore.
	AppWeb AppKind = "web"
	// AppNative is a mobile or desktop app: a public client using PKCE.
	AppNative AppKind = "native"
	// AppNativeBroker is the app behind native sign-in: the product's own
	// login form, with a server-side broker that drives the OIDC flow. It is a
	// public PKCE client whose login pages are LoginURI, plus a broker machine
	// account holding IAM_LOGIN_CLIENT and an expiring access token.
	AppNativeBroker AppKind = "native_broker"
)

type AppOutput

type AppOutput struct {
	AppID          string   `json:"app_id"`
	ClientID       string   `json:"client_id"`
	RedirectURIs   []string `json:"redirect_uris,omitempty"`
	PostLogoutURIs []string `json:"post_logout_uris,omitempty"`
	LoginURI       string   `json:"login_uri,omitempty"`
	// ClientSecret is set for a web app whose secret is in the KeyStore.
	ClientSecret string `json:"-"`
	// BrokerUser / BrokerToken / BrokerTokenExpires describe an
	// AppNativeBroker's account and its CURRENT token. The token is the
	// idp_broker_token the product's server runs with.
	BrokerUser         string    `json:"broker_user,omitempty"`
	BrokerToken        string    `json:"-"`
	BrokerTokenID      string    `json:"broker_token_id,omitempty"`
	BrokerTokenExpires time.Time `json:"broker_token_expires,omitempty"`
}

AppOutput is one application's published values.

type Application

type Application struct {
	// Name is the application's natural key.
	Name string
	Kind AppKind

	// RedirectURIs and PostLogoutURIs are compared LITERALLY by the issuer, so
	// a changed host silently breaks the callback until this converges. With
	// DevMode they are globs.
	RedirectURIs   []string
	PostLogoutURIs []string

	// LoginURI is the app's own sign-in route. When set the issuer redirects
	// /oauth/v2/authorize INTO the app (login V2) instead of rendering its own
	// pages. It is set per application, so two apps sharing an instance each
	// point at their own route. Empty restores the issuer-hosted login.
	LoginURI string

	// DevMode permits plain-http redirect URIs and turns them into globs. Only
	// for a loopback dev IdP.
	DevMode bool

	// IDTokenRoleAssertion / IDTokenUserinfoAssertion put the project roles
	// and the profile claims into the ID token. The access token always
	// carries the roles claim.
	IDTokenRoleAssertion     bool
	IDTokenUserinfoAssertion bool

	// AdoptClientID pins the client id the application must have; see the
	// package comment.
	AdoptClientID string

	// RegenerateSecret mints a new client secret for an AppWeb whose secret is
	// not in the KeyStore. It invalidates the previous secret, so it is never
	// done implicitly for an application that already existed.
	RegenerateSecret bool

	// Broker configures the broker account of an AppNativeBroker.
	Broker *BrokerAccount
}

Application is an OIDC application registered in the project.

func (Application) BrokerUsername

func (a Application) BrokerUsername() string

BrokerUsername is the machine username of an application's broker account.

type BrokerAccount

type BrokerAccount struct {
	// Username defaults to "broker-" plus a slug of the application name.
	Username string
}

BrokerAccount is the machine account the login broker authenticates as. It is a privileged credential (IAM_LOGIN_CLIENT can complete an authorization request for any user of the instance), kept apart from the provisioning credential so the busiest surface of an app never holds the credential that administers the instance.

type Change

type Change struct {
	Action Action
	Kind   string
	Name   string
	Detail string
}

Change is one write.

func (Change) String

func (c Change) String() string

type Credential

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

Credential authenticates the library to the instance. Use PAT for a personal access token (the dev bootstrap) and MachineKey for an instance-owner machine key (the control plane). Both must carry IAM_OWNER: the library edits instance-level policies and SMTP as well as the project.

func MachineKey

func MachineKey(raw []byte) (Credential, error)

MachineKey authenticates as a machine user through the JWT-profile grant: a signed assertion is exchanged for an access token, which is cached until shortly before it expires. raw is the key document Zitadel returned when the key was created.

func MachineKeyFile

func MachineKeyFile(path string) (Credential, error)

MachineKeyFile is MachineKey for a key document on disk (a projected Secret).

func PAT

func PAT(token string) Credential

PAT authenticates with a personal access token, which is already a bearer token for every API: no exchange happens.

func PATFile

func PATFile(path string) Credential

PATFile reads a personal access token from path on every request, so a token the owner rotates in place is picked up without a restart.

type CredentialFunc

type CredentialFunc func(ctx context.Context) (string, error)

CredentialFunc adapts a function (a token minted elsewhere, rotated by its owner) to a Credential. It is called for every request, so it should cache.

type DirKeyStore

type DirKeyStore struct{ Dir string }

DirKeyStore keeps one 0600 file per entry under Dir. It is the dev store: the directory is gitignored and holds nothing a throwaway local instance would not.

func (DirKeyStore) Load

func (d DirKeyStore) Load(_ context.Context, name string) ([]byte, error)

func (DirKeyStore) Save

func (d DirKeyStore) Save(_ context.Context, name string, value []byte) error

type FileOutputs

type FileOutputs struct{ Path string }

FileOutputs writes the non-secret Output as JSON to Path, atomically, 0644. It is the dev publication: `.forge/identity/<name>.json`.

func (FileOutputs) Publish

func (f FileOutputs) Publish(_ context.Context, out Output) error

type KeyStore

type KeyStore interface {
	// Load returns the stored value, or (nil, nil) when none is stored.
	Load(ctx context.Context, name string) ([]byte, error)
	// Save durably replaces the value. An error means the value was NOT
	// stored; the library then discards what it had just issued and keeps the
	// previous credential.
	Save(ctx context.Context, name string, value []byte) error
}

KeyStore is where credentials the library GENERATES live, and it is also the library's memory of them: Zitadel returns a machine key's private half, a personal access token and a client secret exactly once, so the store holds the ONLY copy. A pass that finds an entry missing issues a new credential rather than failing, which is what makes an emptied store recoverable by simply running again.

Names are opaque, stable strings such as "machine-key:ops-automation". Dev maps them to files (DirKeyStore); the control plane maps them to OpenBao paths.

type Lockout

type Lockout struct {
	MaxPasswordAttempts int
	MaxOTPAttempts      int
}

Lockout is the instance lockout policy; zero disables that lockout.

type LoginPolicy

type LoginPolicy struct {
	AllowUsernamePassword bool
	AllowRegister         bool
	AllowExternalIDP      bool
	ForceMFA              bool
	HidePasswordReset     bool
	AllowDomainDiscovery  bool
	// PasswordlessType is "PASSWORDLESS_TYPE_ALLOWED" or
	// "PASSWORDLESS_TYPE_NOT_ALLOWED"; empty means not allowed.
	PasswordlessType string
	// MFAInitSkipLifetime is how long a user may skip enrolling a second
	// factor; zero never prompts.
	MFAInitSkipLifetime time.Duration
}

LoginPolicy is the instance login policy. Fields not exposed here (check lifetimes, factor lists) keep their current values.

type MachineKeyDoc

type MachineKeyDoc struct {
	Type   string `json:"type"`
	KeyID  string `json:"keyId"`
	Key    string `json:"key"`
	UserID string `json:"userId"`
}

MachineKeyDoc is Zitadel's machine-key JSON document: the shape its own SDKs and the JWT-profile grant consume, and the value stored verbatim in a KeyStore.

func ParseMachineKey

func ParseMachineKey(raw []byte) (MachineKeyDoc, error)

ParseMachineKey decodes a key document and checks it is usable.

type MachineOutput

type MachineOutput struct {
	UserID       string    `json:"user_id"`
	KeyID        string    `json:"key_id,omitempty"`
	KeyExpires   time.Time `json:"key_expires,omitempty"`
	TokenID      string    `json:"token_id,omitempty"`
	TokenExpires time.Time `json:"token_expires,omitempty"`
	// Token is the machine user's current personal access token, if it has one.
	Token string `json:"-"`
}

MachineOutput is one machine user's published values. The key itself is in the KeyStore, not here.

type MachineUser

type MachineUser struct {
	// Username is the natural key and a DNS-label-shaped name: it is part of a
	// secret name downstream.
	Username    string
	Name        string
	Description string
	// Roles are the project roles the account holds.
	Roles []string
	// InstanceRoles are instance-level memberships (for example
	// IAM_LOGIN_CLIENT). They are only ever added, never removed.
	InstanceRoles []string
	// Key issues a JSON machine key into the KeyStore and keeps it rotated.
	Key bool
	// Token issues an expiring personal access token into the KeyStore.
	Token bool
}

MachineUser is a service account.

type MemKeyStore

type MemKeyStore struct {

	// SaveErr, when set, makes every Save fail; tests use it to prove a failed
	// delivery leaves the previous credential intact.
	SaveErr error
	// contains filtered or unexported fields
}

MemKeyStore is an in-memory KeyStore for tests and one-shot tools.

func (*MemKeyStore) Delete

func (s *MemKeyStore) Delete(name string)

Delete drops an entry (test helper: simulates an emptied store).

func (*MemKeyStore) Load

func (s *MemKeyStore) Load(_ context.Context, name string) ([]byte, error)

func (*MemKeyStore) Save

func (s *MemKeyStore) Save(_ context.Context, name string, value []byte) error

type ObjectError

type ObjectError struct {
	Kind string
	Name string
	Err  error
}

ObjectError names the object a failure concerns. Every error leaving the package is wrapped in one, so "the pass failed" always says WHICH thing.

func (*ObjectError) Error

func (e *ObjectError) Error() string

func (*ObjectError) Unwrap

func (e *ObjectError) Unwrap() error

type Options

type Options struct {
	// BaseURL is the address this process DIALS. Required.
	BaseURL string
	// Host overrides the HTTP Host header. Zitadel resolves which instance a
	// request belongs to from the Host, so when the address you dial (a
	// Service DNS name, a compose service) differs from the domain the
	// instance is registered under, set Host to the latter.
	Host string
	// Issuer pins the issuer string the published outputs carry and the JWT
	// audience for machine-key exchange. Empty reads it from the instance's
	// discovery document, which is right unless discovery is not served at the
	// dialled address.
	Issuer string
	// Credential authenticates the library; it must hold IAM_OWNER. Required.
	Credential Credential
	// OrgID scopes the content to one organization. Empty resolves it: OrgName
	// if set, else the credential's own organization.
	OrgID   string
	OrgName string

	// Keys remembers generated credentials. Required when the Spec generates
	// any (machine keys, tokens, web client secrets, an SMTP password).
	Keys KeyStore
	// Outputs receives the Output at the end of every successful pass.
	Outputs Outputs

	Rotation Rotation

	// Adopt converges an instance that already holds content: the project and
	// every application must already exist and are never created. Objects are
	// matched by natural key (or by a pinned id) and kept whatever their
	// internal ids.
	Adopt bool
	// DryRun computes the Report without writing anything.
	DryRun bool
	// AllowSeedUsers permits Spec.SeedUsers. Local and ephemeral instances only.
	AllowSeedUsers bool
	// SignInCodes mints a one-time password-reset code, returned in the Report,
	// for every declared member who has no credential. Use it where nothing can
	// deliver mail.
	SignInCodes bool
	// HeldSignInCode, when set, reports whether the caller still holds an
	// unexpired code it delivered for a member. Such a member is not minted
	// another: minting invalidates the code they hold, so a converge loop that
	// minted on every pass would change the code under someone about to use it,
	// and no pass would ever be quiet. Nil: every credential-less member gets a
	// fresh code on every pass. Consulted only with SignInCodes.
	HeldSignInCode func(email string) bool
	// KeepSuperseded disables deleting credentials that a newer one has
	// replaced, once the overlap has passed. They are then only reported.
	KeepSuperseded bool

	// ReadyTimeout bounds the wait for the instance to answer its readiness
	// probe (default 2m). RequestTimeout bounds one API call (default 15s) and
	// MaxAttempts the tries one call gets (default 4).
	ReadyTimeout   time.Duration
	RequestTimeout time.Duration
	MaxAttempts    int

	HTTPClient *http.Client
	Logger     *slog.Logger
	// Now is the clock credential lifetimes are judged against.
	Now func() time.Time
}

Options configure a Reconciler.

type Output

type Output struct {
	// Issuer is the `iss` the instance mints; the validator's jwt_issuer.
	Issuer string `json:"issuer"`
	// OrgID is the organization the content lives in.
	OrgID string `json:"org_id"`
	// ProjectID is the project id, which is also the token audience.
	ProjectID string `json:"project_id"`
	// Audience is the value a validator must enforce as `aud` (ProjectID).
	Audience string `json:"audience"`
	// RolesClaim is the namespaced claim the project's roles are asserted
	// under: a map of role key to org id to org domain, not a flat list.
	RolesClaim string `json:"roles_claim"`
	// Scopes are what a client must request for an access token that carries
	// the audience and the roles claim.
	Scopes []string `json:"scopes"`
	// JWKS is a snapshot of the instance's public signing keys, for a validator
	// that must start while the issuer is unreachable.
	JWKS json.RawMessage `json:"jwks,omitempty"`
	// Apps are the registered applications by name.
	Apps map[string]AppOutput `json:"apps,omitempty"`
	// Machine are the machine users by username.
	Machine map[string]MachineOutput `json:"machine,omitempty"`
}

Output is everything a consumer of the instance is configured from.

type Outputs

type Outputs interface {
	Publish(ctx context.Context, out Output) error
}

Outputs is the write-only publication of what consumers of the instance need. Publish is called once at the end of every successful pass, so an implementation must be idempotent; it should treat Output's secret fields (which json omits) as credentials.

type OutputsFunc

type OutputsFunc func(ctx context.Context, out Output) error

OutputsFunc adapts a function to Outputs.

func (OutputsFunc) Publish

func (f OutputsFunc) Publish(ctx context.Context, out Output) error

type PasswordComplexity

type PasswordComplexity struct {
	MinLength        int
	RequireUppercase bool
	RequireLowercase bool
	RequireNumber    bool
	RequireSymbol    bool
}

PasswordComplexity is the instance password complexity policy.

type Policies

type Policies struct {
	Login              *LoginPolicy
	PasswordComplexity *PasswordComplexity
	Lockout            *Lockout
}

Policies are the instance-level login policies. A nil policy is left alone.

type ProjectSpec

type ProjectSpec struct {
	// Name is the project's natural key.
	Name string
	// AdoptID pins the id the project must have. See the package comment.
	AdoptID string
	// MustHoldRole makes Zitadel refuse tokens for users who hold no role in the
	// project; ProjectCheck refuses tokens for users whose org was not granted
	// the project. Both default to off.
	MustHoldRole bool
	ProjectCheck bool
}

ProjectSpec is the Zitadel project. Its generated id is the token audience, which is why it is an OUTPUT (Result.Audience) and not something a spec can choose.

type Reconciler

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

Reconciler converges one instance. It is safe to reuse across passes but not for concurrent passes against the same instance.

func New

func New(opts Options) (*Reconciler, error)

New builds a Reconciler. It does not contact the instance.

func (*Reconciler) Reconcile

func (r *Reconciler) Reconcile(ctx context.Context, spec Spec) (*Report, error)

Reconcile makes the instance match spec and reports what it changed.

Steps run in dependency order. The project is foundational and its failure ends the pass; every other step records its error and the pass continues, so one broken object does not hide the state of the rest. All errors are joined and each names its object (see ObjectError). The Output is published only when nothing failed.

func (*Reconciler) Rotate

func (r *Reconciler) Rotate(ctx context.Context, spec Spec, targets ...RotateTarget) (*Report, error)

Rotate runs a full pass and additionally replaces the named credentials. The superseded ones stay valid for the configured overlap (Rotation) and are listed in Report.Retiring; a later pass deletes them once it has elapsed. The Output published at the end carries the new credentials, so a consumer that re-reads it switches over before the old one is removed.

type Report

type Report struct {
	// Changes lists every write. A converged instance yields none.
	Changes []Change
	// Warnings are things left alone on purpose: a role nobody declared, a
	// credential that cannot be recovered.
	Warnings    []string
	Retiring    []Retiring
	SignInCodes []SignInCode
	// Credentialed lists the declared members this pass CONFIRMED hold a
	// credential (filled only with Options.SignInCodes): any code a caller
	// holds for them is spent or moot. A member whose check failed is in
	// neither this list nor SignInCodes.
	Credentialed []string
	// Output is the published view, filled even when Options.Outputs is nil.
	Output Output
	DryRun bool
}

Report is what a pass did.

func (*Report) Changed

func (r *Report) Changed() bool

Changed reports whether the pass wrote (or, in a dry run, would write) anything.

type Retiring

type Retiring struct {
	Username    string
	Kind        string // "machine key" or "token"
	ID          string
	DeleteAfter time.Time
}

Retiring is a superseded credential that still works until DeleteAfter.

type Role

type Role struct {
	Key         string
	DisplayName string
	Group       string
	// Declared makes the holders of this role a SET converged from the spec:
	// the humans in Members plus the machine users that list the role. A holder
	// not in that set loses THIS role (other roles in the same grant are kept).
	// Leave it false for a role whose members are managed in a console.
	Declared bool
	// Members are the emails that hold the role when Declared is set.
	Members []string
}

Role is a project role. Roles are only ever created and relabelled, never deleted: a role may have grants, and removing it silently strips them.

type RotateTarget

type RotateTarget struct {
	// Application rotates the broker token of this native_broker application.
	Application string
	// MachineUser rotates this machine user's key (and token, if it has one).
	MachineUser string
}

RotateTarget names credentials to replace NOW, whatever their remaining lifetime. The old credential keeps working for the overlap window.

type Rotation

type Rotation struct {
	// BrokerTokenTTL is the lifetime of a minted broker token (default 30d); a
	// new one is minted once less than BrokerRenewWithin (default 10d) is left.
	BrokerTokenTTL    time.Duration
	BrokerRenewWithin time.Duration
	// BrokerOverlap is how long a superseded token keeps working (default 10d).
	BrokerOverlap time.Duration
	// MachineKeyTTL (default 90d), MachineKeyRenewWithin (default 30d) and
	// MachineKeyOverlap (default 7d) are the same three for machine keys.
	MachineKeyTTL         time.Duration
	MachineKeyRenewWithin time.Duration
	MachineKeyOverlap     time.Duration
}

Rotation tunes credential lifetimes. Zero values take the defaults.

type SMTP

type SMTP struct {
	// Host is host:port.
	Host          string
	User          string
	Password      string `json:"-"`
	SenderAddress string
	SenderName    string
	ReplyTo       string
	TLS           bool
}

SMTP is the instance mail relay.

type SeedUser

type SeedUser struct {
	Email      string
	Password   string `json:"-"`
	GivenName  string
	FamilyName string
	Roles      []string
}

SeedUser is a throwaway account. Its password is set when the account is created and never again, so a user who changed it is not reset.

type SignInCode

type SignInCode struct {
	Email string
	Code  string
	// ExpiresAt is Options.Now at minting plus SignInCodeLifetime.
	ExpiresAt time.Time
}

SignInCode is a one-time code that lets a credential-less member set a first password. It is single-use and short-lived; treat it as a secret.

type Spec

type Spec struct {
	Project      ProjectSpec
	Roles        []Role
	Applications []Application
	MachineUsers []MachineUser
	Policies     Policies
	// SMTP is the instance mail relay. Nil leaves the instance's mail
	// configuration alone.
	SMTP *SMTP
	// SeedUsers are throwaway accounts for local and ephemeral environments.
	// Reconcile refuses them unless Options.AllowSeedUsers is set.
	SeedUsers []SeedUser
}

Spec is the DECLARED content of one Zitadel instance. Reconcile makes the instance match it. Objects are matched by natural key (project name, role key, application name, username, email), never by internal id.

func (Spec) Validate

func (s Spec) Validate() error

Validate checks the spec offline and reports every problem, each naming the object it concerns.

Jump to

Keyboard shortcuts

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