PR #27 (squash-merged via Gitea API)
Some checks failed
CI / Frontend Lint (push) Failing after 6s
CI / Frontend Type Check (push) Failing after 7s
CI / Frontend Build (push) Failing after 7s
CI / Frontend E2E Tests (push) Failing after 7s
CI / Orchestrator Build (push) Failing after 7s
CI / Orchestrator Unit Tests (push) Failing after 5s
CI / Orchestrator E2E (Testcontainers) (push) Failing after 6s
CI / Contracts Compile (push) Failing after 7s
CI / Contracts Test (push) Failing after 5s
Security Scan / Dependency Vulnerability Scan (push) Failing after 5s
Security Scan / OWASP ZAP Scan (push) Failing after 3s
Some checks failed
CI / Frontend Lint (push) Failing after 6s
CI / Frontend Type Check (push) Failing after 7s
CI / Frontend Build (push) Failing after 7s
CI / Frontend E2E Tests (push) Failing after 7s
CI / Orchestrator Build (push) Failing after 7s
CI / Orchestrator Unit Tests (push) Failing after 5s
CI / Orchestrator E2E (Testcontainers) (push) Failing after 6s
CI / Contracts Compile (push) Failing after 7s
CI / Contracts Test (push) Failing after 5s
Security Scan / Dependency Vulnerability Scan (push) Failing after 5s
Security Scan / OWASP ZAP Scan (push) Failing after 3s
This commit was merged in pull request #27.
This commit is contained in:
183
orchestrator/src/services/completeCredential/controlsMatrix.ts
Normal file
183
orchestrator/src/services/completeCredential/controlsMatrix.ts
Normal 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;
|
||||
}
|
||||
155
orchestrator/src/services/completeCredential/identityClient.ts
Normal file
155
orchestrator/src/services/completeCredential/identityClient.ts
Normal 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();
|
||||
}
|
||||
28
orchestrator/src/services/completeCredential/index.ts
Normal file
28
orchestrator/src/services/completeCredential/index.ts
Normal 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";
|
||||
49
orchestrator/src/services/completeCredential/types.ts
Normal file
49
orchestrator/src/services/completeCredential/types.ts
Normal 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[];
|
||||
}
|
||||
Reference in New Issue
Block a user