PR W: DBIS Complete Credential (cc-*) adapters with provider-switch #27

Merged
nsatoshi merged 1 commits from devin/1776892271-pr-w-cc-adapters into main 2026-04-22 21:12:23 +00:00
5 changed files with 647 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
/**
* Loader for the `DBIS/cc-compliance-controls` controls matrix.
*
* `cc-compliance-controls` ships a v0 matrix at
* `controls/matrix/v0.yaml`. When `CC_CONTROLS_MATRIX_URL` is set the
* loader fetches that remote YAML; otherwise it returns an embedded
* snapshot so the orchestrator always has a usable matrix to assert
* against in validation/obligation flows without a network hop.
*
* The embedded snapshot is a faithful copy of the upstream v0 matrix
* at recon time — if upstream evolves, re-sync by fetching and
* replacing the `EMBEDDED_V0_MATRIX` literal.
*/
import { logger } from "../../logging/logger";
import type { CcControlsMatrix } from "./types";
export interface CcControlsConfig {
url?: string;
timeoutMs?: number;
fetchImpl?: typeof fetch;
}
/**
* Embedded v0 matrix — kept small and hand-typed rather than parsed
* from YAML so the orchestrator doesn't drag in a YAML runtime.
*/
const EMBEDDED_V0_MATRIX: CcControlsMatrix = {
version: 0,
source: "embedded",
domains: [
{
id: "identity_proofing",
controls: [
{
id: "IDP-001",
title: "Identity enrollment recorded in audit ledger",
evidenceType: "audit_event",
ownerTeam: "TBD",
frequency: "continuous",
},
],
},
{
id: "payment_issuance",
controls: [
{
id: "PAY-001",
title: "No production PAN in non-production",
evidenceType: "config_scan",
ownerTeam: "TBD",
frequency: "per_release",
},
],
},
{
id: "audit_non_repudiation",
controls: [
{
id: "AUD-001",
title: "Credential state change only via workflow + immutable event",
evidenceType: "architecture_review",
ownerTeam: "TBD",
frequency: "quarterly",
},
],
},
{
id: "registry_verticals",
controls: [
{
id: "REG-001",
title: "Judicial registry data classified high sensitivity; tenant-scoped APIs only",
evidenceType: "policy_review",
ownerTeam: "TBD",
frequency: "quarterly",
},
],
},
],
};
function loadConfigFromEnv(): CcControlsConfig {
return {
url: process.env.CC_CONTROLS_MATRIX_URL,
timeoutMs: process.env.CC_CONTROLS_MATRIX_TIMEOUT_MS
? parseInt(process.env.CC_CONTROLS_MATRIX_TIMEOUT_MS, 10)
: 10_000,
};
}
/**
* Minimal JSON-or-YAML-ish adapter: upstream ships YAML today but
* could add a JSON endpoint. This loader only accepts `application/
* json` responses — if the endpoint is pure YAML, serve it via a thin
* JSON-convert proxy or extend this loader.
*/
export async function loadControlsMatrix(
cfg: CcControlsConfig = loadConfigFromEnv(),
): Promise<CcControlsMatrix> {
if (!cfg.url) {
logger.info(
{ source: "embedded" },
"[CcControls] controls matrix (no CC_CONTROLS_MATRIX_URL — embedded v0)",
);
return EMBEDDED_V0_MATRIX;
}
const fetchImpl = cfg.fetchImpl ?? fetch;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs ?? 10_000);
try {
const resp = await fetchImpl(cfg.url, {
method: "GET",
headers: { Accept: "application/json" },
signal: controller.signal,
});
if (!resp.ok) {
throw new Error(
`cc-controls matrix GET failed: HTTP ${resp.status}`,
);
}
const body = (await resp.json()) as unknown;
const parsed = normaliseMatrix(body);
logger.info(
{ source: "remote", url: cfg.url, version: parsed.version },
"[CcControls] controls matrix (remote)",
);
return parsed;
} finally {
clearTimeout(timer);
}
}
function normaliseMatrix(raw: unknown): CcControlsMatrix {
if (typeof raw !== "object" || raw === null) {
throw new Error("cc-controls matrix: response is not an object");
}
const r = raw as Record<string, unknown>;
const version = typeof r.version === "number" ? r.version : 0;
const domains = Array.isArray(r.domains) ? r.domains : [];
return {
version,
source: "remote",
domains: domains.map((d) => normaliseDomain(d)),
};
}
function normaliseDomain(raw: unknown): CcControlsMatrix["domains"][number] {
const r = (raw ?? {}) as Record<string, unknown>;
const controls = Array.isArray(r.controls) ? r.controls : [];
return {
id: String(r.id ?? ""),
controls: controls.map((c) => normaliseControl(c)),
};
}
function normaliseControl(raw: unknown): CcControlsMatrix["domains"][number]["controls"][number] {
const r = (raw ?? {}) as Record<string, unknown>;
return {
id: String(r.id ?? ""),
title: String(r.title ?? ""),
evidenceType: String(r.evidence_type ?? r.evidenceType ?? ""),
ownerTeam: String(r.owner_team ?? r.ownerTeam ?? "TBD"),
frequency: String(r.frequency ?? ""),
};
}
/**
* Convenience helper — resolve a control by id across all domains.
* Used by evaluator flows that need to attach control evidence to a
* transition.
*/
export function findControl(
matrix: CcControlsMatrix,
controlId: string,
): CcControlsMatrix["domains"][number]["controls"][number] | undefined {
for (const d of matrix.domains) {
for (const c of d.controls) {
if (c.id === controlId) return c;
}
}
return undefined;
}

