Lands the BFF-side skeleton for the apf-ai-service relay per ADR-0024,
step 2 of the chantier (skeleton + tests against an in-process fake
gRPC server; no live HTTP endpoint yet — that ships in the SSE-bridge
PR).
What ships:
- contract/proto vendoring at apps/portal-bff/src/grpc/proto/apf-ai/
(common, chat, rag, ingestion, models). pnpm run grpc:sync refreshes
from the sibling apf-ai-service tree; both .proto and the ts-proto
TypeScript stubs under grpc/gen/ are committed for hermetic builds
and reviewable diffs.
- Codegen via pnpm run grpc:codegen using grpc-tools' bundled protoc
and ts-proto (outputServices=grpc-js, esModuleInterop, forceLong,
useOptionals=messages). Generated tree is excluded from Prettier
and ESLint so hand-rules do not chase codegen output.
- assertAiServiceConfig() pre-flight validator alongside the other
config validators (AI_SERVICE_GRPC_ENDPOINT, AI_SERVICE_CLIENT_ID,
AI_SERVICE_GRPC_TLS).
- AiClientModule provides ChatClient / RagClient / IngestionClient /
ModelsClient wrappers, a GrpcMetadataBuilder that stamps every call
with x-client-id + x-correlation-id (W3C trace-id from OTel when a
span is active, explicit override or UUID fallback otherwise), and
a PrincipalMapper that hashes the Entra oid via HashUserIdService
so the proto Principal.subject matches audit.events.actor_id_hash
exactly (ADR-0013 cross-reference contract).
- Specs cover: env validator, principal mapper, metadata builder
(OTel span / override / fallback paths), chat client streaming +
cancellation against an in-process fake gRPC ChatService, rag
client unary happy path + error propagation, and module bootstrap.
AiClientModule is NOT imported in AppModule yet — the SSE-bridge
controller and its live route ship in the next PR.