Documentation
¶
Overview ¶
Package shared provides cross-cutting utilities used by all domain modules.
Index ¶
- Constants
- func AccountID(ctx context.Context) string
- func CORSMiddleware(allowedOrigins []string) func(http.Handler) http.Handler
- func CorrelationID(ctx context.Context) string
- func CorrelationIDMiddleware(next http.Handler) http.Handler
- func DomainErrorResponse(w http.ResponseWriter, err *DomainError)
- func Error(w http.ResponseWriter, status int, code, message string)
- func GenerateOpaqueToken() (plaintext, hash string, err error)
- func HandleDomainErr(w http.ResponseWriter, err error)
- func HashToken(plaintext string) string
- func JSON(w http.ResponseWriter, status int, v any)
- func JWTMiddleware(ts *TokenService) func(http.Handler) http.Handler
- func LocalIdentityMiddleware(ts ...*TokenService) func(http.Handler) http.Handler
- func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler
- func MasterKeyFromBase64(encoded string) ([]byte, error)
- func NewID() string
- func NewRevision() string
- func OpenDB(dsn string) (*sql.DB, error)
- func RunMigrations(db *sql.DB, dir string) error
- func TranslatePGError(err error) error
- type AESEncryptor
- type Claims
- type DB
- func (d *DB) Begin() (*Tx, error)
- func (d *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
- func (d *DB) BindMillis(m aikeytime.Millis) any
- func (d *DB) BindMillisPtr(m *aikeytime.Millis) any
- func (d *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
- func (d *DB) InsertOrIgnore(table, columns, placeholders string) string
- func (d *DB) IsSQLite() bool
- func (d *DB) Now() string
- func (d *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
- func (d *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
- func (d *DB) TranslateError(err error) error
- type DomainError
- func BizAuthAccessDenied() *DomainError
- func BizAuthAccountInactive() *DomainError
- func BizAuthEmailTaken(email string) *DomainError
- func BizAuthInvalidCredentials() *DomainError
- func BizAuthTokenExpired() *DomainError
- func BizAuthTokenInvalid() *DomainError
- func BizAuthTokenNotActive() *DomainError
- func BizAuthTokenRecycled() *DomainError
- func BizAuthTokenRevoked() *DomainError
- func BizBindAliasTaken() *DomainError
- func BizBindDuplicateTarget(protocol, providerID string) *DomainError
- func BizBindNoActive() *DomainError
- func BizBindNotDelivered() *DomainError
- func BizBindNotFound(id string) *DomainError
- func BizBindProtocolMismatch(bindingProtocol, credProtocol string) *DomainError
- func BizCredInactive(id string) *DomainError
- func BizCredNameTaken() *DomainError
- func BizCredNotFound(id string) *DomainError
- func BizKeyAliasTaken() *DomainError
- func BizKeyDuplicateProtocol(protocol string) *DomainError
- func BizKeyNotActive() *DomainError
- func BizKeyNotFound(id string) *DomainError
- func BizLoginResendCooldown(retryAfterSeconds int) *DomainError
- func BizLoginSessionDenied() *DomainError
- func BizLoginSessionExpired() *DomainError
- func BizLoginSessionNotFound(id string) *DomainError
- func BizLoginSessionTerminated(currentStatus string) *DomainError
- func BizLoginTokenAlreadyUsed() *DomainError
- func BizLoginTokenInvalid() *DomainError
- func BizOrgNotFound(id string) *DomainError
- func BizProvCodeTaken() *DomainError
- func BizProvNotFound(id string) *DomainError
- func BizRefreshTokenInvalid() *DomainError
- func BizRefreshTokenRevoked() *DomainError
- func BizSeatAlreadyClaimed() *DomainError
- func BizSeatEmailTaken(email, orgID string) *DomainError
- func BizSeatNotFound(id string) *DomainError
- func BizSeatStatusConflict(msg string) *DomainError
- func DataInvalidBody() *DomainError
- func DataInvalidEmail() *DomainError
- func DataInvalidField(field, rule, reason string) *DomainError
- func DataMissingField(field string) *DomainError
- func ExtProviderAuthFailure(provider string, upstreamMessage string) *DomainError
- func ExtProviderRateLimited(provider string) *DomainError
- func ExtProviderUnavailable(provider string) *DomainError
- func ExtProviderUpstream(provider string, upstreamStatus int, upstreamMessage string) *DomainError
- func SysConfig() *DomainError
- func SysDB() *DomainError
- func SysInternal() *DomainError
- type TokenService
- type Tx
Constants ¶
const ( DialectPostgres = "postgres" DialectSQLite = "sqlite" )
Dialect constants for database selection.
const ( // BIZ — Auth CodeBizAuthEmailTaken = "BIZ_AUTH_EMAIL_TAKEN" CodeBizAuthInvalidCredentials = "BIZ_AUTH_INVALID_CREDENTIALS" CodeBizAuthAccountInactive = "BIZ_AUTH_ACCOUNT_INACTIVE" CodeBizAuthTokenInvalid = "BIZ_AUTH_TOKEN_INVALID" CodeBizAuthTokenRevoked = "BIZ_AUTH_TOKEN_REVOKED" CodeBizAuthTokenExpired = "BIZ_AUTH_TOKEN_EXPIRED" CodeBizAuthTokenRecycled = "BIZ_AUTH_TOKEN_RECYCLED" CodeBizAuthTokenNotActive = "BIZ_AUTH_TOKEN_NOT_ACTIVE" CodeBizAuthAccessDenied = "BIZ_AUTH_ACCESS_DENIED" // BIZ — Organization CodeBizOrgNotFound = "BIZ_ORG_NOT_FOUND" // BIZ — Seat CodeBizSeatNotFound = "BIZ_SEAT_NOT_FOUND" CodeBizSeatEmailTaken = "BIZ_SEAT_EMAIL_TAKEN" CodeBizSeatAlreadyClaimed = "BIZ_SEAT_ALREADY_CLAIMED" // BIZ — Virtual Key CodeBizKeyNotFound = "BIZ_KEY_NOT_FOUND" CodeBizKeyNotActive = "BIZ_KEY_NOT_ACTIVE" CodeBizKeyDuplicateProtocol = "BIZ_KEY_DUPLICATE_PROTOCOL" // BIZ — Protocol Binding CodeBizBindNotFound = "BIZ_BIND_NOT_FOUND" CodeBizBindProtocolMismatch = "BIZ_BIND_PROTOCOL_MISMATCH" CodeBizBindNoActive = "BIZ_BIND_NO_ACTIVE" CodeBizBindNotDelivered = "BIZ_BIND_NOT_DELIVERED" // CodeBizBindDuplicateTarget: same (protocol_type, provider_id) pair already active on this VK. CodeBizBindDuplicateTarget = "BIZ_BIND_DUPLICATE_TARGET" // BIZ — Login Session / OAuth CodeBizLoginSessionNotFound = "BIZ_LOGIN_SESSION_NOT_FOUND" CodeBizLoginSessionExpired = "BIZ_LOGIN_SESSION_EXPIRED" CodeBizLoginSessionDenied = "BIZ_LOGIN_SESSION_DENIED" // CodeBizLoginResendCooldown is returned when Begin is called within the // per-session cooldown window after a previous send. Pass remaining // seconds via WithMeta("retry_after_seconds", ...) so the browser UI // can render a live countdown. CodeBizLoginResendCooldown = "BIZ_LOGIN_RESEND_COOLDOWN" // CodeBizLoginSessionTerminated is returned when Begin is called on a // session that has already reached a terminal state (approved, denied, // cancelled, token_issued). The user must restart `aikey account login`. CodeBizLoginSessionTerminated = "BIZ_LOGIN_SESSION_TERMINATED" CodeBizLoginTokenInvalid = "BIZ_LOGIN_TOKEN_INVALID" CodeBizLoginTokenAlreadyUsed = "BIZ_LOGIN_TOKEN_ALREADY_USED" CodeBizRefreshTokenInvalid = "BIZ_REFRESH_TOKEN_INVALID" CodeBizRefreshTokenRevoked = "BIZ_REFRESH_TOKEN_REVOKED" // BIZ — unique-conflict specialisations CodeBizBindAliasTaken = "BIZ_BIND_ALIAS_TAKEN" CodeBizKeyAliasTaken = "BIZ_KEY_ALIAS_TAKEN" CodeBizCredNameTaken = "BIZ_CRED_NAME_TAKEN" CodeBizProvCodeTaken = "BIZ_PROV_CODE_TAKEN" // BIZ — Credential CodeBizCredNotFound = "BIZ_CRED_NOT_FOUND" CodeBizCredInactive = "BIZ_CRED_INACTIVE" // BIZ — Provider CodeBizProvNotFound = "BIZ_PROV_NOT_FOUND" // DATA — client input validation CodeDataInvalidBody = "DATA_INVALID_BODY" CodeDataMissingField = "DATA_MISSING_FIELD" CodeDataInvalidField = "DATA_INVALID_FIELD" CodeDataInvalidEmail = "DATA_INVALID_EMAIL" // EXT — external / upstream service CodeExtProviderUpstream = "EXT_PROVIDER_UPSTREAM" CodeExtProviderAuthFailure = "EXT_PROVIDER_AUTH_FAILURE" CodeExtProviderRateLimited = "EXT_PROVIDER_RATE_LIMITED" // SYS — system / infrastructure (details logged, never exposed) CodeSysInternal = "SYS_INTERNAL" CodeSysDB = "SYS_DB" CodeSysConfig = "SYS_CONFIG" )
const ( LocalOwnerAccountID = "local-owner" LocalOwnerEmail = "local@localhost" )
LocalOwnerAccountID is the sentinel account identity injected by LocalIdentityMiddleware when a request arrives without a Bearer token. Handlers that respond with profile-like data should recognise this value and synthesise a response from the middleware claims instead of querying the DB (the trial edition does not seed a DB row for this sentinel).
const AccessTokenTTL = 1 * time.Hour
AccessTokenTTL is the validity window for OAuth access tokens issued to CLI.
const TokenTTL = 24 * time.Hour
TokenTTL is the JWT validity window for legacy/admin tokens.
Variables ¶
This section is empty.
Functions ¶
func CORSMiddleware ¶
CORSMiddleware sets CORS headers per an explicit origin allowlist.
Semantics:
- Empty allowedOrigins → deny all cross-origin requests. Same-origin requests (where the browser omits Origin, or Origin matches the served host) are unaffected because browsers don't enforce CORS on them. This is the safe default for local/trial installs.
- allowedOrigins contains "*" → echo back the request Origin for any cross-origin caller (dev/testing only — do not use in prod).
- Otherwise → echo back the Origin only when it's in the allowlist.
Why the default flipped (2026-04-24 security review): the previous behavior treated an empty allowlist as "allow all", which contradicted the documented "Empty = same-origin only" contract and let any website read /api/user/vault/* via the browser when combined with local_bypass auth (anonymous → LocalOwnerAccountID). A malicious page could enumerate vault metadata including route_token — a usable proxy bearer.
func CorrelationID ¶
CorrelationID returns the request correlation ID from context, or empty string.
func CorrelationIDMiddleware ¶
CorrelationIDMiddleware attaches a correlation/request ID to every request context. It honours the X-Correlation-ID header if present; otherwise generates a new UUID.
func DomainErrorResponse ¶
func DomainErrorResponse(w http.ResponseWriter, err *DomainError)
DomainErrorResponse converts a DomainError to an HTTP response, including any structured meta fields (field, rule, upstream_status, etc.). Internal-only meta (db_detail, constraint) is stripped from the response but logged server-side for debugging. See Issue #17.
func Error ¶
func Error(w http.ResponseWriter, status int, code, message string)
Error writes a structured JSON error response without meta context. Prefer DomainErrorResponse for typed errors.
func GenerateOpaqueToken ¶
GenerateOpaqueToken returns a URL-safe base64-encoded random token and its SHA-256 hex hash. The plaintext is sent to clients; only the hash is stored in the database so the plaintext cannot be recovered from a DB breach.
func HandleDomainErr ¶
func HandleDomainErr(w http.ResponseWriter, err error)
HandleDomainErr converts service errors to HTTP responses. Uses errors.As to unwrap DomainErrors through fmt.Errorf chains. All other errors are logged and returned as SYS_INTERNAL. Exported so that handler sub-packages (master/, user/) can use it without importing the api package (which would cause circular imports).
func HashToken ¶
HashToken returns the SHA-256 hex hash of a client-supplied plaintext token. Use this to look up a stored token hash from a value presented by the client.
func JSON ¶
func JSON(w http.ResponseWriter, status int, v any)
JSON writes a JSON-encoded body with the given status code.
func JWTMiddleware ¶
func JWTMiddleware(ts *TokenService) func(http.Handler) http.Handler
JWTMiddleware validates the Bearer token in Authorization header and injects Claims into the request context. Returns 401 if the token is missing or invalid.
func LocalIdentityMiddleware ¶
func LocalIdentityMiddleware(ts ...*TokenService) func(http.Handler) http.Handler
LocalIdentityMiddleware provides a permissive auth layer for local/trial modes. If the request carries a valid Bearer JWT, it extracts the real account identity (needed for CLI sync where each member must see their own keys). Otherwise it falls back to the fixed "local-owner" identity so web pages work without login.
func LoggingMiddleware ¶
LoggingMiddleware logs every request with method, path, and correlation ID.
func MasterKeyFromBase64 ¶
MasterKeyFromBase64 decodes a base64-encoded 32-byte master key string (e.g. from the MASTER_KEY environment variable).
func NewRevision ¶
func NewRevision() string
NewRevision generates a short random hex string used as an object revision token. Revisions are opaque, monotonically increasing only by convention (latest wins in fact tables).
func RunMigrations ¶
RunMigrations executes unapplied .sql files from the given directory in lexical order. It tracks applied migrations in a `schema_migrations` table so each migration runs only once. Each migration runs in its own transaction; if any fails, the process stops.
func TranslatePGError ¶
TranslatePGError converts a PostgreSQL driver error to a DomainError when a known constraint mapping exists. Unknown DB errors are returned unchanged (they become SYS_INTERNAL in handleDomainErr).
Rule: call this at the repository boundary immediately on INSERT/UPDATE errors, before any fmt.Errorf wrapping, so the domain error can be unwrapped by handleDomainErr via errors.As.
Types ¶
type AESEncryptor ¶
type AESEncryptor struct {
// contains filtered or unexported fields
}
AESEncryptor implements AES-256-GCM encryption.
Format stored in DB:
base64( nonce[12] || ciphertext || tag[16] )
The master key (32 bytes) is loaded from the environment once at startup. For a full envelope-encryption scheme (DEK per credential wrapped by KEK), extend this to store the wrapped DEK alongside each ciphertext row. TODO(evolution): replace with KMS-backed envelope encryption when required.
func NewAESEncryptor ¶
func NewAESEncryptor(masterKey []byte) (*AESEncryptor, error)
NewAESEncryptor creates an AESEncryptor from a 32-byte master key.
type Claims ¶
type Claims struct {
AccountID string `json:"account_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
Claims are the payload fields embedded in every JWT issued by this service.
type DB ¶
DB wraps *sql.DB with dialect awareness. Repository code uses ? placeholders universally; the wrapper rewrites them to $1,$2,... for PostgreSQL.
This eliminates the need for separate postgres.go / sqlite.go repository files in packages whose dialect differences are expression-level (date bucketing, placeholders, casts, ON CONFLICT). Packages with structural differences — e.g. pq.Array ↔ IN (?,?,…), per-row upsert shapes — still split into postgres.go + sqlite.go (see managedkey/ and snapshot/).
func (*DB) BindMillis ¶
BindMillis returns the correct driver argument for an aikeytime.Millis value on the current dialect. β-hybrid:
- SQLite (INTEGER column) → int64 millis; 0 → nil (SQL NULL)
- Postgres (TIMESTAMPTZ col) → time.Time (UTC); zero → nil
See roadmap20260320/技术实现/update/20260424-时间戳统一为int64毫秒-data-service.md.
func (*DB) BindMillisPtr ¶
BindMillisPtr is the nullable-pointer variant. nil → NULL.
func (*DB) ExecContext ¶
ExecContext executes a query with placeholder rewriting.
func (*DB) InsertOrIgnore ¶
InsertOrIgnore returns dialect-appropriate INSERT that silently skips duplicates.
Postgres: INSERT INTO t (...) VALUES (...) ON CONFLICT DO NOTHING SQLite: INSERT OR IGNORE INTO t (...) VALUES (...)
func (*DB) Now ¶
Now returns the SQL expression for current timestamp.
Postgres: NOW()
SQLite: datetime('now')
func (*DB) QueryContext ¶
QueryContext executes a query with placeholder rewriting.
func (*DB) QueryRowContext ¶
QueryRowContext executes a query with placeholder rewriting.
func (*DB) TranslateError ¶
TranslateError converts a database driver error to a DomainError when a known constraint mapping exists. Dispatches to the appropriate handler based on dialect.
type DomainError ¶
type DomainError struct {
Code string
Message string
// Meta holds optional structured context included in the JSON response.
// DATA errors: "field", "rule" keys.
// EXT errors: "provider", "upstream_status", "upstream_message" keys.
Meta map[string]any
}
DomainError carries an HTTP-visible error code, a human-readable message, and optional structured meta for DATA/EXT errors.
Error code format: {LAYER}_{MODULE}_{REASON}
Layers:
BIZ_{MODULE} — business logic violation (user-visible domain rule)
DATA_* — client-supplied data is invalid or missing
EXT_* — upstream/external service returned an error
SYS_* — internal system fault (message sanitised, raw error logged)
All codes kept in sync with web/src/shared/utils/api-error.ts.
func BizAuthAccessDenied ¶
func BizAuthAccessDenied() *DomainError
func BizAuthAccountInactive ¶
func BizAuthAccountInactive() *DomainError
func BizAuthEmailTaken ¶
func BizAuthEmailTaken(email string) *DomainError
func BizAuthInvalidCredentials ¶
func BizAuthInvalidCredentials() *DomainError
func BizAuthTokenExpired ¶
func BizAuthTokenExpired() *DomainError
func BizAuthTokenInvalid ¶
func BizAuthTokenInvalid() *DomainError
func BizAuthTokenNotActive ¶
func BizAuthTokenNotActive() *DomainError
func BizAuthTokenRecycled ¶
func BizAuthTokenRecycled() *DomainError
func BizAuthTokenRevoked ¶
func BizAuthTokenRevoked() *DomainError
func BizBindAliasTaken ¶
func BizBindAliasTaken() *DomainError
func BizBindDuplicateTarget ¶
func BizBindDuplicateTarget(protocol, providerID string) *DomainError
func BizBindNoActive ¶
func BizBindNoActive() *DomainError
func BizBindNotDelivered ¶
func BizBindNotDelivered() *DomainError
func BizBindNotFound ¶
func BizBindNotFound(id string) *DomainError
func BizBindProtocolMismatch ¶
func BizBindProtocolMismatch(bindingProtocol, credProtocol string) *DomainError
func BizCredInactive ¶
func BizCredInactive(id string) *DomainError
func BizCredNameTaken ¶
func BizCredNameTaken() *DomainError
func BizCredNotFound ¶
func BizCredNotFound(id string) *DomainError
func BizKeyAliasTaken ¶
func BizKeyAliasTaken() *DomainError
func BizKeyDuplicateProtocol ¶
func BizKeyDuplicateProtocol(protocol string) *DomainError
func BizKeyNotActive ¶
func BizKeyNotActive() *DomainError
func BizKeyNotFound ¶
func BizKeyNotFound(id string) *DomainError
func BizLoginResendCooldown ¶
func BizLoginResendCooldown(retryAfterSeconds int) *DomainError
BizLoginResendCooldown signals that the caller hit the per-session email resend cooldown. retry_after_seconds is surfaced in both the message and structured Meta so frontends can render a live countdown.
func BizLoginSessionDenied ¶
func BizLoginSessionDenied() *DomainError
func BizLoginSessionExpired ¶
func BizLoginSessionExpired() *DomainError
func BizLoginSessionNotFound ¶
func BizLoginSessionNotFound(id string) *DomainError
func BizLoginSessionTerminated ¶
func BizLoginSessionTerminated(currentStatus string) *DomainError
BizLoginSessionTerminated signals that the session is in a terminal state (approved/denied/cancelled/token_issued) and cannot accept Begin again. The user should restart `aikey account login`.
func BizLoginTokenAlreadyUsed ¶
func BizLoginTokenAlreadyUsed() *DomainError
func BizLoginTokenInvalid ¶
func BizLoginTokenInvalid() *DomainError
func BizOrgNotFound ¶
func BizOrgNotFound(id string) *DomainError
func BizProvCodeTaken ¶
func BizProvCodeTaken() *DomainError
func BizProvNotFound ¶
func BizProvNotFound(id string) *DomainError
func BizRefreshTokenInvalid ¶
func BizRefreshTokenInvalid() *DomainError
func BizRefreshTokenRevoked ¶
func BizRefreshTokenRevoked() *DomainError
func BizSeatAlreadyClaimed ¶
func BizSeatAlreadyClaimed() *DomainError
func BizSeatEmailTaken ¶
func BizSeatEmailTaken(email, orgID string) *DomainError
func BizSeatNotFound ¶
func BizSeatNotFound(id string) *DomainError
func BizSeatStatusConflict ¶
func BizSeatStatusConflict(msg string) *DomainError
func DataInvalidBody ¶
func DataInvalidBody() *DomainError
DataInvalidBody indicates the request body could not be parsed.
func DataInvalidEmail ¶
func DataInvalidEmail() *DomainError
DataInvalidEmail indicates the supplied email address is empty or malformed.
func DataInvalidField ¶
func DataInvalidField(field, rule, reason string) *DomainError
DataInvalidField indicates a field value fails a validation rule. field: JSON field name. rule: machine-readable rule ID. reason: human explanation.
func DataMissingField ¶
func DataMissingField(field string) *DomainError
DataMissingField indicates a required field is absent. field: the JSON field name.
func ExtProviderAuthFailure ¶
func ExtProviderAuthFailure(provider string, upstreamMessage string) *DomainError
ExtProviderAuthFailure indicates the provider rejected the credential.
func ExtProviderRateLimited ¶
func ExtProviderRateLimited(provider string) *DomainError
ExtProviderRateLimited indicates the provider is throttling requests.
func ExtProviderUnavailable ¶
func ExtProviderUnavailable(provider string) *DomainError
ExtProviderUnavailable indicates the provider is unreachable or returning 5xx.
func ExtProviderUpstream ¶
func ExtProviderUpstream(provider string, upstreamStatus int, upstreamMessage string) *DomainError
ExtProviderUpstream wraps a non-auth, non-rate-limit upstream error. upstreamStatus and upstreamMessage are included in the response body.
func SysConfig ¶
func SysConfig() *DomainError
SysConfig returns a sanitised configuration-error response.
func SysInternal ¶
func SysInternal() *DomainError
SysInternal returns a sanitised internal-error response. Always log the raw error before calling this.
func (*DomainError) Error ¶
func (e *DomainError) Error() string
func (*DomainError) InternalMeta ¶
func (e *DomainError) InternalMeta() map[string]any
InternalMeta returns Meta entries that are internal-only (e.g. db_detail, constraint) for server-side logging. Returns nil when there are none.
func (*DomainError) ResponseBody ¶
func (e *DomainError) ResponseBody() map[string]any
ResponseBody returns the JSON-serialisable map sent to the client. Internal-only meta keys (db_detail, constraint) are stripped here to avoid leaking Postgres internals. See Issue #17.
type TokenService ¶
type TokenService struct {
// contains filtered or unexported fields
}
TokenService handles JWT creation and validation.
func NewTokenService ¶
func NewTokenService(secret []byte) *TokenService
NewTokenService creates a TokenService with the given signing secret. secret must be at least 32 bytes.
func (*TokenService) Issue ¶
func (ts *TokenService) Issue(accountID, email string) (string, error)
Issue creates and signs a JWT for the given account.
func (*TokenService) IssueAccessToken ¶
func (ts *TokenService) IssueAccessToken(accountID, email string) (string, error)
IssueAccessToken creates a short-lived (1 h) JWT for OAuth CLI sessions. Use this instead of Issue for tokens issued through the aikey login flow.
type Tx ¶
Tx wraps *sql.Tx with dialect-aware placeholder rewriting.
func (*Tx) ExecContext ¶
ExecContext executes a query within the transaction with placeholder rewriting.
func (*Tx) QueryContext ¶
QueryContext executes a query within the transaction.