View File

@@ -0,0 +1,155 @@
/**
* HTTP client adapter for `DBIS/cc-identity-core`.
*
* Provider-switched: when `CC_IDENTITY_URL` is set the client makes
* real HTTP calls to the upstream Complete Credential identity
* service; otherwise every method returns a deterministic mock so
* unit tests, local dev, and CI still work.
*
* Upstream surface (openapi.yaml + src/server.mjs at recon time):
* GET /health
* GET /ready
* POST /v1/subjects
*
* Extend as additional endpoints ship upstream.
*/
import { randomUUID } from "crypto";
import { logger } from "../../logging/logger";
import type {
CcHealthStatus,
CcSubject,
CcSubjectCreate,
} from "./types";
export interface CcIdentityConfig {
baseUrl?: string;
apiKey?: string;
timeoutMs?: number;
fetchImpl?: typeof fetch;
}
export interface CcIdentityClient {
mode: "live" | "mock";
health(): Promise<CcHealthStatus>;
ready(): Promise<CcHealthStatus>;
createSubject(req: CcSubjectCreate, correlationId?: string): Promise<CcSubject>;
}
function loadConfigFromEnv(): CcIdentityConfig {
return {
baseUrl: process.env.CC_IDENTITY_URL,
apiKey: process.env.CC_IDENTITY_API_KEY,
timeoutMs: process.env.CC_IDENTITY_TIMEOUT_MS
? parseInt(process.env.CC_IDENTITY_TIMEOUT_MS, 10)
: 10_000,
};
}
class HttpCcIdentityClient implements CcIdentityClient {
readonly mode = "live" as const;
private readonly baseUrl: string;
private readonly apiKey?: string;
private readonly timeoutMs: number;
private readonly fetchImpl: typeof fetch;
constructor(
cfg: Required<Pick<CcIdentityConfig, "baseUrl">> & CcIdentityConfig,
) {
this.baseUrl = cfg.baseUrl.replace(/\/+$/, "");
this.apiKey = cfg.apiKey;
this.timeoutMs = cfg.timeoutMs ?? 10_000;
this.fetchImpl = cfg.fetchImpl ?? fetch;
}
private async request<T>(
method: "GET" | "POST",
path: string,
body?: unknown,
correlationId?: string,
): Promise<T> {
const url = `${this.baseUrl}${path}`;
const headers: Record<string, string> = { Accept: "application/json" };
if (body !== undefined) headers["Content-Type"] = "application/json";
if (this.apiKey) headers["X-API-Key"] = this.apiKey;
if (correlationId) headers["X-Correlation-Id"] = correlationId;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const resp = await this.fetchImpl(url, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
if (!resp.ok) {
const text = await resp.text().catch(() => "");
throw new Error(
`cc-identity ${method} ${path} failed: HTTP ${resp.status} ${text.slice(0, 200)}`,
);
}
return (await resp.json()) as T;
} finally {
clearTimeout(timer);
}
}
health(): Promise<CcHealthStatus> {
return this.request<CcHealthStatus>("GET", "/health");
}
ready(): Promise<CcHealthStatus> {
return this.request<CcHealthStatus>("GET", "/ready");
}
createSubject(
req: CcSubjectCreate,
correlationId?: string,
): Promise<CcSubject> {
return this.request<CcSubject>(
"POST",
"/v1/subjects",
req,
correlationId ?? randomUUID(),
);
}
}
class MockCcIdentityClient implements CcIdentityClient {
readonly mode = "mock" as const;
async health(): Promise<CcHealthStatus> {
return { status: "ok", service: "cc-identity-core" };
}
async ready(): Promise<CcHealthStatus> {
return { status: "ok", service: "cc-identity-core", persistence: false };
}
async createSubject(req: CcSubjectCreate): Promise<CcSubject> {
return {
subjectId: randomUUID(),
tenantId: req.tenantId ?? "tenant-demo",
entityId: req.entityId ?? "entity-demo",
createdAt: new Date().toISOString(),
};
}
}
export function createCcIdentityClient(
cfg: CcIdentityConfig = loadConfigFromEnv(),
): CcIdentityClient {
if (cfg.baseUrl) {
logger.info(
{ baseUrl: cfg.baseUrl, mode: "live" },
"[CcIdentity] HTTP client",
);
return new HttpCcIdentityClient({ ...cfg, baseUrl: cfg.baseUrl });
}
logger.info(
{ mode: "mock" },
"[CcIdentity] HTTP client (no CC_IDENTITY_URL — mock mode)",
);
return new MockCcIdentityClient();
}

