Connect sign-up to Sendico Facts intake (#46)

This commit is contained in:
Stephan D committed 2026-10-15 00:55:19 +02:00
1 parent 30f0455122
commit a584c8189a
40 files changed
+1616 -35

No files matched your search

+9 -1
View File
@@ -61,7 +61,7 @@ envelope. Credentials, codes and activation tokens are sent in POST bodies.
| Route | Request | Successful result |
| --- | --- | --- |
| POST `/signup` | `name`, `email`, `password`, `locale`, `idempotencyKey` | 202 with `userId`, after activation email queue acceptance |
| POST `/signup` | `name`, `email`, `password`, `locale`, `idempotencyKey` | 202 with `userId`, after configured Facts intake and activation email queue acceptance |
| POST `/verification/send` | `email`, `idempotencyKey` | 202; generic acceptance also applies to absent/already-active accounts |
| POST `/verification/confirm` | `token` | 202 with `verified: true` after activation commits |
| POST `/login` | `login: {login, password}`, `clientId`, `deviceId` | 202 with `userId`, `pendingToken: {token, expiration}`, `challengeId`, `channel`, masked `target`, `expiresAt`, `resendAt` |
@@ -101,6 +101,14 @@ retries can create replacement messages; they are not distributed exactly-once
operations. Tokens are stored before the RPC, so a process crash between those
steps requires user recovery; there is no product-side delivery outbox.
When `api.sendico` is enabled, sign-up and activation resend first require a
durable Facts intake receipt. Activation checks that receipt in its transaction.
Initial account/token creation uses the same server-secret-derived activation
key as a same-key retry, so an intake outage does not force an extra cooldown for
an unsent initial token. Existing pending customers created before this change
retain the prior cooldown behavior. See [Facts integration](signup-facts.md) for
uncertain registration recovery and the `signup_attention_required` error.
Code verification, one-use proof consumption and session creation now commit in
one transaction. A session-write failure leaves the code retryable; a lost HTTP
response after commit requires restarting password login. The adapter
+52
View File
@@ -828,3 +828,55 @@ existing mount requires reauthentication at its new cookie path. Notification
activation URLs remain configured frontend URLs. No live deployment, optional
security scans or merge were performed. The unrelated local notifications sender
configuration remains uncommitted. See [API version installation](api-versions.md).
## Sign-up Facts intake — task #46
[Task #46](https://git.sendico.io/tech/sxchnge/issues/46), branch
`identity-facts-46`, connects the product sign-up service to Sendico's public
individual registry and immutable Facts intake through optional `api.sendico`
configuration. Owner: `tech`; project: SXCHNGE; labels: backend and frontend.
The adapter sends full name, normalized email and the local customer reference.
The BFF persists a customer-owned subject binding, immutable facts snapshot and
confirmed receipt before activation email or sign-up acceptance. Activation and
resend enforce the handoff. The shared database source can require majority
writes and primary majority reads without bypassing its connection/transaction
lifecycle. Initial account/token creation remains atomic and uses a secret-derived
activation key, allowing same-key recovery after an intake outage without an
extra cooldown for an unsent token. Archived customers cannot resume sign-up.
Backend and frontend completion reviews covered the full task contribution,
including the public contracts, customer ownership, stable replay namespace,
concurrent claims, error classification, credential handling, logging, metrics and
DTO/mapper/service/model/provider separation. UX review preserved input, loading
suppression and confirmation-based success. Typed support guidance reaches both
sign-up and verification resend/confirmation through the shared auth package;
EN/RU feedback preserves live-region semantics and existing responsive controls.
The new localized widget fixture initially double-disposed the app-owned locale
provider; the fixture ownership was corrected. Focused integration tests also
exposed the initial-token cooldown issue, which was fixed in the creation flow.
Individual registration is not idempotent upstream. A durable claim prevents
speculative repeat registration after an ambiguous result; reconciliation uses
the saved request reference and authoritative Sendico records. Intake replay is
safe after receipt loss and retains original assertions. Actual provider IDs,
the endpoint and a same-account Vault-rendered access token must be provisioned
before enabling this integration. Tests use simulated HTTP and disposable MongoDB;
no live provider resources, live email, deployment, merge or optional security
scan is part of this delivery. The unrelated notifications runtime configuration
is excluded. Configuration, rollout, telemetry and recovery details are in
[Sign-up Facts intake](signup-facts.md).
Final validation passed: `GOFLAGS=-p=2 make test` (all backend race tests and
`make test-frontend` suites), `make lint-backend`,
`dart format --output=none --set-exit-if-changed` for all 11 changed Dart files,
and `flutter analyze` in `frontend/app` and `frontend/packages/sxchnge_auth`.
Focused Go race suites and Flutter backend/provider/widget tests also passed;
the diagnostic public API run
`go -C backend/product test -race -v ./auth/http/v1/public -count=1 -timeout=90s`
passed. The initial unrestricted `make test` failed when Testcontainers could
not start its reaper before the Mongo fixture deadline. Lower Go package
parallelism resolved that infrastructure contention. The next full run exposed
the new locale fixture disposal error noted above; the corrected final run
passed, including EN/RU support states and verification recovery. No checks were
weakened or unrelated work changed.
+176
View File
@@ -0,0 +1,176 @@
# Sign-up Facts intake
Task [#46](https://git.sendico.io/tech/sxchnge/issues/46), branch
`identity-facts-46`. Primary module: identity-facts; secondary modules: auth,
customer creation, runtime, shared database source and Flutter error feedback.
## Behavior and boundaries
With `api.sendico` configured, the product BFF creates the local pending customer
and activation token atomically, then registers an independent individual through
Sendico's public HTTP API and submits the sign-up assertions. It persists the
intake receipt before sending activation email or returning sign-up success.
Activation resend runs the same handoff; activation checks the receipt in its
local transaction, without external I/O. A failed receipt check rolls back token
consumption and leaves the account pending. Existing active customers are not
backfilled or blocked at login.
The adapter consumes these existing Sendico contracts:
- `POST /individuals/{organization_ref}`, empty body, HTTP 200 registration
receipt containing `subject_ref` and `registered_at`.
- `POST /identity_facts/{organization_ref}`, HTTP 200 immutable intake receipt
containing `submission_ref`, `subject_ref` and `accepted_at`.
The configured API prefix precedes both paths. See Sendico
[individual subjects](https://git.sendico.io/tech/sendico/src/branch/main/docs/api/individual-subjects.md)
and [Facts intake](https://git.sendico.io/tech/sendico/src/branch/main/docs/api/identity-facts-intake.md).
The only assertions sent are `full_name`, normalized `email` and
`external_customer_ref` (the local customer ID). Passwords, password hashes,
activation tokens, session credentials and device data never enter the adapter.
The input name is retained as a full name, without guessing name parts. No facts
are marked verified; acceptance does not establish identity, KYC completeness,
verification, limits or payout eligibility. The existing KYC preview is unchanged.
Only the BFF owns Sendico credentials and organization scope. Local customer
models have no organization, membership or platform permissions. HTTP DTOs and
mappers stay in `providers/sendico`; the handoff operates on domain facts and
receipts in `identity`. Flutter continues through its DTO, mapper, service,
model and provider layers. It never calls Sendico or supplies subject references.
## Configuration and rollout
Add this block under `api` in the product service configuration, retaining
`api.notifications`:
```yaml
sendico:
api_url: https://your-sendico-host/api/v1
organization_ref: <provisioned-SXCHNGE-organization-object-id>
source_account_ref: <provisioned-integration-account-object-id>
token_file: /run/sxchnge/sendico-token
```
Resolve the real IDs from the target Sendico deployment. The integration account
must be a member of the SXCHNGE organization with the existing organizations
`update` permission; reconciliation GET requests also require `read`. The token
must be a full, unexpired Sendico access JWT whose `id` claim is that account.
The adapter inspects claims only to protect the replay namespace; Sendico checks
the signature and authorizes every call. No login, refresh or privileged account
provisioning is performed by this adapter.
Keep the token in Vault at `services/bff/sendico`, field `access_token`. Render it
with the deployment's Vault Agent into the configured private runtime file; give
only the BFF policy read access to that KV path. Endpoint, organization and source
IDs remain in service YAML. For the local stack, configure
`infra/local/bff/runtime.yml`, render to the BFF's existing `/run/sxchnge` tmpfs,
and extend its Vault Agent policy/template for this provisioned secret. These
deployment values and secret provisioning are deliberately not guessed or
enabled by the example. Do not place tokens in Git, browser builds or shell
history. Refresh the file atomically before expiry using the same Sendico account;
the adapter rereads it on every request. Startup rejects missing, expired or
mismatched credentials and rejects Sendico configuration without notifications.
HTTPS is required except for loopback HTTP test/development endpoints. Redirects
are rejected and each HTTP call has a four-second timeout. Non-200 responses,
malformed/truncated receipts, invalid identifiers, wrong subjects and unsuccessful
envelopes never count as acceptance. Upstream error bodies are not surfaced.
Omitting `api.sendico` keeps existing local-only behavior. Enabling it also gates
activation for previously pending customers; use activation resend to submit
their saved name/email before confirming their old link. There is no automatic
bulk migration. Changing endpoint, organization or source account changes the
adapter fingerprint and blocks existing bindings for explicit reconciliation.
Token rotation for the same account retains that fingerprint. Plan such changes
as migrations; never reset a binding just to bypass the check.
## Persistence, retries and recovery
The local `signup_facts` collection uses the customer ID as `_id`, so its unique
primary key coordinates concurrent requests and separate BFF instances. It stores
an immutable sign-up facts snapshot, adapter fingerprint, random registration
request correlation, registration claim, subject and confirmed receipt. It uses
the shared database repository and connection lifecycle with majority writes and
primary majority reads, overriding weaker connection defaults. No external call
runs in a database transaction. Snapshots and bindings contain personal data and
must follow the customer data retention policy; no TTL silently removes replay
state, and customer deletion does not delete the immutable Sendico submission.
Once a subject is stored, intake retries use
`sxchnge:signup:<local-customer-id>` as `source_submission_ref`. Sendico's
organization/authenticated-account/source-reference uniqueness returns the
original receipt for that subject. Retries send the saved snapshot even if a
later request changes name or email. Losing the HTTP receipt or a local receipt
write is recoverable by replay. A saved receipt skips subsequent external calls.
Retrying sign-up still requires the original password; active, archived or
otherwise conflicting accounts cannot be resumed as a new customer.
**Individual registration is not idempotent.** The BFF commits a claim before
dispatch and never expires or automatically retries that registration. It cannot
prove that a timed-out request, malformed response or failed local binding write
did not create a subject. Another request sees an in-progress claim as temporarily
unavailable for 30 seconds; an older unresolved claim requires support. The
registration caller immediately reports assistance on an uncertain failure. An
accepted registration is saved with a short independent context even if the
browser disconnects. A crash before dispatch can also require reconciliation.
This trades automatic recovery for avoiding duplicate subjects under the current
upstream contract; there is no distributed exactly-once guarantee.
For an unresolved registration, operators must stop requests for the affected
customer and inspect the saved `registrationRequest` plus Sendico identity logs
(`X-Request-ID`, operation `subject_register`) under the configured account and
organization. A successful registration's server-issued `record_ref` identifies
the candidate subject. Confirm it using the organization-scoped individual GET
API before atomically setting the matching local binding's `subjectRef` and
clearing `registering`; retain its snapshot, fingerprint and request reference.
If a local binding write failed after receipt, BFF diagnostics also contain the
subject reference. If authoritative records prove no subject was created, an
operator may clear the claim after fixing the dependency. If neither result can
be established, leave it blocked; never register a speculative replacement.
Resume sign-up with the original credentials or activation resend after this
repair. These are controlled operational repairs, not customer API endpoints.
HTTP 503 `signup_attention_required` maps to typed shared auth and verification
state and localized EN/RU support guidance, including activation resend recovery.
Other intake/receipt failures remain temporary service
failures. Form input is retained, duplicate submissions stay disabled during
loading, and neither failure navigates to an email-sent screen or creates a
session. The existing support channel must be available to handle reconciliation.
## Measurement and UX review
The existing [auth journey](ux/auth-journeys.md) keeps its attempt, retry,
abandonment and 30-minute settled inactivity rules. `auth_signup_succeeded` now
requires server confirmation of configured intake plus activation queue acceptance;
it remains an intermediate step and email confirmation remains the terminal
onboarding outcome. Sign-up dependency failures and assistance outcomes use the
existing allowlisted `unavailable` analytics category; verification assistance
uses it too. Retries retain the journey
and operation semantics. No names, emails or provider response bodies are added.
`sxchnge_signup_facts_total{outcome}` records `accepted`, `replayed`,
`unavailable` and `attention_required` handoff attempts;
`sxchnge_signup_facts_duration_seconds` measures their latency. Counts are
attempts, not distinct users. Use unavailable/attention counts divided by total
handoff attempts for operational failure rates; use the existing correlated auth
journey for conversion, duration and abandonment. Structured handoff logs include
local customer and random registration request references for support correlation,
without asserted values, credentials or raw errors. IDs are never metric labels.
The product team uses the distinction to separate retryable intake outages from
registration reconciliation. The default client analytics sink remains in memory;
production retention and cross-device conversion correlation remain deployment
gaps, as documented by the existing journey contract.
Review covers loading suppression, input preservation, success only on confirmed
handoff, temporary retry and assistance states, existing navigation, mobile layout,
text scaling and live-region error feedback. Focused tests cover these states,
the mapped backend error, non-inflated client success, immutable retry payloads,
credential rotation, timeout/cancellation/redirect behavior, receipt validation,
privacy-safe errors, concurrent durable claims and activation rollback. The tests
use disposable local MongoDB and a simulated Sendico HTTP server; they do not
create live subjects or send email. Live integration and support operations must
be verified against provisioned deployment credentials before enabling the block.
The EN/RU error state is exercised at a 390px viewport with 1.5x text scaling;
existing keyboard regression tests and a code review of the live-region semantics
cover the unchanged controls. No manual browser or screen-reader walkthrough
against a live Sendico deployment was performed.