fix: infra hardening — compose ports, Temporal contract, balance enforcement - #159
Merged
Conversation
…ebit paths (WP6 FIX C) Phase-1 audit: balance enforcement on debits was missing — payments/debits could be created without an available-balance precondition. - billingSettlement.ts: add getTenantAvailableBalanceKobo (available = credits_posted - debits_posted - debits_pending, matching services/tigerbeetle-ledger lib.rs), typed InsufficientFundsError (PRECONDITION_FAILED, reason INSUFFICIENT_FUNDS) and assertSufficientLedgerBalance; enforced in debitTenantForIntelligenceAssessment. - billing.ts recordDebit: enforce AFTER the deterministic idempotency claim and BEFORE /transfers/create; release the just-claimed row on INSUFFICIENT_FUNDS so a post-top-up retry can re-claim; idempotent replays return early without re-enforcement. Idempotency/outbox semantics unchanged. - Tests: new billing.insufficientFunds.test.ts (6 tests); debitClaim test updated for the new balance query.
…nt wiring (WP6 FIX C) - server/billing.ts: recordDebit now calls assertSufficientLedgerBalance after the durable claim and before the TigerBeetle transfer; releases the claim on INSUFFICIENT_FUNDS (tenant-scoped delete) so retries after top-up proceed. - server/billing.insufficientFunds.test.ts: sufficient funds passes; insufficient rejected with typed error and no transfer; holds (pending debits) reduce available balance; fail-closed on unreadable ledger; idempotent replay does not re-enforce.
…losed on unregistered types (WP6 FIX B)
- Wire convention is camelCase end-to-end ({workflowType, taskQueue,
workflowId, input} -> {workflowId, runId, status, mode}); the Node client was
aligned to the gateway (the server of this contract).
- startInvestigationWorkflow maps to the worker's snake_case
InvestigationInput JSON tags at the boundary.
- startKycExpiryWorkflow: renamed to the registered KycExpiryWorkflow on
queue bis-compliance with input {tenantId, gatewayUrl}.
- startScreeningWorkflow: sends the worker's ScreeningOrderInput shape
(incl. required full_name; router accepts fullName/nin/bvn).
- Fail-closed typed TemporalWorkflowUnavailableError for workflows with no
registered worker: PaymentTransferWorkflow (bis-payment), AMLWorkflow
(bis-aml), CaseEscalationWorkflow (bis-cases), AccessReviewWorkflow
(was literal "COMPLIANCE_TASK_QUEUE" via a fake client proxy — removed).
- Namespace default unified to "bis".
…r screening input (WP6 FIX B) - server/temporalWorker.ts: deleted the fake HTTP poll-loop "worker" (polled /v1/worker/poll|heartbeat|complete|fail — endpoints that never existed in the gateway; phantom code). Activity helper functions retained and documented as NOT a Temporal worker; real execution is owned by the Go workers (gateway, compliance-worker). - server/temporalRouter.ts: startScreening accepts optional fullName/nin/bvn required by the registered ScreeningWorkflow worker input.
server/temporal.manifest.json enumerates every client-invoked workflow/query/signal (server/temporal.ts) and every worker registration (services/gateway, services/compliance-worker), with task queues, namespaces, and wire input fields. Guarded entries mark fail-closed starters with no registered worker.
…er registrations (WP6 FIX B) Loads server/temporal.manifest.json and asserts every client-invoked workflow has a registered handler on the same task queue with a matching arg shape, guarded starters behaviorally throw TemporalWorkflowUnavailableError, and status/cancel endpoints exist.
Namespace drift (Node "default" / compliance-worker "bis-platform" / temporal-init "bis") meant workers and the client could land in different namespaces. Unified on "bis"; compose default updated likewise.
… status/cancel, workers actually started (WP6 FIX B) - workflowTaskQueues registry + RegisteredWorkflowTaskQueue; StartWorkflow resolves the queue from the registry and rejects unregistered types (fail closed) instead of hardcoding bis-investigation. - StartWorkflowWithOptions honours workflowId (idempotent re-starts) and validates taskQueue; returns workflowId + runId. - Client.WorkflowStatus (DescribeWorkflowExecution) and Client.CancelWorkflow. - StartWorker now also registers the extended workflows; StartScreeningWorker serves bis-screening.
The previous signature (an ad-hoc interface with non-variadic method types) could never be satisfied by worker.Worker, and the function was never called. It is now callable and is invoked from StartWorker().
…cel endpoints (WP6 FIX B)
- handleWorkflowStart: camelCase wire ({workflowType, taskQueue, workflowId,
input}), validates the type against the registered-worker registry and fails
closed with WORKFLOW_NOT_REGISTERED; honors workflowId/taskQueue; returns
{workflowId, runId, status, mode}.
- New GET /v1/workflow/status/{id} (DescribeWorkflowExecution) and
POST /v1/workflow/cancel/{id} (CancelWorkflow) — the Node client called both
but they never existed.
- main(): start the registered Temporal workers (StartWorker +
StartScreeningWorker) after client init; previously no worker ran.
…P6 FIX B) startPaymentTransferWorkflow now rejects with typed TEMPORAL_WORKFLOW_UNAVAILABLE (no registered worker on bis-payment); tests assert the guard, that the gateway is never contacted, and deterministic replay rejection.
startAmlWorkflow has no registered worker; the smoke test now asserts the typed TEMPORAL_WORKFLOW_UNAVAILABLE rejection and that the gateway is never contacted.
…+ regression test (WP6 FIX A)
…vValidation.ts (WP6 FIX A)
… — runbook.md (WP6 FIX A)
… — architecture.md (WP6 FIX A)
… — README.md (WP6 FIX A)
… — fluvio.ts (WP6 FIX A)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Three infrastructure-hardening fixes from the Phase-2 program (WP6), closing audit structural items:
FIX A — Docker Compose host-port collisions
Nine host-port collisions across the compose files made
docker compose upfail with "port is already allocated":Also: prod
caddyservice now strips its host-port bindings (ports: []) — nginx owns 80/443 in production. New regression testserver/composePorts.test.tsparses everydocker-compose*.ymland fails on any duplicate host binding. All referencing defaults updated:server/_core/env.ts(bisVerifierUrl→18086, ollamaAdapterUrl→18090),server/envValidation.ts(Ollama default→18090),docs/runbook.md(4 curl examples),docs/architecture.md(verifier/lex-matcher rows),README.md(OLLAMA_ADAPTER_URL→18090),server/fluvio.ts(comment 9090→19090).FIX B — Temporal contract unification
bis(env defaultTEMPORAL_NAMESPACE ?? "bis", composeTEMPORAL_NAMESPACEdefault→bis)TemporalWorkflowUnavailableErrorfor workerless workflows —startAmlWorkflownow rejects with codeTEMPORAL_WORKFLOW_UNAVAILABLEand never contacts the gateway when no worker registersAMLWorkflow(smoke test updated accordingly)FIX C — TigerBeetle flow-of-funds balance enforcement
recordDebitnow enforces available balance before posting:available = credits_posted − debits_posted − debits_pending. Insufficient funds → typedInsufficientFundsError(PRECONDITION_FAILED/ reasonINSUFFICIENT_FUNDS) with claim release after durable idempotency claim, before/transfers/create. Idempotent replay skips enforcement (no double-charge).How tested
npx tsc --noEmit: cleanRisks / disclosures
Note
This PR's commits were pushed by the orchestrator from the WP6 coder's verified fallback package (the coder's GitHub MCP lost write access mid-task); every pushed blob was SHA-256 hash-verified against the coder's workspace contents.