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 KVStore
- func (s *KVStore) CompareAndDelete(key string, want []byte) (bool, error)
- func (s *KVStore) CompareAndExtend(key string, want []byte, ttl time.Duration) (bool, error)
- func (s *KVStore) Delete(key string) error
- func (s *KVStore) Exists(key string) (bool, error)
- func (s *KVStore) Get(key string) ([]byte, error)
- func (s *KVStore) Set(key string, value []byte, ttl time.Duration) error
- func (s *KVStore) SetNX(key string, value []byte, ttl time.Duration) (bool, error)
- 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) 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 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 ErrKVNotFound = errors.New("store: kv key not found")
ErrKVNotFound is returned by KVStore.Get and KVStore.GetExpiry when no live (unexpired) entry exists for the key. Callers translate to a cache miss — never to a 500.
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 KVStore ¶
type KVStore struct {
// contains filtered or unexported fields
}
KVStore is the base-backed key/value cache that replaces the former Redis/Valkey client. It persists into the commerce_kv collection (per-store SQLite file, or Postgres when DataDSN is set), giving the same single-tenant embedded-data backend the rest of commerce already uses.
Semantics match the subset of the old KV interface that commerce actually consumed:
- Get — returns the live value or ErrKVNotFound. Expired entries are deleted lazily on read and reported as ErrKVNotFound.
- Set — upserts; ttl==0 means no expiry (sentinel expires_at = 0).
- Delete — removes by key; deleting an absent key is not an error.
- SetNX — atomic set-if-absent (drives distributed locking); an expired entry counts as absent and is overwritten.
- CompareAndDelete / CompareAndExtend — atomic lock release/renew without server-side scripting (base SQLite transactions are serializable).
All mutating operations run inside RunInTransaction so concurrent writers cannot corrupt or race the upsert; SQLite's serializable isolation makes the read-modify-write atomic.
func NewKVStore ¶
NewKVStore wraps a base app. The commerce_kv collection must already exist (the migration under store/migrations creates it on Bootstrap).
func (*KVStore) CompareAndDelete ¶
CompareAndDelete atomically deletes key only if its current live value equals want. Returns true on delete, false when the value differs or the key is absent/expired. Replaces the Redis compare-and-delete Lua script.
func (*KVStore) CompareAndExtend ¶
CompareAndExtend atomically resets key's ttl only if its current live value equals want. Returns true on extend, false when the value differs or the key is absent/expired. Replaces the Redis compare-and-extend Lua script.
func (*KVStore) Delete ¶
Delete removes key. Deleting an absent (or already-expired) key is a no-op, not an error — matching the old Redis DEL semantics commerce relied on.
func (*KVStore) Get ¶
Get returns the live value for key. Expired entries are deleted lazily and reported as ErrKVNotFound.
func (*KVStore) Set ¶
Set upserts key→value with the given ttl (0 == no expiry). Atomic under RunInTransaction: a concurrent Set on the same key serializes.
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
KV *KVStore
}
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. Hostnames are normalized before save; the Name field is required and must be unique (enforced by the idx_commerce_tenants_name index — duplicate returns ErrDuplicateTenant). The caller MUST have already checked that the current session has 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. The input is normalized (lowercase, trailing-dot stripped, port stripped) before any match runs; malformed inputs return ErrInvalidHostname. Exact-match only — suffix spoofing ("pay.tenant.example.com.evil.com") does not match.
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) 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. |