Documentation
¶
Overview ¶
Package store — hanzo/base seam entry point.
Construction:
s, err := store.New(store.Config{DataDir: "/var/lib/commerce"})
Postgres override (multi-instance deployments):
s, err := store.New(store.Config{
DataDir: "/var/lib/commerce", // still used for base's aux
DataDSN: "postgres://...", // takes precedence for the main db
})
The Store is the single construction point for collection-backed repositories. New() runs Bootstrap (DB connections, settings load, system migrations) and then RunAllMigrations (commerce-defined collections). Callers hold the resulting *Store for the process lifetime.
Package store is the hanzo/base-backed persistence seam for commerce.
This package replaces the scattered hanzoai/datastore-go + bespoke model packages with a single, typed repository facade over a base.Collection set. The first collection migrated is `commerce_tenants` — other collections (orders, products, etc.) follow the same shape: Go model struct + typed repo + base migration. The legacy `commerce/datastore` package coexists during migration and is removed collection-by-collection.
Security posture:
- Every repo method is scoped by the caller (handler derives tenant from IAM session claims — never from the body). The store itself does not enforce tenancy; it is the authoritative backing store and the handler layer is the trust boundary.
- JSON columns (brand/iam/idv/providers/return_url_allowlist) are validated at the handler boundary against the canonical Go types in this file. Malformed JSON lands as a 400 at the handler, not a 500 deep inside the store.
- Secret fields on Provider (access_token, webhook_signature_key, etc.) flow to KMS out-of-band; they never live in this store. The Provider struct here intentionally has no credential fields.
Index ¶
- Variables
- type BrandConfig
- type Config
- type IAMConfig
- type IDVConfig
- type Provider
- type Store
- type Tenant
- type TenantRepo
- func (r *TenantRepo) Create(t *Tenant) error
- func (r *TenantRepo) FindByHostname(host string) (*Tenant, error)
- func (r *TenantRepo) FindByID(id string) (*Tenant, error)
- func (r *TenantRepo) List(limit, offset int) ([]*Tenant, error)
- func (r *TenantRepo) UpdateHostnames(id string, hostnames []string) error
- func (r *TenantRepo) UpdateProviders(id string, providers []Provider) error
Constants ¶
This section is empty.
Variables ¶
var ErrDuplicateTenant = errors.New("store: tenant with that name already exists")
ErrDuplicateTenant is returned when a Create would violate the unique-by- name index. Handlers translate to 409 Conflict.
var ErrHostnameClaimed = errors.New("store: hostname already claimed by another tenant")
ErrHostnameClaimed is returned when a Create or UpdateHostnames would cross-assign a hostname already owned by a different tenant. Handlers translate to 409 Conflict with a body that does NOT reveal the colliding tenant's identity — that would be a fingerprinting oracle.
var ErrInvalidHostname = errors.New("store: invalid hostname")
ErrInvalidHostname is returned when a hostname fails normalization or contains characters that are illegal in a Host header value. Handlers translate to 400.
var ErrTenantNotFound = errors.New("store: tenant not found")
ErrTenantNotFound is returned by every lookup that fails. Handlers MUST translate this to HTTP 404 (never 500) and MUST NOT echo the lookup key in the response body — that would be a free fingerprinting oracle.
Functions ¶
This section is empty.
Types ¶
type BrandConfig ¶
type BrandConfig struct {
DisplayName string `json:"display_name"`
LogoURL string `json:"logo_url"`
PrimaryColor string `json:"primary_color"`
}
BrandConfig is the SPA-rendered visible surface.
type Config ¶
type Config struct {
// DataDir is the base filesystem path for SQLite DB files. If empty,
// defaults to ./commerce_data (matches commerced's default).
DataDir string
// DataDSN is an optional PostgreSQL DSN ("postgres://user:pass@host/db").
// When set, overrides the main data DB; aux still lives under DataDir.
DataDSN string
// AuxDSN is an optional PostgreSQL DSN for the auxiliary DB. When empty,
// aux falls back to DataDir/auxiliary.db.
AuxDSN string
// QueryTimeout is applied to all store-issued queries. Defaults to 30s
// when zero; never unlimited — a hung query is a DoS vector.
QueryTimeout time.Duration
}
Config controls how the store connects to its backing database. Zero value is valid and selects the file-path SQLite default under DataDir.
func FromEnv ¶
func FromEnv() Config
FromEnv builds a Config from conventional environment variables. Precedence matches hanzo/base: DSN set → Postgres; otherwise SQLite.
COMMERCE_DATA_DIR data dir (default "./commerce_data") COMMERCE_BASE_URL main data DSN (Postgres) — takes precedence COMMERCE_BASE_AUX aux DSN (Postgres)
type IAMConfig ¶
IAMConfig points the SPA at the tenant's Hanzo IAM app. Only Issuer and ClientID are safe to surface publicly; they already ship in the OIDC well-known doc. No client secret — commerce never needs it; the confidential client flow runs in the tenant's own BD, not in commerce.
type IDVConfig ¶
type IDVConfig struct {
Provider string `json:"provider"`
Endpoint string `json:"endpoint"`
RequiredFields []string `json:"required_fields,omitempty"`
}
IDVConfig is opaque to commerce; the SPA renders the redirect.
type Provider ¶
type Provider struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
KMSPath string `json:"kms_path,omitempty"`
}
Provider is a payment provider configured for the tenant. Credentials are stored in KMS under commerce/{tenant}/{provider}/{field} — this struct holds only the enable flag + a KMS reference (not the secret itself).
type Store ¶
type Store struct {
App core.App
Tenants *TenantRepo
}
Store is the seam between commerce's HTTP layer and hanzo/base. Add a new collection by (a) writing a migration under store/migrations, (b) adding a typed repo, (c) wiring it onto this struct.
type Tenant ¶
type Tenant struct {
ID string `json:"id"`
Name string `json:"name"`
Hostnames []string `json:"hostnames"`
Brand BrandConfig `json:"brand"`
IAM IAMConfig `json:"iam"`
IDV IDVConfig `json:"idv"`
Providers []Provider `json:"providers"`
BDEndpoint string `json:"bd_endpoint"`
ReturnURLAllowlist []string `json:"return_url_allowlist"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
Tenant is the canonical in-memory shape backed by the `commerce_tenants` collection. Hostnames is exact-match only after normalization (lowercase, trailing dot stripped, port stripped) — suffix-match spoofing is rejected by design. See checkout/tenant.go normalizeHost for the rule.
type TenantRepo ¶
type TenantRepo struct {
// contains filtered or unexported fields
}
TenantRepo is the typed persistence API over the commerce_tenants collection. It intentionally does not do tenant scoping — that is the handler layer's responsibility. Repo methods trust their caller.
func NewTenantRepo ¶
func NewTenantRepo(app core.App) *TenantRepo
NewTenantRepo wraps a base app. The collection must already exist (the migration under store/migrations creates it on Bootstrap).
func (*TenantRepo) Create ¶
func (r *TenantRepo) Create(t *Tenant) error
Create persists a new tenant row atomically with its hostname claims.
Atomicity guarantee (P8-C1 fix): tenant row save + hostname inserts run inside a single transaction. A collision on hostname (unique index on commerce_tenant_hostnames.hostname) rolls back the tenant row as well, so a failed create leaves no dangling state and no "dormant" row.
Uniqueness:
- `name` — enforced by idx_commerce_tenants_name (unique). Duplicate returns ErrDuplicateTenant.
- each hostname — enforced by idx_commerce_tenant_hostnames_unique (unique). Cross-tenant collision returns ErrHostnameClaimed.
The caller MUST have already checked superadmin privilege; Create does not verify identity.
func (*TenantRepo) FindByHostname ¶
func (r *TenantRepo) FindByHostname(host string) (*Tenant, error)
FindByHostname resolves a hostname to its owning tenant via the commerce_tenant_hostnames join table. The input is normalized (lowercase, trailing-dot stripped, port stripped) before lookup; malformed inputs return ErrInvalidHostname. Exact-match only — suffix spoofing ("pay.satschel.com.evil.com") is rejected because the row-per-hostname store does a point lookup on the unique index, never a prefix/LIKE scan.
func (*TenantRepo) FindByID ¶
func (r *TenantRepo) FindByID(id string) (*Tenant, error)
FindByID returns the tenant with the given id, or ErrTenantNotFound.
func (*TenantRepo) List ¶
func (r *TenantRepo) List(limit, offset int) ([]*Tenant, error)
List returns tenants ordered by name ascending for admin dashboards. limit is clamped to [1, 500]; offset is clamped to [0, ∞). A zero limit is treated as 50 to avoid accidental full-table scans.
func (*TenantRepo) UpdateHostnames ¶
func (r *TenantRepo) UpdateHostnames(id string, hostnames []string) error
UpdateHostnames replaces the tenant's hostname claim set atomically. All of the tenant's existing commerce_tenant_hostnames rows are deleted and the canonicalized new set is inserted under the same transaction; the unique index on commerce_tenant_hostnames.hostname rejects any incoming hostname already owned by a different tenant (ErrHostnameClaimed).
Concurrency semantics: serializable via the SQL engine's unique-index contention. Two transactions racing to claim "pay.shared.test" — at most one commits; the other returns ErrHostnameClaimed. No application-layer mutex is required or sufficient across replicas.
func (*TenantRepo) UpdateProviders ¶
func (r *TenantRepo) UpdateProviders(id string, providers []Provider) error
UpdateProviders replaces the tenant's providers list atomically. Concurrency model: last-write-wins. If two admins PUT at the same time, the later save overwrites the earlier one. The audit log (logged at the handler layer) records both attempts so operators can reconcile. A future slice may add optimistic-locking via a row version column — that is out of scope here.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package migrations — commerce-owned base migrations.
|
Package migrations — commerce-owned base migrations. |
|
Package seed — one-shot seed helpers for local dev.
|
Package seed — one-shot seed helpers for local dev. |