_format_version: "3.0" _info: select_tags: - product:didi - env:prod defaults: route: # Routes accept both HTTP and HTTPS (matches cluster pattern for lege365/rafai/etc). # HAProxy LB terminates TLS at edge, forwards HTTP to Kong DP — Kong must accept HTTP # internally or it returns 426 "Please use HTTPS protocol". protocols: [http, https] # ============================================================ # DIDI tenant configuration for shared Kong cluster # Cluster CP: kong-cp.local:8001 | DP1: kong-dp1.local | DP2: kong-dp2.local | LB: kong-lb.local # Hosts: didi365.eu (public) + www.didi365.eu + localhost (internal alias) # Upstreams: didi.local (DIDI host) on exposed ports # Plugins are applied per-service (NOT global) — cluster shared with lege365/rafai/biddie/notify # ============================================================ # ============================================================ # CONSUMERS — JWT issuers preserved from local Kong (Keycloak realm didi-clients) # ============================================================ consumers: - username: didi-keycloak-users custom_id: didi-keycloak-users tags: [product:didi, env:prod] jwt_secrets: - algorithm: RS256 key: https://localhost/auth/realms/didi-clients secret: 1qG51hpfTte1TGQzcAhKWjVkXsMWNIun rsa_public_key: |- -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlpv0lfjAFPGV4hLK6jp6 EsVlxax2nPA9I2IzGwNUIK8AsKwC9qu+737jarsjewx3ya/0s1uDP0ilbdh+wEzW Do/8jjWd0DxgTxMxCTS7DU07UZKXJWGc/Z+ansUPUcjqJ+uLTdu331z7ajK2FIZF 7yYH2WgjzApF6YSMx/dqybp/bdmBrvsPDGv1EJK4a72jV3P86WCW4ZDax2Qayw1t iKnO3+o6xvyoVSMeVJbs9ArjpAldueMLfTZqYBSmWe/rlBSIMWYkKTSgS+pdakez G71qs2RcSkI+GxlfJw0DJA8TfSjol6zc+EIUtAYG0pwwAqPB2PvkxMoDbL2UDkZE /wIDAQAB -----END PUBLIC KEY----- - algorithm: RS256 key: https://didi365.eu/auth/realms/didi-clients secret: IS8AJNbvC3taNROALc4bHslsZnSKELQD rsa_public_key: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlpv0lfjAFPGV4hLK6jp6 EsVlxax2nPA9I2IzGwNUIK8AsKwC9qu+737jarsjewx3ya/0s1uDP0ilbdh+wEzW Do/8jjWd0DxgTxMxCTS7DU07UZKXJWGc/Z+ansUPUcjqJ+uLTdu331z7ajK2FIZF 7yYH2WgjzApF6YSMx/dqybp/bdmBrvsPDGv1EJK4a72jV3P86WCW4ZDax2Qayw1t iKnO3+o6xvyoVSMeVJbs9ArjpAldueMLfTZqYBSmWe/rlBSIMWYkKTSgS+pdakez G71qs2RcSkI+GxlfJw0DJA8TfSjol6zc+EIUtAYG0pwwAqPB2PvkxMoDbL2UDkZE /wIDAQAB -----END PUBLIC KEY----- # SSO cluster (sso.local) — added 2026-04-29 after migration la Keycloak SSO public. # Same RSA public key (realm exported/imported from local with key preserved). - algorithm: RS256 key: https://sso.local/realms/didi-clients secret: lEyVoR2eXLgWmQQ5q6fZRbCkVJhSaTuB rsa_public_key: |- -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlpv0lfjAFPGV4hLK6jp6 EsVlxax2nPA9I2IzGwNUIK8AsKwC9qu+737jarsjewx3ya/0s1uDP0ilbdh+wEzW Do/8jjWd0DxgTxMxCTS7DU07UZKXJWGc/Z+ansUPUcjqJ+uLTdu331z7ajK2FIZF 7yYH2WgjzApF6YSMx/dqybp/bdmBrvsPDGv1EJK4a72jV3P86WCW4ZDax2Qayw1t iKnO3+o6xvyoVSMeVJbs9ArjpAldueMLfTZqYBSmWe/rlBSIMWYkKTSgS+pdakez G71qs2RcSkI+GxlfJw0DJA8TfSjol6zc+EIUtAYG0pwwAqPB2PvkxMoDbL2UDkZE /wIDAQAB -----END PUBLIC KEY----- # Internal SSO host (used during transition; also kept for proxy fallback paths) - algorithm: RS256 key: https://sso-admin.local/realms/didi-clients secret: kPwQjA4nFsUzMxRyV8tDeBcGhJlOvIuS rsa_public_key: |- -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlpv0lfjAFPGV4hLK6jp6 EsVlxax2nPA9I2IzGwNUIK8AsKwC9qu+737jarsjewx3ya/0s1uDP0ilbdh+wEzW Do/8jjWd0DxgTxMxCTS7DU07UZKXJWGc/Z+ansUPUcjqJ+uLTdu331z7ajK2FIZF 7yYH2WgjzApF6YSMx/dqybp/bdmBrvsPDGv1EJK4a72jV3P86WCW4ZDax2Qayw1t iKnO3+o6xvyoVSMeVJbs9ArjpAldueMLfTZqYBSmWe/rlBSIMWYkKTSgS+pdakez G71qs2RcSkI+GxlfJw0DJA8TfSjol6zc+EIUtAYG0pwwAqPB2PvkxMoDbL2UDkZE /wIDAQAB -----END PUBLIC KEY----- # didi-admins realm — admin-dashboard SPA auth (added 2026-05-04) # Different RSA key than didi-clients (separate realm). - algorithm: RS256 key: https://didi365.eu/auth/realms/didi-admins secret: aDmInS001PuBlIcEdge2026May04PaSsKonGvErY rsa_public_key: |- -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkcx2UE88P4j4XzWQ/bsd 0C69xZuxo/VXvhyKPj/t7u6ILIpD3/KL0sI0Ei4ZOlfjZ8PHmmEhWVwOYRCyhxgN aq5EMk4MKmUJ0KZd6pixvufKp8ddnI/xKdUROLbdzEHSxP251uHUUCYzpsKYisZ7 t6UidWgDBDrcU42YsV4OggvPlvemtQKFawv0CpQ7BLhyTm2WUD4iq7H17OEI975i ocC7Zqrk9itl2o0w8x9fSMOyMgsEafkVQ/KIuZA1/kKugtpG9eyYE49RFH7iQZd9 KmQow/XDxtKHfaLi1CEU4X2fHCThMWKdRQeK8N1n6qS+yanygcYJlUf8FKybgRxZ NwIDAQAB -----END PUBLIC KEY----- - algorithm: RS256 key: https://sso.local/realms/didi-admins secret: aDmInS002SsOcLuStEr2026May04PaSsKonGvErY rsa_public_key: |- -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkcx2UE88P4j4XzWQ/bsd 0C69xZuxo/VXvhyKPj/t7u6ILIpD3/KL0sI0Ei4ZOlfjZ8PHmmEhWVwOYRCyhxgN aq5EMk4MKmUJ0KZd6pixvufKp8ddnI/xKdUROLbdzEHSxP251uHUUCYzpsKYisZ7 t6UidWgDBDrcU42YsV4OggvPlvemtQKFawv0CpQ7BLhyTm2WUD4iq7H17OEI975i ocC7Zqrk9itl2o0w8x9fSMOyMgsEafkVQ/KIuZA1/kKugtpG9eyYE49RFH7iQZd9 KmQow/XDxtKHfaLi1CEU4X2fHCThMWKdRQeK8N1n6qS+yanygcYJlUf8FKybgRxZ NwIDAQAB -----END PUBLIC KEY----- - algorithm: RS256 key: https://sso-admin.local/realms/didi-admins secret: aDmInS003LoCaLcLuStEr2026May04PaSsKonGv rsa_public_key: |- -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkcx2UE88P4j4XzWQ/bsd 0C69xZuxo/VXvhyKPj/t7u6ILIpD3/KL0sI0Ei4ZOlfjZ8PHmmEhWVwOYRCyhxgN aq5EMk4MKmUJ0KZd6pixvufKp8ddnI/xKdUROLbdzEHSxP251uHUUCYzpsKYisZ7 t6UidWgDBDrcU42YsV4OggvPlvemtQKFawv0CpQ7BLhyTm2WUD4iq7H17OEI975i ocC7Zqrk9itl2o0w8x9fSMOyMgsEafkVQ/KIuZA1/kKugtpG9eyYE49RFH7iQZd9 KmQow/XDxtKHfaLi1CEU4X2fHCThMWKdRQeK8N1n6qS+yanygcYJlUf8FKybgRxZ NwIDAQAB -----END PUBLIC KEY----- - algorithm: RS256 key: https://localhost/auth/realms/didi-admins secret: aDmInS004DiDi11LoCaL2026May04PaSsKonGvE rsa_public_key: |- -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkcx2UE88P4j4XzWQ/bsd 0C69xZuxo/VXvhyKPj/t7u6ILIpD3/KL0sI0Ei4ZOlfjZ8PHmmEhWVwOYRCyhxgN aq5EMk4MKmUJ0KZd6pixvufKp8ddnI/xKdUROLbdzEHSxP251uHUUCYzpsKYisZ7 t6UidWgDBDrcU42YsV4OggvPlvemtQKFawv0CpQ7BLhyTm2WUD4iq7H17OEI975i ocC7Zqrk9itl2o0w8x9fSMOyMgsEafkVQ/KIuZA1/kKugtpG9eyYE49RFH7iQZd9 KmQow/XDxtKHfaLi1CEU4X2fHCThMWKdRQeK8N1n6qS+yanygcYJlUf8FKybgRxZ NwIDAQAB -----END PUBLIC KEY----- # ============================================================ # SERVICES (4 consolidated from 9 local) + ROUTES + PER-SERVICE PLUGINS # ============================================================ services: # ---------------------------------------------------------- # SERVICE 1: didi-agent-v3 (was: agent-api + agent-v3 + agent-v3-api) # ---------------------------------------------------------- - name: didi-agent-v3 protocol: http host: didi.local port: 24803 retries: 5 connect_timeout: 60000 write_timeout: 660000 read_timeout: 660000 tags: [product:didi, env:prod, kind:api] plugins: - name: cors tags: [product:didi, env:prod] config: # chrome-extension://* and moz-extension://* required for browser extension API origins: [https://didi365.eu, https://www.didi365.eu, "chrome-extension://[a-z]+", "moz-extension://[a-z0-9-]+"] methods: [GET, POST, PUT, DELETE, OPTIONS, PATCH] # X-API-Key header required for extension auth headers: [Accept, Authorization, Content-Type, X-Request-ID, X-API-Key] exposed_headers: [X-Request-ID] credentials: true max_age: 3600 preflight_continue: false - name: request-size-limiting tags: [product:didi, env:prod] config: allowed_payload_size: 104857600 size_unit: bytes require_content_length: false - name: response-transformer tags: [product:didi, env:prod] config: add: headers: ["X-Gateway:DIDI-Kong-Cluster", "X-API-Version:2.0"] remove: headers: [Server, Via] - name: correlation-id tags: [product:didi, env:prod] config: header_name: X-Request-ID generator: uuid echo_downstream: true routes: # JWT-protected endpoints - name: didi-agent-abort paths: [/api/abort] methods: [POST, OPTIONS] hosts: &didi-hosts [didi365.eu, www.didi365.eu, localhost] strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: &jwt-config key_claim_name: iss claims_to_verify: [exp] header_names: [authorization] uri_param_names: [jwt] run_on_preflight: true secret_is_base64: false maximum_expiration: 0 - name: didi-agent-admin-services paths: [/api/v1/admin/services] methods: [GET, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-agent-analysis paths: [/api/analysis] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-agent-analyze paths: [/api/analyze] methods: [POST, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-agent-jobs paths: [/api/v1/jobs] methods: [GET, POST, DELETE, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-agent-pipelines paths: [/api/pipelines] methods: [GET, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-agent-progress paths: [/api/progress] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-agent-sessions paths: [/api/sessions] methods: [GET, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-agent-storage paths: [/api/storage] methods: [GET, POST, DELETE, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-agent-subscriptions-v1 # Legacy v1 endpoint on agent-v3 (current v2 is on framework-api) paths: [/api/v1/subscriptions] methods: [GET, POST, DELETE, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod, legacy:v1] plugins: - name: jwt config: *jwt-config - name: didi-agent-upload paths: [/api/upload] methods: [POST, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config # Public endpoints (no JWT) - name: didi-agent-health # Local had strip_path=true: /agent/health -> "/" on agent-v3 (root, returns 200) paths: [/agent/health] methods: [GET, OPTIONS] hosts: *didi-hosts strip_path: true preserve_host: true tags: [product:didi, env:prod, public:true] - name: didi-agent-status paths: [/api/status] methods: [GET, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod, public:true] - name: didi-agent-media-public # Public media playback proxy (range-supported, served by agent-v3 itself) paths: [/api/v3/media/file] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod, public:true] - name: didi-agent-v3-prefix # Edge nginx forwards /agent-v3/* here. Local Kong stripped the prefix # (e.g. /agent-v3/api/v3/health -> /api/v3/health on agent-v3). # Local had JWT plugin on this route — preserved here to match security model. paths: [/agent-v3] hosts: *didi-hosts strip_path: true preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config # Browser extension API endpoints — NO JWT (X-API-Key validated in agent-v3). # All four routes share the same request-transformer pattern + rate-limit. # Regex paths (~ prefix) with `$` anchor prevent /analyze matching /analyze-async. - name: didi-agent-extension-analyze # Sync analyze (legacy — small text only, hits Cloudflare 100s timeout otherwise) paths: [~/agent-v3/api/v3/pipeline/extension/analyze$] methods: [POST, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod, public:true, auth:api-key] plugins: - name: request-transformer config: replace: uri: /api/v3/pipeline/extension/analyze - name: rate-limiting config: &ext-rate-limit minute: 30 hour: 500 policy: local limit_by: header header_name: X-API-Key fault_tolerant: true hide_client_headers: false error_code: 429 error_message: "API rate limit exceeded" - name: didi-agent-extension-analyze-async # Async dispatch — returns 202 + session_id (used by extension for all flows) paths: [~/agent-v3/api/v3/pipeline/extension/analyze-async$] methods: [POST, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod, public:true, auth:api-key] plugins: - name: request-transformer config: replace: uri: /api/v3/pipeline/extension/analyze-async - name: rate-limiting config: *ext-rate-limit - name: didi-agent-extension-upload # Multipart media upload (screenshot/video) — multer expects "file" field paths: [~/agent-v3/api/v3/pipeline/extension/upload$] methods: [POST, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod, public:true, auth:api-key] plugins: - name: request-transformer config: replace: uri: /api/v3/pipeline/extension/upload - name: rate-limiting config: *ext-rate-limit - name: didi-agent-extension-status # Polling endpoint with sessionId capture — rewrite preserves the UUID paths: ['~/agent-v3/api/v3/pipeline/extension/status/(?[\w-]+)$'] methods: [GET, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod, public:true, auth:api-key] plugins: - name: request-transformer config: replace: uri: /api/v3/pipeline/extension/status/$(uri_captures.sid) - name: rate-limiting config: # Polling can hit this every 2s — bump per-minute limit minute: 120 hour: 2000 policy: local limit_by: header header_name: X-API-Key fault_tolerant: true hide_client_headers: false error_code: 429 error_message: "Polling rate limit exceeded" # ---------------------------------------------------------- # SERVICE 2: didi-framework (was: framework-api + didi-framework) # ---------------------------------------------------------- - name: didi-framework protocol: http host: didi.local port: 3005 retries: 5 connect_timeout: 60000 write_timeout: 60000 read_timeout: 60000 tags: [product:didi, env:prod, kind:api] plugins: - name: cors tags: [product:didi, env:prod] config: origins: [https://didi365.eu, https://www.didi365.eu] methods: [GET, POST, PUT, DELETE, OPTIONS, PATCH] headers: [Accept, Authorization, Content-Type, X-Request-ID] exposed_headers: [X-Request-ID] credentials: true max_age: 3600 preflight_continue: false - name: request-size-limiting tags: [product:didi, env:prod] config: allowed_payload_size: 104857600 size_unit: bytes require_content_length: false - name: response-transformer tags: [product:didi, env:prod] config: add: headers: ["X-Gateway:DIDI-Kong-Cluster", "X-API-Version:2.0"] remove: headers: [Server, Via] - name: correlation-id tags: [product:didi, env:prod] config: header_name: X-Request-ID generator: uuid echo_downstream: true routes: - name: didi-framework-auth paths: [/api/auth] methods: [GET, POST, PUT, DELETE, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-framework-auth-verify-email # PUBLIC route — must beat /api/auth (JWT) on priority paths: [/api/auth/verify-email] methods: [GET, POST] hosts: *didi-hosts strip_path: false preserve_host: true regex_priority: 100 tags: [product:didi, env:prod, public:true] - name: didi-framework-history paths: [/api/history] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-framework-subscriptions # Current /api/subscriptions endpoint (v2). Legacy /api/v1/subscriptions is on agent-v3. paths: [/api/subscriptions] methods: [GET, POST, DELETE, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod] plugins: - name: jwt config: *jwt-config - name: didi-framework-waitlist # PUBLIC waitlist signup. Local Kong had this duplicated (one with JWT, one without). # Keeping no-JWT version — public signup is correct behavior. paths: [/api/waitlist] methods: [GET, POST, DELETE, OPTIONS] hosts: *didi-hosts strip_path: false preserve_host: true tags: [product:didi, env:prod, public:true] - name: didi-framework-direct # Legacy /framework prefix — local had strip_path=true so /framework/api/X -> /api/X on framework paths: [/framework] hosts: *didi-hosts strip_path: true preserve_host: true tags: [product:didi, env:prod] # ---------------------------------------------------------- # NOTE: didi-admin NOT migrated — internal-only (VPN access to didi.local directly). # Will be revisited after admin nginx is replaced with simpler setup. # ---------------------------------------------------------- # ---------------------------------------------------------- # NOTE: didi-keycloak service REMOVED 2026-04-29. # DIDI now uses the external SSO cluster (https://sso.local/realms/didi-clients) directly # — SPA goes browser→sso.local, no proxy through cluster Kong needed. # JWT consumer didi-keycloak-users still validates tokens for SSO issuer. # ----------------------------------------------------------