PR Y: wire EXT-* blocker IDs into orchestrator mock-mode logs #28
159
orchestrator/src/config/externalBlockers.ts
Normal file
159
orchestrator/src/config/externalBlockers.ts
Normal file
@@ -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<ExtBlockerId, ExtBlockerDetail> = {
|
||||
"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<string, unknown>, 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`,
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<void> {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -68,9 +68,27 @@ export async function createInProcessFinLinkClient(): Promise<FinLinkClient> {
|
||||
/**
|
||||
* 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<FinLinkClient> {
|
||||
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();
|
||||
}
|
||||
|
||||
119
orchestrator/tests/config/externalBlockers.test.ts
Normal file
119
orchestrator/tests/config/externalBlockers.test.ts
Normal file
@@ -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<string, unknown>; msg: string }> = [];
|
||||
const fakeLogger = {
|
||||
info: (obj: Record<string, unknown>, 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user