chore(aml): drop optional external-Jube provider — luxfi/aml is native

Remove the optional github.com/luxfi/compliance/pkg/jube provider from the
broker compliance layer. AML screening keeps working via the OFAC SDN list
and the ScamSniffer database; the native github.com/luxfi/aml engine (amld)
is the deeper risk-scoring backend. Removed the jube import, the jubeClient
field/option, and every jube-guarded block; docs updated to reflect the
native engine.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-17 12:16:00 -07:00
co-authored by Hanzo Dev
parent b0f5f68db1
commit 53f764d698
5 changed files with 23 additions and 128 deletions
+1 -1
View File
@@ -220,7 +220,7 @@ type Provider interface {
### Compliance (`pkg/compliance/`)
KYC/KYB onboarding, fund management, eSign, RBAC, reporting. Mounted at `/compliance`. PostgreSQL or in-memory store. Includes Jube AML/fraud integration (`compliance/jube/`) and webhook dispatcher (`compliance/webhooks/`).
KYC/KYB onboarding, fund management, eSign, RBAC, reporting. Mounted at `/compliance`. PostgreSQL or in-memory store. AML/fraud screening runs against the OFAC SDN list and the ScamSniffer scam-address database, with the native `github.com/luxfi/aml` engine (`amld`) as the deeper risk-scoring backend, plus a webhook dispatcher (`compliance/webhooks/`).
### Audit (`pkg/audit/`)
+4 -3
View File
@@ -95,7 +95,7 @@ import `provider`, so `provider` cannot import `provider/alpaca`.
### Compliance (pkg/compliance/)
Thin HTTP adapter layer over `github.com/luxfi/compliance` library.
All domain types, store interface, MemoryStore, Jube client, RBAC, and onboarding
All domain types, store interface, MemoryStore, RBAC, and onboarding
logic live in the library. This package re-exports types as aliases and provides:
- HTTP handlers (chi router with RBAC guards, rate limiting, CORS)
- PostgresStore (pgx-backed, implements library's ComplianceStore interface)
@@ -103,11 +103,12 @@ logic live in the library. This package re-exports types as aliases and provides
- Seed data for development mode
Mounted at `/compliance` on the main server. Supports in-memory store (default) or
PostgreSQL via `DATABASE_URL` env var. Jube client imported from library.
PostgreSQL via `DATABASE_URL` env var. AML screening uses OFAC SDN + ScamSniffer
locally, backed by the native `github.com/luxfi/aml` engine (`amld`).
Key endpoint groups under `/compliance`:
- `/kyc` -- Identity verification, document upload, status tracking
- `/aml` -- AML screening (Jube integration), risk assessment, flagged review
- `/aml` -- AML screening (OFAC/ScamSniffer + native luxfi/aml), risk assessment, flagged review
- `/applications` -- 5-step onboarding flow (SSN hashed with HMAC-SHA256)
- `/pipelines` + `/sessions` -- Configurable onboarding pipeline engine
- `/funds` -- Fund management with investor tracking
+8 -107
View File
@@ -7,20 +7,19 @@ import (
"github.com/go-chi/chi/v5"
"github.com/luxfi/broker/pkg/webhook"
"github.com/luxfi/compliance/pkg/jube"
"github.com/rs/zerolog/log"
)
// amlHandler holds AML screening HTTP handler state.
type amlHandler struct {
store ComplianceStore
jubeClient *jube.Client
scamDB *ScamDB
webhookStore webhook.Store
}
// handleScreen runs an AML screening for an account via the Jube sidecar.
// If the Jube client is nil, creates a manual pending screening record.
// handleScreen creates an AML sanctions screening record for an account.
// The record is queued as pending for manual review; wallet screening below
// covers automated OFAC/ScamSniffer checks.
func (h *amlHandler) handleScreen(w http.ResponseWriter, r *http.Request) {
var req struct {
AccountID string `json:"account_id"`
@@ -50,33 +49,6 @@ func (h *amlHandler) handleScreen(w http.ResponseWriter, r *http.Request) {
Provider: "manual",
}
// If Jube is configured, run sanctions check.
if h.jubeClient != nil {
result, err := h.jubeClient.CheckSanctions(r.Context(), req.Name, req.Country)
if err != nil {
log.Error().Err(err).Str("account", req.AccountID).Msg("aml: jube sanctions check failed")
screening.Provider = "jube"
screening.Details = "screening service unavailable"
// Save as pending for manual review.
} else {
screening.Provider = "jube"
screening.SanctionsHit = result.Hit
if result.Hit {
screening.Status = AMLFlagged
screening.RiskLevel = RiskHigh
screening.RiskScore = 80.0
if len(result.Matches) > 0 {
screening.RiskScore = result.Matches[0].Score * 100
}
screening.Details = "sanctions list match found"
} else {
screening.Status = AMLCleared
screening.RiskLevel = RiskLow
screening.RiskScore = 0
}
}
}
if err := h.store.SaveAMLScreening(screening); err != nil {
log.Error().Err(err).Str("account", req.AccountID).Msg("aml: failed to save screening")
writeError(w, http.StatusInternalServerError, "internal error")
@@ -208,10 +180,11 @@ type walletScreenResponse struct {
Risk string `json:"risk"` // low, medium, high, blocked
Sanctioned bool `json:"sanctioned"`
Scam bool `json:"scam"`
Source string `json:"source"` // ofac, scamsniffer, jube
Source string `json:"source"` // ofac, scamsniffer
}
// handleWalletScreen checks a crypto wallet address against OFAC SDN and Jube.
// handleWalletScreen checks a crypto wallet address against the OFAC SDN list
// and the ScamSniffer scam-address database.
func (h *amlHandler) handleWalletScreen(w http.ResponseWriter, r *http.Request) {
var req walletScreenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -278,39 +251,7 @@ func (h *amlHandler) handleWalletScreen(w http.ResponseWriter, r *http.Request)
}
}
// 3. Check via Jube for real-time risk scoring.
if h.jubeClient != nil {
txReq := jube.TransactionRequest{
EntityAnalysisModelID: 1,
EntityInstanceEntryPayload: map[string]interface{}{
"AccountId": req.Address,
"EntityType": "wallet",
"Chain": req.Chain,
"Direction": req.Direction,
},
}
jubeResp, err := h.jubeClient.ScreenTransaction(r.Context(), txReq)
if err != nil {
log.Error().Err(err).Str("address", req.Address).Msg("aml: jube wallet screen failed")
// Jube unavailable -- return OFAC-only result (already low/clear).
} else {
resp.Source = "jube"
switch {
case jubeResp.Action == jube.ActionBlock || jubeResp.Score >= 80:
resp.Risk = "blocked"
resp.Sanctioned = true
case jubeResp.Score >= 60:
resp.Risk = "high"
case jubeResp.Score >= 30:
resp.Risk = "medium"
default:
resp.Risk = "low"
}
}
}
// Default source if neither OFAC nor Jube set it.
// Default source when neither OFAC nor ScamSniffer flagged the address.
if resp.Source == "" {
resp.Source = "ofac"
}
@@ -318,7 +259,7 @@ func (h *amlHandler) handleWalletScreen(w http.ResponseWriter, r *http.Request)
writeJSON(w, http.StatusOK, resp)
}
// handleRiskAssessment runs a risk assessment by screening a transaction through Jube.
// handleRiskAssessment records a transaction risk assessment for manual review.
func (h *amlHandler) handleRiskAssessment(w http.ResponseWriter, r *http.Request) {
var req struct {
AccountID string `json:"account_id"`
@@ -349,46 +290,6 @@ func (h *amlHandler) handleRiskAssessment(w http.ResponseWriter, r *http.Request
Provider: "manual",
}
if h.jubeClient != nil {
txReq := jube.TransactionRequest{
EntityAnalysisModelID: 1,
EntityInstanceEntryPayload: map[string]interface{}{
"AccountId": req.AccountID,
"Amount": req.Amount,
"Currency": req.Currency,
"Type": req.Type,
},
}
resp, err := h.jubeClient.ScreenTransaction(r.Context(), txReq)
if err != nil {
log.Error().Err(err).Str("account", req.AccountID).Msg("aml: jube risk assessment failed")
} else {
screening.Provider = "jube"
screening.RiskScore = resp.Score
switch {
case resp.Score >= 80:
screening.RiskLevel = RiskCritical
case resp.Score >= 60:
screening.RiskLevel = RiskHigh
case resp.Score >= 30:
screening.RiskLevel = RiskMedium
default:
screening.RiskLevel = RiskLow
}
switch resp.Action {
case jube.ActionBlock:
screening.Status = AMLBlocked
case jube.ActionReview:
screening.Status = AMLFlagged
default:
screening.Status = AMLCleared
}
}
}
if err := h.store.SaveAMLScreening(screening); err != nil {
writeError(w, http.StatusInternalServerError, "internal error")
return
+8 -8
View File
@@ -2150,9 +2150,9 @@ func TestAMLListByAccountMissingParam(t *testing.T) {
func TestAMLListFlagged(t *testing.T) {
r, store := newTestRouter()
store.SaveAMLScreening(&AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "jube"})
store.SaveAMLScreening(&AMLScreening{AccountID: "a2", Status: AMLCleared, Provider: "jube"})
store.SaveAMLScreening(&AMLScreening{AccountID: "a3", Status: AMLFlagged, Provider: "jube"})
store.SaveAMLScreening(&AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "manual"})
store.SaveAMLScreening(&AMLScreening{AccountID: "a2", Status: AMLCleared, Provider: "manual"})
store.SaveAMLScreening(&AMLScreening{AccountID: "a3", Status: AMLFlagged, Provider: "manual"})
w := doRequest(r, "GET", "/aml/flagged", nil)
if w.Code != http.StatusOK {
@@ -2168,7 +2168,7 @@ func TestAMLListFlagged(t *testing.T) {
func TestAMLReview(t *testing.T) {
r, store := newTestRouter()
sc := &AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "jube"}
sc := &AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "manual"}
store.SaveAMLScreening(sc)
// Review and clear. reviewed_by comes from JWT context (testadmin), not request body.
@@ -2193,7 +2193,7 @@ func TestAMLReview(t *testing.T) {
func TestAMLReviewBadDecision(t *testing.T) {
r, store := newTestRouter()
sc := &AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "jube"}
sc := &AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "manual"}
store.SaveAMLScreening(sc)
w := doRequest(r, "POST", "/aml/screenings/"+sc.ID+"/review", map[string]string{
@@ -2208,7 +2208,7 @@ func TestAMLReviewBadDecision(t *testing.T) {
func TestAMLReviewIgnoresBodyReviewedBy(t *testing.T) {
r, store := newTestRouter()
sc := &AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "jube"}
sc := &AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "manual"}
store.SaveAMLScreening(sc)
// Send reviewed_by in body attempting to spoof identity.
@@ -2852,7 +2852,7 @@ func TestStoreGetRoleByNameNotFound(t *testing.T) {
func TestSecurityAMLReviewerSpoofingPrevented(t *testing.T) {
r, store := newTestRouter()
sc := &AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "jube"}
sc := &AMLScreening{AccountID: "a1", Status: AMLFlagged, Provider: "manual"}
store.SaveAMLScreening(sc)
// Attacker tries to claim review was by "bob" but JWT says "test-user-001".
@@ -3067,7 +3067,7 @@ func TestWalletScreenCleanAddress(t *testing.T) {
t.Fatal("expected scam=false for clean address")
}
if resp.Source != "ofac" {
t.Fatalf("expected source ofac (no jube configured), got %s", resp.Source)
t.Fatalf("expected source ofac (no external provider configured), got %s", resp.Source)
}
}
+2 -9
View File
@@ -13,24 +13,17 @@ import (
"github.com/luxfi/broker/pkg/auth"
"github.com/luxfi/broker/pkg/provider"
"github.com/luxfi/broker/pkg/webhook"
"github.com/luxfi/compliance/pkg/jube"
)
// RouterOption configures optional dependencies for the compliance router.
type RouterOption func(*routerConfig)
type routerConfig struct {
jubeClient *jube.Client
scamDB *ScamDB
registry *provider.Registry
webhookStore webhook.Store
}
// WithJubeClient adds the Jube AML sidecar client for live screening.
func WithJubeClient(c *jube.Client) RouterOption {
return func(cfg *routerConfig) { cfg.jubeClient = c }
}
// WithScamDB adds the ScamSniffer scam address database for wallet screening.
func WithScamDB(db *ScamDB) RouterOption {
return func(cfg *routerConfig) { cfg.scamDB = db }
@@ -51,7 +44,7 @@ func WithWebhookStore(s webhook.Store) RouterOption {
// The adminStore parameter provides authentication and RBAC. When non-nil,
// all routes (except /healthz) require a valid admin JWT and write operations
// are gated by role-based permissions.
// Optional RouterOption values add the auth store and Jube client.
// Optional RouterOption values add the scam database, provider registry, and webhook store.
func NewRouter(store ComplianceStore, opts ...RouterOption) chi.Router {
if store == nil {
store = NewMemoryStore()
@@ -74,7 +67,7 @@ func NewRouter(store ComplianceStore, opts ...RouterOption) chi.Router {
settings := &settingsHandler{store: store}
creds := &credentialsHandler{store: store}
billing := &billingHandler{}
aml := &amlHandler{store: store, jubeClient: cfg.jubeClient, scamDB: cfg.scamDB, webhookStore: cfg.webhookStore}
aml := &amlHandler{store: store, scamDB: cfg.scamDB, webhookStore: cfg.webhookStore}
apps := &applicationHandler{store: store, registry: cfg.registry, webhookStore: cfg.webhookStore}
// Compliance endpoints are restricted to the admin org (built-in).