View File

@@ -0,0 +1,28 @@
/**
* Public surface for the DBIS Complete Credential (cc-*) adapters.
*
* Covers the upstream bounded-context repos the orchestrator needs:
* - cc-identity-core → identityClient (HTTP, provider-switched)
* - cc-compliance-controls → controlsMatrix (embedded v0 with
* optional remote JSON override)
*
* cc-payment-adapters / cc-audit-ledger / cc-shared-events are still
* template scaffolds upstream at recon time; when those services
* ship, add sibling clients here following the same pattern.
*/
export * from "./types";
export {
createCcIdentityClient,
} from "./identityClient";
export type {
CcIdentityClient,
CcIdentityConfig,
} from "./identityClient";
export {
loadControlsMatrix,
findControl,
} from "./controlsMatrix";
export type {
CcControlsConfig,
} from "./controlsMatrix";

View File

@@ -0,0 +1,49 @@
/**
* Types shared across the Complete Credential (DBIS cc-*) adapters.
*
* Shapes mirror the relevant upstream repos:
* - cc-identity-core (openapi/openapi.yaml + src/server.mjs)
* - cc-compliance-controls (controls/matrix/v0.yaml)
*
* Only the fields the orchestrator actually consumes are typed —
* extend as needed when more of the CC surface is wired.
*/
export interface CcHealthStatus {
status: "ok" | "unready";
service: string;
persistence?: boolean;
error?: string;
}
export interface CcSubjectCreate {
tenantId?: string;
entityId?: string;
metadata?: Record<string, string | number | boolean>;
}
export interface CcSubject {
subjectId: string;
tenantId: string;
entityId: string;
createdAt: string;
}
export interface CcControl {
id: string;
title: string;
evidenceType: string;
ownerTeam: string;
frequency: string;
}
export interface CcControlDomain {
id: string;
controls: CcControl[];
}
export interface CcControlsMatrix {
version: number;
source: "embedded" | "remote";
domains: CcControlDomain[];
}

View File

