Add smart-ide-global API layer, SSO delegates proxy, .logs access logs

- New smart-ide-global-api (127.0.0.1:37149): internal bearer, upstream proxy, X-OIDC forward
- SSO gateway calls global API with GLOBAL_API_INTERNAL_TOKEN; logs to .logs/sso-gateway/
- Aggregated config example, docs, VERSION 0.0.2, claw proxy local URL hint
This commit is contained in:
Nicolas Cantu 2026-04-03 23:08:52 +02:00
parent 3b3e1e67de
commit 0af507143a
35 changed files with 784 additions and 239 deletions

3
.gitignore vendored
View File

@ -15,6 +15,9 @@ config/services.local.env
# logs/ : le répertoire reste versionné (README.md, logs/.gitignore) ; aucun fichier journal versionné
logs/**/*.log
# .logs/ : journaux SSO / API globale (README + .gitignore versionnés)
.logs/**/*.log
# projects/ : ignorer tout répertoire d'id sous projects/ sauf les squelettes versionnés (conf.json par id, gabarit example/)
projects/*
!projects/README.md

1
.logs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**/*.log

5
.logs/README.md Normal file
View File

@ -0,0 +1,5 @@
# Journaux locaux (`.logs/`)
Fichiers produits par les services (ex. passerelle SSO, API globale) : **une ligne JSON par requête** dans `sso-gateway/access.log` et `global-api/access.log`.
Les fichiers `*.log` sous `.logs/` sont ignorés par Git (voir `.gitignore` à la racine du monorepo).

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.0.2

View File

@ -1,3 +1,5 @@
# Configuration locale
# Configuration locale (`config/`)
- **`services.local.env.example`** : variables pour lIDE et tous les services HTTP ; copier vers **`services.local.env`** (non versionné, voir racine **`.gitignore`**).
- **`services.local.env.example`** : variables denvironnement agrégées pour les micro-services (ports sur `127.0.0.1`, jetons, URL de lAPI globale et du SSO). Copier vers **`services.local.env`** (gitignoré à la racine du monorepo).
Voir aussi [docs/README.md](../docs/README.md).

View File

@ -1,74 +1,59 @@
# Local IDE / agent configuration for all smart_ide HTTP services and the tools bridge.
# Copy to config/services.local.env and fill secrets. Do not commit services.local.env.
#
# Load before starting processes, e.g.: set -a && source config/services.local.env && set +a
# Copier vers config/services.local.env (gitignoré) et adapter.
# Profil micro-services : écoute et cibles **127.0.0.1** uniquement pour le maillage smart_ide.
# Les jetons utilisateur OIDC sont validés par la passerelle SSO ; lIdP peut être distant (docv / Enso) — ce fichier concerne les **ports et secrets locaux** du monorepo.
# Monorepo root (path validation for tools-bridge jobs)
SMART_IDE_MONOREPO_ROOT=/absolute/path/to/smart_ide
SMART_IDE_MONOREPO_ROOT=
# Optional: extra allowed path prefixes (comma-separated) for tools-bridge file jobs
# TOOLS_ALLOWED_PATH_PREFIXES=/data/clones,/home/user/projects
# --- smart-ide-global-api (démarrer avant le SSO) ---
GLOBAL_API_HOST=127.0.0.1
GLOBAL_API_PORT=37149
GLOBAL_API_INTERNAL_TOKEN=
# --- smart-ide-tools-bridge (Carbonyl / PageIndex / Chandra + registry) ---
TOOLS_BRIDGE_HOST=127.0.0.1
TOOLS_BRIDGE_PORT=37147
TOOLS_BRIDGE_TOKEN=
TOOLS_BRIDGE_JOB_TIMEOUT_MS=3600000
# --- smart-ide-sso-gateway ---
SSO_GATEWAY_HOST=127.0.0.1
SSO_GATEWAY_PORT=37148
GLOBAL_API_URL=http://127.0.0.1:37149
# OIDC_ISSUER= # URL issuer OpenID (JWKS) — hors maillage HTTP interne
# OIDC_AUDIENCE=
# OIDC_JWKS_URI=
# SSO_CORS_ORIGIN=
# SSO_GATEWAY_MAX_BODY_BYTES=33554432
# --- Jetons / hôtes micro-services (consommés par smart-ide-global-api) ---
ORCHESTRATOR_HOST=127.0.0.1
ORCHESTRATOR_PORT=37145
ORCHESTRATOR_TOKEN=
# --- Core HTTP micro-services ---
REPOS_DEVTOOLS_HOST=127.0.0.1
REPOS_DEVTOOLS_PORT=37140
REPOS_DEVTOOLS_TOKEN=
REPOS_DEVTOOLS_ROOT=
LANGEXTRACT_API_HOST=127.0.0.1
LANGEXTRACT_API_PORT=37141
LANGEXTRACT_SERVICE_TOKEN=
CLAW_PROXY_HOST=127.0.0.1
CLAW_PROXY_PORT=37142
CLAW_PROXY_TOKEN=
CLAW_UPSTREAM_URL=
REGEX_SEARCH_HOST=127.0.0.1
REGEX_SEARCH_PORT=37143
REGEX_SEARCH_TOKEN=
REGEX_SEARCH_ROOT=
LOCAL_OFFICE_URL=http://127.0.0.1:8000
LOCAL_OFFICE_API_KEY=
IA_DEV_GATEWAY_HOST=127.0.0.1
IA_DEV_GATEWAY_PORT=37144
IA_DEV_GATEWAY_TOKEN=
ORCHESTRATOR_HOST=127.0.0.1
ORCHESTRATOR_PORT=37145
ORCHESTRATOR_TOKEN=
ANYTHINGLLM_DEVTOOLS_HOST=127.0.0.1
ANYTHINGLLM_DEVTOOLS_PORT=37146
ANYTHINGLLM_DEVTOOLS_TOKEN=
ANYTHINGLLM_BASE_URL=
ANYTHINGLLM_API_KEY=
REPOS_DEVTOOLS_URL=http://127.0.0.1:37140
# Orchestrator → tools bridge (for intent resolution hints)
TOOLS_BRIDGE_URL=http://127.0.0.1:37147
TOOLS_BRIDGE_HOST=127.0.0.1
TOOLS_BRIDGE_PORT=37147
TOOLS_BRIDGE_TOKEN=
# --- smart-ide-sso-gateway (OIDC user token → proxy to micro-services) ---
SSO_GATEWAY_HOST=127.0.0.1
SSO_GATEWAY_PORT=37148
# SSO_CORS_ORIGIN=
# Required when running the gateway: docv / Enso issuer URL
OIDC_ISSUER=
# OIDC_AUDIENCE=
# OIDC_JWKS_URI=
LANGEXTRACT_API_HOST=127.0.0.1
LANGEXTRACT_API_PORT=37141
LANGEXTRACT_SERVICE_TOKEN=
# Ollama / AnythingLLM (orchestrator)
OLLAMA_URL=http://127.0.0.1:11434
ANYTHINGLLM_BASE_URL=
REGEX_SEARCH_HOST=127.0.0.1
REGEX_SEARCH_PORT=37143
REGEX_SEARCH_TOKEN=
# Carbonyl (docker / native — used by bridge open-plan response)
CARBONYL_DOCKER_IMAGE=fathyb/carbonyl
CARBONYL_RUNNER=docker
CLAW_PROXY_HOST=127.0.0.1
CLAW_PROXY_PORT=37142
CLAW_PROXY_TOKEN=
LOCAL_OFFICE_URL=http://127.0.0.1:8000
LOCAL_OFFICE_API_KEY=
# --- claw-harness-proxy : amont HTTP du binaire claw (machine locale uniquement en profil strict) ---
# CLAW_UPSTREAM_URL=http://127.0.0.1:<port>

View File

