diff --git a/orchestrator/src/config/externalBlockers.ts b/orchestrator/src/config/externalBlockers.ts new file mode 100644 index 0000000..2028c61 --- /dev/null +++ b/orchestrator/src/config/externalBlockers.ts @@ -0,0 +1,159 @@ +/** + * External dependency blocker registry (EXT-* IDs). + * + * Mirrors the blocker gate in `proxmox/scripts/verify/ + * check-external-dependencies.sh` so orchestrator startup logs and + * provider-switch mock-mode logs surface the **same** IDs the + * deployment pipeline already tracks. When operators see + * "[DbisCore] mock mode" they also see `blockerId: EXT-DBIS-CORE`, + * which maps 1:1 to the proxmox checker output. + * + * A blocker is considered **active** when: + * - the upstream dependency is not yet reachable / not yet built, AND + * - the orchestrator env does not point at any live instance (the + * presence of a "live URL" env var flips the blocker to resolved). + * + * Source of truth for the list: proxmox/docs/03-deployment/ + * EXTERNAL_DEPENDENCY_BLOCKERS.md. + */ + +export const EXT_BLOCKER_IDS = [ + "EXT-DBIS-CORE", + "EXT-CC-PAYMENT-ADAPTERS", + "EXT-CC-AUDIT-LEDGER", + "EXT-CC-SHARED-EVENTS", + "EXT-CC-SHARED-SCHEMAS", + "EXT-FIN-GATEWAY", + "EXT-CHAIN138-CI-RPC", +] as const; + +export type ExtBlockerId = (typeof EXT_BLOCKER_IDS)[number]; + +export interface ExtBlockerDetail { + id: ExtBlockerId; + title: string; + /** Env var whose presence resolves this blocker from the orchestrator's POV. */ + resolvingEnvVar?: string; + /** Whether the blocker is structurally resolved independently of env. */ + staticallyResolved?: boolean; + /** Short description suitable for structured logs. */ + description: string; +} + +export const BLOCKER_DETAILS: Record = { + "EXT-DBIS-CORE": { + id: "EXT-DBIS-CORE", + title: "dbis_core live deployment", + resolvingEnvVar: "DBIS_CORE_URL", + description: + "DBIS Core Banking API not deployed; orchestrator falls back to deterministic mock.", + }, + "EXT-CC-PAYMENT-ADAPTERS": { + id: "EXT-CC-PAYMENT-ADAPTERS", + title: "DBIS/cc-payment-adapters implementation", + description: + "Upstream repo is a template scaffold; no orchestrator client wired yet.", + }, + "EXT-CC-AUDIT-LEDGER": { + id: "EXT-CC-AUDIT-LEDGER", + title: "DBIS/cc-audit-ledger implementation", + description: + "Upstream repo is a template scaffold; audit sink remains in-process events table.", + }, + "EXT-CC-SHARED-EVENTS": { + id: "EXT-CC-SHARED-EVENTS", + title: "DBIS/cc-shared-events implementation", + description: + "Upstream repo is a template scaffold; orchestrator uses local eventBus schema.", + }, + "EXT-CC-SHARED-SCHEMAS": { + id: "EXT-CC-SHARED-SCHEMAS", + title: "DBIS/cc-shared-schemas implementation", + description: + "Upstream repo is a template scaffold; orchestrator types are locally defined.", + }, + "EXT-FIN-GATEWAY": { + id: "EXT-FIN-GATEWAY", + title: "Real FIN / Alliance Access gateway", + resolvingEnvVar: "FIN_SANDBOX_URL", + description: + "No real FIN transport; orchestrator routes dispatch through the in-process sandbox.", + }, + "EXT-CHAIN138-CI-RPC": { + id: "EXT-CHAIN138-CI-RPC", + title: "Chain 138 RPC reachable from CI", + resolvingEnvVar: "CHAIN_138_RPC_URL", + description: + "Public Chain 138 RPC endpoint available; E2E and notary-chain paths can target a real chain.", + }, +}; + +export type BlockerStatus = "active" | "resolved"; + +export interface BlockerStatusRecord extends ExtBlockerDetail { + status: BlockerStatus; + /** Value of the resolving env var at the time of evaluation, if any. */ + resolvedVia?: string; +} + +/** + * Evaluate current blocker status against `process.env` (or a + * supplied env object, for tests). + */ +export function evaluateBlockers( + env: NodeJS.ProcessEnv = process.env, +): BlockerStatusRecord[] { + return EXT_BLOCKER_IDS.map((id) => { + const detail = BLOCKER_DETAILS[id]; + if (detail.staticallyResolved) { + return { ...detail, status: "resolved" }; + } + if (detail.resolvingEnvVar) { + const v = env[detail.resolvingEnvVar]; + if (v && v.length > 0) { + return { + ...detail, + status: "resolved", + resolvedVia: detail.resolvingEnvVar, + }; + } + } + return { ...detail, status: "active" }; + }); +} + +/** + * Convenience: same as evaluateBlockers() filtered to active IDs only. + */ +export function activeBlockers( + env: NodeJS.ProcessEnv = process.env, +): ExtBlockerId[] { + return evaluateBlockers(env) + .filter((b) => b.status === "active") + .map((b) => b.id); +} + +/** + * Emit a structured startup summary of external blockers using the + * supplied logger. Shape matches the proxmox checker output so + * operators can grep for the same IDs across the two systems. + */ +export function logBlockerStatusAtBoot(logger: { + info: (obj: Record, msg: string) => void; +}): void { + const records = evaluateBlockers(); + const active = records.filter((b) => b.status === "active").map((b) => b.id); + const resolved = records.filter((b) => b.status === "resolved").map((b) => b.id); + logger.info( + { + externalBlockers: records.map((b) => ({ + id: b.id, + status: b.status, + resolvedVia: b.resolvedVia, + })), + activeCount: active.length, + resolvedCount: resolved.length, + }, + `[ExternalBlockers] ${active.length} active, ${resolved.length} resolved`, + ); +} diff --git a/orchestrator/src/index.ts b/orchestrator/src/index.ts index 26f410c..88b4d74 100644 --- a/orchestrator/src/index.ts +++ b/orchestrator/src/index.ts @@ -2,6 +2,7 @@ import "dotenv/config"; import express from "express"; import cors from "cors"; import { validateEnv } from "./config/env"; +import { logBlockerStatusAtBoot } from "./config/externalBlockers"; import { apiLimiter, securityHeaders, @@ -23,6 +24,11 @@ import { runMigration } from "./db/migrations"; // Validate environment on startup validateEnv(); +// Surface the current EXT-* external-dependency blocker status so +// orchestrator startup logs match the proxmox deployment checker +// (proxmox/scripts/verify/check-external-dependencies.sh) 1:1. +logBlockerStatusAtBoot(logger); + const app = express(); const PORT = process.env.PORT || 8080; diff --git a/orchestrator/src/services/bank.ts b/orchestrator/src/services/bank.ts index a4c2db7..5fab64e 100644 --- a/orchestrator/src/services/bank.ts +++ b/orchestrator/src/services/bank.ts @@ -1,20 +1,44 @@ import type { Plan } from "../types/plan"; import { generatePacs008 } from "./iso20022"; +import { logger } from "../logging/logger"; + +/** + * Bank-instruction client — two-phase-commit adapter for the payment + * leg (arch §4.3 Payment Messaging / Settlement Layer). + * + * Until `d-bis/dbis_core` is reachable as a live API, every call here + * is a deterministic mock. That corresponds to blocker EXT-DBIS-CORE + * in proxmox/docs/03-deployment/EXTERNAL_DEPENDENCY_BLOCKERS.md and + * flips to real once DBIS_CORE_URL is set (see services/dbisCore/). + */ +const BLOCKER_ID = "EXT-DBIS-CORE"; + +function bankMode(): "live" | "mock" { + return process.env.DBIS_CORE_URL ? "live" : "mock"; +} /** * Prepare bank instruction (2PC prepare phase) * Sends provisional ISO-20022 message */ export async function prepareBankInstruction(plan: Plan): Promise { - console.log(`[Bank] Preparing instruction for plan ${plan.plan_id}`); - + const mode = bankMode(); + logger.info( + { + planId: plan.plan_id, + mode, + ...(mode === "mock" ? { blockerId: BLOCKER_ID } : {}), + }, + "[Bank] prepareBankInstruction()", + ); + // Mock: In real implementation, this would: // 1. Generate provisional ISO-20022 message (pacs.008 with conditional settlement) // 2. Send to bank connector // 3. Receive provisional acceptance - + await new Promise((resolve) => setTimeout(resolve, 100)); - + return true; } @@ -27,30 +51,39 @@ export async function commitBankInstruction(plan: Plan): Promise<{ isoMessageId?: string; error?: string; }> { - console.log(`[Bank] Committing instruction for plan ${plan.plan_id}`); - + const mode = bankMode(); + logger.info( + { + planId: plan.plan_id, + mode, + ...(mode === "mock" ? { blockerId: BLOCKER_ID } : {}), + }, + "[Bank] commitBankInstruction()", + ); + try { // Generate final ISO-20022 message const isoMessage = await generatePacs008(plan); - + // Mock: In real implementation, this would: // 1. Send ISO message to bank connector // 2. Receive confirmation and message ID // 3. Store message ID for audit trail - - const isoMessageId = `MSG-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - + + const isoMessageId = `MSG-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; + // Simulate processing delay await new Promise((resolve) => setTimeout(resolve, 300)); - + return { success: true, isoMessageId, }; - } catch (error: any) { + } catch (err) { + const error = err instanceof Error ? err.message : String(err); return { success: false, - error: error.message, + error, }; } } @@ -60,13 +93,20 @@ export async function commitBankInstruction(plan: Plan): Promise<{ * Cancels provisional instruction */ export async function abortBankInstruction(planId: string): Promise { - console.log(`[Bank] Aborting instruction for plan ${planId}`); - + const mode = bankMode(); + logger.info( + { + planId, + mode, + ...(mode === "mock" ? { blockerId: BLOCKER_ID } : {}), + }, + "[Bank] abortBankInstruction()", + ); + // Mock: In real implementation, this would: // 1. Generate cancellation message (camt.056) // 2. Send to bank connector // 3. Confirm cancellation - + await new Promise((resolve) => setTimeout(resolve, 100)); } - diff --git a/orchestrator/src/services/completeCredential/identityClient.ts b/orchestrator/src/services/completeCredential/identityClient.ts index df52c3c..808aef5 100644 --- a/orchestrator/src/services/completeCredential/identityClient.ts +++ b/orchestrator/src/services/completeCredential/identityClient.ts @@ -149,7 +149,7 @@ export function createCcIdentityClient( } logger.info( { mode: "mock" }, - "[CcIdentity] HTTP client (no CC_IDENTITY_URL — mock mode)", + "[CcIdentity] HTTP client (no CC_IDENTITY_URL — mock mode; upstream cc-identity-core ships code but not yet deployed)", ); return new MockCcIdentityClient(); } diff --git a/orchestrator/src/services/dbisCore/client.ts b/orchestrator/src/services/dbisCore/client.ts index b119704..8340997 100644 --- a/orchestrator/src/services/dbisCore/client.ts +++ b/orchestrator/src/services/dbisCore/client.ts @@ -210,6 +210,9 @@ export function createDbisCoreClient( logger.info({ baseUrl: cfg.baseUrl, mode: "live" }, "[DbisCore] HTTP client"); return new HttpDbisCoreClient({ ...cfg, baseUrl: cfg.baseUrl }); } - logger.info({ mode: "mock" }, "[DbisCore] HTTP client (no DBIS_CORE_URL — mock mode)"); + logger.info( + { mode: "mock", blockerId: "EXT-DBIS-CORE" }, + "[DbisCore] HTTP client (no DBIS_CORE_URL — mock mode; blocker EXT-DBIS-CORE active)", + ); return new MockDbisCoreClient(); } diff --git a/orchestrator/src/services/finLink/client.ts b/orchestrator/src/services/finLink/client.ts index a90d1b3..4b6b248 100644 --- a/orchestrator/src/services/finLink/client.ts +++ b/orchestrator/src/services/finLink/client.ts @@ -68,9 +68,27 @@ export async function createInProcessFinLinkClient(): Promise { /** * Factory: returns an HTTP client if FIN_SANDBOX_URL is set, else an * in-process client that short-circuits to the sandbox module. + * + * When falling back to the in-process sandbox we emit blocker + * EXT-FIN-GATEWAY (per proxmox/docs/03-deployment/ + * EXTERNAL_DEPENDENCY_BLOCKERS.md) — that id maps 1:1 with the + * deployment checker and signals "no real FIN / Alliance Access + * transport configured yet". */ export async function getFinLinkClient(): Promise { const url = process.env.FIN_SANDBOX_URL; - if (url) return createHttpFinLinkClient(url); + if (url) { + const { logger } = await import("../../logging/logger"); + logger.info( + { baseUrl: url, mode: "live" }, + "[FinLink] HTTP client (FIN_SANDBOX_URL)", + ); + return createHttpFinLinkClient(url); + } + const { logger } = await import("../../logging/logger"); + logger.info( + { mode: "sandbox", blockerId: "EXT-FIN-GATEWAY" }, + "[FinLink] in-process sandbox (no FIN_SANDBOX_URL — blocker EXT-FIN-GATEWAY active)", + ); return createInProcessFinLinkClient(); } diff --git a/orchestrator/tests/config/externalBlockers.test.ts b/orchestrator/tests/config/externalBlockers.test.ts new file mode 100644 index 0000000..586fe09 --- /dev/null +++ b/orchestrator/tests/config/externalBlockers.test.ts @@ -0,0 +1,119 @@ +/** + * Unit tests for the EXT-* external-dependency blocker registry. + * Headless — no network, no UI. + */ + +import { + EXT_BLOCKER_IDS, + BLOCKER_DETAILS, + evaluateBlockers, + activeBlockers, + logBlockerStatusAtBoot, +} from "../../src/config/externalBlockers"; + +describe("externalBlockers registry", () => { + it("exposes exactly the 7 blocker IDs the proxmox checker tracks", () => { + expect(EXT_BLOCKER_IDS).toEqual([ + "EXT-DBIS-CORE", + "EXT-CC-PAYMENT-ADAPTERS", + "EXT-CC-AUDIT-LEDGER", + "EXT-CC-SHARED-EVENTS", + "EXT-CC-SHARED-SCHEMAS", + "EXT-FIN-GATEWAY", + "EXT-CHAIN138-CI-RPC", + ]); + }); + + it("has a detail record for every id", () => { + for (const id of EXT_BLOCKER_IDS) { + expect(BLOCKER_DETAILS[id]).toBeDefined(); + expect(BLOCKER_DETAILS[id].id).toBe(id); + expect(BLOCKER_DETAILS[id].title.length).toBeGreaterThan(0); + expect(BLOCKER_DETAILS[id].description.length).toBeGreaterThan(0); + } + }); +}); + +describe("evaluateBlockers()", () => { + it("marks everything active on an empty env", () => { + const records = evaluateBlockers({}); + expect(records).toHaveLength(EXT_BLOCKER_IDS.length); + expect(records.every((r) => r.status === "active")).toBe(true); + }); + + it("resolves EXT-DBIS-CORE when DBIS_CORE_URL is set", () => { + const records = evaluateBlockers({ DBIS_CORE_URL: "http://x.test" }); + const rec = records.find((r) => r.id === "EXT-DBIS-CORE"); + expect(rec?.status).toBe("resolved"); + expect(rec?.resolvedVia).toBe("DBIS_CORE_URL"); + }); + + it("resolves EXT-FIN-GATEWAY when FIN_SANDBOX_URL is set", () => { + const records = evaluateBlockers({ FIN_SANDBOX_URL: "http://fin.test" }); + expect(records.find((r) => r.id === "EXT-FIN-GATEWAY")?.status).toBe("resolved"); + }); + + it("resolves EXT-CHAIN138-CI-RPC when CHAIN_138_RPC_URL is set", () => { + const records = evaluateBlockers({ + CHAIN_138_RPC_URL: "https://rpc.public-0138.defi-oracle.io", + }); + expect(records.find((r) => r.id === "EXT-CHAIN138-CI-RPC")?.status).toBe("resolved"); + }); + + it("leaves cc-* scaffold blockers active regardless of env", () => { + const records = evaluateBlockers({ + DBIS_CORE_URL: "http://x", + FIN_SANDBOX_URL: "http://y", + CHAIN_138_RPC_URL: "http://z", + }); + const scaffoldIds = [ + "EXT-CC-PAYMENT-ADAPTERS", + "EXT-CC-AUDIT-LEDGER", + "EXT-CC-SHARED-EVENTS", + "EXT-CC-SHARED-SCHEMAS", + ]; + for (const id of scaffoldIds) { + expect(records.find((r) => r.id === id)?.status).toBe("active"); + } + }); + + it("treats empty-string env var as unset (not resolved)", () => { + const records = evaluateBlockers({ DBIS_CORE_URL: "" }); + expect(records.find((r) => r.id === "EXT-DBIS-CORE")?.status).toBe("active"); + }); +}); + +describe("activeBlockers()", () => { + it("returns 7 when env is empty", () => { + expect(activeBlockers({})).toHaveLength(7); + }); + + it("returns 6 when Chain-138 RPC is resolved", () => { + const ids = activeBlockers({ + CHAIN_138_RPC_URL: "https://rpc.public-0138.defi-oracle.io", + }); + expect(ids).not.toContain("EXT-CHAIN138-CI-RPC"); + expect(ids).toHaveLength(6); + }); +}); + +describe("logBlockerStatusAtBoot()", () => { + it("emits a single summary with active + resolved counts", () => { + const calls: Array<{ obj: Record; msg: string }> = []; + const fakeLogger = { + info: (obj: Record, msg: string) => calls.push({ obj, msg }), + }; + const prev = process.env.CHAIN_138_RPC_URL; + process.env.CHAIN_138_RPC_URL = "https://rpc.public-0138.defi-oracle.io"; + try { + logBlockerStatusAtBoot(fakeLogger); + } finally { + if (prev === undefined) delete process.env.CHAIN_138_RPC_URL; + else process.env.CHAIN_138_RPC_URL = prev; + } + expect(calls).toHaveLength(1); + expect(calls[0].msg).toMatch(/active,.*resolved/); + expect((calls[0].obj.activeCount as number) + (calls[0].obj.resolvedCount as number)).toBe(7); + expect(calls[0].obj.resolvedCount).toBeGreaterThanOrEqual(1); + }); +});