@@ -0,0 +1,232 @@
/**
* Unit tests for the Complete Credential (DBIS cc-*) adapters.
*
* All tests are headless — they either exercise the embedded mock /
* matrix or stub `fetch` directly. No network, no UI.
*/
import {
createCcIdentityClient,
loadControlsMatrix,
findControl,
type CcIdentityClient,
} from "../../src/services/completeCredential";
describe("completeCredential.createCcIdentityClient() — mock mode", () => {
let client: CcIdentityClient;
beforeAll(() => {
delete process.env.CC_IDENTITY_URL;
delete process.env.CC_IDENTITY_API_KEY;
client = createCcIdentityClient();
});
it("reports mock mode", () => {
expect(client.mode).toBe("mock");
});
it("returns ok for health()", async () => {
const h = await client.health();
expect(h.status).toBe("ok");
expect(h.service).toBe("cc-identity-core");
});
it("returns a ready response with persistence=false in mock", async () => {
const r = await client.ready();
expect(r.status).toBe("ok");
expect(r.persistence).toBe(false);
});
it("creates a subject with a uuid and defaulted tenant/entity", async () => {
const s = await client.createSubject({});
expect(s.subjectId).toMatch(/^[0-9a-f-]{36}$/);
expect(s.tenantId).toBe("tenant-demo");
expect(s.entityId).toBe("entity-demo");
});
it("passes tenant/entity through when provided", async () => {
const s = await client.createSubject({
tenantId: "t-acme",
entityId: "e-bank-1",
});
expect(s.tenantId).toBe("t-acme");
expect(s.entityId).toBe("e-bank-1");
});
});
describe("completeCredential.createCcIdentityClient() — live mode (stubbed fetch)", () => {
function makeFetch(
record: (url: string, init: RequestInit) => void,
responseBody: unknown,
status = 200,
): typeof fetch {
return (async (input: string | URL | Request, init?: RequestInit) => {
record(String(input), init ?? {});
return new Response(JSON.stringify(responseBody), {
status,
headers: { "Content-Type": "application/json" },
});
}) as typeof fetch;
}
it("reports live mode when baseUrl is set", () => {
const client = createCcIdentityClient({
baseUrl: "http://cc.example.test",
fetchImpl: makeFetch(() => undefined, { status: "ok", service: "x" }),
});
expect(client.mode).toBe("live");
});
it("hits GET /health", async () => {
const calls: string[] = [];
const client = createCcIdentityClient({
baseUrl: "http://cc.example.test",
fetchImpl: makeFetch(
(url) => {
calls.push(url);
},
{ status: "ok", service: "cc-identity-core" },
),
});
const h = await client.health();
expect(h.status).toBe("ok");
expect(calls[0]).toBe("http://cc.example.test/health");
});
it("posts to /v1/subjects with X-Correlation-Id + api key header", async () => {
const calls: { url: string; headers: Record<string, string>; body?: string }[] = [];
const client = createCcIdentityClient({
baseUrl: "http://cc.example.test",
apiKey: "k-1",
fetchImpl: makeFetch(
(url, init) => {
calls.push({
url,
headers: (init.headers ?? {}) as Record<string, string>,
body: init.body as string,
});
},
{
subjectId: "11111111-2222-3333-4444-555555555555",
tenantId: "t-1",
entityId: "e-1",
createdAt: "2026-01-01T00:00:00Z",
},
),
});
const s = await client.createSubject({ tenantId: "t-1", entityId: "e-1" }, "corr-42");
expect(s.subjectId).toContain("-");
expect(calls[0].url).toBe("http://cc.example.test/v1/subjects");
expect(calls[0].headers["X-API-Key"]).toBe("k-1");
expect(calls[0].headers["X-Correlation-Id"]).toBe("corr-42");
expect(JSON.parse(calls[0].body ?? "{}")).toEqual({
tenantId: "t-1",
entityId: "e-1",
});
});
it("auto-generates a correlation id when not provided", async () => {
const calls: Record<string, string>[] = [];
const client = createCcIdentityClient({
baseUrl: "http://cc.example.test",
fetchImpl: makeFetch(
(_url, init) => {
calls.push((init.headers ?? {}) as Record<string, string>);
},
{
subjectId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
tenantId: "t",
entityId: "e",
createdAt: "2026-01-01T00:00:00Z",
},
),
});
await client.createSubject({});
expect(calls[0]["X-Correlation-Id"]).toMatch(/^[0-9a-f-]{36}$/);
});
it("throws a descriptive error on non-2xx", async () => {
const client = createCcIdentityClient({
baseUrl: "http://cc.example.test",
fetchImpl: makeFetch(() => undefined, { error: "boom" }, 500),
});
await expect(client.health()).rejects.toThrow(/HTTP 500/);
});
});
describe("completeCredential.loadControlsMatrix() — embedded mode", () => {
beforeAll(() => {
delete process.env.CC_CONTROLS_MATRIX_URL;
});
it("returns the embedded v0 matrix when no URL is set", async () => {
const m = await loadControlsMatrix();
expect(m.source).toBe("embedded");
expect(m.version).toBe(0);
expect(m.domains.length).toBeGreaterThan(0);
});
it("exposes expected control ids", async () => {
const m = await loadControlsMatrix();
const ids = m.domains.flatMap((d) => d.controls.map((c) => c.id));
expect(ids).toEqual(expect.arrayContaining(["IDP-001", "PAY-001", "AUD-001", "REG-001"]));
});
it("findControl() resolves by id", async () => {
const m = await loadControlsMatrix();
const c = findControl(m, "PAY-001");
expect(c?.title).toContain("PAN");
});
it("findControl() returns undefined for unknown ids", async () => {
const m = await loadControlsMatrix();
expect(findControl(m, "NOPE-999")).toBeUndefined();
});
});
describe("completeCredential.loadControlsMatrix() — remote mode", () => {
function makeFetch(responseBody: unknown, status = 200): typeof fetch {
return (async () =>
new Response(JSON.stringify(responseBody), {
status,
headers: { "Content-Type": "application/json" },
})) as typeof fetch;
}
it("fetches and normalises a JSON matrix", async () => {
const matrix = await loadControlsMatrix({
url: "http://cc.example.test/controls/matrix/v0.json",
fetchImpl: makeFetch({
version: 1,
domains: [
{
id: "extra",
controls: [
{
id: "X-001",
title: "Extra",
evidence_type: "doc_review",
owner_team: "ops",
frequency: "monthly",
},
],
},
],
}),
});
expect(matrix.source).toBe("remote");
expect(matrix.version).toBe(1);
expect(matrix.domains[0].controls[0].evidenceType).toBe("doc_review");
expect(matrix.domains[0].controls[0].ownerTeam).toBe("ops");
});
it("throws on non-2xx", async () => {
await expect(
loadControlsMatrix({
url: "http://cc.example.test/nope",
fetchImpl: makeFetch({}, 404),
}),
).rejects.toThrow(/HTTP 404/);
});
});