@ -15,6 +15,7 @@ Documentation des **API HTTP** exposées par les services sous [`services/`](../
| **smart_ide-orchestrator** | Bearer (spécification) | `37145` (spécification) | [orchestrator.md](./orchestrator.md) |
| **anythingllm-devtools** | Bearer | `37146` | [anythingllm-devtools-api.md](./anythingllm-devtools-api.md) |
| **smart-ide-tools-bridge** | Bearer (sauf `/health`) | `37147` | [smart-ide-tools-bridge-api.md](./smart-ide-tools-bridge-api.md) |
| **smart-ide-global-api** | Bearer interne (`GLOBAL_API_INTERNAL_TOKEN`, sauf `/health`) | `37149` | [global-api.md](./global-api.md) |
| **smart-ide-sso-gateway** | Bearer utilisateur OIDC (sauf `/health`) | `37148` | [sso-gateway-api.md](./sso-gateway-api.md) |
| **docv** (externe) | selon dépôt Enso | selon déploiement | [docv.md](./docv.md) |

53
docs/API/global-api.md Normal file
View File

@ -0,0 +1,53 @@
# API — smart-ide-global-api
Écoute par défaut : **`127.0.0.1:37149`**. Configuration : `services/smart-ide-global-api/.env.example`, agrégat [config/services.local.env.example](../../config/services.local.env.example).
## Rôle
Agrégateur HTTP **interne** : reçoit les requêtes authentifiées par `Authorization: Bearer <GLOBAL_API_INTERNAL_TOKEN>` (fourni par `smart-ide-sso-gateway`), relaie vers chaque micro-service avec son **jeton technique** et propage les en-têtes `X-OIDC-Sub` / `X-OIDC-Email` lorsquils sont présents.
Les navigateurs et applications utilisateur ne doivent **pas** appeler ce port directement : passer par la **passerelle SSO** (`/proxy/<clé>/...`).
## Authentification
| Route | Auth |
|-------|------|
| `GET /health` | Aucune |
| Toutes les autres | `Authorization: Bearer` égal à `GLOBAL_API_INTERNAL_TOKEN` |
## Endpoints
### `GET /health`
Réponse `200` : `{ "status": "ok", "service": "smart-ide-global-api" }`.
### `GET /v1/upstreams`
Liste les clés damont : `{ "upstreams": [ "orchestrator", ... ] }` (même liste que côté SSO).
### Proxy — `ANY /v1/upstream/<upstream_key>/<path>`
- **`<upstream_key>`** : `orchestrator`, `repos_devtools`, `ia_dev_gateway`, `anythingllm_devtools`, `tools_bridge`, `langextract`, `regex_search`, `claw_proxy`, `local_office`.
- **`<path>`** : transmis à lURL de base du service (ex. `/v1/upstream/orchestrator/v1/...``http://127.0.0.1:37145/v1/...`).
- **Corps** : relayé (limite `GLOBAL_API_MAX_BODY_BYTES`, défaut 32 MiB).
- **Erreurs** : `401` si Bearer interne absent ou incorrect ; `404` si clé inconnue ; `503` si `local_office` sans `LOCAL_OFFICE_API_KEY`.
## Journaux
Fichier **`.logs/global-api/access.log`** : lignes JSON (`ts`, `method`, `path`, `upstream`, `status`, `durationMs`, `oidcSub` si présent).
## Variables denvironnement
| Variable | Rôle |
|----------|------|
| `GLOBAL_API_HOST` / `GLOBAL_API_PORT` | Bind HTTP |
| `GLOBAL_API_INTERNAL_TOKEN` | Obligatoire — secret partagé avec la passerelle SSO |
| `GLOBAL_API_MAX_BODY_BYTES` | Taille max du corps |
| `SMART_IDE_MONOREPO_ROOT` | Racine monorepo pour `.logs/` (sinon déduction depuis le module) |
Jetons et hôtes des micro-services : mêmes noms que dans `config/services.local.env.example`.
## Voir aussi
- [sso-gateway-api.md](./sso-gateway-api.md)
- [sso-gateway-service.md](../features/sso-gateway-service.md)

View File

@ -27,11 +27,15 @@ Liste les clés de proxy disponibles : `{ "upstreams": [ "orchestrator", ... ] }
### Proxy — `ANY /proxy/<upstream_key>/<path>`
- **`<upstream_key>`** : voir liste ci-dessus (`repos_devtools`, `orchestrator`, etc.).
- **`<path>`** : chemin transmis tel quel à lURL de base du service (ex. `/proxy/orchestrator/v1/...``http://ORCHESTRATOR_HOST:PORT/v1/...`).
- **`<path>`** : relayé vers **smart-ide-global-api** sous `/v1/upstream/<upstream_key><path>` (ex. `/proxy/orchestrator/v1/...``GLOBAL_API_URL/v1/upstream/orchestrator/v1/...`).
- **Corps** : relayé pour les méthodes avec body (limite `SSO_GATEWAY_MAX_BODY_BYTES`, défaut 32 MiB).
- **Réponses derreur** : `401` si Bearer utilisateur absent ou invalide ; `404` si clé inconnue ; `503` si `local_office` est ciblé sans `LOCAL_OFFICE_API_KEY`.
- **Réponses derreur** : `401` si Bearer utilisateur absent ou invalide ; `404` si clé inconnue ; erreurs amont si lAPI globale ou un micro-service refuse la requête.
Len-tête `Authorization` utilisateur nest **pas** transmis à lamont ; il est remplacé par le jeton de service configuré. Voir [sso-gateway-service.md](../features/sso-gateway-service.md).
Len-tête `Authorization` utilisateur nest **pas** transmis à lAPI globale ; il est remplacé par `GLOBAL_API_INTERNAL_TOKEN`. Les claims OIDC sont transmis en `X-OIDC-Sub` / `X-OIDC-Email` jusquaux micro-services. Voir [sso-gateway-service.md](../features/sso-gateway-service.md) et [global-api.md](./global-api.md).
### Journaux
Fichier **`.logs/sso-gateway/access.log`** : lignes JSON (`ts`, `method`, `path`, `upstream`, `status`, `durationMs`, `oidcSub` si présent). Les `GET /health` et `OPTIONS` ne sont pas journalisés.
### Comptes et projets
@ -47,8 +51,11 @@ Aucun stockage d**utilisateurs** ou de **comptes par projet** dans ce service
| `SSO_GATEWAY_HOST` / `SSO_GATEWAY_PORT` | Bind HTTP |
| `SSO_CORS_ORIGIN` | Si défini, en-têtes CORS sur les réponses |
| `SSO_GATEWAY_MAX_BODY_BYTES` | Taille max du corps en entrée |
| `GLOBAL_API_URL` | Base HTTP de smart-ide-global-api (défaut `http://127.0.0.1:37149`) |
| `GLOBAL_API_INTERNAL_TOKEN` | Obligatoire — même valeur que sur smart-ide-global-api |
| `SMART_IDE_MONOREPO_ROOT` | Optionnel — racine pour écrire sous `.logs/sso-gateway/` |
Les jetons et hôtes des micro-services : mêmes noms que dans `config/services.local.env.example`.
Les jetons et hôtes des micro-services sont lus par **smart-ide-global-api** ; voir `config/services.local.env.example` et [global-api.md](./global-api.md).
## Voir aussi

View File

@ -33,7 +33,8 @@ Vue densemble et index complet : **[repo/README.md](./repo/README.md)**. Règ
| [repo/service-carbonyl.md](./repo/service-carbonyl.md) | Carbonyl (navigateur terminal), prévisualisation test |
| [repo/service-pageindex.md](./repo/service-pageindex.md) | PageIndex (index vectorless, définition sémantique documents) |
| [repo/service-smart-ide-tools-bridge.md](./repo/service-smart-ide-tools-bridge.md) | Pont HTTP IDE + sous-modules CLI |
| [repo/service-smart-ide-sso-gateway.md](./repo/service-smart-ide-sso-gateway.md) | Passerelle OIDC utilisateur → micro-services |
| [repo/service-smart-ide-global-api.md](./repo/service-smart-ide-global-api.md) | API HTTP interne : proxy vers micro-services (jetons techniques) |
| [repo/service-smart-ide-sso-gateway.md](./repo/service-smart-ide-sso-gateway.md) | Passerelle OIDC utilisateur → API globale → micro-services |
| [repo/service-chandra.md](./repo/service-chandra.md) | Chandra OCR (PDF / images structurés) |
| [repo/extension-anythingllm-workspaces.md](./repo/extension-anythingllm-workspaces.md) | Extension AnythingLLM IDE (supprimée ; voir anythingllm-devtools) |

View File

@ -12,7 +12,7 @@ Permettre au **front web** de la plateforme `smart_ide` (déployé par environne
| **docv / IdP Enso** | Émet `id_token` / `access_token`, expose JWKS |
| **Front SPA** | Échange code OAuth (PKCE recommandé), stocke session |
| **Backend API** (orchestrateur ou BFF) | Valide JWT (signature JWKS, `iss`, `aud`, `exp`), mappe rôles → droits policy |
| **smart-ide-sso-gateway** (monorepo) | Point dentrée optionnel : même validation JWT + proxy vers les micro-services avec jetons techniques — [sso-gateway-service.md](./sso-gateway-service.md) |
| **smart-ide-sso-gateway** (monorepo) | Point dentrée optionnel : validation JWT + proxy via **smart-ide-global-api** vers les micro-services avec jetons techniques — [sso-gateway-service.md](./sso-gateway-service.md) |
## Flux (authorization code + PKCE)

View File

@ -2,12 +2,12 @@
## Rôle
Le service **`smart-ide-sso-gateway`** (`services/smart-ide-sso-gateway/`) centralise la **validation OIDC** (jeton utilisateur émis par **docv / Enso**) et le **proxy** vers les micro-services du monorepo avec leurs **jetons techniques**.
Le service **`smart-ide-sso-gateway`** (`services/smart-ide-sso-gateway/`) centralise la **validation OIDC** (jeton utilisateur émis par **docv / Enso**) puis délègue le **proxy HTTP** au service **`smart-ide-global-api`** (`services/smart-ide-global-api/`), qui applique les **jetons techniques** vers chaque micro-service.
- Le **navigateur** ou un **BFF** présente un `access_token` utilisateur (`Authorization: Bearer`).
- La passerelle vérifie la signature (**JWKS**), `iss`, éventuellement `aud`, puis appelle lamont avec le **Bearer de service** ou la **clé API** attendue par chaque cible.
- La passerelle vérifie la signature (**JWKS**), `iss`, éventuellement `aud`, puis appelle **smart-ide-global-api** avec `GLOBAL_API_INTERNAL_TOKEN` et propage `X-OIDC-Sub` / `X-OIDC-Email` pour les journaux et règles amont.
Les appels **machine à machine** sans contexte utilisateur (scripts, `ia_dev`) restent possibles **en direct** vers chaque service, comme avant : la passerelle est une **option** pour les parcours « utilisateur authentifié par docv ».
Les micro-services **nimplémentent pas le SSO** ; ils écoutent en **local** (`127.0.0.1`) avec authentification par jeton de service. Les appels **machine à machine** sans contexte utilisateur (scripts, `ia_dev`) peuvent toujours cibler **directement** un micro-service ou, pour un seul point dentrée technique, **smart-ide-global-api** avec le Bearer interne.
## Lien avec docv / Enso
@ -15,7 +15,9 @@ Le flux **authorization code + PKCE** et le rôle d**IdP** restent décrits d
## Amonts proxy
Les clés exposées par `GET /v1/upstreams` et utilisées dans `/proxy/<clé>/...` sont : `orchestrator`, `repos_devtools`, `ia_dev_gateway`, `anythingllm_devtools`, `tools_bridge`, `langextract`, `regex_search`, `claw_proxy`, `local_office`. Les variables denvironnement sont les mêmes que pour le reste de la plateforme ([`config/services.local.env.example`](../../config/services.local.env.example)).
Les clés exposées par `GET /v1/upstreams` et utilisées dans `/proxy/<clé>/...` sont : `orchestrator`, `repos_devtools`, `ia_dev_gateway`, `anythingllm_devtools`, `tools_bridge`, `langextract`, `regex_search`, `claw_proxy`, `local_office`. La résolution des URL et jetons par clé est implémentée dans **smart-ide-global-api** ; fichier dexemple agrégé : [`config/services.local.env.example`](../../config/services.local.env.example).
**Journaux** : `.logs/sso-gateway/access.log` (passerelle) et `.logs/global-api/access.log` (agrégateur), lignes JSON par requête (hors `GET /health` pour lAPI globale ; hors `GET /health` et `OPTIONS` pour le SSO).
## En-têtes vers lamont
@ -37,4 +39,6 @@ Les services amont qui reçoivent `X-OIDC-Sub` / `X-OIDC-Email` sont responsable
## Documentation détaillée
- [API/sso-gateway-api.md](../API/sso-gateway-api.md)
- [API/global-api.md](../API/global-api.md)
- [services/smart-ide-sso-gateway/README.md](../../services/smart-ide-sso-gateway/README.md)
- [services/smart-ide-global-api/README.md](../../services/smart-ide-global-api/README.md)

View File

@ -41,7 +41,8 @@ Toute la documentation **opérationnelle** qui vivait auparavant sous des `READM
| [service-pageindex.md](./service-pageindex.md) | PageIndex (index sémantique vectorless), sous-module amont |
| [service-chandra.md](./service-chandra.md) | Chandra OCR, sous-module amont |
| [service-smart-ide-tools-bridge.md](./service-smart-ide-tools-bridge.md) | Pont HTTP IDE + outils sous-modules |
| [service-smart-ide-sso-gateway.md](./service-smart-ide-sso-gateway.md) | Passerelle OIDC utilisateur → micro-services |
| [service-smart-ide-global-api.md](./service-smart-ide-global-api.md) | API HTTP interne : proxy vers micro-services (Bearer partagé avec SSO) |
| [service-smart-ide-sso-gateway.md](./service-smart-ide-sso-gateway.md) | Passerelle OIDC utilisateur → API globale → micro-services |
| [extension-anythingllm-workspaces.md](./extension-anythingllm-workspaces.md) | Extension AnythingLLM IDE (supprimée ; anythingllm-devtools) |
Les **spécifications** détaillées (contrats HTTP, sécurité, orchestration) restent dans [../API/README.md](../API/README.md) et [../features/](../features/).

View File

@ -16,3 +16,7 @@ Variable interne : **`SMART_IDE_LOG_IA_DEV_ROOT`** (racine `ia_dev`), posée par
Contrat service : [ia-dev-smart-ide-integration.md](./ia-dev-smart-ide-integration.md), implémentation `ia_dev/lib/smart_ide_logs.sh`.
Configuration du pull planifié : [cron-git-pull.md](./cron-git-pull.md).
## Journaux passerelle SSO et API globale (`.logs/`)
Sous la **racine du monorepo**, le répertoire **`.logs/`** contient des journaux daccès JSON (fichiers `*.log` ignorés par Git) pour **`smart-ide-sso-gateway`** (`sso-gateway/access.log`) et **`smart-ide-global-api`** (`global-api/access.log`). Voir [`.logs/README.md`](../../.logs/README.md), [sso-gateway-service.md](../features/sso-gateway-service.md) et [global-api.md](../API/global-api.md).

View File

@ -0,0 +1,17 @@
# Service smart-ide-global-api (`services/smart-ide-global-api/`)
Couche HTTP **interne** : proxy vers les micro-services avec jetons techniques, **sans OIDC**. Consommée exclusivement par **`smart-ide-sso-gateway`** (Bearer `GLOBAL_API_INTERNAL_TOKEN`).
## Configuration locale
- Agrégat : **[`config/services.local.env.example`](../../config/services.local.env.example)**
- Service : **`services/smart-ide-global-api/.env.example`**
## Exploitation
Démarrer **avant** la passerelle SSO. Voir **[`services/smart-ide-global-api/README.md`](../../services/smart-ide-global-api/README.md)** et **[`docs/API/global-api.md`](../API/global-api.md)**.
## Voir aussi
- [service-smart-ide-sso-gateway.md](./service-smart-ide-sso-gateway.md)
- [sso-gateway-service.md](../features/sso-gateway-service.md)

View File

@ -1,11 +1,11 @@
# Service smart-ide-sso-gateway (`services/smart-ide-sso-gateway/`)
Passerelle HTTP : validation **JWT utilisateur** (issuer docv / Enso) et **proxy** vers les micro-services `smart_ide` avec authentification **technique** par service.
Passerelle HTTP : validation **JWT utilisateur** (issuer docv / Enso), puis **proxy** via **smart-ide-global-api** vers les micro-services (jetons techniques côté API globale).
## Configuration locale
- Fichier agrégé : **[`config/services.local.env.example`](../../config/services.local.env.example)** — y ajouter `OIDC_ISSUER` (et optionnellement `SSO_GATEWAY_*`, `SSO_CORS_ORIGIN`) lorsque la passerelle est utilisée.
- Service : **`services/smart-ide-sso-gateway/.env.example`**.
- Fichier agrégé : **[`config/services.local.env.example`](../../config/services.local.env.example)** — `OIDC_ISSUER`, `GLOBAL_API_URL`, `GLOBAL_API_INTERNAL_TOKEN`, jetons micro-services pour lAPI globale.
- Passerelle : **`services/smart-ide-sso-gateway/.env.example`** — API globale : **`services/smart-ide-global-api/.env.example`**.
## Exploitation
@ -13,4 +13,5 @@ Voir **[`services/smart-ide-sso-gateway/README.md`](../../services/smart-ide-sso
## Voir aussi
- [service-smart-ide-global-api.md](./service-smart-ide-global-api.md)
- [sso-docv-enso.md](../features/sso-docv-enso.md)

View File

@ -27,7 +27,7 @@ Tous écoutent en principe sur **`127.0.0.1`** ; ports par défaut : [API/README
| **claw-harness-api** (proxy) | **Proxy HTTP** vers un exécuteur claw-code amont | Outillage modèles alternatifs en dev | **Optionnel** si politique projet autorise ce proxy en interne |
| **local-office** | **Manipulation programmatique** de fichiers Office (docx, etc.), clé `X-API-Key` | Tests / génération de documents depuis lIDE | **Possible** pour génération batch ou transformations Office côté serveur applicatif (hors UI ONLYOFFICE) |
| **smart-ide-tools-bridge** | **Registre** des URLs locales ; **jobs** Carbonyl / PageIndex / Chandra (chemins validés sous `SMART_IDE_MONOREPO_ROOT`) | Découverte des services ; lancer OCR / index / plan terminal depuis lIDE | **Non prévu pour le trafic utilisateur final** : jobs longs, liés au poste / au monorepo ; les **backs produit** doivent préférer leurs propres pipelines (OCR, index) en prod si besoin |
| **smart-ide-sso-gateway** | **Validation JWT** docv / Enso + **proxy** vers les micro-services avec jetons techniques | Alternative au BFF maison pour un front ou un outil qui appelle déjà lIdP | **Possible** quand le produit veut un seul point dentrée HTTP local vers la plateforme IA sous identité utilisateur OIDC ; ne remplace pas les appels M2M directs — [features/sso-gateway-service.md](./features/sso-gateway-service.md) |
| **smart-ide-sso-gateway** | **Validation JWT** docv / Enso + **proxy** via **smart-ide-global-api** vers les micro-services | Alternative au BFF maison pour un front ou un outil qui appelle déjà lIdP | **Possible** quand le produit veut un seul point dentrée HTTP local vers la plateforme IA sous identité utilisateur OIDC ; les appels M2M peuvent rester directs vers un micro-service ou vers lAPI globale — [features/sso-gateway-service.md](./features/sso-gateway-service.md), [repo/service-smart-ide-global-api.md](./repo/service-smart-ide-global-api.md) |
## 3. Outils CLI et sous-modules (sans listener HTTP dédié)
@ -54,13 +54,14 @@ Tous écoutent en principe sur **`127.0.0.1`** ; ports par défaut : [API/README
## 6. Rôles respectifs (rappel)
- **IDE** : orchestrateur (intentions), tools-bridge (registre + jobs outils), devtools AnythingLLM, repos-devtools, regex search, ia-dev-gateway, **smart-ide-sso-gateway** (OIDC utilisateur → micro-services, si activé), accès directs Ollama / AnythingLLM selon lUX.
- **IDE** : orchestrateur (intentions), tools-bridge (registre + jobs outils), devtools AnythingLLM, repos-devtools, regex search, ia-dev-gateway, **smart-ide-global-api** + **smart-ide-sso-gateway** (OIDC utilisateur → API globale → micro-services, si activé), accès directs Ollama / AnythingLLM selon lUX.
- **Backends applicatifs** : consommer en priorité les services **stables et documentés** pour lIA générique (**langextract**, **AnythingLLM**, **Ollama**, éventuellement **orchestrateur**) ; éviter de dépendre du **tools-bridge** pour la charge utilisateur ; respecter périmètres fichiers (`REGEX_SEARCH_ROOT`, SSH / données déployées — [remote-deployed-data-ssh.md](./features/remote-deployed-data-ssh.md)).
## Voir aussi
- [config/services.local.env.example](../config/services.local.env.example)
- [repo/service-smart-ide-tools-bridge.md](./repo/service-smart-ide-tools-bridge.md)
- [repo/service-smart-ide-global-api.md](./repo/service-smart-ide-global-api.md)
- [repo/service-smart-ide-sso-gateway.md](./repo/service-smart-ide-sso-gateway.md)
- [platform-target.md](./platform-target.md)
- [deployment-target.md](./deployment-target.md)

View File

@ -44,7 +44,9 @@ Services dappoint sur **`127.0.0.1`** (souvent auth **Bearer**) : Git devtool
**smart-ide-tools-bridge** (`services/smart-ide-tools-bridge/`) : **API** locale (registre + Carbonyl / PageIndex / Chandra) — [API/smart-ide-tools-bridge-api.md](../API/smart-ide-tools-bridge-api.md).
**smart-ide-sso-gateway** (`services/smart-ide-sso-gateway/`) : validation **JWT** docv / Enso et **proxy** vers les autres micro-services avec jetons techniques — [features/sso-gateway-service.md](./features/sso-gateway-service.md), [API/sso-gateway-api.md](./API/sso-gateway-api.md).
**smart-ide-global-api** (`services/smart-ide-global-api/`) : agrégateur HTTP **interne** (Bearer partagé avec le SSO), proxy vers les micro-services avec jetons techniques — [repo/service-smart-ide-global-api.md](./repo/service-smart-ide-global-api.md), [API/global-api.md](./API/global-api.md).
**smart-ide-sso-gateway** (`services/smart-ide-sso-gateway/`) : validation **JWT** docv / Enso puis **proxy** via lAPI globale — [features/sso-gateway-service.md](./features/sso-gateway-service.md), [API/sso-gateway-api.md](./API/sso-gateway-api.md).
**Configuration locale** : [config/services.local.env.example](../config/services.local.env.example).

View File

@ -1,4 +1,5 @@
CLAW_PROXY_HOST=127.0.0.1
CLAW_PROXY_PORT=37142
CLAW_PROXY_TOKEN=
# Profil strictement local : URL HTTP du serveur claw sur cette machine (ex. http://127.0.0.1:<port>)
CLAW_UPSTREAM_URL=

View File

@ -0,0 +1,48 @@
# smart-ide-global-api — copy to .env, do not commit .env
# Or merge into config/services.local.env (repo root)
GLOBAL_API_HOST=127.0.0.1
GLOBAL_API_PORT=37149
# GLOBAL_API_MAX_BODY_BYTES=33554432
# Shared secret with smart-ide-sso-gateway (Bearer toward this service)
GLOBAL_API_INTERNAL_TOKEN=
# Optional: monorepo root for .logs/ (default: three levels above dist/)
# SMART_IDE_MONOREPO_ROOT=
# Micro-services: loopback only — same variables as historical SSO direct proxy
ORCHESTRATOR_HOST=127.0.0.1
ORCHESTRATOR_PORT=37145
ORCHESTRATOR_TOKEN=
REPOS_DEVTOOLS_HOST=127.0.0.1
REPOS_DEVTOOLS_PORT=37140
REPOS_DEVTOOLS_TOKEN=
IA_DEV_GATEWAY_HOST=127.0.0.1
IA_DEV_GATEWAY_PORT=37144
IA_DEV_GATEWAY_TOKEN=
ANYTHINGLLM_DEVTOOLS_HOST=127.0.0.1
ANYTHINGLLM_DEVTOOLS_PORT=37146
ANYTHINGLLM_DEVTOOLS_TOKEN=
TOOLS_BRIDGE_HOST=127.0.0.1
TOOLS_BRIDGE_PORT=37147
TOOLS_BRIDGE_TOKEN=
LANGEXTRACT_API_HOST=127.0.0.1
LANGEXTRACT_API_PORT=37141
LANGEXTRACT_SERVICE_TOKEN=
REGEX_SEARCH_HOST=127.0.0.1
REGEX_SEARCH_PORT=37143
REGEX_SEARCH_TOKEN=
CLAW_PROXY_HOST=127.0.0.1
CLAW_PROXY_PORT=37142
CLAW_PROXY_TOKEN=
LOCAL_OFFICE_URL=http://127.0.0.1:8000
LOCAL_OFFICE_API_KEY=

View File

@ -0,0 +1,2 @@
node_modules/
dist/

View File

@ -0,0 +1,30 @@
# smart-ide-global-api
Couche HTTP **interne** : agrège les appels vers les micro-services `smart_ide` avec les **jetons techniques** par service. **Pas dOIDC** : seul `smart-ide-sso-gateway` appelle cette API, avec `Authorization: Bearer` égal à `GLOBAL_API_INTERNAL_TOKEN`.
Les micro-services nexposent pas le SSO ; ils restent sur **127.0.0.1** avec Bearer (ou clé API pour local-office).
## Run
Démarrer **avant** la passerelle SSO.
```bash
cd services/smart-ide-global-api
cp .env.example .env
# définir GLOBAL_API_INTERNAL_TOKEN et les jetons des services
set -a && source .env && set +a
npm ci
npm run build
npm start
```
Écoute par défaut : `http://127.0.0.1:37149`.
## Journaux
Une ligne JSON par requête (hors `GET /health`) dans **`.logs/global-api/access.log`** (répertoire `.logs/` à la racine du monorepo, créé au besoin).
## Documentation
- API : [`docs/API/global-api.md`](../../docs/API/global-api.md)
- Passerelle SSO : [`docs/features/sso-gateway-service.md`](../../docs/features/sso-gateway-service.md)

View File

@ -0,0 +1,51 @@
{
"name": "@4nk/smart-ide-global-api",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@4nk/smart-ide-global-api",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@types/node": {
"version": "20.19.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@ -0,0 +1,20 @@
{
"name": "@4nk/smart-ide-global-api",
"version": "0.1.0",
"private": true,
"description": "Internal HTTP aggregation layer: service tokens only, no OIDC. Consumed by smart-ide-sso-gateway.",
"license": "MIT",
"type": "module",
"main": "dist/server.js",
"scripts": {
"build": "tsc -p ./",
"start": "node dist/server.js"
},
"engines": {
"node": ">=20"
},
"devDependencies": {
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
}
}

View File

@ -0,0 +1,19 @@
import fs from "node:fs/promises";
import path from "node:path";
import { repoRoot } from "./repoRoot.js";
const logFile = async (): Promise<string> => {
const dir = path.join(repoRoot(), ".logs", "global-api");
await fs.mkdir(dir, { recursive: true });
return path.join(dir, "access.log");
};
export const appendGlobalApiAccessLog = async (
entry: Record<string, unknown>,
): Promise<void> => {
const file = await logFile();
const line =
JSON.stringify({ ts: new Date().toISOString(), service: "smart-ide-global-api", ...entry }) +
"\n";
await fs.appendFile(file, line, "utf8");
};

View File

@ -0,0 +1,11 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
export const repoRoot = (): string => {
const fromEnv = process.env.SMART_IDE_MONOREPO_ROOT?.trim();
if (fromEnv) {
return path.resolve(fromEnv);
}
const here = path.dirname(fileURLToPath(import.meta.url));
return path.resolve(here, "..", "..", "..");
};

View File

@ -0,0 +1,204 @@
import * as http from "node:http";
import { appendGlobalApiAccessLog } from "./accessLog.js";
import { listUpstreamKeys, resolveUpstream, type UpstreamAuth } from "./upstreams.js";
const HOST = process.env.GLOBAL_API_HOST ?? "127.0.0.1";
const PORT = Number(process.env.GLOBAL_API_PORT ?? "37149");
const MAX_BODY_BYTES = Number(process.env.GLOBAL_API_MAX_BODY_BYTES ?? "33554432");
const readExpectedToken = (): string => process.env.GLOBAL_API_INTERNAL_TOKEN?.trim() ?? "";
const json = (res: http.ServerResponse, status: number, body: unknown): void => {
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(body));
};
const readBearer = (req: http.IncomingMessage): string | null => {
const raw = req.headers.authorization ?? "";
const m = /^Bearer\s+(.+)$/i.exec(raw);
return m?.[1]?.trim() ?? null;
};
const readBodyBuffer = async (req: http.IncomingMessage): Promise<Buffer> => {
const chunks: Buffer[] = [];
let total = 0;
for await (const chunk of req) {
const b = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
total += b.length;
if (total > MAX_BODY_BYTES) {
throw new Error(`Request body exceeds ${MAX_BODY_BYTES} bytes`);
}
chunks.push(b);
}
return Buffer.concat(chunks);
};
const hopByHop = new Set([
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailers",
"transfer-encoding",
"upgrade",
"host",
]);
const buildForwardHeaders = (req: http.IncomingMessage, serviceAuth: UpstreamAuth): Headers => {
const out = new Headers();
for (const [k, v] of Object.entries(req.headers)) {
if (!v) {
continue;
}
const lk = k.toLowerCase();
if (hopByHop.has(lk)) {
continue;
}
if (lk === "authorization") {
continue;
}
out.set(k, Array.isArray(v) ? v.join(", ") : v);
}
if (serviceAuth.kind === "bearer") {
if (serviceAuth.token) {
out.set("Authorization", `Bearer ${serviceAuth.token}`);
}
} else if (serviceAuth.value) {
out.set(serviceAuth.name, serviceAuth.value);
}
return out;
};
const responseHopByHop = new Set([
"connection",
"keep-alive",
"transfer-encoding",
"content-encoding",
]);
const proxyToUpstream = async (
res: http.ServerResponse,
targetUrl: string,
headers: Headers,
body: Buffer,
method: string,
): Promise<number> => {
const init: RequestInit = {
method,
headers,
redirect: "manual",
};
if (method !== "GET" && method !== "HEAD" && body.length > 0) {
init.body = new Uint8Array(body);
}
const out = await fetch(targetUrl, init);
res.statusCode = out.status;
for (const [k, v] of out.headers) {
if (responseHopByHop.has(k.toLowerCase())) {
continue;
}
res.setHeader(k, v);
}
const buf = Buffer.from(await out.arrayBuffer());
res.end(buf);
return out.status;
};
const main = (): void => {
const internal = readExpectedToken();
if (internal.length === 0) {
console.error("smart-ide-global-api: set GLOBAL_API_INTERNAL_TOKEN (non-empty secret).");
process.exit(1);
}
const server = http.createServer((req, res) => {
void (async () => {
const started = Date.now();
const method = req.method ?? "GET";
const url = new URL(req.url ?? "/", `http://${HOST}`);
const pathname = url.pathname;
let logPath = pathname;
let upstreamKey = "";
let status = 0;
try {
if (method === "GET" && (pathname === "/health" || pathname === "/health/")) {
status = 200;
json(res, status, { status: "ok", service: "smart-ide-global-api" });
return;
}
const token = readBearer(req);
if (!token || token !== internal) {
status = 401;
json(res, status, { error: "Missing or invalid internal Authorization bearer" });
return;
}
if (method === "GET" && pathname === "/v1/upstreams") {
status = 200;
json(res, status, { upstreams: listUpstreamKeys() });
return;
}
const proxyMatch = /^\/v1\/upstream\/([^/]+)(\/.*)?$/.exec(pathname);
if (!proxyMatch) {
status = 404;
json(res, status, { error: "Not found" });
return;
}
upstreamKey = proxyMatch[1];
const rest = proxyMatch[2] ?? "/";
logPath = `/proxy/${upstreamKey}${rest}`;
const upstream = resolveUpstream(upstreamKey);
if (!upstream) {
status = 404;
json(res, status, { error: `Unknown upstream: ${upstreamKey}` });
return;
}
if (upstream.auth.kind === "header" && !upstream.auth.value) {
status = 503;
json(res, status, {
error: `API key not configured for upstream ${upstreamKey} (LOCAL_OFFICE_API_KEY)`,
});
return;
}
const body = await readBodyBuffer(req);
const targetUrl = `${upstream.baseUrl}${rest}${url.search}`;
const headers = buildForwardHeaders(req, upstream.auth);
status = await proxyToUpstream(res, targetUrl, headers, body, method);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
status = status || 400;
if (!res.headersSent) {
json(res, 400, { error: msg });
status = 400;
}
} finally {
const skipLog =
method === "GET" && (pathname === "/health" || pathname === "/health/");
if (!skipLog) {
void appendGlobalApiAccessLog({
method,
path: logPath,
upstream: upstreamKey || undefined,
status,
durationMs: Date.now() - started,
oidcSub:
typeof req.headers["x-oidc-sub"] === "string"
? req.headers["x-oidc-sub"]
: undefined,
});
}
}
})();
});
server.listen(PORT, HOST, () => {
console.error(`smart-ide-global-api listening on http://${HOST}:${PORT}`);
});
};
main();

View File

@ -0,0 +1,96 @@
export type UpstreamAuth =
| { kind: "bearer"; token: string }
| { kind: "header"; name: string; value: string };
export type UpstreamTarget = {
baseUrl: string;
auth: UpstreamAuth;
};
const trimSlash = (s: string): string => s.replace(/\/+$/, "");
const env = (k: string, d: string): string => process.env[k]?.trim() ?? d;
export const resolveUpstream = (key: string): UpstreamTarget | null => {
switch (key) {
case "orchestrator":
return {
baseUrl: trimSlash(
`http://${env("ORCHESTRATOR_HOST", "127.0.0.1")}:${env("ORCHESTRATOR_PORT", "37145")}`,
),
auth: { kind: "bearer", token: env("ORCHESTRATOR_TOKEN", "") },
};
case "repos_devtools":
return {
baseUrl: trimSlash(
`http://${env("REPOS_DEVTOOLS_HOST", "127.0.0.1")}:${env("REPOS_DEVTOOLS_PORT", "37140")}`,
),
auth: { kind: "bearer", token: env("REPOS_DEVTOOLS_TOKEN", "") },
};
case "ia_dev_gateway":
return {
baseUrl: trimSlash(
`http://${env("IA_DEV_GATEWAY_HOST", "127.0.0.1")}:${env("IA_DEV_GATEWAY_PORT", "37144")}`,
),
auth: { kind: "bearer", token: env("IA_DEV_GATEWAY_TOKEN", "") },
};
case "anythingllm_devtools":
return {
baseUrl: trimSlash(
`http://${env("ANYTHINGLLM_DEVTOOLS_HOST", "127.0.0.1")}:${env("ANYTHINGLLM_DEVTOOLS_PORT", "37146")}`,
),
auth: { kind: "bearer", token: env("ANYTHINGLLM_DEVTOOLS_TOKEN", "") },
};
case "tools_bridge":
return {
baseUrl: trimSlash(
`http://${env("TOOLS_BRIDGE_HOST", "127.0.0.1")}:${env("TOOLS_BRIDGE_PORT", "37147")}`,
),
auth: { kind: "bearer", token: env("TOOLS_BRIDGE_TOKEN", "") },
};
case "langextract":
return {
baseUrl: trimSlash(
`http://${env("LANGEXTRACT_API_HOST", "127.0.0.1")}:${env("LANGEXTRACT_API_PORT", "37141")}`,
),
auth: { kind: "bearer", token: env("LANGEXTRACT_SERVICE_TOKEN", "") },
};
case "regex_search":
return {
baseUrl: trimSlash(
`http://${env("REGEX_SEARCH_HOST", "127.0.0.1")}:${env("REGEX_SEARCH_PORT", "37143")}`,
),
auth: { kind: "bearer", token: env("REGEX_SEARCH_TOKEN", "") },
};
case "claw_proxy":
return {
baseUrl: trimSlash(
`http://${env("CLAW_PROXY_HOST", "127.0.0.1")}:${env("CLAW_PROXY_PORT", "37142")}`,
),
auth: { kind: "bearer", token: env("CLAW_PROXY_TOKEN", "") },
};
case "local_office":
return {
baseUrl: trimSlash(env("LOCAL_OFFICE_URL", "http://127.0.0.1:8000")),
auth: {
kind: "header",
name: "X-API-Key",
value: env("LOCAL_OFFICE_API_KEY", ""),
},
};
default:
return null;
}
};
export const listUpstreamKeys = (): string[] => [
"orchestrator",
"repos_devtools",
"ia_dev_gateway",
"anythingllm_devtools",
"tools_bridge",
"langextract",
"regex_search",
"claw_proxy",
"local_office",
];

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"skipLibCheck": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"declaration": false
},
"include": ["src/**/*.ts"]
}

View File

@ -1,5 +1,5 @@
# smart-ide-sso-gateway — copy to .env, do not commit .env
# Or merge into config/services.local.env (see repo root config/services.local.env.example)
# Or merge into config/services.local.env (repo root)
SSO_GATEWAY_HOST=127.0.0.1
SSO_GATEWAY_PORT=37148
@ -7,45 +7,19 @@ SSO_GATEWAY_PORT=37148
# SSO_CORS_ORIGIN=https://app.example.test
# SSO_GATEWAY_MAX_BODY_BYTES=33554432
# Required: docv / Enso OpenID issuer URL (no trailing slash issues tolerated)
# Required: docv / Enso OpenID issuer URL (JWKS discovery)
OIDC_ISSUER=https://docv.example.test
# Optional: validate access_token audience
# OIDC_AUDIENCE=smart-ide-gateway
# Optional: override JWKS URL (otherwise discovery or {issuer}/.well-known/jwks.json)
# Optional: override JWKS URL
# OIDC_JWKS_URI=https://docv.example.test/.well-known/jwks.json
# Same tokens as other services — gateway injects them toward upstreams
ORCHESTRATOR_HOST=127.0.0.1
ORCHESTRATOR_PORT=37145
ORCHESTRATOR_TOKEN=
# smart-ide-global-api (must be running; same secret on both sides)
GLOBAL_API_URL=http://127.0.0.1:37149
GLOBAL_API_INTERNAL_TOKEN=
REPOS_DEVTOOLS_HOST=127.0.0.1
REPOS_DEVTOOLS_PORT=37140
REPOS_DEVTOOLS_TOKEN=
# Optional: monorepo root for .logs/sso-gateway/
# SMART_IDE_MONOREPO_ROOT=
IA_DEV_GATEWAY_HOST=127.0.0.1
IA_DEV_GATEWAY_PORT=37144
IA_DEV_GATEWAY_TOKEN=
ANYTHINGLLM_DEVTOOLS_HOST=127.0.0.1
ANYTHINGLLM_DEVTOOLS_PORT=37146
ANYTHINGLLM_DEVTOOLS_TOKEN=
TOOLS_BRIDGE_HOST=127.0.0.1
TOOLS_BRIDGE_PORT=37147
TOOLS_BRIDGE_TOKEN=
LANGEXTRACT_API_HOST=127.0.0.1
LANGEXTRACT_API_PORT=37141
LANGEXTRACT_SERVICE_TOKEN=
REGEX_SEARCH_HOST=127.0.0.1
REGEX_SEARCH_PORT=37143
REGEX_SEARCH_TOKEN=
CLAW_PROXY_HOST=127.0.0.1
CLAW_PROXY_PORT=37142
CLAW_PROXY_TOKEN=
LOCAL_OFFICE_URL=http://127.0.0.1:8000
LOCAL_OFFICE_API_KEY=
# Micro-service tokens and hosts are read by smart-ide-global-api, not this process.
# See services/smart-ide-global-api/.env.example

View File

@ -1,21 +1,25 @@
# smart-ide-sso-gateway
HTTP gateway that validates **user** access tokens from the docv / Enso OIDC issuer, then proxies requests to internal `smart_ide` micro-services using each services **technical** credentials (Bearer or `X-API-Key`).
HTTP gateway that validates **user** access tokens from the docv / Enso OIDC issuer, then forwards requests to **`smart-ide-global-api`**, which proxies to internal `smart_ide` micro-services using each services **technical** credentials (Bearer or `X-API-Key`).
## Responsibilities
- Verify `Authorization: Bearer <access_token>` with JWKS (`OIDC_ISSUER`, optional `OIDC_AUDIENCE`, optional `OIDC_JWKS_URI`).
- Expose `GET /health` without auth.
- Expose `GET /v1/token/verify` and `GET /v1/upstreams` with user Bearer.
- Proxy `ANY /proxy/<upstream_key>/<path>` to the configured upstream, replacing the user token with the service token and adding `X-OIDC-Sub` / `X-OIDC-Email` when present in the JWT.
- Proxy `ANY /proxy/<upstream_key>/<path>` to **smart-ide-global-api** (`GLOBAL_API_URL`, `GLOBAL_API_INTERNAL_TOKEN`), which relays to the target service and adds upstream auth plus `X-OIDC-Sub` / `X-OIDC-Email` when present in the JWT.
Structured request logs (except `GET /health` and `OPTIONS`) are appended to **`.logs/sso-gateway/access.log`** under the monorepo root.
User accounts, project membership, and product databases stay in **each applications backend** (docv, Enso, etc.); this gateway does not store them.
## Run
Start **smart-ide-global-api** first, then:
```bash
cd services/smart-ide-sso-gateway
cp .env.example .env # edit OIDC_ISSUER and service tokens
cp .env.example .env # edit OIDC_ISSUER, GLOBAL_API_INTERNAL_TOKEN (match global API)
set -a && source .env && set +a
npm ci
npm run build
@ -24,9 +28,10 @@ npm start
Default listen: `http://127.0.0.1:37148`.
Upstream URLs and tokens reuse the same environment variables as the rest of the monorepo (`ORCHESTRATOR_*`, `TOOLS_BRIDGE_*`, `LOCAL_OFFICE_URL` / `LOCAL_OFFICE_API_KEY`, etc.). See `src/upstreams.ts`.
Micro-service URLs and tokens are configured on **smart-ide-global-api** (`services/smart-ide-global-api/.env.example` or aggregated `config/services.local.env`).
## Documentation
- Feature: [`docs/features/sso-gateway-service.md`](../../docs/features/sso-gateway-service.md)
- API: [`docs/API/sso-gateway-api.md`](../../docs/API/sso-gateway-api.md)
- Global API: [`docs/API/global-api.md`](../../docs/API/global-api.md)

View File

@ -0,0 +1,19 @@
import fs from "node:fs/promises";
import path from "node:path";
import { repoRoot } from "./repoRoot.js";
const logFile = async (): Promise<string> => {
const dir = path.join(repoRoot(), ".logs", "sso-gateway");
await fs.mkdir(dir, { recursive: true });
return path.join(dir, "access.log");
};
export const appendSsoAccessLog = async (
entry: Record<string, unknown>,
): Promise<void> => {
const file = await logFile();
const line =
JSON.stringify({ ts: new Date().toISOString(), service: "smart-ide-sso-gateway", ...entry }) +
"\n";
await fs.appendFile(file, line, "utf8");
};

View File

@ -0,0 +1,11 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
export const repoRoot = (): string => {
const fromEnv = process.env.SMART_IDE_MONOREPO_ROOT?.trim();
if (fromEnv) {
return path.resolve(fromEnv);
}
const here = path.dirname(fileURLToPath(import.meta.url));
return path.resolve(here, "..", "..", "..");
};

View File

@ -1,13 +1,19 @@
import * as http from "node:http";
import type { JWTPayload } from "jose";
import { appendSsoAccessLog } from "./accessLog.js";
import { discoverJwksUri, createVerify, type VerifyFn } from "./oidc.js";
import { listUpstreamKeys, resolveUpstream, type UpstreamAuth } from "./upstreams.js";
import { listUpstreamKeys } from "./upstreams.js";
const HOST = process.env.SSO_GATEWAY_HOST ?? "127.0.0.1";
const PORT = Number(process.env.SSO_GATEWAY_PORT ?? "37148");
const MAX_BODY_BYTES = Number(process.env.SSO_GATEWAY_MAX_BODY_BYTES ?? "33554432");
const CORS_ORIGIN = process.env.SSO_CORS_ORIGIN?.trim() ?? "";
const trimSlash = (s: string): string => s.replace(/\/+$/, "");
const globalApiBase = (): string =>
trimSlash(process.env.GLOBAL_API_URL ?? "http://127.0.0.1:37149");
const globalApiToken = (): string => process.env.GLOBAL_API_INTERNAL_TOKEN?.trim() ?? "";
const corsHeaders = (): Record<string, string> => {
if (!CORS_ORIGIN) {
return {};
@ -65,9 +71,8 @@ const hopByHop = new Set([
"host",
]);
const buildForwardHeaders = (
const buildForwardHeadersToGlobalApi = (
req: http.IncomingMessage,
serviceAuth: UpstreamAuth,
payload: JWTPayload,
): Headers => {
const out = new Headers();
@ -84,12 +89,9 @@ const buildForwardHeaders = (
}
out.set(k, Array.isArray(v) ? v.join(", ") : v);
}
if (serviceAuth.kind === "bearer") {
if (serviceAuth.token) {
out.set("Authorization", `Bearer ${serviceAuth.token}`);
}
} else if (serviceAuth.value) {
out.set(serviceAuth.name, serviceAuth.value);
const gToken = globalApiToken();
if (gToken) {
out.set("Authorization", `Bearer ${gToken}`);
}
const sub = payload.sub;
if (typeof sub === "string" && sub.length > 0) {
@ -109,13 +111,13 @@ const responseHopByHop = new Set([
"content-encoding",
]);
const proxyToUpstream = async (
const proxyToGlobalApi = async (
req: http.IncomingMessage,
res: http.ServerResponse,
targetUrl: string,
headers: Headers,
body: Buffer,
): Promise<void> => {
): Promise<number> => {
const method = req.method ?? "GET";
const init: RequestInit = {
method,
@ -136,6 +138,7 @@ const proxyToUpstream = async (
}
const buf = Buffer.from(await out.arrayBuffer());
res.end(buf);
return out.status;
};
const publicClaims = (payload: JWTPayload): Record<string, unknown> => {
@ -154,6 +157,12 @@ const main = async (): Promise<void> => {
console.error("smart-ide-sso-gateway: set OIDC_ISSUER (docv / Enso IdP issuer URL).");
process.exit(1);
}
if (!globalApiToken()) {
console.error(
"smart-ide-sso-gateway: set GLOBAL_API_INTERNAL_TOKEN (must match smart-ide-global-api).",
);
process.exit(1);
}
const audience = process.env.OIDC_AUDIENCE?.trim();
const jwksUri = await discoverJwksUri(issuer);
console.error(`smart-ide-sso-gateway: JWKS URI ${jwksUri}`);
@ -161,24 +170,32 @@ const main = async (): Promise<void> => {
const server = http.createServer((req, res) => {
void (async () => {
const started = Date.now();
const method = req.method ?? "GET";
const url = new URL(req.url ?? "/", `http://${HOST}`);
const pathname = url.pathname;
let logPath = pathname;
let upstreamKey = "";
let status = 0;
let oidcSub: string | undefined;
try {
if (req.method === "OPTIONS") {
if (method === "OPTIONS") {
res.writeHead(204, corsHeaders());
res.end();
return;
}
const url = new URL(req.url ?? "/", `http://${HOST}`);
const pathname = url.pathname;
if (req.method === "GET" && (pathname === "/health" || pathname === "/health/")) {
json(res, 200, { status: "ok", service: "smart-ide-sso-gateway" });
if (method === "GET" && (pathname === "/health" || pathname === "/health/")) {
status = 200;
json(res, status, { status: "ok", service: "smart-ide-sso-gateway" });
return;
}
const token = readBearer(req);
if (!token) {
json(res, 401, { error: "Missing Authorization: Bearer <access_token>" });
status = 401;
json(res, status, { error: "Missing Authorization: Bearer <access_token>" });
return;
}
@ -186,45 +203,61 @@ const main = async (): Promise<void> => {
try {
payload = await verify(token);
} catch {
json(res, 401, { error: "Invalid or expired token" });
status = 401;
json(res, status, { error: "Invalid or expired token" });
return;
}
if (req.method === "GET" && pathname === "/v1/token/verify") {
json(res, 200, { valid: true, claims: publicClaims(payload) });
if (typeof payload.sub === "string") {
oidcSub = payload.sub;
}
if (method === "GET" && pathname === "/v1/token/verify") {
status = 200;
json(res, status, { valid: true, claims: publicClaims(payload) });
return;
}
if (req.method === "GET" && pathname === "/v1/upstreams") {
json(res, 200, { upstreams: listUpstreamKeys() });
if (method === "GET" && pathname === "/v1/upstreams") {
status = 200;
json(res, status, { upstreams: listUpstreamKeys() });
return;
}
const proxyMatch = /^\/proxy\/([^/]+)(\/.*)?$/.exec(pathname);
if (!proxyMatch || !req.method) {
json(res, 404, { error: "Not found" });
if (!proxyMatch || !method) {
status = 404;
json(res, status, { error: "Not found" });
return;
}
const key = proxyMatch[1];
upstreamKey = proxyMatch[1];
const rest = proxyMatch[2] ?? "/";
const upstream = resolveUpstream(key);
if (!upstream) {
json(res, 404, { error: `Unknown upstream: ${key}` });
return;
}
if (upstream.auth.kind === "header" && !upstream.auth.value) {
json(res, 503, { error: `API key not configured for upstream ${key} (LOCAL_OFFICE_API_KEY)` });
return;
}
logPath = pathname;
const targetUrl = `${globalApiBase()}/v1/upstream/${upstreamKey}${rest}${url.search}`;
const body = await readBodyBuffer(req);
const targetUrl = `${upstream.baseUrl}${rest}${url.search}`;
const headers = buildForwardHeaders(req, upstream.auth, payload);
await proxyToUpstream(req, res, targetUrl, headers, body);
const headers = buildForwardHeadersToGlobalApi(req, payload);
status = await proxyToGlobalApi(req, res, targetUrl, headers, body);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (!res.headersSent) {
json(res, 400, { error: msg });
status = 400;
json(res, status, { error: msg });
} else if (status === 0) {
status = 500;
}
} finally {
const skipLog =
method === "OPTIONS" ||
(method === "GET" && (pathname === "/health" || pathname === "/health/"));
if (!skipLog) {
void appendSsoAccessLog({
method,
path: logPath,
upstream: upstreamKey || undefined,
status,
durationMs: Date.now() - started,
oidcSub,
});
}
}
})();

View File

@ -1,88 +1,4 @@
export type UpstreamAuth =
| { kind: "bearer"; token: string }
| { kind: "header"; name: string; value: string };
export type UpstreamTarget = {
baseUrl: string;
auth: UpstreamAuth;
};
const trimSlash = (s: string): string => s.replace(/\/+$/, "");
const env = (k: string, d: string): string => process.env[k]?.trim() ?? d;
export const resolveUpstream = (key: string): UpstreamTarget | null => {
switch (key) {
case "orchestrator":
return {
baseUrl: trimSlash(
`http://${env("ORCHESTRATOR_HOST", "127.0.0.1")}:${env("ORCHESTRATOR_PORT", "37145")}`,
),
auth: { kind: "bearer", token: env("ORCHESTRATOR_TOKEN", "") },
};
case "repos_devtools":
return {
baseUrl: trimSlash(
`http://${env("REPOS_DEVTOOLS_HOST", "127.0.0.1")}:${env("REPOS_DEVTOOLS_PORT", "37140")}`,
),
auth: { kind: "bearer", token: env("REPOS_DEVTOOLS_TOKEN", "") },
};
case "ia_dev_gateway":
return {
baseUrl: trimSlash(
`http://${env("IA_DEV_GATEWAY_HOST", "127.0.0.1")}:${env("IA_DEV_GATEWAY_PORT", "37144")}`,
),
auth: { kind: "bearer", token: env("IA_DEV_GATEWAY_TOKEN", "") },
};
case "anythingllm_devtools":
return {
baseUrl: trimSlash(
`http://${env("ANYTHINGLLM_DEVTOOLS_HOST", "127.0.0.1")}:${env("ANYTHINGLLM_DEVTOOLS_PORT", "37146")}`,
),
auth: { kind: "bearer", token: env("ANYTHINGLLM_DEVTOOLS_TOKEN", "") },
};
case "tools_bridge":
return {
baseUrl: trimSlash(
`http://${env("TOOLS_BRIDGE_HOST", "127.0.0.1")}:${env("TOOLS_BRIDGE_PORT", "37147")}`,
),
auth: { kind: "bearer", token: env("TOOLS_BRIDGE_TOKEN", "") },
};
case "langextract":
return {
baseUrl: trimSlash(
`http://${env("LANGEXTRACT_API_HOST", "127.0.0.1")}:${env("LANGEXTRACT_API_PORT", "37141")}`,
),
auth: { kind: "bearer", token: env("LANGEXTRACT_SERVICE_TOKEN", "") },
};
case "regex_search":
return {
baseUrl: trimSlash(
`http://${env("REGEX_SEARCH_HOST", "127.0.0.1")}:${env("REGEX_SEARCH_PORT", "37143")}`,
),
auth: { kind: "bearer", token: env("REGEX_SEARCH_TOKEN", "") },
};
case "claw_proxy":
return {
baseUrl: trimSlash(
`http://${env("CLAW_PROXY_HOST", "127.0.0.1")}:${env("CLAW_PROXY_PORT", "37142")}`,
),
auth: { kind: "bearer", token: env("CLAW_PROXY_TOKEN", "") },
};
case "local_office":
return {
baseUrl: trimSlash(env("LOCAL_OFFICE_URL", "http://127.0.0.1:8000")),
auth: {
kind: "header",
name: "X-API-Key",
value: env("LOCAL_OFFICE_API_KEY", ""),
},
};
default:
return null;
}
};
/** Upstream keys exposed to OIDC-authenticated clients; traffic is relayed via smart-ide-global-api. */
export const listUpstreamKeys = (): string[] => [
"orchestrator",
"repos_devtools",