diff --git a/Makefile b/Makefile index 0652fcd..edc5a03 100644 --- a/Makefile +++ b/Makefile @@ -8,3 +8,15 @@ test-frontend: cd frontend/packages/sxchnge_theme && flutter test cd frontend/packages/sxchnge_theme/example && flutter test cd frontend/app && flutter test + +.PHONY: test-backend lint-backend run-backend + +test-backend: + cd backend && go test -race ./... + +lint-backend: + cd backend && test -z "$$(gofmt -l .)" + cd backend && go vet ./... + +run-backend: + cd backend && go run ./cmd/bff diff --git a/README.md b/README.md index fc81f11..c511b64 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ with `go_router`. Its interface uses the shared theme's colors and typography. **Authentication, email verification and analytics use dummy services.** No real account or session is created, no email is delivered, and no analytics data is -transmitted. The backend is documentation only. Session/KYC access guards and -Sendico integration are not implemented. +transmitted. The backend has an independent Go operational shell; product services +are not connected. Session/KYC access guards and Sendico integration are not +implemented. ## Repository structure @@ -23,7 +24,7 @@ Sendico integration are not implemented. | [`sxchnge_theme`](frontend/packages/sxchnge_theme/README.md) | Reusable light/dark themes and bundled Onest font | | [`sxchnge_auth`](frontend/packages/sxchnge_auth/README.md) | Shared auth contracts, providers, two-factor page and dummy services | | [`sxchnge_analytics`](frontend/packages/sxchnge_analytics/README.md) | Typed journey events, recorder and bounded in-memory dummy analytics | -| [`backend/`](backend/README.md) | Planned BFF and Sendico integration boundaries | +| [`backend/`](backend/README.md) | Independent Go BFF starter, adapted foundations and provider integration plan | | [`.agents/skills/`](.agents/skills) | Repository agent skills | | [`docs/`](docs/implementation.md) | Implementation records and UX measurement contracts | @@ -152,9 +153,9 @@ The intended integration is: **consumer app → sxchnge BFF → supported Sendic - The sxchnge organization, backend integration principal and individual customer references are distinct identities. Consumers must not share the integration account's customer identity. -- The BFF should use a supported API contract rather than copying Sendico's BFF - or depending on its local internal modules. Integration credentials belong in - the backend. +- The BFF uses a supported provider API. Selected Sendico components may be + adapted into SXCHNGE-owned code; local Sendico module dependencies are not used. + Integration credentials belong in the backend. Before backend integration, agree on the customer, KYC, eligibility/limits and customer-scoped payment contracts for one complete transaction flow. These are @@ -224,3 +225,14 @@ instructions also support authenticator challenges; actual TOTP enrollment, validation and recovery remain backend integration work. See the [two-factor UX contract](docs/ux/two-factor.md) and [shared auth package](frontend/packages/sxchnge_auth/README.md). + +## Backend integration plan + +The [BFF architecture and Sendico service audit](docs/architecture/bff.md) defines +consumer identity, financial-provider boundaries, reusable components, contract +gaps and delivery phases. Selected Sendico code has been adapted with recorded +[provenance](backend/PROVENANCE.md); the projects share no runtime modules. + +Run `make test-backend`, `make lint-backend` or `make run-backend` from the root. +The starter exposes health and unconfigured readiness only. Authentication, +persistence and live financial operations remain the next implementation phases. diff --git a/backend/AGENTS.md b/backend/AGENTS.md new file mode 100644 index 0000000..1a271dd --- /dev/null +++ b/backend/AGENTS.md @@ -0,0 +1,15 @@ +# BFF guidelines + +Follow root AGENTS.md. This is an independent Go module; Sendico is an external +provider, not a local module dependency. Never add sibling-path replacements, +shared database access or internal Sendico discovery/messaging dependencies. + +Keep HTTP DTOs/mappers, application models/use cases and provider adapters distinct. +Resolve ownership from the authenticated SXCHNGE principal and server-side mappings. +Only adapters may expose provider transport types internally. Adapted code belongs +to SXCHNGE; record source revision and changes in PROVENANCE.md. + +Review the complete backend contribution before delivery. Run `make test-backend` +and `make lint-backend` at the repository root. Add behavioral tests for changed +boundaries and failure paths. Errors returned publicly must be stable typed codes; +never expose raw provider errors, secrets, document contents or credentials. diff --git a/backend/PROVENANCE.md b/backend/PROVENANCE.md new file mode 100644 index 0000000..6a7f17c --- /dev/null +++ b/backend/PROVENANCE.md @@ -0,0 +1,38 @@ +# Sendico reuse provenance + +Source repository: `S_I/sendico`; revision `8bb20dcd622dd8eb92d289b6b34411e7eccab083`. +Inspected 2026-09-12. These are owned forks inside SXCHNGE, not runtime imports. +Source files below match the committed revision; local dependency edits were not used. +No generated Sendico SDK, shared database models, deployment configuration or credentials were copied. + +## `internal/browser/policy.go` + +- Source: [api/edge/shared/auth/browser/browser.go](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/shared/auth/browser/browser.go) +- Source Git blob: `3af7b54aea277d91e0d8c3eba9ee84c155f4b624` +- Adaptation: Adapted cookie creation/clearing, origin validation and CSRF checks. Replaced Sendico names/errors/env path resolution with explicit SXCHNGE policy; checks do not depend on a client-controlled transport header. Host-only Secure/HttpOnly cookie, Lax same-site navigation, duplicate-cookie rejection and explicit expiry validation. +- Tests are SXCHNGE-specific behavioral checks for the adapted boundary. + +## `internal/credential/token.go` + +- Source: [api/edge/shared/storage/verification/hash.go](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/shared/storage/verification/hash.go) +- Source Git blob: `36fadcf404bfc77608b28cca1e669a4a62f31774` +- Adaptation: Adapted generateMagic/tokenHash only: 32-byte crypto/rand, URL-safe encoding, SHA-256 digest. Added constant-time matching. Excluded deterministic token generation, Sendico models and all numeric OTP hashing. +- Tests are SXCHNGE-specific behavioral checks for the adapted boundary. + +## `internal/evidence/key.go` + +- Source: [api/edge/shared/storage/s3key/key.go](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/shared/storage/s3key/key.go) +- Source Git blob: `e3cca16304d2c4cb9f256f510d5a1a30a21e22a6` +- Adaptation: Adapted relative object-key confinement. Replaced merrors with a local sentinel and rejected encoded sequences, backslashes and embedded control characters. Not a filesystem or customer-authorization utility. +- Tests are SXCHNGE-specific behavioral checks for the adapted boundary. + +## Maintenance + +The command, operational HTTP shell, module configuration and tests are new SXCHNGE code. +There is no automatic synchronization or promise of source compatibility with Sendico. +For an upstream fix, review the pinned source difference, port only the relevant behavior, +record a new provenance entry and run backend tests/vet. Provider contract changes belong +in the future Sendico adapter and its transport fixtures, not application models or widgets. + +These primitives do not create sessions, deliver email, validate TOTP or upload documents. +Use them inside the durable flows described in the [BFF plan](../docs/architecture/bff.md). diff --git a/backend/README.md b/backend/README.md index 2ea8c76..5f44a9c 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,14 +1,38 @@ -# SXCHNGE backend +# SXCHNGE BFF -Reserved for a small backend for frontend (BFF) using a supported Sendico API. No service, runtime, or dependencies have been initialized yet. +Independent Go module with an operational HTTP shell and selected, adapted +Sendico foundations. Sendico remains an external financial-services provider; +there are no Sendico module imports, shared databases or sibling-path dependencies. +See the [architecture and service plan](../docs/architecture/bff.md) and +[reuse provenance](PROVENANCE.md). -Planned responsibilities: +## Run and validate -- Consumer authentication and sessions, with a stable mapping to organization-scoped Sendico customer references. -- Consumer-authorized access to onboarding, quotes, orders, and transaction history. -- Product-specific orchestration and presentation of Sendico KYC, eligibility, and remaining-limit responses. -- Server-side integration credentials and request correlation with Sendico. +Requires Go 1.26 or later. From the repository root: -Sendico owns payment execution, authoritative KYC evidence and verification records, policy evaluation, and limit reservations/enforcement. Do not copy its existing BFF or import local Sendico implementation modules. +```sh +make test-backend +make lint-backend +make run-backend +``` -Before implementation, agree on customer registration and identity mapping, customer KYC operations, eligibility/limits reads, customer-scoped payment access, and retry/idempotency behavior. Validate consumer isolation even when multiple customers use the same sxchnge organization and integration principal. +The default address is `127.0.0.1:8082`; override with `SXCHNGE_HTTP_ADDR`. +`GET /healthz` reports process health. `GET /readyz` deliberately returns **503** +with `not_configured`: persistence, authentication and Sendico are not connected. +No product routes return dummy success. The process handles SIGINT/SIGTERM with +bounded graceful shutdown. Production transport must terminate HTTPS. + +## Present components + +- `cmd/bff`, `internal/httpapi`: HTTP process, timeouts and operational endpoints. +- `internal/browser`: explicit HTTPS origin/CSRF policy and Secure HttpOnly + host-only session cookie. Apply this policy to every future browser mutation; + it is not yet connected to product routes because those do not exist. +- `internal/credential`: random opaque credentials and storage digests. Never use + this helper for passwords or short OTP codes. +- `internal/evidence`: confined server-generated object keys; not a substitute + for ownership, upload validation or private storage controls. + +Only the standard library is required. Larger adapted flows, persistence, +provider DTOs/mappers, email and live integration follow the plan in separate +contributions. Source-derived components evolve independently inside this repo. diff --git a/backend/cmd/bff/main.go b/backend/cmd/bff/main.go new file mode 100644 index 0000000..da44535 --- /dev/null +++ b/backend/cmd/bff/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "errors" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "git.sendico.io/tech/sxchnge/bff/internal/httpapi" +) + +func main() { + if err := run(); err != nil { + slog.Error("BFF stopped", "error", err) + os.Exit(1) + } +} + +func run() error { + addr := os.Getenv("SXCHNGE_HTTP_ADDR") + if addr == "" { + addr = "127.0.0.1:8082" + } + server := &http.Server{Addr: addr, Handler: httpapi.New(), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 20} + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + failures := make(chan error, 1) + go func() { failures <- server.ListenAndServe() }() + slog.Info("BFF operational shell starting", "address", addr, "provider_configured", false) + select { + case err := <-failures: + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return err + case <-ctx.Done(): + shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + slog.Info("BFF shutting down") + return server.Shutdown(shutdown) + } +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..e43372c --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,3 @@ +module git.sendico.io/tech/sxchnge/bff + +go 1.26 diff --git a/backend/internal/browser/policy.go b/backend/internal/browser/policy.go new file mode 100644 index 0000000..2e029a9 --- /dev/null +++ b/backend/internal/browser/policy.go @@ -0,0 +1,118 @@ +// Adapted from Sendico edge/shared/auth/browser; see backend/PROVENANCE.md. +package browser + +import ( + "errors" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + SessionCookie = "__Host-sxchnge_session" + CSRFHeader = "X-CSRF-Protection" + CSRFValue = "1" +) + +var ( + ErrOrigin = errors.New("browser origin is not allowed") + ErrCSRF = errors.New("browser CSRF protection is required") + ErrSession = errors.New("browser session is missing or invalid") +) + +type Policy struct{ origins map[string]struct{} } + +// New accepts explicit HTTPS origins. Browser session transport is chosen by the +// server route; a client-controlled transport header cannot turn protection off. +func New(allowedOrigins []string) (*Policy, error) { + p := &Policy{origins: make(map[string]struct{}, len(allowedOrigins))} + for _, value := range allowedOrigins { + origin, ok := canonicalOrigin(value) + if !ok { + return nil, ErrOrigin + } + p.origins[origin] = struct{}{} + } + if len(p.origins) == 0 { + return nil, ErrOrigin + } + return p, nil +} + +func canonicalOrigin(value string) (string, bool) { + u, err := url.Parse(value) + if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.Path != "" || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || u.Opaque != "" { + return "", false + } + return "https://" + strings.ToLower(u.Host), true +} + +// Validate must guard every browser-session mutation, including login/logout. +// It assumes CORS never grants credentialed access to untrusted origins. +func (p *Policy) Validate(r *http.Request) error { + if r == nil { + return ErrCSRF + } + switch r.Method { + case http.MethodGet, http.MethodHead, http.MethodOptions: + return nil + } + if r.Header.Get(CSRFHeader) != CSRFValue { + return ErrCSRF + } + origin, ok := canonicalOrigin(r.Header.Get("Origin")) + if !ok || p == nil { + return ErrOrigin + } + if _, allowed := p.origins[origin]; !allowed { + return ErrOrigin + } + return nil +} + +func Credential(r *http.Request) (string, error) { + if r == nil { + return "", ErrSession + } + var value string + for _, c := range r.Cookies() { + if c.Name != SessionCookie { + continue + } + if value != "" || c.Value == "" { + return "", ErrSession + } + value = c.Value + } + if value == "" { + return "", ErrSession + } + return value, nil +} + +func SetSession(w http.ResponseWriter, token string, now, expires time.Time) error { + age := int(expires.Sub(now).Seconds()) + if token == "" || age < 1 { + return ErrSession + } + c := cookie(token) + c.Expires = expires.UTC() + c.MaxAge = age + if c.Valid() != nil { + return ErrSession + } + http.SetCookie(w, c) + return nil +} + +func ClearSession(w http.ResponseWriter) { + c := cookie("") + c.Expires = time.Unix(1, 0).UTC() + c.MaxAge = -1 + http.SetCookie(w, c) +} + +func cookie(value string) *http.Cookie { + return &http.Cookie{Name: SessionCookie, Value: value, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode} +} diff --git a/backend/internal/browser/policy_test.go b/backend/internal/browser/policy_test.go new file mode 100644 index 0000000..ba01a3c --- /dev/null +++ b/backend/internal/browser/policy_test.go @@ -0,0 +1,79 @@ +package browser + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestMutationRequiresOriginAndHeader(t *testing.T) { + p, err := New([]string{"https://app.example.com"}) + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + origin, header, transport string + valid bool + }{ + {"https://app.example.com", "1", "", true}, + {"https://APP.example.com", "1", "", true}, + {"https://evil.example.com", "1", "", false}, + {"https://app.example.com.evil.test", "1", "", false}, + {"https://app.example.com/path", "1", "", false}, + {"https://app.example.com?query", "1", "", false}, + {"https://app.example.com#fragment", "1", "", false}, + {"https://app.example.com", "", "bearer", false}, + {"", "1", "bearer", false}, + {"null", "1", "cookie", false}, + } { + r := httptest.NewRequest(http.MethodPost, "https://app.example.com/api/session", nil) + r.Header.Set("Origin", tc.origin) + r.Header.Set(CSRFHeader, tc.header) + r.Header.Set("X-Auth-Transport", tc.transport) + if got := p.Validate(r); (got == nil) != tc.valid { + t.Errorf("%+v: %v", tc, got) + } + } + if p.Validate(httptest.NewRequest(http.MethodGet, "https://app.example.com/api/session", nil)) != nil { + t.Fatal("read rejected") + } + for _, origin := range []string{"*", "http://app.example.com", "https://user@app.example.com", "https://app.example.com/"} { + if _, err := New([]string{origin}); err == nil { + t.Errorf("accepted configuration %q", origin) + } + } + if _, err := New(nil); err == nil { + t.Fatal("empty origin policy accepted") + } +} + +func TestSessionCookie(t *testing.T) { + now := time.Date(2026, 9, 12, 0, 0, 0, 0, time.UTC) + w := httptest.NewRecorder() + if err := SetSession(w, "opaque-token", now, now.Add(time.Hour)); err != nil { + t.Fatal(err) + } + c := w.Result().Cookies()[0] + if c.Name != SessionCookie || !c.Secure || !c.HttpOnly || c.Path != "/" || c.Domain != "" || c.SameSite != http.SameSiteLaxMode || c.MaxAge != 3600 { + t.Fatalf("bad cookie: %+v", c) + } + r := httptest.NewRequest(http.MethodGet, "https://app.example.com/api/session", nil) + r.AddCookie(c) + if value, err := Credential(r); err != nil || value != "opaque-token" { + t.Fatal("cookie not read") + } + r.AddCookie(c) + if _, err := Credential(r); err == nil { + t.Fatal("ambiguous credential accepted") + } + if err := SetSession(httptest.NewRecorder(), "token", now, now); err == nil { + t.Fatal("expired cookie accepted") + } + w = httptest.NewRecorder() + ClearSession(w) + c = w.Result().Cookies()[0] + if c.MaxAge != -1 || c.Value != "" || c.Path != "/" || !c.Secure { + t.Fatal("cookie not cleared safely") + } +} diff --git a/backend/internal/credential/token.go b/backend/internal/credential/token.go new file mode 100644 index 0000000..e61c2ab --- /dev/null +++ b/backend/internal/credential/token.go @@ -0,0 +1,29 @@ +// Adapted from Sendico verification/hash.go; see backend/PROVENANCE.md. +package credential + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" +) + +// New returns an opaque credential and its storage digest. Store only the digest. +// This is for high-entropy tokens, never passwords or short numeric OTP codes. +func New() (raw, digest string, err error) { + bytes := make([]byte, 32) + if _, err = rand.Read(bytes); err != nil { + return "", "", err + } + raw = base64.RawURLEncoding.EncodeToString(bytes) + return raw, Digest(raw), nil +} + +func Digest(raw string) string { + sum := sha256.Sum256([]byte(raw)) + return base64.RawURLEncoding.EncodeToString(sum[:]) +} + +func Matches(raw, digest string) bool { + return raw != "" && subtle.ConstantTimeCompare([]byte(Digest(raw)), []byte(digest)) == 1 +} diff --git a/backend/internal/credential/token_test.go b/backend/internal/credential/token_test.go new file mode 100644 index 0000000..1c5ef23 --- /dev/null +++ b/backend/internal/credential/token_test.go @@ -0,0 +1,24 @@ +package credential + +import ( + "encoding/base64" + "testing" +) + +func TestOpaqueCredential(t *testing.T) { + raw, digest, err := New() + if err != nil { + t.Fatal(err) + } + decoded, err := base64.RawURLEncoding.DecodeString(raw) + if err != nil || len(decoded) != 32 { + t.Fatal("credential must contain 256 random bits") + } + if raw == digest || !Matches(raw, digest) || Matches("other", digest) || Matches("", Digest("")) { + t.Fatal("invalid digest matching behavior") + } + other, _, err := New() + if err != nil || other == raw { + t.Fatal("credentials must be independently generated") + } +} diff --git a/backend/internal/evidence/key.go b/backend/internal/evidence/key.go new file mode 100644 index 0000000..287e669 --- /dev/null +++ b/backend/internal/evidence/key.go @@ -0,0 +1,43 @@ +// Adapted from Sendico edge/shared/storage/s3key; see backend/PROVENANCE.md. +package evidence + +import ( + "errors" + "path" + "strings" + "unicode" +) + +var ErrInvalidKey = errors.New("invalid evidence key") + +// JoinKey confines a server-generated object reference to a relative prefix. +// It is not an ownership check or a local-filesystem path sanitizer. Inputs must +// not be URL-decoded or interpreted as filesystem paths after validation. +func JoinKey(prefix, reference string) (string, error) { + a, err := relative(prefix) + if err != nil { + return "", err + } + b, err := relative(reference) + if err != nil { + return "", err + } + return path.Join(a, b), nil +} + +func relative(value string) (string, error) { + value = strings.TrimSpace(value) + if strings.ContainsAny(value, "\\%") || strings.ContainsFunc(value, unicode.IsControl) { + return "", ErrInvalidKey + } + for _, segment := range strings.Split(value, "/") { + if segment == ".." { + return "", ErrInvalidKey + } + } + clean := path.Clean(value) + if value == "" || clean == "." || path.IsAbs(clean) { + return "", ErrInvalidKey + } + return clean, nil +} diff --git a/backend/internal/evidence/key_test.go b/backend/internal/evidence/key_test.go new file mode 100644 index 0000000..d9a7c85 --- /dev/null +++ b/backend/internal/evidence/key_test.go @@ -0,0 +1,18 @@ +package evidence + +import "testing" + +func TestKeyConfinement(t *testing.T) { + for _, ref := range []string{"", ".", "..", "../other", "/other", "a/../../other", "a\\..\\other", "%2e%2e/other", "bad\x00key"} { + if _, err := JoinKey("customer/documents", ref); err == nil { + t.Errorf("accepted %q", ref) + } + if _, err := JoinKey(ref, "document"); err == nil { + t.Errorf("accepted prefix %q", ref) + } + } + got, err := JoinKey("customer/documents/", "passport/front") + if err != nil || got != "customer/documents/passport/front" { + t.Fatalf("got %q, %v", got, err) + } +} diff --git a/backend/internal/httpapi/router.go b/backend/internal/httpapi/router.go new file mode 100644 index 0000000..50a5eba --- /dev/null +++ b/backend/internal/httpapi/router.go @@ -0,0 +1,28 @@ +package httpapi + +import ( + "encoding/json" + "net/http" +) + +// New is the operational shell. Product routes will be added only with their +// persistence, authentication and provider adapters, never simulated success. +func New() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { + write(w, http.StatusOK, "ok") + }) + mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) { + write(w, http.StatusServiceUnavailable, "not_configured") + }) + return mux +} + +func write(w http.ResponseWriter, status int, value string) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(struct { + Status string `json:"status"` + }{value}) +} diff --git a/backend/internal/httpapi/router_test.go b/backend/internal/httpapi/router_test.go new file mode 100644 index 0000000..3bc4efe --- /dev/null +++ b/backend/internal/httpapi/router_test.go @@ -0,0 +1,17 @@ +package httpapi + +import ( + "net/http/httptest" + "testing" +) + +func TestOperationalShellDoesNotClaimReadiness(t *testing.T) { + handler := New() + for path, want := range map[string]int{"/healthz": 200, "/readyz": 503, "/api/v1/session": 404} { + w := httptest.NewRecorder() + handler.ServeHTTP(w, httptest.NewRequest("GET", path, nil)) + if w.Code != want { + t.Errorf("%s: %d, want %d", path, w.Code, want) + } + } +} diff --git a/docs/architecture/bff.md b/docs/architecture/bff.md new file mode 100644 index 0000000..989402a --- /dev/null +++ b/docs/architecture/bff.md @@ -0,0 +1,300 @@ +# Independent SXCHNGE BFF — task #34 + +## Decision and delivery scope + +Build a small, independently deployed Go BFF in `backend/`. Adapt selected Sendico +code into SXCHNGE ownership; treat Sendico as an external financial provider through +a versioned partner contract. Use one BFF process initially, with a worker entrypoint +from the same module when durable asynchronous work is introduced. Do not reproduce +Sendico's service topology inside SXCHNGE. + +This contribution delivers the architecture/reuse audit and a tested operational +starter. It does **not** implement account persistence, real authentication, email, +KYC uploads, money operations or a live Sendico adapter. The starter has health +and explicitly unconfigured readiness endpoints. Selected browser-session, +opaque-credential and evidence-key components are adapted now; the larger flows +below are planned extractions, not claims of completed integration. + +Audit basis: local `S_I/sendico` checkout, branch `compliance-ingestor-2167`, commit +`8bb20dcd622dd8eb92d289b6b34411e7eccab083`, inspected 2026-09-12. The checkout has +unrelated dependency-manifest edits, which were not copied or modified. Findings +establish source capabilities at that revision, not deployment availability, +external API support, or production readiness. See [source references](#source-references) +and [reuse provenance](../../backend/PROVENANCE.md). + +## Ownership and separation + +```mermaid +flowchart LR + UI[Flutter web / mobile] --> BFF[SXCHNGE BFF\nconsumer API and authorization] + BFF --> DB[(SXCHNGE database\nsessions, mappings, commands)] + BFF --> MAIL[Email delivery adapter] + BFF --> ADAPTER[SXCHNGE Sendico adapter\nDTOs and mapping] + ADAPTER --> EDGE[Sendico partner API\nauthenticated customer scope] + EDGE --> FIN[Sendico ledger, quotes, payments\nlimits, compliance, inbound] + EDGE --> EVENTS[Signed callback delivery] + EVENTS --> BFF +``` + +| Owner | Responsibilities | +| --- | --- | +| SXCHNGE | Consumer accounts/passwords, email confirmation, sessions, language, MFA policy/enrollment, local customer mapping, user authorization, operation intents, idempotency/outbox/inbox, product DTOs and safe status explanations | +| Sendico | Canonical financial accounts and posted money, spendability rules, currency/rail catalog, quotes/rates/fees, execution/settlement, authoritative limits and reservations, KYC evidence/results, compliance decisions and review workflows | +| Provider boundary | Authenticated partner principal, organization and customer isolation, stable identifiers, versioned transport, error/status semantics, replay guarantees, signed events and recovery reads | + +Sendico integration credentials stay exclusively in the BFF's secret configuration. +SXCHNGE users do not receive Sendico credentials, internal gRPC addresses, database +access, or membership/admin privileges on its integration organization. The BFF +never repairs balances by posting arbitrary ledger entries and never independently +reserves limits already owned by Sendico's payment execution. + +### Three identities must stay distinct + +Persist a server-owned mapping from `sxchnge_user_id` to a Sendico provider scope: +`organization_ref`, `customer_ref`, `compliance_subject_ref`, and, where Sendico's +ownership contracts require it, `ledger_owner_account_ref`. These fields are not +assumed equal. The partner integration principal is a fifth, separate identity. +Use unique indexes for local user/provider and provider organization/customer. +Provisioning is idempotent using a stable external customer reference. + +Do not accept organization, customer, initiator or owner refs from browser payloads. +Resolve local account/quote/operation IDs under the authenticated user first; send +only mapped refs. Sendico must enforce the same scope independently. List filtering +is mandatory before results are returned; direct reads must return the same safe +not-found outcome for missing and inaccessible objects. + +**Blocking contract gap:** the inspected protocols have customer-related fields, +but no customer provisioning RPC was found. Current ledger/payment ownership is +expressed with Sendico account refs; current personal-compliance HTTP routes resolve +organizations owned by the logged-in account. Neither is automatically a delegated +customer API for many SXCHNGE users under one organization. Establish a supported +mapping/provisioning facade on Sendico before connecting customer money or KYC. +Do not create a new Sendico organization per consumer to work around this. + +## Reuse decisions + +| Sendico starting point | Decision | SXCHNGE adaptation | +| --- | --- | --- | +| `edge/shared/auth/browser/browser.go` | Adapted now | Own `browser` package; fixed `__Host-sxchnge_session`, explicit HTTPS origins, server-selected cookie routes, no client-header CSRF bypass, same-site Lax cookie for return navigation; no Sendico error/config imports | +| `edge/shared/storage/verification/hash.go` | Selected functions adapted now | 32-byte random opaque credentials and SHA-256 storage digests; constant-time matching; no deterministic magic links or short-OTP hashing copied | +| `edge/shared/storage/s3key/key.go` | Adapted now | Own evidence object-key confinement and errors, additional encoded/backslash/control-character rejection; not an ownership check or local-filesystem sanitizer | +| `edge/shared/storage/verification/{create,consume,consume_update,db}.go` | Adapt in authentication phase | Account + purpose + operation binding, atomic consumption, expiry checked during reads, idempotent issuance, cooldown/attempt indexes. Replace model/repository/logging/notification dependencies with SXCHNGE contracts | +| `edge/shared/storage/refreshtokens/{filters,crud,revoke}.go` | Adapt model and persistence patterns in session phase | Session families/versioning, account binding, rotation and revocation; own database and credential format. Do not copy account-bound API-client login wholesale | +| `edge/shared/.../router/public/{login,refresh,logout}.go` | Adapt flow and tests | Pending login before session issuance; uniform credential failures; confirmed logout and refresh recovery. Preserve password input verbatim, unlike the inspected decoder's trim behavior | +| `edge/product/internal/server/paymentapiimp/{personal_source,visibility,quote,payquote}.go` | Adapt orchestration/authorization rules | Mandatory customer ownership even when partner has organization-wide rights; request-scoped deadlines, quote-bound execution, safe not-found projection. No BSON/permissions/discovery types in SXCHNGE models | +| `edge/product/internal/compliance/{read_service,upload_service,submit_service}.go` | Adapt orchestration pattern | Separate status/limits slices, bounded evidence ingestion, ownership, submission idempotency. Replace “account owns organization” assumption with provider customer mapping | +| `edge/callbacks/internal/signing/service.go` | Implement matching receiver after contract confirmation | Use approved wire algorithm and exact raw-body verification, bounded clock skew, event-ID inbox deduplication, reconciliation reads. Do not copy Vault-backed outbound sender into receiver | +| Ledger, limits, FX, AML/policy, gateways, treasury, KYC engines | External services | Call the partner facade; no cloned financial/compliance state machines | +| Sendico `pkg`, generic Mongo repositories, NATS/discovery bootstrapping, BFF router/DI graph | Do not import wholesale | Small local packages and explicit construction; no sibling Go replacements, shared database, submodule, automatic upstream sync or Sendico runtime dependencies | + +Reuse includes meaningful tests and behavioral invariants, not just filenames. +Each extraction records its source commit and intentional deviations. Future +Sendico fixes are assessed and ported selectively through SXCHNGE PRs. Contract +fixtures/version checks protect the adapter as the products diverge. + +## BFF structure + +Use a single Go module and explicit constructor injection. The following is the +**target layout**; only the shell and three selected foundations exist today. +Create additional directories when their first use case is implemented. + +```text +backend/ + cmd/bff/ HTTP process and graceful shutdown + cmd/worker/ future outbox / provider reconciliation worker + internal/httpapi/ routes, HTTP DTOs, mappers, stable errors + internal/identity/ users, registration, email confirmation + internal/session/ authenticated principal, restore/rotate/revoke + internal/verification/ challenges, factors, operation approvals + internal/profile/ account preferences + internal/wallet/ accounts and spendable-balance projection + internal/operations/ quotes, commands, status, history + internal/compliance/ KYC and visible limits use cases + internal/provider/ narrow customer/financial provider ports + internal/sendico/ external DTOs, HTTP client, mappers, contract tests + internal/notification/ email delivery port and adapter + internal/storage/ SXCHNGE repositories and migrations/indexes + internal/jobs/ durable outbox, event inbox, reconciliation + internal/browser/ browser cookie and CSRF policy (exists) + internal/credential/ opaque credentials and digests (exists) + internal/evidence/ object key confinement (exists) +``` + +A handler decodes an HTTP DTO, validates it, maps to a use-case input, and supplies +the authenticated principal. Use cases own application models and repository/provider +ports. The Sendico adapter alone converts its external DTOs into these models. +The Flutter client retains DTO → mapper → service → model → provider → UI; +provider DTOs do not leak through either boundary. Avoid one interface per function +or an all-purpose “Sendico service” spanning unrelated capabilities. + +Proposed persistence: a separate SXCHNGE MongoDB database/credentials, using native +driver repositories and replica-set transactions for critical local transitions. +This preserves useful Sendico persistence experience without importing its generic +repository module. Collections: users, sessions, challenges/factors, provider +mappings, quotes, operation commands, outbox and callback inbox. Unique indexes +scope email, token digest, issuance/idempotency keys and provider events. TTL indexes +clean up expired data; application checks enforce expiry immediately. Financial +balances and KYC decisions are not authoritative local records. + +Start with the existing deployment's secret facility where available. Do not make +NATS, Redis, Sendico discovery or direct Vault imports prerequisites for the BFF. +Email has an independent adapter and durable delivery jobs; adopting an existing +mail provider does not require adopting Sendico notification/event envelopes. + +## Required Sendico services and partner capabilities + +The BFF should call a versioned partner HTTP API. The names below are **existing +internal services**, useful to implement that facade; they are not instructions to +expose unauthenticated gRPC or for the browser to call them. If a supported partner +API is already deployed, validate it against this checklist instead of creating a +second one. No deployed endpoint has been verified in this task. + +| Capability needed by SXCHNGE | Existing source capability | Required partner behavior / gap | +| --- | --- | --- | +| Partner authentication and customer provisioning | Product account/API-client login and organization provisioning; customer fields in payment/KYC contracts | Confirm server-to-server grant/rotation/scopes. Existing `/login/api` is password + client-secret based, not evidence of OAuth client credentials. Add/confirm idempotent customer registration and canonical owner/subject mapping | +| Customer currency accounts | `LedgerService.ListAccounts`, `GetBalance`; treasury currency catalog | Enforce mapped customer scope; provision accounts through approved workflow. Return server-defined **available to spend**, currency precision, timestamp/version. `BalanceResponse.balance` alone does not define holds, pending debits or spendability | +| Rates, fees and quotes | `QuotationService.QuotePayment`, `QuotePayments`, `ListFXDirections` | Bind initiator and required source owner; return immutable fee/debit/credit/expiry quote snapshot, supported directions and exact money representation. An advisory quote is not execution authorization | +| Exchange and payout | `PaymentOrchestratorService.ExecutePayment`, `GetPayment`, `ListPayments` | Mandatory mapped organization/initiator/source-owner constraints; execute by accepted quote ref; scoped idempotency conflict/replay and lookup by client operation ref. Confirm FX ledger-to-ledger and payout rail availability | +| Top-up | `inbound.routes.RouteService`, payment methods and gateway capabilities; `inbound.crypto_deposits` for crypto | Return customer-bound funding instructions or hosted payment action. Confirm bank/card rails and incoming attribution. A top-up cannot immediately credit funds merely because the browser submitted a form | +| Limits and upgrade benefits | `LimitsService.ListVisibleLimits`, `CheckLimit`; reservation lifecycle used by payments | Customer-visible policy including used, remaining, period/reset and verified-tier projection. Policy chooses tier from authoritative KYC. Payments owns execution reservation/commit/release; BFF never double-reserves | +| KYC evidence and status | `KYCService.SubmitVerification`, `GetVerification`, `Reverify`, `ListVerifications`; existing edge compliance upload/submit | Delegated customer access, passport/selfie evidence roles, retention/access rules and immutable evidence IDs. Confirm manual review/correction/denial transitions and their public reason codes; public KYC proto alone does not expose a manual-review decision RPC | +| Backoffice review and eligibility | KYC plus `CaseManagementService`, Assessment and Policy | Confirm review routing and authoritative decision application; resolving a workflow case is not itself a KYC approval. Ensure KYC/tier changes feed payment eligibility, rather than assuming the separate KYC service already gates every payment | +| Status changes and reconciliation | Orchestrator reads, ledger statements, outbound `edge/callbacks` | Signed customer-scoped payment/KYC events, stable event IDs, at-least-once semantics, replay/ordering/version rules, retries and replayable status reads. Polling is a valid initial fallback; missing callbacks cannot leave funds permanently ambiguous | + +The current limits README says reservations are in process, but source now requires +`RuntimeRepository` and applies durable mutations with conflict/retry handling. +Use implementation and contract tests as evidence; confirm deployed durability and +restart/multi-instance behavior before money rollout rather than repeating the +README claim. + +## Client-facing contracts to implement + +Paths here are proposed **SXCHNGE API**, not existing Sendico endpoints. Keep them +versioned under `/api/v1`. HTTP bodies use explicit DTOs; product error responses +carry a stable code, request ID and, where useful, `retry_after` or field codes. +No raw upstream exceptions or internal compliance evidence are returned. + +| Client journey | Proposed BFF operations | Important contract | +| --- | --- | --- | +| Registration/email | `POST /registrations`, `/email-verifications`, `/email-verifications/confirm` | Idempotent registration/delivery; explicit unverified state; single-use expiring links; no session from merely creating an account | +| Login/session | `POST /sessions`, `POST /sessions/complete`, `GET /session`, `DELETE /session` | Credentials return pending challenge or confirmed session; only server can complete it. Restore, expiry, revocation, logout and frontend route guards | +| Recovery | `POST /password-resets`, `/password-resets/confirm` | Non-enumerating delivery, bounded retries and session revocation policy; do not reuse email-confirmation token purposes | +| 2FA | `GET /security`, `POST /security/challenges`, `POST /security/challenges/{id}/verify`, `/resend`, `PATCH /security` | User/session/purpose/operation-bound challenges and server-selected methods. Verified enable/disable; future TOTP setup is pending until a valid code confirms the secret | +| Preferences | `GET /profile`, `PATCH /profile` | Server-owned account language and validated mutable fields | +| Accounts/limits | `GET /accounts`, `/accounts/{id}`, `/limits` | Available balance only in primary UI; current usage and upgrade benefits from service; distinguish unavailable/stale from zero | +| Money | `POST /quotes`, `POST /operations`, `GET /operations`, `/operations/{id}`, `/operations/by-key/{key}` | Exact immutable quote, server confirmation proof, scoped idempotency and non-final acceptance. Funding instructions/hosted-action state where required | +| KYC | `GET /identity`, `POST /identity/uploads`, `POST /identity/submissions` | Bounded content validation and customer ownership; two evidence roles; idempotent submission; status, correction requests and denials | + +Define money on the wire as decimal-string minor units plus currency and scale; +map into checked application types. Never use floating-point amounts or unchecked +JavaScript-sized integers. Existing Flutter models use integer minor units; their +mappers must reject unsupported precision/overflow. Never invent balances, fees, +limits or rates on provider timeout. + +Current Flutter stubs need targeted extensions during integration: a session model/ +provider and HTTP transport; typed funding methods/recipients instead of the current +free-text recipient; `requiresAction`/funding instructions and settlement-aware funds +status. Keep the three broad operation states if useful visually, but represent +awaiting funding, review, execution, cancellation/reversal and returned funds in +models so “failed” cannot incorrectly promise that money has already returned. +The current `GuardedLoginService` is a local stand-in: replace it with the BFF's +pending-login contract, not a production client that mints its own pending session. +There is no public endpoint that simply trusts `authorize(purpose, operation)`; +the BFF consumes proof as part of the protected server-side action. + +## Critical transaction and identity flows + +### Authentication and verification + +Registration owns local credentials, confirms email, then idempotently provisions +or resumes the provider mapping. An incomplete provider mapping can yield an +explicit “account setup pending” state without losing the local registration. +No money access until mapping is confirmed; the optional unverified KYC state +still receives whatever basic policy Sendico returns. + +Adapt Sendico's pending-login and durable consumption patterns into SXCHNGE-owned +models. Browser routes use an HttpOnly Secure host-only opaque session cookie; +unsafe requests require an exact allowed Origin plus the CSRF header, independently +of client transport hints. SameSite Lax permits safe return navigation from email; +state-changing confirmation still requires POST validation. Production sits behind +HTTPS. Native mobile uses a separate server-selected token transport with secure +platform storage and rotation/revocation, not browser-origin bypass flags. + +Scope challenges by user, session/pending-login, purpose, operation ID and method. +Use independent purposes for login, email, password reset and setting changes. +Use durable attempt/cooldown state across instances; a resend must not reset the +attempt budget. High-entropy token digests can reuse the extracted helper; numeric +codes need a separately keyed verifier and strict attempt controls, never this +helper as a password/OTP hash. Future TOTP secrets require encryption, confirmed +enrollment, bounded clock drift/replay controls and recovery before availability. + +### Money execution without duplicate payment + +1. Resolve the authenticated customer and source/destination ownership server-side. +2. Obtain a Sendico quote carrying the mapped initiator and mandatory source-owner + constraint. Persist its local ID, user, provider ref, expiry and exact snapshot. +3. Bind the user's verification to that quote and action. A changed amount, + recipient, account or expired quote requires fresh review and authorization. +4. In one **local database transaction**, validate/consume approval and insert the + immutable operation command, scoped idempotency key, request digest and outbox + job. Reusing the key with another payload is a conflict. Replaying the same key + returns the same command before checking an already-consumed approval. +5. A worker submits the stored command using the same provider idempotency key. + Sendico rechecks ownership, quote validity, eligibility, spendability and limits. + A BFF approval does not override provider policy. +6. If a response is lost, reconcile by the provider/client operation reference; + retry only under Sendico's documented idempotency contract. Never create a new + key merely to escape uncertainty. A crash after approval consumption is recovered + by the persisted outbox, not by asking for and spending another authorization. +7. Persist observed status/version and correlate events; update balances/limits from + authoritative reads. Pending/manual review is not completion, and failed/reversed + status must describe whether funds actually returned. Callback duplicates and + out-of-order events must not regress terminal state; ambiguous order triggers a read. + +There is no cross-database atomic transaction with Sendico. Durable commands, +idempotent provider execution and reconciliation are the explicit bridge. This +behavior must be proven with an “accepted upstream, response lost” contract test. + +### KYC and operational observability + +Use customer-bound private evidence references; validate size/type/content and +read access before forwarding to the provider. Do not pass arbitrary public URLs, +local file paths or untrusted object keys. If uploads stage in SXCHNGE, lifecycle, +retention, orphan cleanup and delete responsibility must be explicit; prefer +provider-issued scoped upload intents when supported. Passport and selfie are +separate evidence roles in one idempotent submission. Backoffice decisions remain +in Sendico; project only safe actionable reasons and the resulting limits. + +Correlate requests, local commands and provider IDs on the server with structured +logs. Record provider latency/errors, outbox lag, reconciliation age, callback +retries and final outcomes. Exclude passwords, codes, cookies, documents, recipient +payloads and secrets. Existing dummy analytics remains a client sink; backend +settlement evidence supplies real outcomes, not browser “submitted” events. + +## Implementation sequence and acceptance gates + +| Phase | Deliverable | Exit evidence | +| --- | --- | --- | +| 0 — this task | Audit, reuse choices, independent shell and primitives | Unit/race tests, Go formatting/vet, no Sendico imports/replacements, source provenance | +| 1 — provider contract agreement | Customer provisioning/mapping, scoped access, spendability, quote/payment replay, KYC review and callback contract | Sandbox fixtures/API schema and two-customer isolation tests; determine whether existing partner edge can be extended | +| 2 — own authentication | Mongo repositories, email outbox, signup/email/login/session/2FA/recovery plus Flutter adapters/guards | Restart-safe consumption and delivery, session isolation/revocation, no session before required factor, web/mobile transport tests | +| 3 — read-only provider integration | Customer mapping, currency accounts, profile, limits, KYC status | Available-money semantics, no cross-customer list/direct reads, safe unknown/error handling, contract version validation | +| 4 — first money slice | One supported top-up rail followed by exchange and payout, recipient/funding UI extensions | Provider-confirmed credit/debit, fees and expiry; idempotent concurrency and lost-response recovery; funds explanations match actual statuses | +| 5 — evidence/review integration | Passport/selfie uploads, backoffice review, correction/deny, policy propagation | Customer isolation, evidence lifecycle, review-driven limits change and execution policy enforcement | +| 6 — rollout | Deployment, migrations, secret rotation, health/readiness, reconciliation operations and event delivery | Sandbox end-to-end runs, restart/recovery drills, ownership tests on every public resource and explicit supported-rail configuration | + +Authentication work can proceed while the provider contract is agreed. Do not block +it on cloning financial services. Conversely, no live money rollout until the scope, +spendability, idempotency and settlement contracts are verified. Keep each phase in +its own task/PR; Sendico-side facade work is a separate provider contribution. + +## Source references + +All paths below are in the audited Sendico revision; links pin that revision. + +- [Browser transport](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/shared/auth/browser/browser.go), [verification hashing](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/shared/storage/verification/hash.go), [evidence keys](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/shared/storage/s3key/key.go). +- [Pending/API login](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/shared/internal/transport/http/router/public/login.go), [session binding](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/shared/storage/refreshtokens/filters.go), [verification indexes](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/shared/storage/verification/db.go). +- [Ledger contract](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/proto/ledger/v1/ledger.proto), [payment visibility](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/product/internal/server/paymentapiimp/visibility.go), [personal source constraints](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/product/internal/server/paymentapiimp/personal_source.go). +- [Quotation](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/proto/payments/quotation/v2/quotation.proto), [orchestration](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/proto/payments/orchestration/v2/orchestration.proto), [inbound routes](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/proto/inbound/routes/v1/routes.proto). +- [Limits projection](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/proto/limits/v1/limits.proto), [durable mutation implementation](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/limits/internal/service/limits/reservation_mutation_persistence.go), [required runtime storage](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/limits/internal/service/limits/service.go). +- [Personal-compliance ownership](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/product/internal/server/accountapiimp/compliance.go), [KYC](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/proto/compliance/kyc/v1/kyc.proto), [case workflow](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/proto/workflow/case/v1/case.proto), [callback signing](https://git.sendico.io/S_I/sendico/src/commit/8bb20dcd622dd8eb92d289b6b34411e7eccab083/api/edge/callbacks/internal/signing/service.go). diff --git a/docs/implementation.md b/docs/implementation.md index 8e0b6b8..85d57f4 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -362,3 +362,36 @@ See [the UX and measurement contract](ux/two-factor.md) and Delivery: [PR #33](https://git.sendico.io/tech/sxchnge/pulls/33) targets `main`; committed and pushed, unmerged. Issue #32 and PR #33 metadata verified: frontend label, tech owner and SXCHNGE project. No stacked dependencies. + +## Independent BFF plan and Sendico reuse — task #34 + +Audited `S_I/sendico` at `8bb20dcd622dd8eb92d289b6b34411e7eccab083` without +modifying its unrelated working-tree dependency edits. The +[BFF plan](architecture/bff.md) records source-backed reuse choices, independent +ownership, target structure, proposed Mongo persistence, client/API mapping, +required provider services and phased acceptance criteria. Source availability +is distinguished from supported/deployed external APIs. + +The main provider gaps are delegated customer provisioning/owner mapping, a +spendable-balance projection, customer-scoped compliance access/manual review, +and verified payment/callback replay contracts. Existing quotation/orchestration +owner constraints and durable limits mutation code can underpin the provider +facade. Sendico's personal-compliance ownership assumptions and stale limits +README are called out explicitly. Sendico remains authoritative for financial +execution, limits and compliance; the BFF does not clone those engines. + +Created a separate Go module and operational shell under `backend/`, plus adapted +browser cookie/CSRF policy, opaque credential generation/digests and evidence-key +confinement. [Provenance](../backend/PROVENANCE.md) pins source blobs and deviations. +No Sendico imports, sibling replacements, external Go modules, shared database +access or live provider calls. The starter exposes health and deliberately 503 +readiness; authentication, persistence and financial product routes remain planned. +Root/backend README and backend guidelines describe the current scope. + +Validation: `make test-backend` (race-enabled tests across four packages), +`make lint-backend` (gofmt/vet), `go build` and `git diff --check` passed. +CLI smoke verified health 200, readiness 503, unimplemented session 404 and SIGTERM +exit 0. Module/dependency inspection confirms standard-library-only dependencies. +Pinned source paths, source blobs and local documentation links were checked. +Reviewed all new backend files and the task diff. No Flutter code changed; +frontend tests and security scans were not run for this backend starter/plan task.