shared

package
v0.2.3-dev Latest Latest
Warning

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

Go to latest
Published: Apr 30, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package shared provides cross-cutting utilities used by all domain modules.

Index

Constants

View Source
const (
	DialectPostgres = "postgres"
	DialectSQLite   = "sqlite"
)

Dialect constants for database selection.

View Source
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"
	CodeExtProviderUnavailable = "EXT_PROVIDER_UNAVAILABLE"

	// SYS — system / infrastructure (details logged, never exposed)
	CodeSysInternal = "SYS_INTERNAL"
	CodeSysDB       = "SYS_DB"
	CodeSysConfig   = "SYS_CONFIG"
)
View Source
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).

View Source
const AccessTokenTTL = 1 * time.Hour

AccessTokenTTL is the validity window for OAuth access tokens issued to CLI.

View Source
const TokenTTL = 24 * time.Hour

TokenTTL is the JWT validity window for legacy/admin tokens.

Variables

This section is empty.

Functions

func AccountID

func AccountID(ctx context.Context) string

AccountID returns the authenticated account ID from context, or empty string.

func CORSMiddleware

func CORSMiddleware(allowedOrigins []string) func(http.Handler) http.Handler

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

func CorrelationID(ctx context.Context) string

CorrelationID returns the request correlation ID from context, or empty string.

func CorrelationIDMiddleware

func CorrelationIDMiddleware(next http.Handler) http.Handler

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

func GenerateOpaqueToken() (plaintext, hash string, err error)

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

func HashToken(plaintext string) string

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

func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler

LoggingMiddleware logs every request with method, path, and correlation ID.

func MasterKeyFromBase64

func MasterKeyFromBase64(encoded string) ([]byte, error)

MasterKeyFromBase64 decodes a base64-encoded 32-byte master key string (e.g. from the MASTER_KEY environment variable).

func NewID

func NewID() string

NewID generates a new UUID-based string ID.

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 OpenDB

func OpenDB(dsn string) (*sql.DB, error)

OpenDB opens a PostgreSQL connection pool from the given DSN.

func RunMigrations

func RunMigrations(db *sql.DB, dir string) error

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

func TranslatePGError(err error) error

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.

func (*AESEncryptor) Decrypt

func (e *AESEncryptor) Decrypt(encoded string) (string, error)

Decrypt reverses Encrypt. Returns the original plaintext or an error.

func (*AESEncryptor) Encrypt

func (e *AESEncryptor) Encrypt(plaintext string) (string, error)

Encrypt seals plaintext with AES-256-GCM and returns a base64-encoded blob.

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

type DB struct {
	*sql.DB
	Dialect string
}

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 NewDB

func NewDB(db *sql.DB, dialect string) *DB

NewDB wraps an existing *sql.DB with a dialect tag.

func (*DB) Begin

func (d *DB) Begin() (*Tx, error)

Begin starts a transaction and returns a dialect-aware Tx wrapper.

func (*DB) BeginTx

func (d *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)

BeginTx starts a transaction with options.

func (*DB) BindMillis

func (d *DB) BindMillis(m aikeytime.Millis) any

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

func (d *DB) BindMillisPtr(m *aikeytime.Millis) any

BindMillisPtr is the nullable-pointer variant. nil → NULL.

func (*DB) ExecContext

func (d *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)

ExecContext executes a query with placeholder rewriting.

func (*DB) InsertOrIgnore

func (d *DB) InsertOrIgnore(table, columns, placeholders string) string

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) IsSQLite

func (d *DB) IsSQLite() bool

IsSQLite returns true if the dialect is SQLite.

func (*DB) Now

func (d *DB) Now() string

Now returns the SQL expression for current timestamp.

Postgres: NOW()
SQLite:   datetime('now')

func (*DB) QueryContext

func (d *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)

QueryContext executes a query with placeholder rewriting.

func (*DB) QueryRowContext

func (d *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row

QueryRowContext executes a query with placeholder rewriting.

func (*DB) TranslateError

func (d *DB) TranslateError(err error) error

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 SysDB

func SysDB() *DomainError

SysDB returns a sanitised database-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.

func (*TokenService) Verify

func (ts *TokenService) Verify(tokenStr string) (*Claims, error)

Verify parses and validates a JWT, returning the embedded claims.

type Tx

type Tx struct {
	*sql.Tx
	// contains filtered or unexported fields
}

Tx wraps *sql.Tx with dialect-aware placeholder rewriting.

func (*Tx) ExecContext

func (t *Tx) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)

ExecContext executes a query within the transaction with placeholder rewriting.

func (*Tx) QueryContext

func (t *Tx) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)

QueryContext executes a query within the transaction.

func (*Tx) QueryRowContext

func (t *Tx) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row

QueryRowContext executes a query within the transaction.

Jump to

Keyboard shortcuts

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