Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Orchestrator Unit Tests (push) Has been cancelled
CI / Orchestrator E2E (Testcontainers) (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
156 lines
4.3 KiB
TypeScript
156 lines
4.3 KiB
TypeScript
/**
|
|
* 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; upstream cc-identity-core ships code but not yet deployed)",
|
|
);
|
|
return new MockCcIdentityClient();
|
|
}
|