Plan independent BFF and adapt selected Sendico foundations (#34)
This commit is contained in:
1 parent
1e191fca42
commit
45666d33fc
17 files changed
+854
-15
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
+33
-9
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module git.sendico.io/tech/sxchnge/bff
|
||||
|
||||
go 1.26
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user