feat(portal-bff): ai-client skeleton — vendored protos + grpc client + Principal mapper
CI / commits (pull_request) Successful in 3m34s
CI / scan (pull_request) Successful in 3m47s
CI / check (pull_request) Successful in 6m15s
CI / a11y (pull_request) Successful in 2m26s
Docs site / build (pull_request) Successful in 1m7s
CI / perf (pull_request) Successful in 4m33s

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.
This commit is contained in:
Julien Gautier
2026-05-19 20:50:04 +02:00
parent 3f3f47317b
commit 428d19f60f
32 changed files with 6396 additions and 11 deletions
+110 -6
View File
@@ -49,6 +49,12 @@ importers:
'@azure/msal-node':
specifier: ^5.2.1
version: 5.2.1
'@bufbuild/protobuf':
specifier: ^2.10.2
version: 2.12.0
'@grpc/grpc-js':
specifier: ^1.13.0
version: 1.14.3
'@nestjs/common':
specifier: ^11.0.0
version: 11.1.21(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -160,6 +166,9 @@ importers:
jose:
specifier: ^6.2.3
version: 6.2.3
long:
specifier: ^5.2.3
version: 5.3.2
lucide-angular:
specifier: ^1.0.0
version: 1.0.0(@angular/common@21.2.13(@angular/core@21.2.13(@angular/compiler@21.2.13)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@21.2.13(@angular/compiler@21.2.13)(rxjs@7.8.2)(zone.js@0.16.2))
@@ -347,6 +356,9 @@ importers:
eslint-plugin-playwright:
specifier: ^1.6.2
version: 1.8.3(eslint@9.39.4(jiti@2.7.0))
grpc-tools:
specifier: ^1.13.0
version: 1.13.1(encoding@0.1.13)
husky:
specifier: ^9.1.7
version: 9.1.7
@@ -395,6 +407,9 @@ importers:
ts-node:
specifier: 10.9.2
version: 10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.21))(@types/node@24.12.4)(typescript@5.9.3)
ts-proto:
specifier: ^2.7.0
version: 2.11.8
tslib:
specifier: ^2.3.0
version: 2.8.1
@@ -2541,6 +2556,11 @@ packages:
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
engines: {node: '>=8'}
'@mapbox/node-pre-gyp@2.0.3':
resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==}
engines: {node: '>=18'}
hasBin: true
'@mermaid-js/mermaid-mindmap@9.3.0':
resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==}
@@ -5354,6 +5374,10 @@ packages:
resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==}
hasBin: true
abbrev@3.0.1:
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
engines: {node: ^18.17.0 || >=20.5.0}
abbrev@4.0.0:
resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
engines: {node: ^20.17.0 || >=22.9.0}
@@ -5855,6 +5879,10 @@ packages:
caniuse-lite@1.0.30001793:
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
case-anything@2.1.13:
resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
engines: {node: '>=12.13'}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -6613,6 +6641,11 @@ packages:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
hasBin: true
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -6679,6 +6712,9 @@ packages:
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
engines: {node: '>=12'}
dprint-node@1.0.8:
resolution: {integrity: sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -7319,6 +7355,10 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
grpc-tools@1.13.1:
resolution: {integrity: sha512-0sttMUxThNIkCTJq5qI0xXMz5zWqV2u3yG1kR3Sj9OokGIoyRBFjoInK9NyW7x5fH7knj48Roh1gq5xbl0VoDQ==}
hasBin: true
hachure-fill@0.5.2:
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
@@ -8715,6 +8755,11 @@ packages:
non-layered-tidy-tree-layout@2.0.2:
resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
nopt@8.1.0:
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true
nopt@9.0.0:
resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==}
engines: {node: ^20.17.0 || >=22.9.0}
@@ -10547,6 +10592,16 @@ packages:
'@swc/wasm':
optional: true
ts-poet@6.12.0:
resolution: {integrity: sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==}
ts-proto-descriptors@2.1.0:
resolution: {integrity: sha512-S5EZYEQ6L9KLFfjSRpZWDIXDV/W7tAj8uW7pLsihIxyr62EAVSiKuVPwE8iWnr849Bqa53enex1jhDUcpgquzA==}
ts-proto@2.11.8:
resolution: {integrity: sha512-+5hzECnyVB33jxjG1BIdzAHcRBm7hjnm8womdJVp2A7xJWihP0drHHVsXYTr9i/LpWNGfh80I+AVVNzFM5AwJw==}
hasBin: true
tsconfig-paths-webpack-plugin@4.2.0:
resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==}
engines: {node: '>=10.13.0'}
@@ -13702,6 +13757,19 @@ snapshots:
'@lukeed/csprng@1.1.0': {}
'@mapbox/node-pre-gyp@2.0.3(encoding@0.1.13)':
dependencies:
consola: 3.4.2
detect-libc: 2.1.2
https-proxy-agent: 7.0.6
node-fetch: 2.7.0(encoding@0.1.13)
nopt: 8.1.0
semver: 7.8.0
tar: 7.5.15
transitivePeerDependencies:
- encoding
- supports-color
'@mermaid-js/mermaid-mindmap@9.3.0':
dependencies:
'@braintree/sanitize-url': 6.0.4
@@ -14157,7 +14225,7 @@ snapshots:
'@npmcli/fs@5.0.0':
dependencies:
semver: 7.7.4
semver: 7.8.0
'@npmcli/git@7.0.2':
dependencies:
@@ -14167,7 +14235,7 @@ snapshots:
lru-cache: 11.3.6
npm-pick-manifest: 11.0.3
proc-log: 6.1.0
semver: 7.7.4
semver: 7.8.0
which: 6.0.1
'@npmcli/installed-package-contents@4.0.0':
@@ -14184,7 +14252,7 @@ snapshots:
hosted-git-info: 9.0.3
json-parse-even-better-errors: 5.0.0
proc-log: 6.1.0
semver: 7.7.4
semver: 7.8.0
spdx-expression-parse: 4.0.0
'@npmcli/promise-spawn@9.0.1':
@@ -16984,6 +17052,8 @@ snapshots:
dependencies:
argparse: 2.0.1
abbrev@3.0.1: {}
abbrev@4.0.0: {}
accepts@1.3.8:
@@ -17564,6 +17634,8 @@ snapshots:
caniuse-lite@1.0.30001793: {}
case-anything@2.1.13: {}
ccount@2.0.1: {}
chai@6.2.2: {}
@@ -18318,6 +18390,8 @@ snapshots:
destroy@1.2.0: {}
detect-libc@1.0.3: {}
detect-libc@2.1.2: {}
detect-newline@3.1.0: {}
@@ -18376,6 +18450,10 @@ snapshots:
dotenv@16.6.1: {}
dprint-node@1.0.8:
dependencies:
detect-libc: 1.0.3
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -19158,6 +19236,13 @@ snapshots:
graceful-fs@4.2.11: {}
grpc-tools@1.13.1(encoding@0.1.13):
dependencies:
'@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13)
transitivePeerDependencies:
- encoding
- supports-color
hachure-fill@0.5.2: {}
handle-thing@2.0.1: {}
@@ -20756,7 +20841,7 @@ snapshots:
graceful-fs: 4.2.11
nopt: 9.0.0
proc-log: 6.1.0
semver: 7.7.4
semver: 7.8.0
tar: 7.5.15
tinyglobby: 0.2.16
undici: 6.25.0
@@ -20775,6 +20860,10 @@ snapshots:
non-layered-tidy-tree-layout@2.0.2:
optional: true
nopt@8.1.0:
dependencies:
abbrev: 3.0.1
nopt@9.0.0:
dependencies:
abbrev: 4.0.0
@@ -20787,7 +20876,7 @@ snapshots:
npm-install-checks@8.0.0:
dependencies:
semver: 7.7.4
semver: 7.8.0
npm-normalize-package-bin@5.0.0: {}
@@ -20808,7 +20897,7 @@ snapshots:
npm-install-checks: 8.0.0
npm-normalize-package-bin: 5.0.0
npm-package-arg: 13.0.2
semver: 7.7.4
semver: 7.8.0
npm-registry-fetch@19.1.1:
dependencies:
@@ -22881,6 +22970,21 @@ snapshots:
optionalDependencies:
'@swc/core': 1.15.33(@swc/helpers@0.5.21)
ts-poet@6.12.0:
dependencies:
dprint-node: 1.0.8
ts-proto-descriptors@2.1.0:
dependencies:
'@bufbuild/protobuf': 2.12.0
ts-proto@2.11.8:
dependencies:
'@bufbuild/protobuf': 2.12.0
case-anything: 2.1.13
ts-poet: 6.12.0
ts-proto-descriptors: 2.1.0
tsconfig-paths-webpack-plugin@4.2.0:
dependencies:
chalk: 4.1.2