diff --git a/.github/workflows/ai-workspace-pr-check.yml b/.github/workflows/ai-workspace-pr-check.yml index e727003177..437eb82783 100644 --- a/.github/workflows/ai-workspace-pr-check.yml +++ b/.github/workflows/ai-workspace-pr-check.yml @@ -84,7 +84,7 @@ jobs: # requests; 502/000 means it is still warming up. run: | for i in $(seq 1 30); do - STATUS=$(curl -sk -o /dev/null -w "%{http_code}" https://host.docker.internal:5380/api/proxy/api/v0.9/organizations) + STATUS=$(curl -sk -o /dev/null -w "%{http_code}" https://host.docker.internal:5380/proxy/api/v0.9/organizations) if [ "$STATUS" = "200" ] || [ "$STATUS" = "401" ] || [ "$STATUS" = "403" ]; then echo "API proxy ready (HTTP $STATUS)" exit 0 diff --git a/.github/workflows/devportal-integration-test.yml b/.github/workflows/devportal-integration-test.yml index 93f094aad2..0cd7e8c80d 100644 --- a/.github/workflows/devportal-integration-test.yml +++ b/.github/workflows/devportal-integration-test.yml @@ -33,10 +33,15 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Build platform-api image + run: make -C platform-api build IMAGE_NAME=platform-api VERSION=it-devportal + - name: Build developer-portal image run: make -C portals/developer-portal build - name: Run REST API integration tests (${{ matrix.db }}) + env: + PLATFORM_API_IMAGE: platform-api:it-devportal run: | if [ "${{ matrix.db }}" = "postgres" ]; then make -C portals/developer-portal/it test-rest-api-postgres @@ -70,10 +75,15 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Build platform-api image + run: make -C platform-api build IMAGE_NAME=platform-api VERSION=it-devportal + - name: Build developer-portal image run: make -C portals/developer-portal build - name: Run Cypress integration tests + env: + PLATFORM_API_IMAGE: platform-api:it-devportal run: make -C portals/developer-portal it - name: Upload Cypress reports diff --git a/.gitignore b/.gitignore index 287e2a9583..68bc16f8af 100644 --- a/.gitignore +++ b/.gitignore @@ -169,7 +169,5 @@ dev-policies/ gateway/target/ -portals/ai-workspace/docker-compose.yaml - # Devportal portals/developer-portal/target/ diff --git a/distribution/all-in-one/devportal-config.toml b/distribution/all-in-one/devportal-config.toml new file mode 100644 index 0000000000..4d9f43710e --- /dev/null +++ b/distribution/all-in-one/devportal-config.toml @@ -0,0 +1,48 @@ +# Developer Portal configuration for the all-in-one (build-from-source) compose +# stack. Mounted at /app/configs/config.toml — replaces the image's shipped +# configs/config.toml, which fails closed at startup on the required security +# secrets and only wires the SQLite `file` key (this stack shares one Postgres +# server with platform-api). +# +# There is NO automatic APIP_DP_* environment-variable override (see +# src/config/configLoader.js) — an env var set in docker-compose.yaml's +# `devportal.environment` block only takes effect where a key below explicitly +# references it via a {{ env "..." }} token. + +[server] +base_url = '{{ env "APIP_DP_SERVER_BASEURL" "http://localhost:3001" }}' +port = 3000 + +[tls] +# Plain HTTP — nothing in this stack talks to devportal over TLS. +enabled = false + +[logging] +console_only = true + +[database] +type = '{{ env "APIP_DP_DATABASE_TYPE" "postgres" }}' +host = '{{ env "APIP_DP_DATABASE_HOST" "postgres" }}' +port = '{{ env "APIP_DP_DATABASE_PORT" "5432" }}' +name = '{{ env "APIP_DP_DATABASE_NAME" "devportal" }}' +username = '{{ env "APIP_DP_DATABASE_USERNAME" "postgres" }}' +password = '{{ env "APIP_DP_DATABASE_PASSWORD" "postgres" }}' + +[organization] +default_name = '{{ env "APIP_DP_ORGANIZATION_DEFAULTNAME" "default" }}' + +[platform_api] +# platform-api signs admin JWTs with RS256 — there is no shared HMAC secret to +# set here. jwt_secret left empty makes the devportal decode the token payload +# without verifying its signature, trusting the direct HTTPS connection +# (insecure=true) instead — see extractPlatformJwtClaims in +# src/utils/platformJwt.js. platform-api's cert is self-signed (platform-api-certgen). +base_url = '{{ env "APIP_DP_PLATFORMAPI_BASEURL" "https://platform-api:9243" }}' +insecure = true + +[security] +# Required — devportal fails closed at startup if either doesn't resolve to a +# 64-char hex string. Set APIP_DP_SECURITY_ENCRYPTIONKEY / APIP_DP_SECURITY_SESSIONSECRET +# in the environment before `docker compose up` (e.g. `openssl rand -hex 32` each). +encryption_key = '{{ env "APIP_DP_SECURITY_ENCRYPTIONKEY" }}' +session_secret = '{{ env "APIP_DP_SECURITY_SESSIONSECRET" }}' diff --git a/distribution/all-in-one/docker-compose.yaml b/distribution/all-in-one/docker-compose.yaml index c4a6c7b5f0..a32a34066e 100644 --- a/distribution/all-in-one/docker-compose.yaml +++ b/distribution/all-in-one/docker-compose.yaml @@ -41,17 +41,31 @@ services: dockerfile: Dockerfile container_name: devportal environment: - - APIP_DP_PLATFORMAPI_JWTSECRET=${APIP_CP_AUTH_JWT_SECRET_KEY:-} + - APIP_DP_SERVER_BASEURL=http://localhost:3001 + - APIP_DP_DATABASE_TYPE=postgres + - APIP_DP_DATABASE_HOST=postgres + - APIP_DP_DATABASE_PORT=5432 + - APIP_DP_DATABASE_NAME=devportal + - APIP_DP_DATABASE_USERNAME=postgres + - APIP_DP_DATABASE_PASSWORD=postgres + - APIP_DP_PLATFORMAPI_BASEURL=https://platform-api:9243 + # Required — devportal fails closed at startup if either doesn't resolve + # to a 64-char hex string. Set both before `docker compose up`, e.g.: + # export APIP_DP_SECURITY_ENCRYPTIONKEY=$(openssl rand -hex 32) + # export APIP_DP_SECURITY_SESSIONSECRET=$(openssl rand -hex 32) + - APIP_DP_SECURITY_ENCRYPTIONKEY=${APIP_DP_SECURITY_ENCRYPTIONKEY:-} + - APIP_DP_SECURITY_SESSIONSECRET=${APIP_DP_SECURITY_SESSIONSECRET:-} ports: - - "3001:3001" + - "3001:3000" volumes: - - ../../portals/developer-portal/config.json:/app/config.json:ro - - ../../portals/developer-portal/secret.json:/app/secret.json:ro + # Replaces the image's shipped configs/config.toml (SQLite-only, fails + # closed on security secrets) — see devportal-config.toml for why. + - ./devportal-config.toml:/app/configs/config.toml:ro depends_on: postgres: condition: service_healthy - platform-api-certgen: - condition: service_completed_successfully + platform-api: + condition: service_healthy # One-shot init container: generates the TLS pair the platform-api HTTPS # listener requires (the server no longer generates a self-signed fallback). @@ -68,20 +82,50 @@ services: volumes: - platform-api-certs:/certs + # One-shot init container: generates the RS256 keypair platform-api's + # auth.jwt config requires (config.toml reads it via {{ file }}). Tokens are + # signed asymmetrically — there is no shared HMAC secret anymore. + platform-api-jwtkeygen: + image: alpine/openssl + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + [ -f /keys/jwt_private.pem ] && [ -f /keys/jwt_public.pem ] && exit 0 + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + -out /keys/jwt_private.pem 2>/dev/null + openssl rsa -in /keys/jwt_private.pem -pubout \ + -out /keys/jwt_public.pem 2>/dev/null + # jwtkeygen runs as root; platform-api runs as uid 10001, so the private + # key must be owned by that uid, not made world-readable. + chown 10001 /keys/jwt_private.pem + chmod 0600 /keys/jwt_private.pem + chmod 0644 /keys/jwt_public.pem + volumes: + - platform-api-jwt-keys:/keys + platform-api: - image: ghcr.io/wso2/api-platform/platform-api:latest + image: ghcr.io/wso2/api-platform/platform-api:latest build: context: ../../platform-api dockerfile: Dockerfile additional_contexts: common: ../../common + # Mounts the shipped default config (../../platform-api/config/config.toml) + # so the APIP_CP_* env vars below actually take effect — {{ env }} / {{ file }} + # tokens only resolve from a loaded config file, they are not read directly. + command: ["-config", "/etc/platform-api/config.toml"] ports: - "9243:9243" volumes: + - ../../platform-api/config/config.toml:/etc/platform-api/config.toml:ro - platform-api-data:/api-platform/data - platform-api-certs:/app/data/certs + # RS256 JWT signing/verification keys — on the Platform API's {{ file }} + # allowlist (/etc/platform-api). + - platform-api-jwt-keys:/etc/platform-api/keys:ro environment: - - APIP_CP_DEFAULT_DEVPORTAL_API_URL=http://devportal:3001 + - APIP_CP_DEFAULT_DEVPORTAL_API_URL=http://devportal:3000 - APIP_CP_DATABASE_DRIVER=postgres - APIP_CP_DATABASE_HOST=postgres - APIP_CP_DATABASE_PORT=5432 @@ -93,11 +137,16 @@ services: - APIP_CP_DATABASE_MAX_IDLE_CONNS=10 - APIP_CP_DATABASE_CONN_MAX_LIFETIME=300 - APIP_CP_DATABASE_EXECUTE_SCHEMA_DDL=true + # Required — set before `docker compose up`, e.g.: + # export APIP_CP_ENCRYPTION_KEY=$(openssl rand -hex 32) - APIP_CP_ENCRYPTION_KEY=${APIP_CP_ENCRYPTION_KEY:-} - - APIP_CP_AUTH_JWT_SECRET_KEY=${APIP_CP_AUTH_JWT_SECRET_KEY:-} depends_on: postgres: condition: service_healthy + platform-api-certgen: + condition: service_completed_successfully + platform-api-jwtkeygen: + condition: service_completed_successfully healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "-O", "/dev/null", "--no-check-certificate", "https://localhost:9243/health"] interval: 30s @@ -120,4 +169,6 @@ volumes: driver: local platform-api-certs: driver: local + platform-api-jwt-keys: + driver: local postgres_data: diff --git a/docs/ai-workspace/authentication/README.md b/docs/ai-workspace/authentication/README.md index 5cdbe2fe5b..87673c0554 100644 --- a/docs/ai-workspace/authentication/README.md +++ b/docs/ai-workspace/authentication/README.md @@ -1,6 +1,6 @@ # Authentication -AI Workspace supports two authentication modes, configured via the `auth_mode` key in `config.toml`. +AI Workspace supports two authentication modes, configured via the `mode` key in `[ai_workspace.auth]` in `config.toml`. ## Modes diff --git a/docs/ai-workspace/authentication/asgardeo-setup.md b/docs/ai-workspace/authentication/asgardeo-setup.md index 31b4cd222c..93c19b65a5 100644 --- a/docs/ai-workspace/authentication/asgardeo-setup.md +++ b/docs/ai-workspace/authentication/asgardeo-setup.md @@ -117,23 +117,19 @@ In each sub-organization that should have access: Update `configs/config-platform-api.toml`: ```toml -[auth.jwt] -enabled = false +[auth] +mode = "idp" [auth.idp] -enabled = true name = "asgardeo" jwks_url = "https://api.asgardeo.io/t//oauth2/jwks" issuer = ["https://api.asgardeo.io/t//oauth2/token"] audience = [""] -[auth.idp.claim_mappings] -organization_claim_name = "org_id" -org_name_claim_name = "org_name" -org_handle_claim_name = "org_handle" - -[auth.file_based] -enabled = false +[auth.claim_mappings] +organization = "org_id" +org_name = "org_name" +org_handle = "org_handle" ``` > Asgardeo uses `org_id` as the claim for the organization UUID. The Platform API defaults to `organization`, so the claim name override above is required. @@ -145,15 +141,20 @@ enabled = false Update `configs/config.toml`: ```toml +[ai_workspace] domain = "" -auth_mode = "oidc" -controlplane_host = "" default_org_region = "us" -[platform_api] +[ai_workspace.control_plane] url = "https://" -[oidc] +[ai_workspace.gateway] +controlplane_host = "" + +[ai_workspace.auth] +mode = "oidc" + +[ai_workspace.auth.oidc] authority = "https://api.asgardeo.io/t//oauth2/token" client_id = "" @@ -162,23 +163,23 @@ redirect_url = "https:///api/auth/callback" # the BFF post_logout_redirect_url = "https:///login" # Preferred — a mounted secret file. To read it from the git-ignored api-platform.env instead, swap the -# token for '{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}': the key needs one token or the other. +# token for '{{ env "APIP_AIW_AUTH_OIDC_CLIENT_SECRET" }}': the key needs one token or the other. client_secret = '{{ file "/secrets/ai-workspace/oidc_client_secret" }}' -# Mirrors [auth.idp.claim_mappings] in config-platform-api.toml — the two must agree. -# Must stay the last table under [oidc]: plain [oidc] keys placed below this header -# would land in [oidc.claim_mappings] instead. -[oidc.claim_mappings] -organization_claim_name = "org_id" -org_name_claim_name = "org_name" -org_handle_claim_name = "org_handle" +# Mirrors [platform_api.auth.claim_mappings] in config-platform-api.toml — the two must +# agree. A sibling of [ai_workspace.auth.oidc], not nested in it: this table applies to +# both auth modes, since basic-mode tokens are signed using these same mapped claim names. +[ai_workspace.auth.claim_mappings] +organization = "org_id" +org_name = "org_name" +org_handle = "org_handle" ``` The redirect URLs and the client secret are BFF settings and never reach the browser. The redirect URLs are ordinary `config.toml` keys; the secret is referenced with an interpolation token so the raw value never lands in the file. -> `[oidc] redirect_url` must exactly match the authorized redirect URL registered in section 2. +> `[ai_workspace.auth.oidc] redirect_url` must exactly match the authorized redirect URL registered in section 2. > A missing client secret fails startup — see [Configuration → Secrets](../configuration.md#secrets). --- @@ -188,13 +189,13 @@ token so the raw value never lands in the file. ``` Asgardeo token ├── sub → user identity - ├── org_id → organization UUID (→ organization_claim_name in Platform API) - ├── org_name → org display name (→ org_name_claim_name in Platform API) - ├── org_handle → org slug (→ org_handle_claim_name in Platform API) + ├── org_id → organization UUID (→ organization in Platform API) + ├── org_name → org display name (→ org_name in Platform API) + ├── org_handle → org slug (→ org_handle in Platform API) └── scope → space-separated ap:* scopes validated by Platform API ``` The claim names must be consistent across all three places: - Asgardeo token mapper output -- `[oidc.claim_mappings]` in `config.toml` -- `*_claim_name` in Platform API `[auth.idp.claim_mappings]` +- `[ai_workspace.auth.claim_mappings]` in `config.toml` +- The matching keys in Platform API `[auth.claim_mappings]` diff --git a/docs/ai-workspace/authentication/file-based-auth.md b/docs/ai-workspace/authentication/file-based-auth.md index c3ae309ca4..e282eb2cb4 100644 --- a/docs/ai-workspace/authentication/file-based-auth.md +++ b/docs/ai-workspace/authentication/file-based-auth.md @@ -4,31 +4,32 @@ File-based auth (also called `basic` mode) stores a user list in the Platform AP ## How It Works -When `auth_mode = "basic"`, the AI Workspace login page renders a username/password form. Credentials are sent to the Platform API, which validates them against a hashed user list in `config-platform-api.toml`. On success, the Platform API issues a signed JWT that the UI stores and sends with subsequent API requests. +When `[ai_workspace.auth] mode = "basic"`, the AI Workspace login page renders a username/password form. Credentials are sent to the Platform API, which validates them against a hashed user list in `config-platform-api.toml`. On success, the Platform API issues a signed JWT that the UI stores and sends with subsequent API requests. ## Configuration ### 1. Set auth mode in `configs/config.toml` ```toml -auth_mode = "basic" +[ai_workspace.auth] +mode = "basic" ``` ### 2. Define users in `configs/config-platform-api.toml` ```toml -[auth.file_based] -enabled = true +[auth] +mode = "file" -[[auth.file_based.users]] -username = "admin" +[[auth.file.users]] +username = "admin" password_hash = "$2a$10$..." # bcrypt hash of the password -role = "admin" +scopes = "ap:organization:manage ap:gateway:manage ..." # space-separated ap:* scopes -[[auth.file_based.users]] -username = "viewer" +[[auth.file.users]] +username = "viewer" password_hash = "$2a$10$..." -role = "viewer" +scopes = "ap:organization:read ap:gateway:read" ``` ### 3. Generate password hashes @@ -59,13 +60,14 @@ The Quick Start bundle ships with a default `admin` / `admin` credential — **c In file-based mode, all users belong to a single organization defined in `config-platform-api.toml`: ```toml -[auth.file_based.organization] -id = "" # Leave empty to auto-generate a UUID on first start -name = "My Organization" -handle = "my-org" +[auth.file.organization] +id = "default" # organization handle (URL-safe slug) +display_name = "My Organization" +region = "us" +uuid = "" # Leave empty to auto-generate a UUID on first start ``` -If `id` is left empty, the Platform API generates a stable UUID on first startup and writes it back to the config. This UUID persists across restarts as long as the config file is preserved. +If `uuid` is left empty, the Platform API generates a stable UUID on first startup. Pin it to keep the organization stable across fresh databases. ## Limitations diff --git a/docs/ai-workspace/authentication/oidc-auth.md b/docs/ai-workspace/authentication/oidc-auth.md index 62cea56b2d..0baa01f124 100644 --- a/docs/ai-workspace/authentication/oidc-auth.md +++ b/docs/ai-workspace/authentication/oidc-auth.md @@ -22,15 +22,20 @@ Tested IDPs: [Asgardeo](asgardeo-setup.md), Keycloak, Auth0, Okta. ### AI Workspace (`configs/config.toml`) ```toml -domain = "app.example.com" -auth_mode = "oidc" -controlplane_host = "api.example.com" +[ai_workspace] +domain = "app.example.com" -[platform_api] +[ai_workspace.control_plane] # The upstream the BFF proxies to (an origin — the API paths are appended by the proxy). url = "https://api.example.com" -[oidc] +[ai_workspace.gateway] +controlplane_host = "api.example.com" + +[ai_workspace.auth] +mode = "oidc" + +[ai_workspace.auth.oidc] # IDP issuer URL — the discovery doc is fetched from {authority}/.well-known/openid-configuration authority = "https://idp.example.com/realms/my-realm" @@ -46,64 +51,59 @@ post_logout_redirect_url = "https:///login" client_secret = '{{ file "/secrets/ai-workspace/oidc_client_secret" }}' # JWT claim names for organization identity. This table mirrors -# [auth.idp.claim_mappings] in the Platform API config (below) key for key — both -# services read the same claims out of the same token, so the two must agree. -# Must stay the last table under [oidc]: plain [oidc] keys placed below this -# header would land in [oidc.claim_mappings] instead. -[oidc.claim_mappings] -organization_claim_name = "org_id" -org_name_claim_name = "org_name" -org_handle_claim_name = "org_handle" +# [platform_api.auth.claim_mappings] in the Platform API config (below) key for +# key — both services read the same claims out of the same token, so the two must +# agree. A sibling of [ai_workspace.auth.oidc], not nested in it: this table applies +# to both auth modes, since basic-mode tokens are signed using these same mapped +# claim names. +[ai_workspace.auth.claim_mappings] +organization = "org_id" +org_name = "org_name" +org_handle = "org_handle" ``` The redirect URLs and the client secret are BFF settings — they are **never sent to the browser**. The redirect URLs are ordinary `config.toml` keys; the secret is *referenced* by the config rather than written into it, so the raw value never lands in a committed file. -For a simpler local setup, swap the `{{ file }}` token for `'{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}'` and keep +For a simpler local setup, swap the `{{ file }}` token for `'{{ env "APIP_AIW_AUTH_OIDC_CLIENT_SECRET" }}'` and keep the value in the git-ignored `api-platform.env`. The key must carry one token or the other — a variable set with no token to read it is ignored. Either token fails closed: a missing secret aborts startup rather than yielding an empty credential. See [Configuration → Secrets](../configuration.md#secrets). -`[oidc] redirect_url` (the BFF callback `/api/auth/callback`) and `post_logout_redirect_url` +`[ai_workspace.auth.oidc] redirect_url` (the BFF callback `/api/auth/callback`) and `post_logout_redirect_url` must be registered as allowed redirect URIs in your IDP application. The redirect is **not** the SPA `/signin` route — the BFF, not the browser, completes the code exchange. ### Platform API (`configs/config-platform-api.toml`) ```toml -# Disable local JWT signing — tokens come from the IDP -[auth.jwt] -enabled = false +# Validate tokens against the IDP's JWKS instead of signing locally +[auth] +mode = "idp" -# Enable JWKS-based validation of IDP tokens [auth.idp] -enabled = true name = "my-idp" jwks_url = "https://idp.example.com/realms/my-realm/protocol/openid-connect/certs" issuer = ["https://idp.example.com/realms/my-realm"] -audience = ["ai-workspace"] # must match [oidc] client_id +audience = ["ai-workspace"] # must match [ai_workspace.auth.oidc] client_id # Map IDP-specific claim names to Platform API's expected fields -# These must match the [oidc.claim_mappings] values in config.toml above -[auth.idp.claim_mappings] -organization_claim_name = "org_id" -org_name_claim_name = "org_name" -org_handle_claim_name = "org_handle" - -# Disable file-based auth -[auth.file_based] -enabled = false +# These must match the [ai_workspace.auth.claim_mappings] values in config.toml above +[auth.claim_mappings] +organization = "org_id" +org_name = "org_name" +org_handle = "org_handle" ``` Optional claim overrides (defaults shown): ```toml -[auth.idp.claim_mappings] -user_id_claim_name = "sub" -username_claim_name = "username" -email_claim_name = "email" -scope_claim_name = "scope" +[auth.claim_mappings] +user_id = "sub" +username = "username" +email = "email" +scope = "scope" ``` Validation mode (default `scope`): @@ -139,16 +139,16 @@ You must register the `ap:*` scopes as an API resource in your IDP and grant the **Users see a blank screen or redirect loop after login** - Verify `domain` in `config.toml` matches the actual host:port in the browser. - Verify the redirect URI `https:///api/auth/callback` (the BFF callback) is registered - in the IDP and matches `[oidc] redirect_url`. + in the IDP and matches `[ai_workspace.auth.oidc] redirect_url`. **Token endpoint rejects the BFF with `unauthorized_client` / "not authorized to use the requested grant type"** - The app is registered as a public/SPA client. Re-register it as a **confidential** client - (authorization-code + refresh-token grants, PKCE) and set `[oidc] client_secret`. + (authorization-code + refresh-token grants, PKCE) and set `[ai_workspace.auth.oidc] client_secret`. **Platform API returns 401** - Check that `jwks_url` and `issuer` in Platform API config match the IDP's discovery doc values. -- Check that `audience` matches the `[oidc] client_id` of the confidential application. -- Ensure `organization_claim_name` matches on both sides — `[auth.idp.claim_mappings]` in the Platform API and `[oidc.claim_mappings]` in AI Workspace. +- Check that `audience` matches the `[ai_workspace.auth.oidc] client_id` of the confidential application. +- Ensure `organization` matches on both sides — `[platform_api.auth.claim_mappings]` in the Platform API and `[ai_workspace.auth.claim_mappings]` in AI Workspace. **"Organization not found" error** - The `org_id` claim in the token does not match any organization in Platform API's database. diff --git a/docs/ai-workspace/configuration.md b/docs/ai-workspace/configuration.md index af9befdfa7..31ed393b94 100644 --- a/docs/ai-workspace/configuration.md +++ b/docs/ai-workspace/configuration.md @@ -2,19 +2,19 @@ AI Workspace is configured through a `config.toml` file mounted into the container at `/etc/ai-workspace/config.toml`. -Keys are grouped into TOML tables (`[platform_api]`, `[tls]`, `[session]`, `[cookie]`, `[oidc]`); deployment-identity keys such as `domain` and `auth_mode` sit at the top level. +All AI Workspace settings live under a single top-level `[ai_workspace]` table — the same namespacing convention the Platform API uses for its own `[platform_api]` table — so one `config.toml` can hold both services' sections side by side without their keys colliding. Keys are grouped into TOML tables (`[ai_workspace.logging]`, `[ai_workspace.control_plane]`, `[ai_workspace.server.https]`, `[ai_workspace.session]`, `[ai_workspace.auth]`, `[ai_workspace.auth.oidc]`); deployment-identity keys such as `domain` sit directly under `[ai_workspace]`. The session cookie's name, `Secure`, and `SameSite` attributes are not configurable — they are internal details of the BFF's session mechanism. The file is the **only** source of configuration. Each value in it is written as an interpolation token that is resolved once at startup, so where the value comes from is visible in place: ```toml -[oidc] -client_id = '{{ env "APIP_AIW_OIDC_CLIENT_ID" "default" }}' +[ai_workspace.auth.oidc] +client_id = '{{ env "APIP_AIW_AUTH_OIDC_CLIENT_ID" "default" }}' # ^ environment variable ^ value used when the variable is unset ``` A key written this way can be set from the environment without editing the file. That token is the *only* thing that lets an environment variable reach a config key — there is no implicit override, so a key written as a plain literal (`key = "value"`), or absent from the file, ignores the variable entirely. Add the key with a token to make it settable that way. -By convention the variable a token names is the key's full path — table and key, uppercased, dots as underscores — prefixed with **`APIP_AIW_`**: `[oidc] client_id` → `APIP_AIW_OIDC_CLIENT_ID`, `[platform_api] url` → `APIP_AIW_PLATFORM_API_URL`, and a top-level `log_level` → `APIP_AIW_LOG_LEVEL`. (The same prefix convention gives the Platform API `APIP_CP_` and the Developer Portal `APIP_DP_`.) It is only a convention: a token may name any variable, which is what lets a key read an existing secret under its own name. +By convention the variable a token names is the key's path **under `[ai_workspace]`** (the `ai_workspace` segment itself is not part of the name) — table and key, uppercased, dots as underscores — prefixed with **`APIP_AIW_`**: `[ai_workspace.auth.oidc] client_id` → `APIP_AIW_AUTH_OIDC_CLIENT_ID`, `[ai_workspace.control_plane] url` → `APIP_AIW_CONTROL_PLANE_URL`, and `[ai_workspace.logging] level` → `APIP_AIW_LOGGING_LEVEL`. (The same prefix convention gives the Platform API `APIP_CP_` and the Developer Portal `APIP_DP_`.) It is only a convention: a token may name any variable, which is what lets a key read an existing secret under its own name. The file's own location is not a config key — it cannot be, since it is needed before the file is read. The server reads its mount, `/etc/ai-workspace/config.toml`, unless `-config` names another path (`bff -config ../configs/config.toml`, which is what `make bff-run` does). One variable is likewise read directly by the server rather than through a token: `APIP_CONFIG_FILE_SOURCE_ALLOWLIST`, which bounds where `{{ file }}` tokens may read from (see below). @@ -27,14 +27,14 @@ Never write a secret as a literal in `config.toml`, and never hardcode one in `d **Environment variable (default)** — the key's token names the variable and has no default value, so an unset variable fails startup rather than running with an empty credential. Keep the value in the git-ignored `api-platform.env` (loaded into both services via `env_file`): ```toml -[oidc] -client_secret = '{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}' +[ai_workspace.auth.oidc] +client_secret = '{{ env "APIP_AIW_AUTH_OIDC_CLIENT_SECRET" }}' ``` **Mounted secret file (preferred in production)** — swap the token so the value never enters the environment at all: ```toml -[oidc] +[ai_workspace.auth.oidc] client_secret = '{{ file "/secrets/ai-workspace/oidc_client_secret" }}' ``` @@ -42,92 +42,125 @@ Both forms fail closed: if the variable is unset, or the file is missing or outs ## All Configuration Keys -The "Env var" column is the variable each key's shipped `{{ env }}` token names — set it and the key picks the value up. It works only while the key is present (uncommented) in `config.toml`. +Every key's `{{ env }}` token and default value are written inline in +[`configs/config-template.toml`](../../portals/ai-workspace/configs/config-template.toml) — that +file is the single source of truth for both, so they are not duplicated here. What follows is a +map of what each table is for. -### Top level (deployment identity) +### `[ai_workspace]` (deployment identity) -| Key | Env var | Default | Description | -|-----|-------------|---------|-------------| -| `domain` | `APIP_AIW_DOMAIN` | `localhost:5380` | Host (and optional port) shown in the browser address bar. | -| `auth_mode` | `APIP_AIW_AUTH_MODE` | `basic` | Authentication mode. `"basic"` for file-based local auth; `"oidc"` for external IDP. | -| `controlplane_host` | `APIP_AIW_CONTROLPLANE_HOST` | `localhost:9243` | Externally reachable `host:port` that deployed gateways use to reach the Platform API. Shown in gateway setup instructions. Must be an absolute address, not a relative path. | -| `default_org_region` | `APIP_AIW_DEFAULT_ORG_REGION` | `us` | Default region label assigned to new organizations on first login. | -| `log_level` | `APIP_AIW_LOG_LEVEL` | `info` | `debug` \| `info` \| `warn` \| `error`. | +| Key | Description | +|-----|-------------| +| `domain` | Host (and optional port) shown in the browser address bar. | +| `default_org_region` | Default region label assigned to new organizations on first login. | -### `[platform_api]` — the upstream hop +### `[ai_workspace.logging]` -| Key | Env var | Default | Description | -|-----|-------------|---------|-------------| -| `url` | `APIP_AIW_PLATFORM_API_URL` | — | **Required.** Absolute URL the BFF uses to reach the Platform API server-to-server (e.g. `https://platform-api:9243`) — an origin, not a base path; the API paths are appended by the proxy. Its scheme decides whether the upstream hop uses TLS. | -| `tls_skip_verify` | `APIP_AIW_PLATFORM_API_TLS_SKIP_VERIFY` | `false` | Skip upstream certificate verification entirely (local development only) — prefer `ca_file`. | -| `ca_file` | `APIP_AIW_PLATFORM_API_CA_FILE` | — | PEM bundle trusted for the upstream certificate, appended to the system roots. Prefer this over `tls_skip_verify`. | +| Key | Description | +|-----|-------------| +| `level` | `debug` \| `info` \| `warn` \| `error` (matched case-insensitively). | +| `format` | `text` \| `json`. | -### `[oidc]` (only required when `auth_mode = "oidc"`) +### `[ai_workspace.auth]` — login mode -| Key | Env var | Default | Description | -|-----|-------------|---------|-------------| -| `authority` | `APIP_AIW_OIDC_AUTHORITY` | — | OIDC issuer URL. Endpoints (authorization, token, JWKS, etc.) are auto-discovered from `{authority}/.well-known/openid-configuration`. | -| `client_id` | `APIP_AIW_OIDC_CLIENT_ID` | — | Client ID of the AI Workspace confidential application registered in your IDP. | -| `client_secret` | `APIP_AIW_OIDC_CLIENT_SECRET` | — | Confidential-client secret, held only by the BFF and never sent to the browser. Set it via the env var, or from a mounted file with a `{{ file }}` token — see [Secrets](#secrets). | -| `redirect_url` | `APIP_AIW_OIDC_REDIRECT_URL` | — | The BFF callback, e.g. `https:///api/auth/callback`. | -| `post_logout_redirect_url` | `APIP_AIW_OIDC_POST_LOGOUT_REDIRECT_URL` | — | Post-logout URL, e.g. `https:///login`. Must be an absolute, pre-registered URL. | +| Key | Description | +|-----|-------------| +| `mode` | Authentication mode. `"basic"` for file-based local auth; `"oidc"` for external IDP. | -### `[oidc.claim_mappings]` — which token claim carries each field +### `[ai_workspace.control_plane]` — the upstream hop -This table mirrors the Platform API's `[auth.idp.claim_mappings]` key for key, and the two must agree: both services read the same claims out of the same token. The variables line up one-to-one too — `APIP_AIW_OIDC_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME` against `APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME`. +| Key | Description | +|-----|-------------| +| `url` | **Required.** Absolute URL the BFF uses to reach the Platform API server-to-server (e.g. `https://platform-api:9243`) — an origin, not a base path; the API paths are appended by the proxy. Its scheme decides whether the upstream hop uses TLS. | +| `tls_skip_verify` | Skip upstream certificate verification entirely (local development only) — prefer `ca_file`. | +| `ca_file` | PEM bundle trusted for the upstream certificate, appended to the system roots. Prefer this over `tls_skip_verify`. | -| Key | Env var | Default | Description | -|-----|-------------|---------|-------------| -| `organization_claim_name` | `APIP_AIW_OIDC_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME` | `org_id` | Claim carrying the organization UUID. | -| `org_name_claim_name` | `APIP_AIW_OIDC_CLAIM_MAPPINGS_ORG_NAME_CLAIM_NAME` | `org_name` | Claim carrying the human-readable organization name. | -| `org_handle_claim_name` | `APIP_AIW_OIDC_CLAIM_MAPPINGS_ORG_HANDLE_CLAIM_NAME` | `org_handle` | Claim carrying the organization handle (slug). | -| `username_claim_name` | `APIP_AIW_OIDC_CLAIM_MAPPINGS_USERNAME_CLAIM_NAME` | `username` | Claim carrying the display name. | -| `email_claim_name` | `APIP_AIW_OIDC_CLAIM_MAPPINGS_EMAIL_CLAIM_NAME` | `email` | Claim carrying the email address. | -| `scope_claim_name` | `APIP_AIW_OIDC_CLAIM_MAPPINGS_SCOPE_CLAIM_NAME` | `scope` | Claim carrying the space-separated scope string. | -| `role_claim_name` | `APIP_AIW_OIDC_CLAIM_MAPPINGS_ROLE_CLAIM_NAME` | `platform_role` | Claim carrying the platform role. Server-side only — not published to the browser. | +### `[ai_workspace.gateway]` — gateway deployment info shown to the browser -`[oidc.claim_mappings]` must be the **last** table under `[oidc]`: in TOML a sub-table header ends the parent table's key section, so a plain `[oidc]` key written below it would land in the sub-table instead. +Distinct from `[ai_workspace.control_plane]` above: that table is the BFF's own +server-to-server hop, while this one is what an externally deployed gateway needs to +reach the Platform API itself. -`[oidc] redirect_url` and `post_logout_redirect_url` must be registered as authorized redirect +| Key | Description | +|-----|-------------| +| `controlplane_host` | Externally reachable `host:port` that deployed gateways use to reach the Platform API. Shown in gateway setup instructions. Must be an absolute address, not a relative path. | +| `platform_gateway_versions` | Gateway versions offered in the create-gateway version selector (JSON array string). | + +### `[ai_workspace.auth.oidc]` (only required when `[ai_workspace.auth] mode = "oidc"`) + +| Key | Description | +|-----|-------------| +| `authority` | OIDC issuer URL. Endpoints (authorization, token, JWKS, etc.) are auto-discovered from `{authority}/.well-known/openid-configuration`. | +| `client_id` | Client ID of the AI Workspace confidential application registered in your IDP. | +| `client_secret` | Confidential-client secret, held only by the BFF and never sent to the browser. Referenced from an env var or a mounted file — see [Secrets](#secrets). | +| `redirect_url` | The BFF callback, e.g. `https:///api/auth/callback`. | +| `post_logout_redirect_url` | Post-logout URL, e.g. `https:///login`. Must be an absolute, pre-registered URL. | + +### `[ai_workspace.auth.claim_mappings]` — which token claim carries each field + +A sibling of `[ai_workspace.auth.oidc]`, not nested inside it: this table applies to **both** auth modes. In basic mode the Platform API's file-based login endpoint signs its JWTs using these same mapped claim names, so the BFF reads basic-mode tokens by this mapping too. It mirrors the Platform API's `[platform_api.auth.claim_mappings]` key for key, and the two must agree: both services read the same claims out of the same token. + +| Key | Description | +|-----|-------------| +| `organization` | Claim carrying the organization UUID. | +| `org_name` | Claim carrying the human-readable organization name. | +| `org_handle` | Claim carrying the organization handle (slug). | +| `username` | Claim carrying the display name. | +| `email` | Claim carrying the email address. | +| `scope` | Claim carrying the space-separated scope string. | +| `roles` | Claim carrying the platform role. Server-side only — not published to the browser. | + +`[ai_workspace.auth.oidc] redirect_url` and `post_logout_redirect_url` must be registered as authorized redirect URLs in your IDP application. The sign-in redirect is the **BFF callback** `/api/auth/callback` (the BFF, not the browser, completes the code exchange) — not a `/signin` route. -The remaining tables (`[tls]`, `[session]`, `[cookie]`) and the top-level listener keys are documented inline in +The remaining tables (`[ai_workspace.server.https]`, `[ai_workspace.session]`) and the `[ai_workspace]` listener keys are documented inline in [`configs/config-template.toml`](../../portals/ai-workspace/configs/config-template.toml). ## Minimal Quick-Start Config (basic auth) ```toml -domain = "localhost:8080" -auth_mode = "basic" -controlplane_host = "localhost:9243" +[ai_workspace] +domain = "localhost:8080" -[platform_api] +[ai_workspace.control_plane] url = "https://localhost:9243" + +[ai_workspace.gateway] +controlplane_host = "localhost:9243" + +[ai_workspace.auth] +mode = "basic" ``` ## Minimal Production Config (OIDC) ```toml +[ai_workspace] domain = "app.example.com" -auth_mode = "oidc" -controlplane_host = "api.example.com" default_org_region = "us" -[platform_api] +[ai_workspace.control_plane] url = "https://api.example.com" -[oidc] +[ai_workspace.gateway] +controlplane_host = "api.example.com" + +[ai_workspace.auth] +mode = "oidc" + +[ai_workspace.auth.oidc] authority = "https://api.asgardeo.io/t//oauth2/token" client_id = "" client_secret = '{{ file "/secrets/ai-workspace/oidc_client_secret" }}' redirect_url = "https://app.example.com/api/auth/callback" -# Mirrors [auth.idp.claim_mappings] in the Platform API config — the two must agree. -[oidc.claim_mappings] -organization_claim_name = "org_id" -org_name_claim_name = "org_name" -org_handle_claim_name = "org_handle" +# Mirrors [platform_api.auth.claim_mappings] in the Platform API config — the two must agree. +# Applies to both auth modes, so it's a sibling of [ai_workspace.auth.oidc], not nested in it. +[ai_workspace.auth.claim_mappings] +organization = "org_id" +org_name = "org_name" +org_handle = "org_handle" ``` ## Platform API Configuration @@ -135,23 +168,19 @@ org_handle_claim_name = "org_handle" The Platform API has its own config file: `configs/config-platform-api.toml` (mounted at `/etc/platform-api/config.toml`). Key sections: ```toml -[auth.jwt] -enabled = false # Disable local JWT signing when using an external IDP +[auth] +mode = "idp" # "external_token", "file", or "idp" — exactly one mode is active [auth.idp] -enabled = true name = "asgardeo" jwks_url = "https://api.asgardeo.io/t//oauth2/jwks" issuer = ["https://api.asgardeo.io/t//oauth2/token"] audience = [""] -[auth.idp.claim_mappings] -organization_claim_name = "org_id" -org_name_claim_name = "org_name" -org_handle_claim_name = "org_handle" - -[auth.file_based] -enabled = false # Disable file-based auth in production +[auth.claim_mappings] +organization = "org_id" +org_name = "org_name" +org_handle = "org_handle" ``` Never write sensitive values (JWT signing key, encryption key, database password) as raw @@ -165,29 +194,22 @@ secret_key = '{{ file "/secrets/platform-api/jwt_secret" }}' # from a file ( ``` Supply the env values from a git-ignored `api-platform.env` and start with `docker compose up` -Or mount a secret file and use `{{ file }}`. - -The `APIP_CP_`-prefixed names referenced by the tokens above: - -| Key referenced by the token | Description | -|--------------------------|-----------------------------------------------------------------------------------| -| `APIP_CP_AUTH_JWT_SECRET_KEY` | JWT signing key (required when `auth.jwt.enabled = true`) | -| `APIP_CP_ENCRYPTION_KEY` | 32-byte key (64 hex / base64) — encrypts secrets & subscription tokens (required) | -| `APIP_CP_DATABASE_PASSWORD` | Database password | +Or mount a secret file and use `{{ file }}`. The variable each token names, and which keys +require one, are documented inline in `config-platform-api-template.toml`. ## Environment Variable Override -A key can be overridden from the environment only when its value carries an `{{ env }}` token naming the variable — there is no implicit environment overlay, so a literal or absent key ignores the environment entirely. Every key in the shipped `config-template.toml` carries such a token, conventionally named `APIP_AIW_` + the uppercased dotted key path (`[oidc] authority` → `APIP_AIW_OIDC_AUTHORITY`). This is useful in container orchestration environments (Kubernetes `env:` blocks, Docker Compose `environment:` sections) where file mounts are less convenient. +A key can be overridden from the environment only when its value carries an `{{ env }}` token naming the variable — there is no implicit environment overlay, so a literal or absent key ignores the environment entirely. Every key in the shipped `config-template.toml` carries such a token, conventionally named `APIP_AIW_` + the uppercased dotted key path under `[ai_workspace]`. This is useful in container orchestration environments (Kubernetes `env:` blocks, Docker Compose `environment:` sections) where file mounts are less convenient. Example — override just the authority for a staging environment: ```bash docker run \ - -e APIP_AIW_OIDC_AUTHORITY=https://api.asgardeo.io/t/staging-tenant/oauth2/token \ + -e APIP_AIW_AUTH_OIDC_AUTHORITY=https://api.asgardeo.io/t/staging-tenant/oauth2/token \ -v ./configs/config.toml:/etc/ai-workspace/config.toml \ ghcr.io/wso2/api-platform/ai-workspace: ``` -A browser-safe key keeps the **same name everywhere** — in `config.toml` (`domain`), as an environment override (`APIP_AIW_DOMAIN`), in Vite's `import.meta.env` at build time, and in the `window.__RUNTIME_CONFIG__` payload the BFF serves to the SPA. (Vite's `envPrefix` is configured with an explicit allowlist of these browser-safe `APIP_AIW_*` names — mirroring the BFF allowlist — so secrets sharing the namespace never reach the bundle; the legacy `VITE_*` names are gone and setting one has no effect.) +A browser-safe key keeps the **same name everywhere** — in `config.toml`, as an environment override, in Vite's `import.meta.env` at build time, and in the `window.__RUNTIME_CONFIG__` payload the BFF serves to the SPA. (Vite's `envPrefix` is configured with an explicit allowlist of these browser-safe names — mirroring the BFF allowlist — so secrets sharing the namespace never reach the bundle.) Only keys on the BFF's browser-safe allowlist (`bff/internal/config/runtime_config.go`) are ever emitted to the page — server-side settings and the OIDC client credentials are not. diff --git a/event-gateway/it/go.sum b/event-gateway/it/go.sum index bf8f5e7c44..49167fa379 100644 --- a/event-gateway/it/go.sum +++ b/event-gateway/it/go.sum @@ -33,7 +33,6 @@ github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/compose-spec/compose-go/v2 v2.11.0 h1:xoq/ootgIL6TsHmbJHrkuh7+bzjhPV3NHftHRPPyVXM= diff --git a/go.work.sum b/go.work.sum index dc4e112f80..3706d4197f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -12,35 +12,6 @@ cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= @@ -49,13 +20,13 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go v0.110.6/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk= cloud.google.com/go v0.110.9/go.mod h1:rpxevX/0Lqvlbc88b7Sc1SPNdyK1riNBTUU6JXhYNpM= -cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= @@ -75,6 +46,35 @@ cloud.google.com/go v0.120.1/go.mod h1:56Vs7sf/i2jYM6ZL9NYlC82r04PThNcPS5YgFmb0r cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s= cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -102,6 +102,7 @@ cloud.google.com/go/accesscontextmanager v1.9.3 h1:8zVoeiBa4erMCLEXltOcqVEsZhS26 cloud.google.com/go/accesscontextmanager v1.9.3/go.mod h1:S1MEQV5YjkAKBoMekpGrkXKfrBdsi4x6Dybfq6gZ8BU= cloud.google.com/go/accesscontextmanager v1.9.6/go.mod h1:884XHwy1AQpCX5Cj2VqYse77gfLaq9f8emE2bYriilk= cloud.google.com/go/accesscontextmanager v1.9.7/go.mod h1:i6e0nd5CPcrh7+YwGq4bKvju5YB9sgoAip+mXU73aMM= +cloud.google.com/go/aiplatform v1.109.0/go.mod h1:4rwKOMdubQOND81AlO3EckcskvEFCYSzXKfn42GMm8k= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= @@ -123,7 +124,6 @@ cloud.google.com/go/aiplatform v1.60.0 h1:0cSrii1ZeLr16MbBoocyy5KVnrSdiQ3KN/vtrT cloud.google.com/go/aiplatform v1.60.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM= cloud.google.com/go/aiplatform v1.74.0 h1:rE2P5H7FOAFISAZilmdkapbk4CVgwfVs6FDWlhGfuy0= cloud.google.com/go/aiplatform v1.74.0/go.mod h1:hVEw30CetNut5FrblYd1AJUWRVSIjoyIvp0EVUh51HA= -cloud.google.com/go/aiplatform v1.109.0/go.mod h1:4rwKOMdubQOND81AlO3EckcskvEFCYSzXKfn42GMm8k= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= @@ -164,6 +164,7 @@ cloud.google.com/go/apigeeconnect v1.6.5/go.mod h1:MEKm3AiT7s11PqTfKE3KZluZA9O91 cloud.google.com/go/apigeeconnect v1.7.3 h1:Wlr+30Tha0SMCvQYZKdrh+HkpOyl0CQFSlzeY/Gg1gs= cloud.google.com/go/apigeeconnect v1.7.3/go.mod h1:2ZkT5VCAqhYrDqf4dz7lGp4N/+LeNBSfou8Qs5bIuSg= cloud.google.com/go/apigeeconnect v1.7.7/go.mod h1:ftGK3nca0JePiVLl0A6alaMjKdOc5C+sAkFMyH2RH8U= +cloud.google.com/go/apigeeregistry v0.10.0/go.mod h1:SAlF5OhKvyLDuwWAaFAIVJjrEqKRrGTPkJs+TWNnSqg= cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= @@ -175,7 +176,6 @@ cloud.google.com/go/apigeeregistry v0.8.3 h1:C+QU2K+DzDjk4g074ouwHQGkoff1h5OMQp6 cloud.google.com/go/apigeeregistry v0.8.3/go.mod h1:aInOWnqF4yMQx8kTjDqHNXjZGh/mxeNlAf52YqtASUs= cloud.google.com/go/apigeeregistry v0.9.3 h1:j9CJg/oC884OX5cDpiwNt1ZlDXNV6Zb9Mp1YmRrOG0k= cloud.google.com/go/apigeeregistry v0.9.3/go.mod h1:oNCP2VjOeI6U8yuOuTmU4pkffdcXzR5KxeUD71gF+Dg= -cloud.google.com/go/apigeeregistry v0.10.0/go.mod h1:SAlF5OhKvyLDuwWAaFAIVJjrEqKRrGTPkJs+TWNnSqg= cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= @@ -206,10 +206,6 @@ cloud.google.com/go/area120 v0.8.5/go.mod h1:BcoFCbDLZjsfe4EkCnEq1LKvHSK0Ew/zk5U cloud.google.com/go/area120 v0.9.3 h1:dPQ07rW4eku8OgNWDOaQaVGcE4+XfhH8BSbVwdVQ+wU= cloud.google.com/go/area120 v0.9.3/go.mod h1:F3vxS/+hqzrjJo55Xvda3Jznjjbd+4Foo43SN5eMd8M= cloud.google.com/go/area120 v0.9.7/go.mod h1:5nJ0yksmjOMfc4Zpk+okWfJ3A1004FvB82rfia+ZLaY= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= @@ -224,10 +220,10 @@ cloud.google.com/go/artifactregistry v1.14.7/go.mod h1:0AUKhzWQzfmeTvT4SjfI4zjot cloud.google.com/go/artifactregistry v1.16.1 h1:ZNXGB6+T7VmWdf6//VqxLdZ/sk0no8W0ujanHeJwDRw= cloud.google.com/go/artifactregistry v1.16.1/go.mod h1:sPvFPZhfMavpiongKwfg93EOwJ18Tnj9DIwTU9xWUgs= cloud.google.com/go/artifactregistry v1.17.2/go.mod h1:h4CIl9TJZskg9c9u1gC9vTsOTo1PrAnnxntprqS3AjM= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= @@ -245,11 +241,10 @@ cloud.google.com/go/asset v1.17.2/go.mod h1:SVbzde67ehddSoKf5uebOD1sYw8Ab/jD/9EI cloud.google.com/go/asset v1.20.4 h1:6oNgjcs5KCPGBD71G0IccK6TfeFsEtBTyQ3Q+Dn09bs= cloud.google.com/go/asset v1.20.4/go.mod h1:DP09pZ+SoFWUZyPZx26xVroHk+6+9umnQv+01yfJxbM= cloud.google.com/go/asset v1.22.0/go.mod h1:q80JP2TeWWzMCazYnrAfDf36aQKf1QiKzzpNLflJwf8= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= cloud.google.com/go/assuredworkloads v1.11.1/go.mod h1:+F04I52Pgn5nmPG36CWFtxmav6+7Q+c5QyJoL18Lry0= cloud.google.com/go/assuredworkloads v1.11.2/go.mod h1:O1dfr+oZJMlE6mw0Bp0P1KZSlj5SghMBvTpZqIcUAW4= @@ -260,20 +255,11 @@ cloud.google.com/go/assuredworkloads v1.11.5/go.mod h1:FKJ3g3ZvkL2D7qtqIGnDufFkH cloud.google.com/go/assuredworkloads v1.12.3 h1:RU1WhF1zMggdXAZ+ezYTn4Eh/FdiX7sz8lLXGERn4Po= cloud.google.com/go/assuredworkloads v1.12.3/go.mod h1:iGBkyMGdtlsxhCi4Ys5SeuvIrPTeI6HeuEJt7qJgJT8= cloud.google.com/go/assuredworkloads v1.13.0/go.mod h1:o/oHEOnUlribR+uJWTKQo8A5RhSl9K9FNeMOew4TJ3M= -cloud.google.com/go/auth v0.2.1/go.mod h1:khQRBNrvNoHiHhV1iu2x8fSnlNbCaVHilznW5MAI5GY= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= -cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= -cloud.google.com/go/auth v0.4.2/go.mod h1:Kqvlz1cf1sNA0D+sYJnkPQOP+JMHkuHeIgVmCRtZOLc= -cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= -cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4= -cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw= -cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= -cloud.google.com/go/auth v0.7.3/go.mod h1:HJtWUx1P5eqjy/f6Iq5KeytNpbAcGolPhOgyop2LlzA= -cloud.google.com/go/auth v0.8.0/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc= -cloud.google.com/go/auth v0.9.0/go.mod h1:2HsApZBr9zGZhC9QAXsYVYaWk8kNUt37uny+XVKi7wM= -cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= -cloud.google.com/go/auth v0.9.4/go.mod h1:SHia8n6//Ya940F1rLimhJCjjx7KE17t0ctFEci3HkA= -cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/auth v0.10.1/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= cloud.google.com/go/auth v0.11.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= cloud.google.com/go/auth v0.12.1/go.mod h1:BFMu+TNpF3DmvfBO9ClqTR/SiqVIm7LukKF9mbendF4= @@ -289,6 +275,20 @@ cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9D cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo= +cloud.google.com/go/auth v0.2.1/go.mod h1:khQRBNrvNoHiHhV1iu2x8fSnlNbCaVHilznW5MAI5GY= +cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= +cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= +cloud.google.com/go/auth v0.4.2/go.mod h1:Kqvlz1cf1sNA0D+sYJnkPQOP+JMHkuHeIgVmCRtZOLc= +cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= +cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4= +cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw= +cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= +cloud.google.com/go/auth v0.7.3/go.mod h1:HJtWUx1P5eqjy/f6Iq5KeytNpbAcGolPhOgyop2LlzA= +cloud.google.com/go/auth v0.8.0/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc= +cloud.google.com/go/auth v0.9.0/go.mod h1:2HsApZBr9zGZhC9QAXsYVYaWk8kNUt37uny+XVKi7wM= +cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= +cloud.google.com/go/auth v0.9.4/go.mod h1:SHia8n6//Ya940F1rLimhJCjjx7KE17t0ctFEci3HkA= +cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= @@ -297,10 +297,6 @@ cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6 cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= cloud.google.com/go/automl v1.13.1/go.mod h1:1aowgAHWYZU27MybSCFiukPO7xnyawv7pt3zK4bheQE= cloud.google.com/go/automl v1.13.2/go.mod h1:gNY/fUmDEN40sP8amAX3MaXkxcqPIn7F1UIIPZpy4Mg= @@ -311,6 +307,10 @@ cloud.google.com/go/automl v1.13.5/go.mod h1:MDw3vLem3yh+SvmSgeYUmUKqyls6NzSumDm cloud.google.com/go/automl v1.14.4 h1:vkD+hQ75SMINMgJBT/KDpFYvfQLzJbtIQZdw0AWq8Rs= cloud.google.com/go/automl v1.14.4/go.mod h1:sVfsJ+g46y7QiQXpVs9nZ/h8ntdujHm5xhjHW32b3n4= cloud.google.com/go/automl v1.15.0/go.mod h1:U9zOtQb8zVrFNGTuW3BfxeqmLyeleLgT9B12EaXfODg= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= @@ -327,6 +327,9 @@ cloud.google.com/go/baremetalsolution v1.4.0/go.mod h1:K6C6g4aS8LW95I0fEHZiBsBlh cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/batch v1.12.0 h1:lXuTaELvU0P0ARbTFxxdpOC/dFnZZeGglSw06BtO//8= +cloud.google.com/go/batch v1.12.0/go.mod h1:CATSBh/JglNv+tEU/x21Z47zNatLQ/gpGnpyKOzbbcM= +cloud.google.com/go/batch v1.13.0/go.mod h1:yHFeqBn8wUjmJs4sYbwZ7N3HdeGA+FkPAXjoCKMwGak= cloud.google.com/go/batch v1.3.1/go.mod h1:VguXeQKXIYaeeIYbuozUmBR13AfL4SJP7IltNPS+A4A= cloud.google.com/go/batch v1.4.1/go.mod h1:KdBmDD61K0ovcxoRHGrN6GmOBWeAOyCgKD0Mugx4Fkk= cloud.google.com/go/batch v1.5.0/go.mod h1:KdBmDD61K0ovcxoRHGrN6GmOBWeAOyCgKD0Mugx4Fkk= @@ -336,9 +339,6 @@ cloud.google.com/go/batch v1.6.3/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3J cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= cloud.google.com/go/batch v1.8.0 h1:2HK4JerwVaIcCh/lJiHwh6+uswPthiMMWhiSWLELayk= cloud.google.com/go/batch v1.8.0/go.mod h1:k8V7f6VE2Suc0zUM4WtoibNrA6D3dqBpB+++e3vSGYc= -cloud.google.com/go/batch v1.12.0 h1:lXuTaELvU0P0ARbTFxxdpOC/dFnZZeGglSw06BtO//8= -cloud.google.com/go/batch v1.12.0/go.mod h1:CATSBh/JglNv+tEU/x21Z47zNatLQ/gpGnpyKOzbbcM= -cloud.google.com/go/batch v1.13.0/go.mod h1:yHFeqBn8wUjmJs4sYbwZ7N3HdeGA+FkPAXjoCKMwGak= cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= @@ -356,15 +356,13 @@ cloud.google.com/go/beyondcorp v1.2.0/go.mod h1:sszcgxpPPBEfLzbI0aYCTg6tT1tyt3Cm cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= cloud.google.com/go/bigquery v1.52.0/go.mod h1:3b/iXjRQGU4nKa87cXeg6/gogLjO8C6PmuM8i5Bi/u4= cloud.google.com/go/bigquery v1.53.0/go.mod h1:3b/iXjRQGU4nKa87cXeg6/gogLjO8C6PmuM8i5Bi/u4= @@ -376,7 +374,9 @@ cloud.google.com/go/bigquery v1.59.1 h1:CpT+/njKuKT3CEmswm6IbhNu9u35zt5dO4yPDLW+ cloud.google.com/go/bigquery v1.59.1/go.mod h1:VP1UJYgevyTwsV7desjzNzDND5p6hZB+Z8gZJN1GQUc= cloud.google.com/go/bigquery v1.66.2 h1:EKOSqjtO7jPpJoEzDmRctGea3c2EOGoexy8VyY9dNro= cloud.google.com/go/bigquery v1.66.2/go.mod h1:+Yd6dRyW8D/FYEjUGodIbu0QaoEmgav7Lwhotup6njo= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.72.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigtable v1.18.1/go.mod h1:NAVyfJot9jlo+KmgWLUJ5DJGwNDoChzAcrecLpmuAmY= cloud.google.com/go/bigtable v1.20.0/go.mod h1:upJDn8frsjzpRMfybiWkD1PG6WCCL7CRl26MgVeoXY4= cloud.google.com/go/bigtable v1.33.0/go.mod h1:HtpnH4g25VT1pejHRtInlFPnN5sjTxbQlsYBjh9t5l0= @@ -384,10 +384,6 @@ cloud.google.com/go/bigtable v1.35.0 h1:UEacPwaejN2mNbz67i1Iy3G812rxtgcs6ePj1TAg cloud.google.com/go/bigtable v1.35.0/go.mod h1:EabtwwmTcOJFXp+oMZAT/jZkyDIjNwrv53TrS4DGrrM= cloud.google.com/go/bigtable v1.37.0/go.mod h1:HXqddP6hduwzrtiTCqZPpj9ij4hGZb4Zy1WF/dT+yaU= cloud.google.com/go/bigtable v1.40.1/go.mod h1:LtPzCcrAFaGRZ82Hs8xMueUeYW9Jw12AmNdUTMfDnh4= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= cloud.google.com/go/billing v1.16.0/go.mod h1:y8vx09JSSJG02k5QxbycNRrN7FGZB6F3CAcgum7jvGA= @@ -402,7 +398,12 @@ cloud.google.com/go/billing v1.18.2/go.mod h1:PPIwVsOOQ7xzbADCwNe8nvK776QpfrOAUk cloud.google.com/go/billing v1.20.1 h1:xMlO3hc5BI0s23tRB40bL40xSpxUR1x3E07Y5/VWcjU= cloud.google.com/go/billing v1.20.1/go.mod h1:DhT80hUZ9gz5UqaxtK/LNoDELfxH73704VTce+JZqrY= cloud.google.com/go/billing v1.21.0/go.mod h1:ZGairB3EVnb3i09E2SxFxo50p5unPaMTuo1jh6jW9js= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.10.0/go.mod h1:WOuiaQkI4PU/okwrcREjSAr2AUtjQgVe+PlrXKOmKKw= cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= @@ -417,7 +418,6 @@ cloud.google.com/go/binaryauthorization v1.8.1 h1:1jcyh2uIUwSZkJ/JmL8kd5SUkL/Krb cloud.google.com/go/binaryauthorization v1.8.1/go.mod h1:1HVRyBerREA/nhI7yLang4Zn7vfNVA3okoAR9qYQJAQ= cloud.google.com/go/binaryauthorization v1.9.3 h1:X8JRfmk0/vyRqLusEyAPr0nZCK6RKae9omB4lrit0XI= cloud.google.com/go/binaryauthorization v1.9.3/go.mod h1:f3xcb/7vWklDoF+q2EaAIS+/A/e1278IgiYxonRX+Jk= -cloud.google.com/go/binaryauthorization v1.10.0/go.mod h1:WOuiaQkI4PU/okwrcREjSAr2AUtjQgVe+PlrXKOmKKw= cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= @@ -430,8 +430,6 @@ cloud.google.com/go/certificatemanager v1.7.5/go.mod h1:uX+v7kWqy0Y3NG/ZhNvffh0k cloud.google.com/go/certificatemanager v1.9.3 h1:2UP31fg7b+y3F0OmNbPHOKPEJ+6LOMfxAXX4p8xGCy4= cloud.google.com/go/certificatemanager v1.9.3/go.mod h1:O5T4Lg/dHbDHLFFooV2Mh/VsT3Mj2CzPEWRo4qw5prc= cloud.google.com/go/certificatemanager v1.9.6/go.mod h1:vWogV874jKZkSRDFCMM3r7wqybv8WXs3XhyNff6o/Zo= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= cloud.google.com/go/channel v1.16.0/go.mod h1:eN/q1PFSl5gyu0dYdmxNXscY/4Fi7ABmeHCJNf/oHmc= @@ -445,11 +443,8 @@ cloud.google.com/go/channel v1.17.5/go.mod h1:FlpaOSINDAXgEext0KMaBq/vwpLMkkPAw9 cloud.google.com/go/channel v1.19.2 h1:oHyO3QAZ6kdf6SwqnUTBz50ND6Nk2rxZtboUiF4dgLE= cloud.google.com/go/channel v1.19.2/go.mod h1:syX5opXGXFt17DHCyCdbdlM464Tx0gHMi46UlEWY9Gg= cloud.google.com/go/channel v1.20.0/go.mod h1:nBR1Lz+/1TjSA16HTllvW9Y+QULODj3o3jEKrNNeOp4= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= -cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= -cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= cloud.google.com/go/cloudbuild v1.10.1/go.mod h1:lyJg7v97SUIPq4RC2sGsz/9tNczhyv2AjML/ci4ulzU= cloud.google.com/go/cloudbuild v1.13.0/go.mod h1:lyJg7v97SUIPq4RC2sGsz/9tNczhyv2AjML/ci4ulzU= cloud.google.com/go/cloudbuild v1.14.0/go.mod h1:lyJg7v97SUIPq4RC2sGsz/9tNczhyv2AjML/ci4ulzU= @@ -462,6 +457,11 @@ cloud.google.com/go/cloudbuild v1.15.1/go.mod h1:gIofXZSu+XD2Uy+qkOrGKEx45zd7s28 cloud.google.com/go/cloudbuild v1.22.0 h1:zmDznviZpvkCla0adbp7jJsMYZ9bABCbcPK2cBUHwg8= cloud.google.com/go/cloudbuild v1.22.0/go.mod h1:p99MbQrzcENHb/MqU3R6rpqFRk/X+lNG3PdZEIhM95Y= cloud.google.com/go/cloudbuild v1.23.1/go.mod h1:Gh/k1NnFRw1DkhekO2BaR4MTg30Op6EQQHCUZCIyTAg= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= @@ -475,11 +475,6 @@ cloud.google.com/go/clouddms v1.7.4/go.mod h1:RdrVqoFG9RWI5AvZ81SxJ/xvxPdtcRhFot cloud.google.com/go/clouddms v1.8.4 h1:CDOd1nwmP4uek+nZhl4bhRIpzj8jMqoMRqKAfKlgLhw= cloud.google.com/go/clouddms v1.8.4/go.mod h1:RadeJ3KozRwy4K/gAs7W74ZU3GmGgVq5K8sRqNs3HfA= cloud.google.com/go/clouddms v1.8.8/go.mod h1:QtCyw+a73dlkDb2q20aTAPvfaTZCepDDi6Gb1AKq0a4= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= -cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= cloud.google.com/go/cloudtasks v1.11.1/go.mod h1:a9udmnou9KO2iulGscKR0qBYjreuX8oHwpmFsKspEvM= cloud.google.com/go/cloudtasks v1.12.1/go.mod h1:a9udmnou9KO2iulGscKR0qBYjreuX8oHwpmFsKspEvM= @@ -491,12 +486,12 @@ cloud.google.com/go/cloudtasks v1.12.6/go.mod h1:b7c7fe4+TJsFZfDyzO51F7cjq7HLUlR cloud.google.com/go/cloudtasks v1.13.3 h1:rXdznKjCa7WpzmvR2plrn2KJ+RZC1oYxPiRWNQjjf3k= cloud.google.com/go/cloudtasks v1.13.3/go.mod h1:f9XRvmuFTm3VhIKzkzLCPyINSU3rjjvFUsFVGR5wi24= cloud.google.com/go/cloudtasks v1.13.7/go.mod h1:H0TThOUG+Ml34e2+ZtW6k6nt4i9KuH3nYAJ5mxh7OM4= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= @@ -517,9 +512,14 @@ cloud.google.com/go/compute v1.23.4/go.mod h1:/EJMj55asU6kAFnuZET8zqgwgJ9FvXWXOk cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.34.0 h1:+k/kmViu4TEi97NGaxAATYtpYBviOWJySPZ+ekA95kk= cloud.google.com/go/compute v1.34.0/go.mod h1:zWZwtLwZQyonEvIQBuIa0WvraMYK69J5eDCOw9VZU4g= cloud.google.com/go/compute v1.49.1/go.mod h1:1uoZvP8Avyfhe3Y4he7sMOR16ZiAm2Q+Rc2P5rrJM28= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= @@ -535,10 +535,6 @@ cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyz cloud.google.com/go/compute/metadata v0.8.4/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= -cloud.google.com/go/contactcenterinsights v1.9.1/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM= cloud.google.com/go/contactcenterinsights v1.10.0/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM= cloud.google.com/go/contactcenterinsights v1.11.0/go.mod h1:hutBdImE4XNZ1NV4vbPJKSFOnQruhC5Lj9bZqWMTKiU= cloud.google.com/go/contactcenterinsights v1.11.1/go.mod h1:FeNP3Kg8iteKM80lMwSk3zZZKVxr+PGnAId6soKuXwE= @@ -551,8 +547,10 @@ cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3am cloud.google.com/go/contactcenterinsights v1.17.1 h1:xJoZbX0HM1zht8KxAB38hs2v4Hcl+vXGLo454LrdwxA= cloud.google.com/go/contactcenterinsights v1.17.1/go.mod h1:n8OiNv7buLA2AkGVkfuvtW3HU13AdTmEwAlAu46bfxY= cloud.google.com/go/contactcenterinsights v1.17.4/go.mod h1:kZe6yOnKDfpPz2GphDHynxk/Spx+53UX/pGf+SmWAKM= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/contactcenterinsights v1.9.1/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM= cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= @@ -570,10 +568,8 @@ cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njc cloud.google.com/go/container v1.42.2 h1:8ncSEBjkng6ucCICauaUGzBomoM2VyYzleAum1OFcow= cloud.google.com/go/container v1.42.2/go.mod h1:y71YW7uR5Ck+9Vsbst0AF2F3UMgqmsN4SP8JR9xEsR8= cloud.google.com/go/container v1.45.0/go.mod h1:eB6jUfJLjne9VsTDGcH7mnj6JyZK+KOUIA6KZnYE/ds= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= -cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= cloud.google.com/go/containeranalysis v0.10.1/go.mod h1:Ya2jiILITMY68ZLPaogjmOMNkwsDrWBSTyBubGXO7j0= cloud.google.com/go/containeranalysis v0.11.0/go.mod h1:4n2e99ZwpGxpNcz+YsFT1dfOHPQFGcAC8FN2M2/ne/U= cloud.google.com/go/containeranalysis v0.11.1/go.mod h1:rYlUOM7nem1OJMKwE1SadufX0JP3wnXj844EtZAwWLY= @@ -584,12 +580,10 @@ cloud.google.com/go/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWb cloud.google.com/go/containeranalysis v0.13.3 h1:1D8U75BeotZxrG4jR6NYBtOt+uAeBsWhpBZmSYLakQw= cloud.google.com/go/containeranalysis v0.13.3/go.mod h1:0SYnagA1Ivb7qPqKNYPkCtphhkJn3IzgaSp3mj+9XAY= cloud.google.com/go/containeranalysis v0.14.2/go.mod h1:FjppROiUtP9cyMegdWdY/TsBSGc6kqh1GjA2NOJXXL8= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= cloud.google.com/go/datacatalog v1.14.0/go.mod h1:h0PrGtlihoutNMp/uvwhawLQ9+c63Kz65UFqh49Yo+E= @@ -608,6 +602,15 @@ cloud.google.com/go/datacatalog v1.24.3 h1:3bAfstDB6rlHyK0TvqxEwaeOvoN9UgCs2bn03 cloud.google.com/go/datacatalog v1.24.3/go.mod h1:Z4g33XblDxWGHngDzcpfeOU0b1ERlDPTuQoYG6NkF1s= cloud.google.com/go/datacatalog v1.26.0/go.mod h1:bLN2HLBAwB3kLTFT5ZKLHVPj/weNz6bR0c7nYp0LE14= cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/dataflow v0.10.3 h1:+7IfIXzYWSybIIDGK9FN2uqBsP/5b/Y0pBYzNhcmKSU= +cloud.google.com/go/dataflow v0.10.3/go.mod h1:5EuVGDh5Tg4mDePWXMMGAG6QYAQhLNyzxdNQ0A1FfW4= +cloud.google.com/go/dataflow v0.11.1/go.mod h1:3s6y/h5Qz7uuxTmKJKBifkYZ3zs63jS+6VGtSu8Cf7Y= cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= @@ -617,9 +620,9 @@ cloud.google.com/go/dataflow v0.9.3/go.mod h1:HI4kMVjcHGTs3jTHW/kv3501YW+eloiJSL cloud.google.com/go/dataflow v0.9.4/go.mod h1:4G8vAkHYCSzU8b/kmsoR2lWyHJD85oMJPHMtan40K8w= cloud.google.com/go/dataflow v0.9.5 h1:RYHtcPhmE664+F0Je46p+NvFbG8z//KCXp+uEqB4jZU= cloud.google.com/go/dataflow v0.9.5/go.mod h1:udl6oi8pfUHnL0z6UN9Lf9chGqzDMVqcYTcZ1aPnCZQ= -cloud.google.com/go/dataflow v0.10.3 h1:+7IfIXzYWSybIIDGK9FN2uqBsP/5b/Y0pBYzNhcmKSU= -cloud.google.com/go/dataflow v0.10.3/go.mod h1:5EuVGDh5Tg4mDePWXMMGAG6QYAQhLNyzxdNQ0A1FfW4= -cloud.google.com/go/dataflow v0.11.1/go.mod h1:3s6y/h5Qz7uuxTmKJKBifkYZ3zs63jS+6VGtSu8Cf7Y= +cloud.google.com/go/dataform v0.10.3 h1:ZpGkZV8OyhUhvN/tfLffU2ki5ERTtqOunkIaiVAhmw0= +cloud.google.com/go/dataform v0.10.3/go.mod h1:8SruzxHYCxtvG53gXqDZvZCx12BlsUchuV/JQFtyTCw= +cloud.google.com/go/dataform v0.12.1/go.mod h1:atGS8ReRjfNDUQib0X/o/7Gi2bqHI2G7/J86LKiGimE= cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= @@ -631,9 +634,6 @@ cloud.google.com/go/dataform v0.8.3/go.mod h1:8nI/tvv5Fso0drO3pEjtowz58lodx8MVkd cloud.google.com/go/dataform v0.9.1/go.mod h1:pWTg+zGQ7i16pyn0bS1ruqIE91SdL2FDMvEYu/8oQxs= cloud.google.com/go/dataform v0.9.2 h1:5e4eqGrd0iDTCg4Q+VlAao5j2naKAA7xRurNtwmUknU= cloud.google.com/go/dataform v0.9.2/go.mod h1:S8cQUwPNWXo7m/g3DhWHsLBoufRNn9EgFrMgne2j7cI= -cloud.google.com/go/dataform v0.10.3 h1:ZpGkZV8OyhUhvN/tfLffU2ki5ERTtqOunkIaiVAhmw0= -cloud.google.com/go/dataform v0.10.3/go.mod h1:8SruzxHYCxtvG53gXqDZvZCx12BlsUchuV/JQFtyTCw= -cloud.google.com/go/dataform v0.12.1/go.mod h1:atGS8ReRjfNDUQib0X/o/7Gi2bqHI2G7/J86LKiGimE= cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= @@ -658,13 +658,6 @@ cloud.google.com/go/datalabeling v0.8.5/go.mod h1:IABB2lxQnkdUbMnQaOl2prCOfms20m cloud.google.com/go/datalabeling v0.9.3 h1:PqoA3gnOWaLcHCnqoZe4jh3jmiv6+Z7W2xUUkw/j4jE= cloud.google.com/go/datalabeling v0.9.3/go.mod h1:3LDFUgOx+EuNUzDyjU7VElO8L+b5LeaZEFA/ZU1O1XU= cloud.google.com/go/datalabeling v0.9.7/go.mod h1:EEUVn+wNn3jl19P2S13FqE1s9LsKzRsPuuMRq2CMsOk= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= -cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= -cloud.google.com/go/dataplex v1.8.1/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= -cloud.google.com/go/dataplex v1.9.0/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= -cloud.google.com/go/dataplex v1.9.1/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= cloud.google.com/go/dataplex v1.10.1/go.mod h1:1MzmBv8FvjYfc7vDdxhnLFNskikkB+3vl475/XdCDhs= cloud.google.com/go/dataplex v1.10.2/go.mod h1:xdC8URdTrCrZMW6keY779ZT1cTOfV8KEPNsw+LTRT1Y= cloud.google.com/go/dataplex v1.11.1/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= @@ -677,11 +670,21 @@ cloud.google.com/go/dataplex v1.14.2/go.mod h1:0oGOSFlEKef1cQeAHXy4GZPB/Ife0fz/P cloud.google.com/go/dataplex v1.22.0 h1:j4hD6opb+gq9CJNPFIlIggoW8Kjymg8Wmy2mdHmQoiw= cloud.google.com/go/dataplex v1.22.0/go.mod h1:g166QMCGHvwc3qlTG4p34n+lHwu7JFfaNpMfI2uO7b8= cloud.google.com/go/dataplex v1.28.0/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataplex v1.8.1/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= +cloud.google.com/go/dataplex v1.9.0/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= +cloud.google.com/go/dataplex v1.9.1/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= cloud.google.com/go/dataproc/v2 v2.0.1/go.mod h1:7Ez3KRHdFGcfY7GcevBbvozX+zyWGcwLJvvAMwCaoZ4= +cloud.google.com/go/dataproc/v2 v2.11.0 h1:6aRpyoRfNOP+r2+pGb7HeHtF+SYQID8kzztfHuK0plk= +cloud.google.com/go/dataproc/v2 v2.11.0/go.mod h1:9vgGrn57ra7KBqz+B2KD+ltzEXvnHAUClFgq/ryU99g= +cloud.google.com/go/dataproc/v2 v2.15.0/go.mod h1:tSdkodShfzrrUNPDVEL6MdH9/mIEvp/Z9s9PBdbsZg8= cloud.google.com/go/dataproc/v2 v2.2.0/go.mod h1:lZR7AQtwZPvmINx5J87DSOOpTfof9LVZju6/Qo4lmcY= cloud.google.com/go/dataproc/v2 v2.2.1/go.mod h1:QdAJLaBjh+l4PVlVZcmrmhGccosY/omC1qwfQ61Zv/o= cloud.google.com/go/dataproc/v2 v2.2.2/go.mod h1:aocQywVmQVF4i8CL740rNI/ZRpsaaC1Wh2++BJ7HEJ4= @@ -689,9 +692,6 @@ cloud.google.com/go/dataproc/v2 v2.2.3/go.mod h1:G5R6GBc9r36SXv/RtZIVfB8SipI+xVn cloud.google.com/go/dataproc/v2 v2.3.0/go.mod h1:G5R6GBc9r36SXv/RtZIVfB8SipI+xVn0bX5SxUzVYbY= cloud.google.com/go/dataproc/v2 v2.4.0 h1:/u81Fd+BvCLp+xjctI1DiWVJn6cn9/s3Akc8xPH02yk= cloud.google.com/go/dataproc/v2 v2.4.0/go.mod h1:3B1Ht2aRB8VZIteGxQS/iNSJGzt9+CA0WGnDVMEm7Z4= -cloud.google.com/go/dataproc/v2 v2.11.0 h1:6aRpyoRfNOP+r2+pGb7HeHtF+SYQID8kzztfHuK0plk= -cloud.google.com/go/dataproc/v2 v2.11.0/go.mod h1:9vgGrn57ra7KBqz+B2KD+ltzEXvnHAUClFgq/ryU99g= -cloud.google.com/go/dataproc/v2 v2.15.0/go.mod h1:tSdkodShfzrrUNPDVEL6MdH9/mIEvp/Z9s9PBdbsZg8= cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= @@ -717,13 +717,6 @@ cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWW cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= cloud.google.com/go/datastore v1.21.0/go.mod h1:9l+KyAHO+YVVcdBbNQZJu8svF17Nw5sMKuFR0LYf1nY= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= -cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= -cloud.google.com/go/datastream v1.9.1/go.mod h1:hqnmr8kdUBmrnk65k5wNRoHSCYksvpdZIcZIEl8h43Q= cloud.google.com/go/datastream v1.10.0/go.mod h1:hqnmr8kdUBmrnk65k5wNRoHSCYksvpdZIcZIEl8h43Q= cloud.google.com/go/datastream v1.10.1/go.mod h1:7ngSYwnw95YFyTd5tOGBxHlOZiL+OtpjheqU7t2/s/c= cloud.google.com/go/datastream v1.10.2/go.mod h1:W42TFgKAs/om6x/CdXX5E4oiAsKlH+e8MTGy81zdYt0= @@ -733,10 +726,13 @@ cloud.google.com/go/datastream v1.10.4/go.mod h1:7kRxPdxZxhPg3MFeCSulmAJnil8NJGG cloud.google.com/go/datastream v1.13.0 h1:C5AeEdze55feJVb17a40QmlnyH/aMhn/uf3Go3hIqPA= cloud.google.com/go/datastream v1.13.0/go.mod h1:GrL2+KC8mV4GjbVG43Syo5yyDXp3EH+t6N2HnZb1GOQ= cloud.google.com/go/datastream v1.15.1/go.mod h1:aV1Grr9LFon0YvqryE5/gF1XAhcau2uxN2OvQJPpqRw= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= -cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/datastream v1.9.1/go.mod h1:hqnmr8kdUBmrnk65k5wNRoHSCYksvpdZIcZIEl8h43Q= cloud.google.com/go/deploy v1.11.0/go.mod h1:tKuSUV5pXbn67KiubiUNUejqLs4f5cxxiCNCeyl0F2g= cloud.google.com/go/deploy v1.13.0/go.mod h1:tKuSUV5pXbn67KiubiUNUejqLs4f5cxxiCNCeyl0F2g= cloud.google.com/go/deploy v1.13.1/go.mod h1:8jeadyLkH9qu9xgO3hVWw8jVr29N1mnW42gRJT8GY6g= @@ -750,6 +746,10 @@ cloud.google.com/go/deploy v1.17.1/go.mod h1:SXQyfsXrk0fBmgBHRzBjQbZhMfKZ3hMQBw5 cloud.google.com/go/deploy v1.26.2 h1:1c2Cd3jdb0mrKHHfyzSQ5DRmxgYd07tIZZzuMNrwDxU= cloud.google.com/go/deploy v1.26.2/go.mod h1:XpS3sG/ivkXCfzbzJXY9DXTeCJ5r68gIyeOgVGxGNEs= cloud.google.com/go/deploy v1.27.3/go.mod h1:7LFIYYTSSdljYRqY3n+JSmIFdD4lv6aMD5xg0crB5iw= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= @@ -774,9 +774,6 @@ cloud.google.com/go/dialogflow v1.49.0/go.mod h1:dhVrXKETtdPlpPhE7+2/k4Z8FRNUp6k cloud.google.com/go/dialogflow v1.66.0 h1:/kfpZw20/3v4sC8czEIuvn3Bu3qOne5aHDYlRYHbu18= cloud.google.com/go/dialogflow v1.66.0/go.mod h1:BPiRTnnXP/tHLot5h/U62Xcp+i6ekRj/bq6uq88p+Lw= cloud.google.com/go/dialogflow v1.71.0/go.mod h1:mP4XrpgDvPYBP+cdLxFC1WJJlkwuy0H8L1Lada9No/M= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= cloud.google.com/go/dlp v1.10.1/go.mod h1:IM8BWz1iJd8njcNcG0+Kyd9OPnqnRNkDV8j42VT5KOI= cloud.google.com/go/dlp v1.10.2/go.mod h1:ZbdKIhcnyhILgccwVDzkwqybthh7+MplGC3kZVZsIOQ= cloud.google.com/go/dlp v1.10.3/go.mod h1:iUaTc/ln8I+QT6Ai5vmuwfw8fqTk2kaz0FvCwhLCom0= @@ -786,9 +783,9 @@ cloud.google.com/go/dlp v1.11.2/go.mod h1:9Czi+8Y/FegpWzgSfkRlyz+jwW6Te9Rv26P3Uf cloud.google.com/go/dlp v1.21.0 h1:9kz7+gaB/0gBZsDUnNT1asDihNZSrRFSeUTBcBdUAkk= cloud.google.com/go/dlp v1.21.0/go.mod h1:Y9HOVtPoArpL9sI1O33aN/vK9QRwDERU9PEJJfM8DvE= cloud.google.com/go/dlp v1.27.0/go.mod h1:PY4DMzV7lqRC5JvpxL05fXNeL8dknxYpFp4WjxmE22M= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= @@ -807,6 +804,12 @@ cloud.google.com/go/documentai v1.25.0/go.mod h1:ftLnzw5VcXkLItp6pw1mFic91tMRyfv cloud.google.com/go/documentai v1.35.2 h1:hswVobCWUTXtmn+4QqUIVkai7sDOe0QS2KB3IpqLkik= cloud.google.com/go/documentai v1.35.2/go.mod h1:oh/0YXosgEq3hVhyH4ZQ7VNXPaveRO4eLVM3tBSZOsI= cloud.google.com/go/documentai v1.39.0/go.mod h1:KmlLO93F7GRU8dENXRxvt+7V8o7eCG6Y6WDitKbcYJs= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/domains v0.10.3 h1:wnqN5YwMrtLSjn+HB2sChgmZ6iocOta4Q41giQsiRjY= +cloud.google.com/go/domains v0.10.3/go.mod h1:m7sLe18p0PQab56bVH3JATYOJqyRHhmbye6gz7isC7o= +cloud.google.com/go/domains v0.10.7/go.mod h1:T3WG/QUAO/52z4tUPooKS8AY7yXaFxPYn1V3F0/JbNQ= cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= @@ -816,9 +819,6 @@ cloud.google.com/go/domains v0.9.3/go.mod h1:29k66YNDLDY9LCFKpGFeh6Nj9r62ZKm5EsU cloud.google.com/go/domains v0.9.4/go.mod h1:27jmJGShuXYdUNjyDG0SodTfT5RwLi7xmH334Gvi3fY= cloud.google.com/go/domains v0.9.5 h1:Mml/R6s3vQQvFPpi/9oX3O5dRirgjyJ8cksK8N19Y7g= cloud.google.com/go/domains v0.9.5/go.mod h1:dBzlxgepazdFhvG7u23XMhmMKBjrkoUNaw0A8AQB55Y= -cloud.google.com/go/domains v0.10.3 h1:wnqN5YwMrtLSjn+HB2sChgmZ6iocOta4Q41giQsiRjY= -cloud.google.com/go/domains v0.10.3/go.mod h1:m7sLe18p0PQab56bVH3JATYOJqyRHhmbye6gz7isC7o= -cloud.google.com/go/domains v0.10.7/go.mod h1:T3WG/QUAO/52z4tUPooKS8AY7yXaFxPYn1V3F0/JbNQ= cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= @@ -848,8 +848,6 @@ cloud.google.com/go/essentialcontacts v1.6.6/go.mod h1:XbqHJGaiH0v2UvtuucfOzFXN+ cloud.google.com/go/essentialcontacts v1.7.3 h1:Paw495vxVyKuAgcQ2NQk09iRZBhPYRytknydEnvzcv4= cloud.google.com/go/essentialcontacts v1.7.3/go.mod h1:uimfZgDbhWNCmBpwUUPHe4vcMY2azsq/axC9f7vZFKI= cloud.google.com/go/essentialcontacts v1.7.7/go.mod h1:ytycWAEn/aKUMRKQPMVgMrAtphEMgjbzL8vFwM3tqXs= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= cloud.google.com/go/eventarc v1.12.1/go.mod h1:mAFCW6lukH5+IZjkvrEss+jmt2kOdYlN8aMx3sRJiAI= @@ -862,6 +860,9 @@ cloud.google.com/go/eventarc v1.13.4/go.mod h1:zV5sFVoAa9orc/52Q+OuYUG9xL2IIZTbb cloud.google.com/go/eventarc v1.15.1 h1:RMymT7R87LaxKugOKwooOoheWXUm1NMeOfh3CVU9g54= cloud.google.com/go/eventarc v1.15.1/go.mod h1:K2luolBpwaVOujZQyx6wdG4n2Xum4t0q1cMBmY1xVyI= cloud.google.com/go/eventarc v1.17.0/go.mod h1:wB3NTIQ+l4QPirJiTMeU+YpSc5+iyoDYWV4n2/Vmh78= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/filestore v1.10.3/go.mod h1:94ZGyLTx9j+aWKozPQ6Wbq1DuImie/L/HIdGMshtwac= cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= @@ -875,9 +876,7 @@ cloud.google.com/go/filestore v1.8.1 h1:X5G4y/vrUo1B8Nsz93qSWTMAcM8LXbGUldq33Odc cloud.google.com/go/filestore v1.8.1/go.mod h1:MbN9KcaM47DRTIuLfQhJEsjaocVebNtNQhSLhKCF5GM= cloud.google.com/go/filestore v1.9.3 h1:vTXQI5qYKZ8dmCyHN+zVfaMyXCYbyZNM0CkPzpPUn7Q= cloud.google.com/go/filestore v1.9.3/go.mod h1:Me0ZRT5JngT/aZPIKpIK6N4JGMzrFHRtGHd9ayUS4R4= -cloud.google.com/go/filestore v1.10.3/go.mod h1:94ZGyLTx9j+aWKozPQ6Wbq1DuImie/L/HIdGMshtwac= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= cloud.google.com/go/firestore v1.11.0/go.mod h1:b38dKhgzlmNNGTNZZwe7ZRFEuRab1Hay3/DBsIGKKy4= cloud.google.com/go/firestore v1.12.0/go.mod h1:b38dKhgzlmNNGTNZZwe7ZRFEuRab1Hay3/DBsIGKKy4= cloud.google.com/go/firestore v1.13.0/go.mod h1:QojqqOh8IntInDUSTAh0c8ZsPYAr68Ma8c5DWOy8xb8= @@ -886,10 +885,7 @@ cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LR cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/firestore v1.20.0/go.mod h1:jqu4yKdBmDN5srneWzx3HlKrHFWFdlkgjgQ6BKIOFQo= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= @@ -902,12 +898,16 @@ cloud.google.com/go/functions v1.16.0/go.mod h1:nbNpfAG7SG7Duw/o1iZ6ohvL7mc6MapW cloud.google.com/go/functions v1.19.3 h1:V0vCHSgFTUqKn57+PUXp1UfQY0/aMkveAw7wXeM3Lq0= cloud.google.com/go/functions v1.19.3/go.mod h1:nOZ34tGWMmwfiSJjoH/16+Ko5106x+1Iji29wzrBeOo= cloud.google.com/go/functions v1.19.7/go.mod h1:xbcKfS7GoIcaXr2FSwmtn9NXal1JR4TV6iYZlgXffwA= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/gaming v1.10.1/go.mod h1:XQQvtfP8Rb9Rxnxm5wFVpAp9zCQkJi2bLIb7iHGwB3s= cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= -cloud.google.com/go/gaming v1.10.1/go.mod h1:XQQvtfP8Rb9Rxnxm5wFVpAp9zCQkJi2bLIb7iHGwB3s= cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= @@ -921,6 +921,9 @@ cloud.google.com/go/gkebackup v1.3.5/go.mod h1:KJ77KkNN7Wm1LdMopOelV6OodM01pMuK2 cloud.google.com/go/gkebackup v1.6.3 h1:djdExe/QgoKdp1gnIO1G5BoO1o/yGQOQJJEZ4QKTEXQ= cloud.google.com/go/gkebackup v1.6.3/go.mod h1:JJzGsA8/suXpTDtqI7n9RZW97PXa2CIp+n8aRC/y57k= cloud.google.com/go/gkebackup v1.8.1/go.mod h1:GAaAl+O5D9uISH5MnClUop2esQW4pDa2qe/95A4l7YQ= +cloud.google.com/go/gkeconnect v0.12.1 h1:YVpR0vlHSP/wD74PXEbKua4Aamud+wiYm4TiewNjD3M= +cloud.google.com/go/gkeconnect v0.12.1/go.mod h1:L1dhGY8LjINmWfR30vneozonQKRSIi5DWGIHjOqo58A= +cloud.google.com/go/gkeconnect v0.12.5/go.mod h1:wMD2RXcsAWlkREZWJDVeDV70PYka1iEb9stFmgpw+5o= cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= @@ -930,10 +933,6 @@ cloud.google.com/go/gkeconnect v0.8.3/go.mod h1:i9GDTrfzBSUZGCe98qSu1B8YB8qfapT5 cloud.google.com/go/gkeconnect v0.8.4/go.mod h1:84hZz4UMlDCKl8ifVW8layK4WHlMAFeq8vbzjU0yJkw= cloud.google.com/go/gkeconnect v0.8.5 h1:17d+ZSSXKqG/RwZCq3oFMIWLPI8Zw3b8+a9/BEVlwH0= cloud.google.com/go/gkeconnect v0.8.5/go.mod h1:LC/rS7+CuJ5fgIbXv8tCD/mdfnlAadTaUufgOkmijuk= -cloud.google.com/go/gkeconnect v0.12.1 h1:YVpR0vlHSP/wD74PXEbKua4Aamud+wiYm4TiewNjD3M= -cloud.google.com/go/gkeconnect v0.12.1/go.mod h1:L1dhGY8LjINmWfR30vneozonQKRSIi5DWGIHjOqo58A= -cloud.google.com/go/gkeconnect v0.12.5/go.mod h1:wMD2RXcsAWlkREZWJDVeDV70PYka1iEb9stFmgpw+5o= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= @@ -946,6 +945,7 @@ cloud.google.com/go/gkehub v0.14.5/go.mod h1:6bzqxM+a+vEH/h8W8ec4OJl4r36laxTs3A/ cloud.google.com/go/gkehub v0.15.3 h1:yZ6lNJ9rNIoQmWrG14dB3+BFjS/EIRBf7Bo6jc5QWlE= cloud.google.com/go/gkehub v0.15.3/go.mod h1:nzFT/Q+4HdQES/F+FP1QACEEWR9Hd+Sh00qgiH636cU= cloud.google.com/go/gkehub v0.16.0/go.mod h1:ADp27Ucor8v81wY+x/5pOxTorxkPj/xswH3AUpN62GU= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= @@ -962,11 +962,11 @@ cloud.google.com/go/gkemulticloud v1.5.1/go.mod h1:OdmhfSPXuJ0Kn9dQ2I3Ou7XZ3QK8c cloud.google.com/go/gkemulticloud v1.5.4/go.mod h1:7l9+6Tp4jySSGj4PStO8CE6RrHFdcRARK4ScReHX1bU= cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/grafeas v0.3.0/go.mod h1:P7hgN24EyONOTMyeJH6DxG4zD7fwiYa5Q6GUgyFSOU8= -cloud.google.com/go/grafeas v0.3.4 h1:D4x32R/cHX3MTofKwirz015uEdVk4uAxvZkZCZkOrF4= -cloud.google.com/go/grafeas v0.3.4/go.mod h1:A5m316hcG+AulafjAbPKXBO/+I5itU4LOdKO2R/uDIc= cloud.google.com/go/grafeas v0.3.11 h1:CobnwnyeY1j1Defi5vbEircI+jfrk3ci5m004ZjiFP4= cloud.google.com/go/grafeas v0.3.11/go.mod h1:dcQyG2+T4tBgG0MvJAh7g2wl/xHV2w+RZIqivwuLjNg= cloud.google.com/go/grafeas v0.3.16/go.mod h1:I/yrRMOEsLasrmZXQzmDXwrJ3ZPn3dQWLaWt4lXmYvE= +cloud.google.com/go/grafeas v0.3.4 h1:D4x32R/cHX3MTofKwirz015uEdVk4uAxvZkZCZkOrF4= +cloud.google.com/go/grafeas v0.3.4/go.mod h1:A5m316hcG+AulafjAbPKXBO/+I5itU4LOdKO2R/uDIc= cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= @@ -980,17 +980,18 @@ cloud.google.com/go/gsuiteaddons v1.7.4 h1:f3eMYsCDdg2AeldIPdKmBRxN1WoiTpE3RvX5o cloud.google.com/go/gsuiteaddons v1.7.4/go.mod h1:gpE2RUok+HUhuK7RPE/fCOEgnTffS0lCHRaAZLxAMeE= cloud.google.com/go/gsuiteaddons v1.7.8/go.mod h1:DBKNHH4YXAdd/rd6zVvtOGAJNGo0ekOh+nIjTUDEJ5U= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= +cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg= cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE= cloud.google.com/go/iam v1.1.4/go.mod h1:l/rg8l1AaA+VFMho/HYx2Vv6xinPSLMF8qfhRPIZ0L8= @@ -998,7 +999,6 @@ cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXE cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg= cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q= cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= @@ -1010,6 +1010,9 @@ cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdRe cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/iap v1.10.3 h1:OWNYFHPyIBNHEAEFdVKOltYWe0g3izSrpFJW6Iidovk= +cloud.google.com/go/iap v1.10.3/go.mod h1:xKgn7bocMuCFYhzRizRWP635E2LNPnIXT7DW0TlyPJ8= +cloud.google.com/go/iap v1.11.3/go.mod h1:+gXO0ClH62k2LVlfhHzrpiHQNyINlEVmGAE3+DB4ShU= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= @@ -1022,9 +1025,6 @@ cloud.google.com/go/iap v1.9.2/go.mod h1:GwDTOs047PPSnwRD0Us5FKf4WDRcVvHg1q9WVkK cloud.google.com/go/iap v1.9.3/go.mod h1:DTdutSZBqkkOm2HEOTBzhZxh2mwwxshfD/h3yofAiCw= cloud.google.com/go/iap v1.9.4 h1:94zirc2r4t6KzhAMW0R6Dme005eTP6yf7g6vN4IhRrA= cloud.google.com/go/iap v1.9.4/go.mod h1:vO4mSq0xNf/Pu6E5paORLASBwEmphXEjgCFg7aeNu1w= -cloud.google.com/go/iap v1.10.3 h1:OWNYFHPyIBNHEAEFdVKOltYWe0g3izSrpFJW6Iidovk= -cloud.google.com/go/iap v1.10.3/go.mod h1:xKgn7bocMuCFYhzRizRWP635E2LNPnIXT7DW0TlyPJ8= -cloud.google.com/go/iap v1.11.3/go.mod h1:+gXO0ClH62k2LVlfhHzrpiHQNyINlEVmGAE3+DB4ShU= cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= @@ -1050,11 +1050,6 @@ cloud.google.com/go/iot v1.7.5/go.mod h1:nq3/sqTz3HGaWJi1xNiX7F41ThOzpud67vwk0Ys cloud.google.com/go/iot v1.8.3 h1:aPWYQ+A1NX6ou/5U0nFAiXWdVT8OBxZYVZt2fBl2gWA= cloud.google.com/go/iot v1.8.3/go.mod h1:dYhrZh+vUxIQ9m3uajyKRSW7moF/n0rYmA2PhYAkMFE= cloud.google.com/go/iot v1.8.7/go.mod h1:HvVcypV8LPv1yTXSLCNK+YCtqGHhq+p0F3BXETfpN+U= -cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= -cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= cloud.google.com/go/kms v1.11.0/go.mod h1:hwdiYC0xjnWsKQQCQQmIQnS9asjYVSK6jtXm+zFqXLM= @@ -1073,11 +1068,11 @@ cloud.google.com/go/kms v1.21.0 h1:x3EeWKuYwdlo2HLse/876ZrKjk2L5r7Uexfm8+p6mSI= cloud.google.com/go/kms v1.21.0/go.mod h1:zoFXMhVVK7lQ3JC9xmhHMoQhnjEDZFoLAr5YMwzBLtk= cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/language v1.10.1/go.mod h1:CPp94nsdVNiQEt1CNjF5WkTcisLiHPyIbMhvR8H2AW0= cloud.google.com/go/language v1.11.0/go.mod h1:uDx+pFDdAKTY8ehpWbiXyQdz8tDSYLJbQcXsCkjYyvQ= cloud.google.com/go/language v1.11.1/go.mod h1:Xyid9MG9WOX3utvDbpX7j3tXDmmDooMyMDqgUVpH17U= @@ -1088,6 +1083,14 @@ cloud.google.com/go/language v1.12.3/go.mod h1:evFX9wECX6mksEva8RbRnr/4wi/vKGYnA cloud.google.com/go/language v1.14.3 h1:8hmFMiS3wjjj3TX/U1zZYTgzwZoUjDbo9PaqcYEmuB4= cloud.google.com/go/language v1.14.3/go.mod h1:hjamj+KH//QzF561ZuU2J+82DdMlFUjmiGVWpovGGSA= cloud.google.com/go/language v1.14.6/go.mod h1:7y3J9OexQsfkWNGCxhT+7lb64pa60e12ZCoWDOHxJ1M= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.10.3 h1:Z05C+Ui953f0EQx9hJ1la6+QQl8ADrIs3iNwP5Elkpg= +cloud.google.com/go/lifesciences v0.10.3/go.mod h1:hnUUFht+KcZcliixAg+iOh88FUwAzDQQt5tWd7iIpNg= +cloud.google.com/go/lifesciences v0.10.7/go.mod h1:v3AbTki9iWttEls/Wf4ag3EqeLRHofploOcpsLnu7iY= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= @@ -1097,25 +1100,23 @@ cloud.google.com/go/lifesciences v0.9.3/go.mod h1:gNGBOJV80IWZdkd+xz4GQj4mbqaz73 cloud.google.com/go/lifesciences v0.9.4/go.mod h1:bhm64duKhMi7s9jR9WYJYvjAFJwRqNj+Nia7hF0Z7JA= cloud.google.com/go/lifesciences v0.9.5 h1:gXvN70m2p+4zgJFzaz6gMKaxTuF9WJ0USYoMLWAOm8g= cloud.google.com/go/lifesciences v0.9.5/go.mod h1:OdBm0n7C0Osh5yZB7j9BXyrMnTRGBJIZonUMxo5CzPw= -cloud.google.com/go/lifesciences v0.10.3 h1:Z05C+Ui953f0EQx9hJ1la6+QQl8ADrIs3iNwP5Elkpg= -cloud.google.com/go/lifesciences v0.10.3/go.mod h1:hnUUFht+KcZcliixAg+iOh88FUwAzDQQt5tWd7iIpNg= -cloud.google.com/go/lifesciences v0.10.7/go.mod h1:v3AbTki9iWttEls/Wf4ag3EqeLRHofploOcpsLnu7iY= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI= -cloud.google.com/go/logging v1.9.0 h1:iEIOXFO9EmSiTjDmfpbRjOxECO7R8C7b8IXUGOj7xZw= -cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE= cloud.google.com/go/logging v1.10.0/go.mod h1:EHOwcxlltJrYGqMGfghSet736KR3hX1MAj614mrMk9I= cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI= +cloud.google.com/go/logging v1.9.0 h1:iEIOXFO9EmSiTjDmfpbRjOxECO7R8C7b8IXUGOj7xZw= +cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE= cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= cloud.google.com/go/longrunning v0.5.0/go.mod h1:0JNuqRShmscVAhIACGtskSAWtqtOoPkwP0YF1oVEchc= cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= +cloud.google.com/go/longrunning v0.5.12/go.mod h1:S5hMV8CDJ6r50t2ubVJSKQVv5u0rmik5//KgLO3k4lU= cloud.google.com/go/longrunning v0.5.2/go.mod h1:nqo6DQbNV2pXhGDbDMoN2bWz68MjZUzqv2YttZiveCs= cloud.google.com/go/longrunning v0.5.3/go.mod h1:y/0ga59EYu58J6SHmmQOvekvND2qODbu8ywBBW7EK7Y= cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= @@ -1124,7 +1125,6 @@ cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDn cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c= -cloud.google.com/go/longrunning v0.5.12/go.mod h1:S5hMV8CDJ6r50t2ubVJSKQVv5u0rmik5//KgLO3k4lU= cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts= cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= @@ -1150,6 +1150,9 @@ cloud.google.com/go/managedidentities v1.7.7/go.mod h1:nwNlMxtBo2YJMvsKXRtAD1bL4 cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/maps v1.19.0 h1:deVm1ZFyCrUwxG11CdvtBz350VG5JUQ/LHTLnQrBgrM= +cloud.google.com/go/maps v1.19.0/go.mod h1:goHUXrmzoZvQjUVd0KGhH8t3AYRm17P8b+fsyR1UAmQ= +cloud.google.com/go/maps v1.26.0/go.mod h1:+auempdONAP8emtm48aCfNo1ZC+3CJniRA1h8J4u7bY= cloud.google.com/go/maps v1.3.0/go.mod h1:6mWTUv+WhnOwAgjVsSW2QPPECmW+s3PcRyOa9vgG/5s= cloud.google.com/go/maps v1.4.0/go.mod h1:6mWTUv+WhnOwAgjVsSW2QPPECmW+s3PcRyOa9vgG/5s= cloud.google.com/go/maps v1.4.1/go.mod h1:BxSa0BnW1g2U2gNdbq5zikLlHUuHW0GFWh7sgML2kIY= @@ -1159,9 +1162,6 @@ cloud.google.com/go/maps v1.6.2/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJn cloud.google.com/go/maps v1.6.3/go.mod h1:VGAn809ADswi1ASofL5lveOHPnE6Rk/SFTTBx1yuOLw= cloud.google.com/go/maps v1.6.4 h1:EVCZAiDvog9So46460BGbCasPhi613exoaQbpilMVlk= cloud.google.com/go/maps v1.6.4/go.mod h1:rhjqRy8NWmDJ53saCfsXQ0LKwBHfi6OSh5wkq6BaMhI= -cloud.google.com/go/maps v1.19.0 h1:deVm1ZFyCrUwxG11CdvtBz350VG5JUQ/LHTLnQrBgrM= -cloud.google.com/go/maps v1.19.0/go.mod h1:goHUXrmzoZvQjUVd0KGhH8t3AYRm17P8b+fsyR1UAmQ= -cloud.google.com/go/maps v1.26.0/go.mod h1:+auempdONAP8emtm48aCfNo1ZC+3CJniRA1h8J4u7bY= cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= @@ -1174,11 +1174,6 @@ cloud.google.com/go/mediatranslation v0.8.5/go.mod h1:y7kTHYIPCIfgyLbKncgqouXJtL cloud.google.com/go/mediatranslation v0.9.3 h1:nRBjeaMLipw05Br+qDAlSCcCQAAlat4mvpafztbEVgc= cloud.google.com/go/mediatranslation v0.9.3/go.mod h1:KTrFV0dh7duYKDjmuzjM++2Wn6yw/I5sjZQVV5k3BAA= cloud.google.com/go/mediatranslation v0.9.7/go.mod h1:mz3v6PR7+Fd/1bYrRxNFGnd+p4wqdc/fyutqC5QHctw= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= cloud.google.com/go/memcache v1.10.1/go.mod h1:47YRQIarv4I3QS5+hoETgKO40InqzLP6kpNLvyXuyaA= cloud.google.com/go/memcache v1.10.2/go.mod h1:f9ZzJHLBrmd4BkguIAa/l/Vle6uTHzHokdnzSWOdQ6A= cloud.google.com/go/memcache v1.10.3/go.mod h1:6z89A41MT2DVAW0P4iIRdu5cmRTsbsFn4cyiIx8gbwo= @@ -1188,10 +1183,11 @@ cloud.google.com/go/memcache v1.10.5/go.mod h1:/FcblbNd0FdMsx4natdj+2GWzTq+cjZvM cloud.google.com/go/memcache v1.11.3 h1:XH/qT3GbbSH//R0JTqR77lRpBxaa0N9sHgAzfwbTrv0= cloud.google.com/go/memcache v1.11.3/go.mod h1:UeWI9cmY7hvjU1EU6dwJcQb6EFG4GaM3KNXOO2OFsbI= cloud.google.com/go/memcache v1.11.7/go.mod h1:AU1jYlUqCihxapcJ1GGMtlMWDVhzjbfUWBXqsXa4rBg= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= cloud.google.com/go/metastore v1.11.1/go.mod h1:uZuSo80U3Wd4zi6C22ZZliOUJ3XeM/MlYi/z5OAOWRA= cloud.google.com/go/metastore v1.12.0/go.mod h1:uZuSo80U3Wd4zi6C22ZZliOUJ3XeM/MlYi/z5OAOWRA= @@ -1204,8 +1200,10 @@ cloud.google.com/go/metastore v1.13.4/go.mod h1:FMv9bvPInEfX9Ac1cVcRXp8EBBQnBcqH cloud.google.com/go/metastore v1.14.3 h1:jDqeCw6NGDRAPT9+2Y/EjnWAB0BfCcUfmPLOyhB0eHs= cloud.google.com/go/metastore v1.14.3/go.mod h1:HlbGVOvg0ubBLVFRk3Otj3gtuzInuzO/TImOBwsKlG4= cloud.google.com/go/metastore v1.14.8/go.mod h1:h1XI2LpD4ohJhQYn9TwXqKb5sVt6KSo47ft96SiFF1s= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= cloud.google.com/go/monitoring v1.15.1/go.mod h1:lADlSAlFdbqQuwwpaImhsJXu1QSdd3ojypXrFSMr2rM= @@ -1227,10 +1225,8 @@ cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys904 cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= cloud.google.com/go/networkconnectivity v1.12.1/go.mod h1:PelxSWYM7Sh9/guf8CFhi6vIqf19Ir/sbfZRUwXh92E= @@ -1244,6 +1240,13 @@ cloud.google.com/go/networkconnectivity v1.14.4/go.mod h1:PU12q++/IMnDJAB+3r+tJt cloud.google.com/go/networkconnectivity v1.16.1 h1:YsVhG71ZC4FkqCP2oCI55x/JeGFyd7738Lt8iNTrzJw= cloud.google.com/go/networkconnectivity v1.16.1/go.mod h1:GBC1iOLkblcnhcnfRV92j4KzqGBrEI6tT7LP52nZCTk= cloud.google.com/go/networkconnectivity v1.19.1/go.mod h1:Q5v6uNNNz8BP232uuXM66XgWML9m379xhwv58Y+8Kb0= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkmanagement v1.18.0 h1:oEoFGPYxTBsY47h0zdoE2ojV5aU/541D83UmxfjHWaE= +cloud.google.com/go/networkmanagement v1.18.0/go.mod h1:yTxpAFuvQOOKgL3W7+k2Rp1bSKTxyRcZ5xNHGdHUM6w= +cloud.google.com/go/networkmanagement v1.21.0/go.mod h1:clG/5Yt0wQ57qSH6Yh7oehQYlobHw3F6nb3Pn4ig5hU= cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= @@ -1254,9 +1257,9 @@ cloud.google.com/go/networkmanagement v1.9.2/go.mod h1:iDGvGzAoYRghhp4j2Cji7sF89 cloud.google.com/go/networkmanagement v1.9.3/go.mod h1:y7WMO1bRLaP5h3Obm4tey+NquUvB93Co1oh4wpL+XcU= cloud.google.com/go/networkmanagement v1.9.4 h1:aLV5GcosBNmd6M8+a0ekB0XlLRexv4fvnJJrYnqeBcg= cloud.google.com/go/networkmanagement v1.9.4/go.mod h1:daWJAl0KTFytFL7ar33I6R/oNBH8eEOX/rBNHrC/8TA= -cloud.google.com/go/networkmanagement v1.18.0 h1:oEoFGPYxTBsY47h0zdoE2ojV5aU/541D83UmxfjHWaE= -cloud.google.com/go/networkmanagement v1.18.0/go.mod h1:yTxpAFuvQOOKgL3W7+k2Rp1bSKTxyRcZ5xNHGdHUM6w= -cloud.google.com/go/networkmanagement v1.21.0/go.mod h1:clG/5Yt0wQ57qSH6Yh7oehQYlobHw3F6nb3Pn4ig5hU= +cloud.google.com/go/networksecurity v0.10.3 h1:JLJBFbxc8D7/OS81MyRoKhc2OvnVJxy5VMoQqqAhA7k= +cloud.google.com/go/networksecurity v0.10.3/go.mod h1:G85ABVcPscEgpw+gcu+HUxNZJWjn3yhTqEU7+SsltFM= +cloud.google.com/go/networksecurity v0.10.7/go.mod h1:FgoictpfaJkeBlM1o2m+ngPZi8mgJetbFDH4ws1i2fQ= cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= @@ -1267,16 +1270,6 @@ cloud.google.com/go/networksecurity v0.9.3/go.mod h1:l+C0ynM6P+KV9YjOnx+kk5IZqMS cloud.google.com/go/networksecurity v0.9.4/go.mod h1:E9CeMZ2zDsNBkr8axKSYm8XyTqNhiCHf1JO/Vb8mD1w= cloud.google.com/go/networksecurity v0.9.5 h1:+caSxBTj0E8OYVh/5wElFdjEMO1S/rZtE1152Cepchc= cloud.google.com/go/networksecurity v0.9.5/go.mod h1:KNkjH/RsylSGyyZ8wXpue8xpCEK+bTtvof8SBfIhMG8= -cloud.google.com/go/networksecurity v0.10.3 h1:JLJBFbxc8D7/OS81MyRoKhc2OvnVJxy5VMoQqqAhA7k= -cloud.google.com/go/networksecurity v0.10.3/go.mod h1:G85ABVcPscEgpw+gcu+HUxNZJWjn3yhTqEU7+SsltFM= -cloud.google.com/go/networksecurity v0.10.7/go.mod h1:FgoictpfaJkeBlM1o2m+ngPZi8mgJetbFDH4ws1i2fQ= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= -cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= -cloud.google.com/go/notebooks v1.9.1/go.mod h1:zqG9/gk05JrzgBt4ghLzEepPHNwE5jgPcHZRKhlC1A8= cloud.google.com/go/notebooks v1.10.0/go.mod h1:SOPYMZnttHxqot0SGSFSkRrwE29eqnKPBJFqgWmiK2k= cloud.google.com/go/notebooks v1.10.1/go.mod h1:5PdJc2SgAybE76kFQCWrTfJolCOUQXF97e+gteUUA6A= cloud.google.com/go/notebooks v1.11.1/go.mod h1:V2Zkv8wX9kDCGRJqYoI+bQAaoVeE5kSiz4yYHd2yJwQ= @@ -1286,6 +1279,13 @@ cloud.google.com/go/notebooks v1.11.3/go.mod h1:0wQyI2dQC3AZyQqWnRsp+yA+kY4gC7ZI cloud.google.com/go/notebooks v1.12.3 h1:+9DrGJcZhCu6B2t0JJorekjIUBvg/KvBmXJYGmfvVvA= cloud.google.com/go/notebooks v1.12.3/go.mod h1:I0pMxZct+8Rega2LYrXL8jGAGZgLchSmh8Ksc+0xNyA= cloud.google.com/go/notebooks v1.12.7/go.mod h1:uR9pxAkKmlNloibMr9Q1t8WhIu4P2JeqJs7c064/0Mo= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/notebooks v1.9.1/go.mod h1:zqG9/gk05JrzgBt4ghLzEepPHNwE5jgPcHZRKhlC1A8= cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= @@ -1299,6 +1299,9 @@ cloud.google.com/go/optimization v1.6.3/go.mod h1:8ve3svp3W6NFcAEFr4SfJxrldzhUl4 cloud.google.com/go/optimization v1.7.3 h1:JwQjjoBZJpsoMQe/3mhVBMVZuSdagHg2pGOnwh2Jk+E= cloud.google.com/go/optimization v1.7.3/go.mod h1:GlYFp4Mju0ybK5FlOUtV6zvWC00TIScdbsPyF6Iv144= cloud.google.com/go/optimization v1.7.7/go.mod h1:OY2IAlX23o52qwMAZ0w65wibKuV12a4x6IHDTCq6kcU= +cloud.google.com/go/orchestration v1.11.10/go.mod h1:tz7m1s4wNEvhNNIM3JOMH0lYxBssu9+7si5MCPw/4/0= +cloud.google.com/go/orchestration v1.11.4 h1:SFAsKyqvtS8VFcsq+JgXAeRkrksB9UH+AH7iFamkmlc= +cloud.google.com/go/orchestration v1.11.4/go.mod h1:UKR2JwogaZmDGnAcBgAQgCPn89QMqhXFUCYVhHd31vs= cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= @@ -1308,11 +1311,6 @@ cloud.google.com/go/orchestration v1.8.3/go.mod h1:xhgWAYqlbYjlz2ftbFghdyqENYW+J cloud.google.com/go/orchestration v1.8.4/go.mod h1:d0lywZSVYtIoSZXb0iFjv9SaL13PGyVOKDxqGxEf/qI= cloud.google.com/go/orchestration v1.8.5 h1:YHgWMlrPttIVGItgGfuvO2KM7x+y9ivN/Yk92pMm1a4= cloud.google.com/go/orchestration v1.8.5/go.mod h1:C1J7HesE96Ba8/hZ71ISTV2UAat0bwN+pi85ky38Yq8= -cloud.google.com/go/orchestration v1.11.4 h1:SFAsKyqvtS8VFcsq+JgXAeRkrksB9UH+AH7iFamkmlc= -cloud.google.com/go/orchestration v1.11.4/go.mod h1:UKR2JwogaZmDGnAcBgAQgCPn89QMqhXFUCYVhHd31vs= -cloud.google.com/go/orchestration v1.11.10/go.mod h1:tz7m1s4wNEvhNNIM3JOMH0lYxBssu9+7si5MCPw/4/0= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= cloud.google.com/go/orgpolicy v1.11.0/go.mod h1:2RK748+FtVvnfuynxBzdnyu7sygtoZa1za/0ZfpOs1M= cloud.google.com/go/orgpolicy v1.11.1/go.mod h1:8+E3jQcpZJQliP+zaFfayC2Pg5bmhuLK755wKhIIUCE= @@ -1326,9 +1324,8 @@ cloud.google.com/go/orgpolicy v1.14.2 h1:WFvgmjq/FO5GiXlhebltA9N14KdbLMcgG88ME+S cloud.google.com/go/orgpolicy v1.14.2/go.mod h1:2fTDMT3X048iFKxc6DEgkG+a/gN+68qEgtPrHItKMzo= cloud.google.com/go/orgpolicy v1.15.0/go.mod h1:NTQLwgS8N5cJtdfK55tAnMGtvPSsy95JJhESwYHaJVs= cloud.google.com/go/orgpolicy v1.15.1/go.mod h1:bpvi9YIyU7wCW9WiXL/ZKT7pd2Ovegyr2xENIeRX5q0= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= cloud.google.com/go/osconfig v1.12.0/go.mod h1:8f/PaYzoS3JMVfdfTubkowZYGmAhUCjjwnjqWI7NVBc= @@ -1342,11 +1339,9 @@ cloud.google.com/go/osconfig v1.14.3 h1:cyf1PMK5c2/WOIr5r2lxjH/XBJMA9P4zC8Tm10i0 cloud.google.com/go/osconfig v1.14.3/go.mod h1:9D2MS1Etne18r/mAeW5jtto3toc9H1qu9wLNDG3NvQg= cloud.google.com/go/osconfig v1.15.0/go.mod h1:0nY8bfGKWJB0Ft5bBKd2zMkjT4Uf0rM3NBFrAGUv1Lk= cloud.google.com/go/osconfig v1.15.1/go.mod h1:NegylQQl0+5m+I+4Ey/g3HGeQxKkncQ1q+Il4DZ8PME= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= cloud.google.com/go/oslogin v1.10.1/go.mod h1:x692z7yAue5nE7CsSnoG0aaMbNoRJRXO4sn73R+ZqAs= cloud.google.com/go/oslogin v1.11.0/go.mod h1:8GMTJs4X2nOAUVJiPGqIWVcDaF0eniEto3xlOxaboXE= cloud.google.com/go/oslogin v1.11.1/go.mod h1:OhD2icArCVNUxKqtK0mcSmKL7lgr0LVlQz+v9s1ujTg= @@ -1358,6 +1353,11 @@ cloud.google.com/go/oslogin v1.13.1/go.mod h1:vS8Sr/jR7QvPWpCjNqy6LYZr5Zs1e8ZGW/ cloud.google.com/go/oslogin v1.14.3 h1:yomxnFPk+ye0zd0mJ15nn9fH4Ns7ex4xA3ll+u2q59A= cloud.google.com/go/oslogin v1.14.3/go.mod h1:fDEGODTG/W9ZGUTHTlMh8euXWC1fTcgjJ9Kcxxy14a8= cloud.google.com/go/oslogin v1.14.7/go.mod h1:NB6NqBHfDMwznePdBVX+ILllc1oPCdNSGp5u/WIyndY= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= @@ -1370,6 +1370,13 @@ cloud.google.com/go/phishingprotection v0.8.5/go.mod h1:g1smd68F7mF1hgQPuYn3z8HD cloud.google.com/go/phishingprotection v0.9.3 h1:T5mGFV0ggBKg3qt9myFRiGJu+nIUucuHLAtVpAuQ08I= cloud.google.com/go/phishingprotection v0.9.3/go.mod h1:ylzN9HruB/X7dD50I4sk+FfYzuPx9fm5JWsYI0t7ncc= cloud.google.com/go/phishingprotection v0.9.7/go.mod h1:JTI4HNGyAbWolBoNOoCyCF0e3cqPNrYnlievHU49EwE= +cloud.google.com/go/policytroubleshooter v1.10.1/go.mod h1:5C0rhT3TDZVxAu8813bwmTvd57Phbl8mr9F4ipOsxEs= +cloud.google.com/go/policytroubleshooter v1.10.2/go.mod h1:m4uF3f6LseVEnMV6nknlN2vYGRb+75ylQwJdnOXfnv0= +cloud.google.com/go/policytroubleshooter v1.10.3 h1:c0WOzC6hz964QWNBkyKfna8A2jOIx1zzZa43Gx/P09o= +cloud.google.com/go/policytroubleshooter v1.10.3/go.mod h1:+ZqG3agHT7WPb4EBIRqUv4OyIwRTZvsVDHZ8GlZaoxk= +cloud.google.com/go/policytroubleshooter v1.11.3 h1:ekIWI8JbKkpOfrgH/THGamQE/D16tcVBYJyrkseVcYI= +cloud.google.com/go/policytroubleshooter v1.11.3/go.mod h1:AFHlORqh4AnMC0twc2yPKfzlozp3DO0yo9OfOd9aNOs= +cloud.google.com/go/policytroubleshooter v1.11.7/go.mod h1:JP/aQ+bUkt4Gz6lQXBi/+A/6nyNRZ0Pvxui5Xl9ieyk= cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= @@ -1378,13 +1385,9 @@ cloud.google.com/go/policytroubleshooter v1.7.1/go.mod h1:0NaT5v3Ag1M7U5r0GfDCpU cloud.google.com/go/policytroubleshooter v1.8.0/go.mod h1:tmn5Ir5EToWe384EuboTcVQT7nTag2+DuH3uHmKd1HU= cloud.google.com/go/policytroubleshooter v1.9.0/go.mod h1:+E2Lga7TycpeSTj2FsH4oXxTnrbHJGRlKhVZBLGgU64= cloud.google.com/go/policytroubleshooter v1.9.1/go.mod h1:MYI8i0bCrL8cW+VHN1PoiBTyNZTstCg2WUw2eVC4c4U= -cloud.google.com/go/policytroubleshooter v1.10.1/go.mod h1:5C0rhT3TDZVxAu8813bwmTvd57Phbl8mr9F4ipOsxEs= -cloud.google.com/go/policytroubleshooter v1.10.2/go.mod h1:m4uF3f6LseVEnMV6nknlN2vYGRb+75ylQwJdnOXfnv0= -cloud.google.com/go/policytroubleshooter v1.10.3 h1:c0WOzC6hz964QWNBkyKfna8A2jOIx1zzZa43Gx/P09o= -cloud.google.com/go/policytroubleshooter v1.10.3/go.mod h1:+ZqG3agHT7WPb4EBIRqUv4OyIwRTZvsVDHZ8GlZaoxk= -cloud.google.com/go/policytroubleshooter v1.11.3 h1:ekIWI8JbKkpOfrgH/THGamQE/D16tcVBYJyrkseVcYI= -cloud.google.com/go/policytroubleshooter v1.11.3/go.mod h1:AFHlORqh4AnMC0twc2yPKfzlozp3DO0yo9OfOd9aNOs= -cloud.google.com/go/policytroubleshooter v1.11.7/go.mod h1:JP/aQ+bUkt4Gz6lQXBi/+A/6nyNRZ0Pvxui5Xl9ieyk= +cloud.google.com/go/privatecatalog v0.10.4 h1:fu2LABMi7CgZORQ2oNGbc0hoZ0FTqLkjGqIgAV/Kc7U= +cloud.google.com/go/privatecatalog v0.10.4/go.mod h1:n/vXBT+Wq8B4nSRUJNDsmqla5BYjbVxOlHzS6PjiF+w= +cloud.google.com/go/privatecatalog v0.10.8/go.mod h1:BkLHi+rtAGYBt5DocXLytHhF0n6F03Tegxgty40Y7aA= cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= @@ -1395,17 +1398,14 @@ cloud.google.com/go/privatecatalog v0.9.3/go.mod h1:K5pn2GrVmOPjXz3T26mzwXLcKivf cloud.google.com/go/privatecatalog v0.9.4/go.mod h1:SOjm93f+5hp/U3PqMZAHTtBtluqLygrDrVO8X8tYtG0= cloud.google.com/go/privatecatalog v0.9.5 h1:UZ0assTnATXSggoxUIh61RjTQ4P9zCMk/kEMbn0nMYA= cloud.google.com/go/privatecatalog v0.9.5/go.mod h1:fVWeBOVe7uj2n3kWRGlUQqR/pOd450J9yZoOECcQqJk= -cloud.google.com/go/privatecatalog v0.10.4 h1:fu2LABMi7CgZORQ2oNGbc0hoZ0FTqLkjGqIgAV/Kc7U= -cloud.google.com/go/privatecatalog v0.10.4/go.mod h1:n/vXBT+Wq8B4nSRUJNDsmqla5BYjbVxOlHzS6PjiF+w= -cloud.google.com/go/privatecatalog v0.10.8/go.mod h1:BkLHi+rtAGYBt5DocXLytHhF0n6F03Tegxgty40Y7aA= cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= cloud.google.com/go/pubsub v1.32.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= @@ -1428,7 +1428,10 @@ cloud.google.com/go/pubsublite v1.8.2 h1:jLQozsEVr+c6tOU13vDugtnaBSUy/PD5zK6mhm+ cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.19.4 h1:T5YGzaXwTesHaPDNTAuU3neDwZEnfjce70zufPFUwno= +cloud.google.com/go/recaptchaenterprise/v2 v2.19.4/go.mod h1:WaglfocMJGkqZVdXY/FVB7OhoVRONPS4uXqtNn6HfX0= cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.20.5/go.mod h1:TCHn8+vtwgygBOwwbUJgRi6R9qglIpTeImsWsWDr5Lo= cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= @@ -1443,9 +1446,6 @@ cloud.google.com/go/recaptchaenterprise/v2 v2.8.4/go.mod h1:Dak54rw6lC2gBY8FBznp cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= cloud.google.com/go/recaptchaenterprise/v2 v2.9.2 h1:U3Wfq12X9cVMuTpsWDSURnXF0Z9hSPTHj+xsnXDRLsw= cloud.google.com/go/recaptchaenterprise/v2 v2.9.2/go.mod h1:trwwGkfhCmp05Ll5MSJPXY7yvnO0p4v3orGANAFHAuU= -cloud.google.com/go/recaptchaenterprise/v2 v2.19.4 h1:T5YGzaXwTesHaPDNTAuU3neDwZEnfjce70zufPFUwno= -cloud.google.com/go/recaptchaenterprise/v2 v2.19.4/go.mod h1:WaglfocMJGkqZVdXY/FVB7OhoVRONPS4uXqtNn6HfX0= -cloud.google.com/go/recaptchaenterprise/v2 v2.20.5/go.mod h1:TCHn8+vtwgygBOwwbUJgRi6R9qglIpTeImsWsWDr5Lo= cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= @@ -1458,11 +1458,6 @@ cloud.google.com/go/recommendationengine v0.8.5/go.mod h1:A38rIXHGFvoPvmy6pZLozr cloud.google.com/go/recommendationengine v0.9.3 h1:kBpcYPx4ys4lrDGKp4OhP2uy8h7UjlmLW/qoO5Xb2bY= cloud.google.com/go/recommendationengine v0.9.3/go.mod h1:QRnX5aM7DCvtqtSs7I0zay5Zfq3fzxqnsPbZF7pa1G8= cloud.google.com/go/recommendationengine v0.9.7/go.mod h1:snZ/FL147u86Jqpv1j95R+CyU5NvL/UzYiyDo6UByTM= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= cloud.google.com/go/recommender v1.10.1/go.mod h1:XFvrE4Suqn5Cq0Lf+mCP6oBHD/yRMA8XxP5sb7Q7gpA= cloud.google.com/go/recommender v1.11.0/go.mod h1:kPiRQhPyTJ9kyXPCG6u/dlPLbYfFlkwHNRwdzPVAoII= cloud.google.com/go/recommender v1.11.1/go.mod h1:sGwFFAyI57v2Hc5LbIj+lTwXipGu9NW015rkaEM5B18= @@ -1474,9 +1469,11 @@ cloud.google.com/go/recommender v1.12.1/go.mod h1:gf95SInWNND5aPas3yjwl0I572dtud cloud.google.com/go/recommender v1.13.3 h1:dVlOjxsbjuhlwu4MIcyPWe09qVcDqc419iOjdPl5RHk= cloud.google.com/go/recommender v1.13.3/go.mod h1:6yAmcfqJRKglZrVuTHsieTFEm4ai9JtY3nQzmX4TC0Q= cloud.google.com/go/recommender v1.13.6/go.mod h1:y5/5womtdOaIM3xx+76vbsiA+8EBTIVfWnxHDFHBGJM= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= cloud.google.com/go/redis v1.13.1/go.mod h1:VP7DGLpE91M6bcsDdMuyCm2hIpB6Vp2hI090Mfd1tcg= @@ -1488,6 +1485,12 @@ cloud.google.com/go/redis v1.14.2/go.mod h1:g0Lu7RRRz46ENdFKQ2EcQZBAJ2PtJHJLuiiR cloud.google.com/go/redis v1.18.0 h1:xcu35SCyHSp+nKV6QNIklgkBKTH1qb0aLUXjl0mSR8I= cloud.google.com/go/redis v1.18.0/go.mod h1:fJ8dEQJQ7DY+mJRMkSafxQCuc8nOyPUwo9tXJqjvNEY= cloud.google.com/go/redis v1.18.3/go.mod h1:x8HtXZbvMBDNT6hMHaQ022Pos5d7SP7YsUH8fCJ2Wm4= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/resourcemanager v1.10.3 h1:SHOMw0kX0xWratC5Vb5VULBeWiGlPYAs82kiZqNtWpM= +cloud.google.com/go/resourcemanager v1.10.3/go.mod h1:JSQDy1JA3K7wtaFH23FBGld4dMtzqCoOpwY55XYR8gs= +cloud.google.com/go/resourcemanager v1.10.7/go.mod h1:rScGkr6j2eFwxAjctvOP/8sqnEpDbQ9r5CKwKfomqjs= cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= @@ -1499,9 +1502,6 @@ cloud.google.com/go/resourcemanager v1.9.3/go.mod h1:IqrY+g0ZgLsihcfcmqSe+RKp1hz cloud.google.com/go/resourcemanager v1.9.4/go.mod h1:N1dhP9RFvo3lUfwtfLWVxfUWq8+KUQ+XLlHLH3BoFJ0= cloud.google.com/go/resourcemanager v1.9.5 h1:AZWr1vWVDKGwfLsVhcN+vcwOz3xqqYxtmMa0aABCMms= cloud.google.com/go/resourcemanager v1.9.5/go.mod h1:hep6KjelHA+ToEjOfO3garMKi/CLYwTqeAw7YiEI9x8= -cloud.google.com/go/resourcemanager v1.10.3 h1:SHOMw0kX0xWratC5Vb5VULBeWiGlPYAs82kiZqNtWpM= -cloud.google.com/go/resourcemanager v1.10.3/go.mod h1:JSQDy1JA3K7wtaFH23FBGld4dMtzqCoOpwY55XYR8gs= -cloud.google.com/go/resourcemanager v1.10.7/go.mod h1:rScGkr6j2eFwxAjctvOP/8sqnEpDbQ9r5CKwKfomqjs= cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= @@ -1513,8 +1513,6 @@ cloud.google.com/go/resourcesettings v1.6.5 h1:BTr5MVykJwClASci/7Og4Qfx70aQ4n3ep cloud.google.com/go/resourcesettings v1.6.5/go.mod h1:WBOIWZraXZOGAgoR4ukNj0o0HiSMO62H9RpFi9WjP9I= cloud.google.com/go/resourcesettings v1.8.3 h1:13HOFU7v4cEvIHXSAQbinF4wp2Baybbq7q9FMctg1Ek= cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= @@ -1528,10 +1526,13 @@ cloud.google.com/go/retail v1.16.0/go.mod h1:LW7tllVveZo4ReWt68VnldZFWJRzsh9np+0 cloud.google.com/go/retail v1.19.2 h1:PT6CUlazIFIOLLJnV+bPBtiSH8iusKZ+FZRzZYFt2vk= cloud.google.com/go/retail v1.19.2/go.mod h1:71tRFYAcR4MhrZ1YZzaJxr030LvaZiIcupH7bXfFBcY= cloud.google.com/go/retail v1.25.1/go.mod h1:J75G8pd+DH0SHueL9IJw7Y5d2VhTsjFsk+F1t9f8jXc= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/run v1.12.1/go.mod h1:DdMsf2m0/n3WHNDcyoqZmfE+LMd/uEJ7j1yIooDrgXU= cloud.google.com/go/run v1.2.0/go.mod h1:36V1IlDzQ0XxbQjUx6IYbw8H3TJnWvhii963WW3B/bo= cloud.google.com/go/run v1.3.0/go.mod h1:S/osX/4jIPZGg+ssuqh6GNgg7syixKe3YnprwehzHKU= cloud.google.com/go/run v1.3.1/go.mod h1:cymddtZOzdwLIAsmS6s+Asl4JoXIDm/K1cpZTxV4Q5s= @@ -1541,13 +1542,6 @@ cloud.google.com/go/run v1.3.4 h1:m9WDA7DzTpczhZggwYlZcBWgCRb+kgSIisWn1sbw2rQ= cloud.google.com/go/run v1.3.4/go.mod h1:FGieuZvQ3tj1e9GnzXqrMABSuir38AJg5xhiYq+SF3o= cloud.google.com/go/run v1.9.0 h1:9WeTqeEcriXqRViXMNwczjFJjixOSBlSlk/fW3lfKPg= cloud.google.com/go/run v1.9.0/go.mod h1:Dh0+mizUbtBOpPEzeXMM22t8qYQpyWpfmUiWQ0+94DU= -cloud.google.com/go/run v1.12.1/go.mod h1:DdMsf2m0/n3WHNDcyoqZmfE+LMd/uEJ7j1yIooDrgXU= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= -cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= cloud.google.com/go/scheduler v1.10.1/go.mod h1:R63Ldltd47Bs4gnhQkmNDse5w8gBRrhObZ54PxgR2Oo= cloud.google.com/go/scheduler v1.10.2/go.mod h1:O3jX6HRH5eKCA3FutMw375XHZJudNIKVonSCHv7ropY= cloud.google.com/go/scheduler v1.10.3/go.mod h1:8ANskEM33+sIbpJ+R4xRfw/jzOG+ZFE8WVLy7/yGvbc= @@ -1558,9 +1552,12 @@ cloud.google.com/go/scheduler v1.10.6/go.mod h1:pe2pNCtJ+R01E06XCDOJs1XvAMbv28Zs cloud.google.com/go/scheduler v1.11.4 h1:ewVvigBnEnrr9Ih8CKnLVoB5IiULaWfYU5nEnnfVAto= cloud.google.com/go/scheduler v1.11.4/go.mod h1:0ylvH3syJnRi8EDVo9ETHW/vzpITR/b+XNnoF+GPSz4= cloud.google.com/go/scheduler v1.11.8/go.mod h1:bNKU7/f04eoM6iKQpwVLvFNBgGyJNS87RiFN73mIPik= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= cloud.google.com/go/secretmanager v1.11.1/go.mod h1:znq9JlXgTNdBeQk9TBW/FnR/W4uChEKGeqQWAJ8SXFw= cloud.google.com/go/secretmanager v1.11.2/go.mod h1:MQm4t3deoSub7+WNwiC4/tRYgDBHJgJPvswqQVB1Vss= @@ -1573,10 +1570,9 @@ cloud.google.com/go/secretmanager v1.14.5/go.mod h1:GXznZF3qqPZDGZQqETZwZqHw4R6K cloud.google.com/go/secretmanager v1.14.6 h1:/ooktIMSORaWk9gm3vf8+Mg+zSrUplJFKBztP993oL0= cloud.google.com/go/secretmanager v1.14.6/go.mod h1:0OWeM3qpJ2n71MGgNfKsgjC/9LfVTcUqXFUlGxo5PzY= cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= @@ -1589,6 +1585,10 @@ cloud.google.com/go/security v1.15.5/go.mod h1:KS6X2eG3ynWjqcIX976fuToN5juVkF6Ra cloud.google.com/go/security v1.18.3 h1:ya9gfY1ign6Yy25VMMMgZ9xy7D/TczDB0ElXcyWmEVE= cloud.google.com/go/security v1.18.3/go.mod h1:NmlSnEe7vzenMRoTLehUwa/ZTZHDQE59IPRevHcpCe4= cloud.google.com/go/security v1.19.2/go.mod h1:KXmf64mnOsLVKe8mk/bZpU1Rsvxqc0Ej0A6tgCeN93w= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= @@ -1605,17 +1605,11 @@ cloud.google.com/go/securitycenter v1.24.4/go.mod h1:PSccin+o1EMYKcFQzz9HMMnZ2r9 cloud.google.com/go/securitycenter v1.36.0 h1:IdDiAa7gYtL7Gdx+wEaNHimudk3ZkEGNhdz9FuEuxWM= cloud.google.com/go/securitycenter v1.36.0/go.mod h1:AErAQqIvrSrk8cpiItJG1+ATl7SD7vQ6lgTFy/Tcs4Q= cloud.google.com/go/securitycenter v1.38.1/go.mod h1:Ge2D/SlG2lP1FrQD7wXHy8qyeloRenvKXeB4e7zO6z0= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= -cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= cloud.google.com/go/servicedirectory v1.10.1/go.mod h1:Xv0YVH8s4pVOwfM/1eMTl0XJ6bzIOSLDt8f8eLaGOxQ= cloud.google.com/go/servicedirectory v1.11.0/go.mod h1:Xv0YVH8s4pVOwfM/1eMTl0XJ6bzIOSLDt8f8eLaGOxQ= cloud.google.com/go/servicedirectory v1.11.1/go.mod h1:tJywXimEWzNzw9FvtNjsQxxJ3/41jseeILgwU/QLrGI= @@ -1626,6 +1620,12 @@ cloud.google.com/go/servicedirectory v1.11.4/go.mod h1:Bz2T9t+/Ehg6x+Y7Ycq5xiShY cloud.google.com/go/servicedirectory v1.12.3 h1:oFkCp6ti7fc7hzeROmOPQuPBHFqwyhcsv3Yrma28+uc= cloud.google.com/go/servicedirectory v1.12.3/go.mod h1:dwTKSCYRD6IZMrqoBCIvZek+aOYK/6+jBzOGw8ks5aY= cloud.google.com/go/servicedirectory v1.12.7/go.mod h1:gOtN+qbuCMH6tj2dqlDY3qQL7w3V0+nkWaZElnJK8Ps= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= @@ -1663,10 +1663,6 @@ cloud.google.com/go/spanner v1.57.0/go.mod h1:aXQ5QDdhPRIqVhYmnkAdwPYvj/DRN0Fguc cloud.google.com/go/spanner v1.76.1 h1:vYbVZuXfnFwvNcvH3lhI2PeUA+kHyqKmLC7mJWaC4Ok= cloud.google.com/go/spanner v1.76.1/go.mod h1:YtwoE+zObKY7+ZeDCBtZ2ukM+1/iPaMfUM+KnTh/sx0= cloud.google.com/go/spanner v1.86.1/go.mod h1:bbwCXbM+zljwSPLZ44wZOdzcdmy89hbUGmM/r9sD0ws= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= cloud.google.com/go/speech v1.17.1/go.mod h1:8rVNzU43tQvxDaGvqOhpDqgkJTFowBpDvCJ14kGlJYo= @@ -1680,10 +1676,11 @@ cloud.google.com/go/speech v1.21.1/go.mod h1:E5GHZXYQlkqWQwY5xRSLHw2ci5NMQNG52Ff cloud.google.com/go/speech v1.26.0 h1:qvURtJs7BQzQhbxWxwai0pT79S8KLVKJ/4W8igVkt1Y= cloud.google.com/go/speech v1.26.0/go.mod h1:78bqDV2SgwFlP/M4n3i3PwLthFq6ta7qmyG6lUV7UCA= cloud.google.com/go/speech v1.28.1/go.mod h1:+EN8Zuy6y2BKe9P1RAmMaFPAgBns6m+XMgXAfkYtSSE= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= @@ -1699,6 +1696,7 @@ cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OL cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80= cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs= cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= @@ -1706,10 +1704,8 @@ cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5 cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU= cloud.google.com/go/storage v1.57.1/go.mod h1:329cwlpzALLgJuu8beyJ/uvQznDHpa2U5lGjWednkzg= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= -cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storagetransfer v1.10.0/go.mod h1:DM4sTlSmGiNczmV6iZyceIh2dbs+7z2Ayg6YAiQlYfA= cloud.google.com/go/storagetransfer v1.10.1/go.mod h1:rS7Sy0BtPviWYTTJVWCSV4QrbBitgPeuK4/FKa4IdLs= cloud.google.com/go/storagetransfer v1.10.2/go.mod h1:meIhYQup5rg9juQJdyppnA/WLQCOguxtk1pr3/vBWzA= @@ -1719,6 +1715,10 @@ cloud.google.com/go/storagetransfer v1.10.4/go.mod h1:vef30rZKu5HSEf/x1tK3WfWrL0 cloud.google.com/go/storagetransfer v1.12.1 h1:W3v9A7MGBN7H9sAFstyciwP/1XEQhUhZfrjclmDnpMs= cloud.google.com/go/storagetransfer v1.12.1/go.mod h1:hQqbfs8/LTmObJyCC0KrlBw8yBJ2bSFlaGila0qBMk4= cloud.google.com/go/storagetransfer v1.13.1/go.mod h1:S858w5l383ffkdqAqrAA+BC7KlhCqeNieK3sFf5Bj4Y= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= @@ -1733,6 +1733,9 @@ cloud.google.com/go/talent v1.6.6/go.mod h1:y/WQDKrhVz12WagoarpAIyKKMeKGKHWPoReZ cloud.google.com/go/talent v1.8.0 h1:olv+s2g+LGXeJi+MYF1wI44/TwHaVnO0N7PiucVf5ZQ= cloud.google.com/go/talent v1.8.0/go.mod h1:/gvOzSrtMcfTL/9xWhdYaZATaxUNhQ+L+3ZaGOGs7bA= cloud.google.com/go/talent v1.8.4/go.mod h1:3yukBXUTVFNyKcJpUExW/k5gqEy8qW6OCNj7WdN0MWo= +cloud.google.com/go/texttospeech v1.11.0 h1:YF/RdNb+jUEp22cIZCvqiFjfA5OxGE+Dxss3mhXU7oQ= +cloud.google.com/go/texttospeech v1.11.0/go.mod h1:7M2ro3I2QfIEvArFk1TJ+pqXJqhszDtxUpnIv/150As= +cloud.google.com/go/texttospeech v1.16.0/go.mod h1:AeSkoH3ziPvapsuyI07TWY4oGxluAjntX+pF4PJ2jy0= cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= @@ -1742,9 +1745,6 @@ cloud.google.com/go/texttospeech v1.7.3/go.mod h1:Av/zpkcgWfXlDLRYob17lqMstGZ3Gq cloud.google.com/go/texttospeech v1.7.4/go.mod h1:vgv0002WvR4liGuSd5BJbWy4nDn5Ozco0uJymY5+U74= cloud.google.com/go/texttospeech v1.7.5 h1:dxY2Q5mHCbrGa3oPR2O3PCicdnvKa1JmwGQK36EFLOw= cloud.google.com/go/texttospeech v1.7.5/go.mod h1:tzpCuNWPwrNJnEa4Pu5taALuZL4QRRLcb+K9pbhXT6M= -cloud.google.com/go/texttospeech v1.11.0 h1:YF/RdNb+jUEp22cIZCvqiFjfA5OxGE+Dxss3mhXU7oQ= -cloud.google.com/go/texttospeech v1.11.0/go.mod h1:7M2ro3I2QfIEvArFk1TJ+pqXJqhszDtxUpnIv/150As= -cloud.google.com/go/texttospeech v1.16.0/go.mod h1:AeSkoH3ziPvapsuyI07TWY4oGxluAjntX+pF4PJ2jy0= cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= @@ -1757,10 +1757,6 @@ cloud.google.com/go/tpu v1.6.5/go.mod h1:P9DFOEBIBhuEcZhXi+wPoVy/cji+0ICFi4TtTkM cloud.google.com/go/tpu v1.8.0 h1:BvMNijOb6Vd46Rr/SR5jWv1MPosOhVsi0UaeAGNjeds= cloud.google.com/go/tpu v1.8.0/go.mod h1:XyNzyK1xc55WvL5rZEML0Z9/TUHDfnq0uICkQw6rWMo= cloud.google.com/go/tpu v1.8.4/go.mod h1:ul0cyWSHr6jHGZYElZe6HvQn35VY93RAlwpDiSBRnPA= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= -cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= cloud.google.com/go/trace v1.10.1/go.mod h1:gbtL94KE5AJLH3y+WVpfWILmqgc6dXcqgNXdOPAQTYk= cloud.google.com/go/trace v1.10.2/go.mod h1:NPXemMi6MToRFcSxRl2uDnu/qAlAQ3oULUphcHGh1vA= cloud.google.com/go/trace v1.10.3/go.mod h1:Ke1bgfc73RV3wUFml+uQp7EsDw4dGaETLxB7Iq/r4CY= @@ -1773,6 +1769,17 @@ cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/translate v1.10.0/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= +cloud.google.com/go/translate v1.10.1 h1:upovZ0wRMdzZvXnu+RPam41B0mRJ+coRXFP2cYFJ7ew= +cloud.google.com/go/translate v1.10.1/go.mod h1:adGZcQNom/3ogU65N9UXHOnnSvjPwA/jKQUMnsYXOyk= +cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs= +cloud.google.com/go/translate v1.12.3 h1:XJ7LipYJi80BCgVk2lx1fwc7DIYM6oV2qx1G4IAGQ5w= +cloud.google.com/go/translate v1.12.3/go.mod h1:qINOVpgmgBnY4YTFHdfVO4nLrSBlpvlIyosqpGEgyEg= +cloud.google.com/go/translate v1.12.7/go.mod h1:wwJp14NZyWvcrFANhIXutXj0pOBkYciBHwSlUOykcjI= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= @@ -1784,15 +1791,6 @@ cloud.google.com/go/translate v1.9.0/go.mod h1:d1ZH5aaOA0CNhWeXeC8ujd4tdCFw8XoNW cloud.google.com/go/translate v1.9.1/go.mod h1:TWIgDZknq2+JD4iRcojgeDtqGEp154HN/uL6hMvylS8= cloud.google.com/go/translate v1.9.2/go.mod h1:E3Tc6rUTsQkVrXW6avbUhKJSr7ZE3j7zNmqzXKHqRrY= cloud.google.com/go/translate v1.9.3/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= -cloud.google.com/go/translate v1.10.0/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= -cloud.google.com/go/translate v1.10.1 h1:upovZ0wRMdzZvXnu+RPam41B0mRJ+coRXFP2cYFJ7ew= -cloud.google.com/go/translate v1.10.1/go.mod h1:adGZcQNom/3ogU65N9UXHOnnSvjPwA/jKQUMnsYXOyk= -cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs= -cloud.google.com/go/translate v1.12.3 h1:XJ7LipYJi80BCgVk2lx1fwc7DIYM6oV2qx1G4IAGQ5w= -cloud.google.com/go/translate v1.12.3/go.mod h1:qINOVpgmgBnY4YTFHdfVO4nLrSBlpvlIyosqpGEgyEg= -cloud.google.com/go/translate v1.12.7/go.mod h1:wwJp14NZyWvcrFANhIXutXj0pOBkYciBHwSlUOykcjI= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= @@ -1808,10 +1806,8 @@ cloud.google.com/go/video v1.20.4/go.mod h1:LyUVjyW+Bwj7dh3UJnUGZfyqjEto9DnrvTe1 cloud.google.com/go/video v1.23.3 h1:C2FH+6yr6LCZC4fP0gm9FwJB/SRh5Ul88O5Sc/bL83I= cloud.google.com/go/video v1.23.3/go.mod h1:Kvh/BheubZxGZDXSb0iO6YX7ZNcaYHbLjnnaC8Qyy3g= cloud.google.com/go/video v1.27.1/go.mod h1:xzfAC77B4vtnbi/TT3UUxEjCa/+Ehy5EA8w470ytOig= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= cloud.google.com/go/videointelligence v1.11.1/go.mod h1:76xn/8InyQHarjTWsBR058SmlPCwQjgcvoW0aZykOvo= cloud.google.com/go/videointelligence v1.11.2/go.mod h1:ocfIGYtIVmIcWk1DsSGOoDiXca4vaZQII1C85qtoplc= @@ -1822,6 +1818,10 @@ cloud.google.com/go/videointelligence v1.11.5/go.mod h1:/PkeQjpRponmOerPeJxNPuxv cloud.google.com/go/videointelligence v1.12.3 h1:zNTOUQyatGQtnCJ2dR3faRtpWQOlC8wszJqwG5CtwVM= cloud.google.com/go/videointelligence v1.12.3/go.mod h1:dUA6V+NH7CVgX6TePq0IelVeBMGzvehxKPR4FGf1dtw= cloud.google.com/go/videointelligence v1.12.7/go.mod h1:XAk5hCMY+GihxJ55jNoMdwdXSNZnCl3wGs2+94gK7MA= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= @@ -1877,6 +1877,9 @@ cloud.google.com/go/vpcaccess v1.7.5/go.mod h1:slc5ZRvvjP78c2dnL7m4l4R9GwL3wDLcp cloud.google.com/go/vpcaccess v1.8.3 h1:vxVaoFM64M/ht619c4wZNF0iq0QPaMWElOh7Ns4r41A= cloud.google.com/go/vpcaccess v1.8.3/go.mod h1:bqOhyeSh/nEmLIsIUoCiQCBHeNPNjaK9M3bIvKxFdsY= cloud.google.com/go/vpcaccess v1.8.7/go.mod h1:9RYw5bVvk4Z51Rc8vwXT63yjEiMD/l7XyEaDyrNHgmk= +cloud.google.com/go/webrisk v1.10.3 h1:yh0v/5n49VO4/i9pYfDm1gLJUj1Ph3Xzegn8WvK9YRA= +cloud.google.com/go/webrisk v1.10.3/go.mod h1:rRAqCA5/EQOX8ZEEF4HMIrLHGTK/Y1hEQgWMnih+jAw= +cloud.google.com/go/webrisk v1.11.2/go.mod h1:yH44GeXz5iz4HFsIlGeoVvnjwnmfbni7Lwj1SelV4f0= cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= @@ -1888,9 +1891,6 @@ cloud.google.com/go/webrisk v1.9.3/go.mod h1:RUYXe9X/wBDXhVilss7EDLW9ZNa06aowPui cloud.google.com/go/webrisk v1.9.4/go.mod h1:w7m4Ib4C+OseSr2GL66m0zMBywdrVNTDKsdEsfMl7X0= cloud.google.com/go/webrisk v1.9.5 h1:251MvGuC8wisNN7+jqu9DDDZAi38KiMXxOpA/EWy4dE= cloud.google.com/go/webrisk v1.9.5/go.mod h1:aako0Fzep1Q714cPEM5E+mtYX8/jsfegAuS8aivxy3U= -cloud.google.com/go/webrisk v1.10.3 h1:yh0v/5n49VO4/i9pYfDm1gLJUj1Ph3Xzegn8WvK9YRA= -cloud.google.com/go/webrisk v1.10.3/go.mod h1:rRAqCA5/EQOX8ZEEF4HMIrLHGTK/Y1hEQgWMnih+jAw= -cloud.google.com/go/webrisk v1.11.2/go.mod h1:yH44GeXz5iz4HFsIlGeoVvnjwnmfbni7Lwj1SelV4f0= cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= @@ -1903,10 +1903,6 @@ cloud.google.com/go/websecurityscanner v1.6.5/go.mod h1:QR+DWaxAz2pWooylsBF854/I cloud.google.com/go/websecurityscanner v1.7.3 h1:/uxhVCWKXzPw5pVfnBOVjaSiQ6Bm0tDExDOCLV40thw= cloud.google.com/go/websecurityscanner v1.7.3/go.mod h1:gy0Kmct4GNLoCePWs9xkQym1D7D59ld5AjhXrjipxSs= cloud.google.com/go/websecurityscanner v1.7.7/go.mod h1:ng/PzARaus3Bj4Os4LpUnyYHsbtJky1HbBDmz148v1o= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= cloud.google.com/go/workflows v1.11.1/go.mod h1:Z+t10G1wF7h8LgdY/EmRcQY8ptBD/nvofaL6FqlET6g= cloud.google.com/go/workflows v1.12.0/go.mod h1:PYhSk2b6DhZ508tj8HXKaBh+OFe+xdl0dHF/tJdzPQM= @@ -1918,6 +1914,10 @@ cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9p cloud.google.com/go/workflows v1.13.3 h1:lNFDMranJymDEB7cTI7DI9czbc1WU0RWY9KCEv9zuDY= cloud.google.com/go/workflows v1.13.3/go.mod h1:Xi7wggEt/ljoEcyk+CB/Oa1AHBCk0T1f5UH/exBB5CE= cloud.google.com/go/workflows v1.14.3/go.mod h1:CC9+YdVI2Kvp0L58WajHpEfKJxhrtRh3uQ0SYWcmAk4= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= codeberg.org/go-fonts/dejavu v0.4.0/go.mod h1:abni088lmhQJvso2Lsb7azCKzwkfcnttl6tL1UTWKzg= codeberg.org/go-fonts/latin-modern v0.4.0/go.mod h1:BF68mZznJ9QHn+hic9ks2DaFl4sR5YhfM6xTYaP9vNw= codeberg.org/go-fonts/liberation v0.4.1/go.mod h1:Gu6FTZHMMpGxPBfc8WFL8RfwMYFTvG7TIFOMx8oM4B8= @@ -1958,135 +1958,15 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN github.com/99designs/keyring v1.1.5/go.mod h1:7hsVvt2qXgtadGevGJ4ujg+u8m6SpJ5TpHqTozIPqf0= github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2/go.mod h1:gCLVsLfv1egrcZu+GoJATN5ts75F2s62ih/457eWzOw= -github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= -github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= -github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4= -github.com/AthenZ/athenz v1.12.13 h1:OhZNqZsoBXNrKBJobeUUEirPDnwt0HRo4kQMIO1UwwQ= -github.com/AthenZ/athenz v1.12.13/go.mod h1:XXDXXgaQzXaBXnJX6x/bH4yF6eon2lkyzQZ0z/dxprE= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.0.0 h1:Kb8eVvjdP6kZqYnER5w/PiGCFp91yVgaxve3d7kCEpY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.0.0/go.mod h1:lYq15QkJyEsNegz5EhI/0SXQ6spvGfgwBH/Qyzkoc/s= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 h1:nVocQV40OQne5613EeLayJiRAJuKlBGy+m22qWG+WRg= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0/go.mod h1:7QJP7dr2wznCMeqIrhMgWGf7XpAQnVrJqDm9nvV3Cu4= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 h1:mlmW46Q0B79I+Aj4azKC6xDMFN9a9SyZWESlGWYXbFs= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0/go.mod h1:PXe2h+LKcWTX9afWdZoHyODqR4fBa5boUM/8uJfZ0Jo= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= -github.com/CloudyKit/jet/v3 v3.0.0 h1:1PwO5w5VCtlUUl+KTOBsTGZlhjWkcybsGaAau52tOy8= -github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= -github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= -github.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo= -github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.0/go.mod h1:p2puVVSKjQ84Qb1gzw2XHLs34WQyHTYFZLaVxypAFYs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0/go.mod h1:6fTWu4m3jocfUZLYF5KsZC1TUfRvEjs7lM4crme/irw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1/go.mod h1:0wEl7vrAD8mehJyohS9HZy+WyEOaQO2mJx86Cvh93kM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0 h1:jJKWl98inONJAr/IZrdFQUWcwUO95DLY1XMD1ZIut+g= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0/go.mod h1:l2fIqmwB+FKSfvn3bAD/0i+AXAxhIZjTK2svT/mgUXs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 h1:GYUJLfvd++4DMuMhCFLgLXvFwofIxh/qOwoGuS/LTew= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0/go.mod h1:wRbFgBQUVm1YXrvWKofAEmq9HNJTDphbAaJSSX01KUI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= -github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= -github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= -github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= -github.com/KarpelesLab/reflink v1.0.1/go.mod h1:WGkTOKNjd1FsJKBw3mu4JvrPEDJyJJ+JPtxBkbPoCok= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE= -github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= -github.com/Microsoft/cosesign1go v1.4.0/go.mod h1:1La/HcGw19rRLhPW0S6u55K6LKfti+GQSgGCtrfhVe8= -github.com/Microsoft/didx509go v0.0.3/go.mod h1:wWt+iQsLzn3011+VfESzznLIp/Owhuj7rLF7yLglYbk= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= -github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= -github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= -github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA= github.com/actgardner/gogen-avro/v10 v10.2.1 h1:z3pOGblRjAJCYpkIJ8CmbMJdksi4rAhaygw0dyXZ930= github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0RsT/ee8YIgGY/xpEQgQ= github.com/actgardner/gogen-avro/v9 v9.1.0 h1:YZ5tCwV5xnDZrG4uRDQYT2VAWZCRAG3eyQH/WYR2T6Q= github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2/go.mod h1:gCLVsLfv1egrcZu+GoJATN5ts75F2s62ih/457eWzOw= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= @@ -2101,6 +1981,8 @@ github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= @@ -2127,6 +2009,7 @@ github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeG github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/aliyun/credentials-go v1.2.7 h1:gLtFylxLZ1TWi1pStIt1O6a53GFU1zkNwjtJir2B4ow= github.com/aliyun/credentials-go v1.2.7/go.mod h1:/KowD1cfGSLrLsH28Jr8W+xwoId0ywIy5lNzDz6O1vw= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= @@ -2157,6 +2040,7 @@ github.com/apache/thrift v0.17.0 h1:cMd2aj52n+8VoAtvSvLn4kDC3aZ6IAkBuqWQ2IDu7wo= github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q= github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U= github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/ardielle/ardielle-go v1.5.2 h1:TilHTpHIQJ27R1Tl/iITBzMwiUGSlVfiVhwDNGM3Zj4= @@ -2175,6 +2059,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/AthenZ/athenz v1.12.13 h1:OhZNqZsoBXNrKBJobeUUEirPDnwt0HRo4kQMIO1UwwQ= +github.com/AthenZ/athenz v1.12.13/go.mod h1:XXDXXgaQzXaBXnJX6x/bH4yF6eon2lkyzQZ0z/dxprE= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= @@ -2194,9 +2080,9 @@ github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGy github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 h1:u1KOU1S15ufyZqmH/rA3POkiRH6EcDANHj2xHRzq+zc= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8/go.mod h1:WPv2FRnkIOoDv/8j2gSUsI4qDc7392w5anFB/I89GZ8= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= @@ -2246,6 +2132,44 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.0.0 h1:Kb8eVvjdP6kZqYnER5w/PiGCFp91yVgaxve3d7kCEpY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.0.0/go.mod h1:lYq15QkJyEsNegz5EhI/0SXQ6spvGfgwBH/Qyzkoc/s= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 h1:nVocQV40OQne5613EeLayJiRAJuKlBGy+m22qWG+WRg= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0/go.mod h1:7QJP7dr2wznCMeqIrhMgWGf7XpAQnVrJqDm9nvV3Cu4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 h1:mlmW46Q0B79I+Aj4azKC6xDMFN9a9SyZWESlGWYXbFs= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0/go.mod h1:PXe2h+LKcWTX9afWdZoHyODqR4fBa5boUM/8uJfZ0Jo= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bazelbuild/rules_go v0.49.0/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOYow= @@ -2275,6 +2199,12 @@ github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/bufbuild/protovalidate-go v0.9.1/go.mod h1:5jptBxfvlY51RhX32zR6875JfPBRXUsQjyZjm/NqkLQ= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= github.com/bytecodealliance/wasmtime-go/v37 v37.0.0/go.mod h1:Pf1l2JCTUFMnOqDIwkjzx1qfVJ09xbaXETKgRVE4jZ0= @@ -2345,6 +2275,10 @@ github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/jet/v3 v3.0.0 h1:1PwO5w5VCtlUUl+KTOBsTGZlhjWkcybsGaAau52tOy8= +github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= +github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= @@ -2409,9 +2343,9 @@ github.com/containerd/imgcrypt v1.1.8 h1:ZS7TuywcRNLoHpU0g+v4/PsKynl6TYlw5xDVWWo github.com/containerd/imgcrypt v1.1.8/go.mod h1:x6QvFIkMyO2qGIY2zXc88ivEzcbgvLdWjoZyGqDap5U= github.com/containerd/imgcrypt/v2 v2.0.1 h1:gQcmeCKA97fAl0wlpq0itSY/PagFBsn4/mlKUy6kOio= github.com/containerd/imgcrypt/v2 v2.0.1/go.mod h1:/qIJL8nxzdzMA2n5iYyyuIY36KfoVQWmgTWdfVtyebM= +github.com/containerd/nri v0.11.0/go.mod h1:bjGTLdUA58WgghKHg8azFMGXr05n1wDHrt3NSVBHiGI= github.com/containerd/nri v0.8.0 h1:n1S753B9lX8RFrHYeSgwVvS1yaUcHjxbB+f+xzEncRI= github.com/containerd/nri v0.8.0/go.mod h1:uSkgBrCdEtAiEz4vnrq8gmAC4EnVAM5Klt0OuK5rZYQ= -github.com/containerd/nri v0.11.0/go.mod h1:bjGTLdUA58WgghKHg8azFMGXr05n1wDHrt3NSVBHiGI= github.com/containerd/otelttrpc v0.1.0 h1:UOX68eVTE8H/T45JveIg+I22Ev2aFj4qPITCmXsskjw= github.com/containerd/otelttrpc v0.1.0/go.mod h1:XhoA2VvaGPW1clB2ULwrBZfXVuEWuyOd2NUD1IM0yTg= github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= @@ -2458,17 +2392,17 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cucumber/messages/go/v22 v22.0.0 h1:hk3ITpEWQ+KWDe619zYcqtaLOfcu9jgClSeps3DlNWI= github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= @@ -2480,6 +2414,8 @@ github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/data-accelerator/zdfs v0.1.5/go.mod h1:/MyNTsQHHKVLznaRBz+PivhIDwglu+wuXKoZxUNmLKI= +github.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo= +github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= @@ -2542,11 +2478,6 @@ github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaI github.com/elastic/crd-ref-docs v0.1.0 h1:Cr5kz89QB3Iuuj7dhAfLMApCrChEGAaIBTxGk/xuRKw= github.com/elastic/crd-ref-docs v0.1.0/go.mod h1:X83mMBdJt05heJUYiS3T0yJ/JkCuliuhSUNav5Gjo/U= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= @@ -2558,14 +2489,19 @@ github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnv github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane/envoy v1.32.2/go.mod h1:eR2SOX2IedqlPvmiKjUH7Wu//S602JKI7HPC/L3SRq8= github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= @@ -2588,12 +2524,12 @@ github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+ne github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= @@ -2610,18 +2546,18 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= -github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= +github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/freddierice/go-losetup v0.0.0-20220711213114-2a14873012db/go.mod h1:pwuQfHWn6j2Fpl2AWw/bPLlKfojHxIIEa5TeKIgDFW4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= @@ -2670,10 +2606,10 @@ github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8 github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= @@ -2724,9 +2660,9 @@ github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3i github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg= github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0= @@ -2751,14 +2687,14 @@ github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198 h1:FSii2UQeSLngl3jFoR4tUKZLprO7qUlh/TKKticc0BM= github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM= github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -2939,14 +2875,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= @@ -2962,10 +2890,52 @@ github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEP github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.0/go.mod h1:p2puVVSKjQ84Qb1gzw2XHLs34WQyHTYFZLaVxypAFYs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0/go.mod h1:6fTWu4m3jocfUZLYF5KsZC1TUfRvEjs7lM4crme/irw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1/go.mod h1:0wEl7vrAD8mehJyohS9HZy+WyEOaQO2mJx86Cvh93kM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0 h1:jJKWl98inONJAr/IZrdFQUWcwUO95DLY1XMD1ZIut+g= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0/go.mod h1:l2fIqmwB+FKSfvn3bAD/0i+AXAxhIZjTK2svT/mgUXs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 h1:GYUJLfvd++4DMuMhCFLgLXvFwofIxh/qOwoGuS/LTew= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0/go.mod h1:wRbFgBQUVm1YXrvWKofAEmq9HNJTDphbAaJSSX01KUI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -2993,17 +2963,17 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hamba/avro v1.5.6 h1:/UBljlJ9hLjkcY7PhpI/bFYb4RMEXHEwHr17gAm/+l8= @@ -3099,18 +3069,18 @@ github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= -github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/in-toto/in-toto-golang v0.10.0/go.mod h1:wjT4RiyFlLWCmLUJjwB8oZcjaq7HA390aMJcD3xXgmg= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic= +github.com/intel/goresctrl v0.10.0/go.mod h1:1S8GDqL46GuKb525bxNhIEEkhf4rhVcbSf9DuKhp7mw= github.com/intel/goresctrl v0.8.0 h1:N3shVbS3kA1Hk2AmcbHv8805Hjbv+zqsCIZCGktxx50= github.com/intel/goresctrl v0.8.0/go.mod h1:T3ZZnuHSNouwELB5wvOoUJaB7l/4Rm23rJy/wuWJlr0= -github.com/intel/goresctrl v0.10.0/go.mod h1:1S8GDqL46GuKb525bxNhIEEkhf4rhVcbSf9DuKhp7mw= github.com/invopop/jsonschema v0.4.0 h1:Yuy/unfgCnfV5Wl7H0HgFufp/rlurqPOOuacqyByrws= github.com/invopop/jsonschema v0.4.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4= @@ -3157,6 +3127,11 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= +github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= +github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/jolestar/go-commons-pool/v2 v2.1.2 h1:E+XGo58F23t7HtZiC/W6jzO2Ux2IccSH/yx4nD+J1CM= github.com/jolestar/go-commons-pool/v2 v2.1.2/go.mod h1:r4NYccrkS5UqP1YQI1COyTZ9UjPJAAGTUxzcsK1kqhY= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -3167,10 +3142,10 @@ github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= @@ -3187,15 +3162,16 @@ github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= +github.com/KarpelesLab/reflink v1.0.1/go.mod h1:WGkTOKNjd1FsJKBw3mu4JvrPEDJyJJ+JPtxBkbPoCok= github.com/kataras/blocks v0.0.7 h1:cF3RDY/vxnSRezc7vLFlQFTYXG/yAr1o7WImJuZbzC4= github.com/kataras/blocks v0.0.7/go.mod h1:UJIU97CluDo0f+zEjbnbkeMRlvYORtmc1304EeyXf4I= github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= +github.com/kataras/golog v0.1.11/go.mod h1:mAkt1vbPowFUuUGvexyQ5NFW6djEgGyxQBIARJ0AH4A= github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk= github.com/kataras/golog v0.1.9/go.mod h1:jlpk/bOaYCyqDqH18pgDHdaJab72yBE6i0O3s30hpWY= -github.com/kataras/golog v0.1.11/go.mod h1:mAkt1vbPowFUuUGvexyQ5NFW6djEgGyxQBIARJ0AH4A= +github.com/kataras/iris/v12 v12.2.11/go.mod h1:uMAeX8OqG9vqdhyrIPv8Lajo/wXTtAF43wchP9WHt2w= github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 h1:Vx8kDVhO2qepK8w44lBtp+RzN3ld743i+LYPzODJSpQ= github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9/go.mod h1:ldkoR3iXABBeqlTibQ3MYaviA1oSlPvim6f55biwBh4= -github.com/kataras/iris/v12 v12.2.11/go.mod h1:uMAeX8OqG9vqdhyrIPv8Lajo/wXTtAF43wchP9WHt2w= github.com/kataras/jwt v0.1.10 h1:GBXOF9RVInDPhCFBiDumRG9Tt27l7ugLeLo8HL5SeKQ= github.com/kataras/jwt v0.1.10/go.mod h1:xkimAtDhU/aGlQqjwvgtg+VyuPwMiyZHaY8LJRh0mYo= github.com/kataras/neffos v0.0.14 h1:pdJaTvUG3NQfeMbbVCI8JT2T5goPldyyfUB2PJfh1Bs= @@ -3247,6 +3223,7 @@ github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPucc github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/knqyf263/go-plugin v0.9.0/go.mod h1:2z5lCO1/pez6qGo8CvCxSlBFSEat4MEp1DrnA+f7w8Q= github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= @@ -3315,14 +3292,20 @@ github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNf github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE= +github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8B5ajsLIjeuEHLi8xE4fk997o= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM= github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -3352,18 +3335,24 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyex github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/Microsoft/cosesign1go v1.4.0/go.mod h1:1La/HcGw19rRLhPW0S6u55K6LKfti+GQSgGCtrfhVe8= +github.com/Microsoft/didx509go v0.0.3/go.mod h1:wWt+iQsLzn3011+VfESzznLIp/Owhuj7rLF7yLglYbk= github.com/microsoft/go-mssqldb v1.9.4/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/milvus-io/milvus-proto/go-api/v2 v2.6.8-0.20251223041313-25746c47c1a7/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= github.com/milvus-io/milvus-proto/go-api/v2 v2.6.13 h1:l5IlHYtRT1ve+JNFe7JZ3NyvxuPvIg4jkjJBfAltyGg= github.com/milvus-io/milvus-proto/go-api/v2 v2.6.13/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= +github.com/milvus-io/milvus-proto/go-api/v2 v2.6.8-0.20251223041313-25746c47c1a7/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= github.com/milvus-io/milvus/client/v2 v2.6.3 h1:r4u5ceZwmAiNCaqVw3QgenRM585SP3jrq02Jy20W0U8= github.com/milvus-io/milvus/client/v2 v2.6.3/go.mod h1:5FhVBoOUvY3FhDHJ4fqEHYOCExsKK9pCCYbEQ8jEh6E= -github.com/milvus-io/milvus/pkg/v2 v2.6.7-0.20251201120310-af64f2acba38/go.mod h1:ak5nlCCbtImG4/WWcI/csU5ht6EyF/9QQ/tmivMzF4c= github.com/milvus-io/milvus/pkg/v2 v2.6.13 h1:HkGztTddlGBas/kbfj1A/3grat03EAwwcSrCX/zDs5c= github.com/milvus-io/milvus/pkg/v2 v2.6.13/go.mod h1:u9SfTrlDsdkdGKP6q+swshAzipck3OG42lgVIJwQ0Gk= +github.com/milvus-io/milvus/pkg/v2 v2.6.7-0.20251201120310-af64f2acba38/go.mod h1:ak5nlCCbtImG4/WWcI/csU5ht6EyF/9QQ/tmivMzF4c= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= @@ -3426,9 +3415,9 @@ github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak= github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= github.com/nats-io/nats.go v1.28.0 h1:Th4G6zdsz2d0OqXdfzKLClo6bOfoI/b1kInhRtFIy5c= github.com/nats-io/nats.go v1.28.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc= +github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= github.com/nats-io/nkeys v0.1.0 h1:qMd4+pRHgdr1nAClu+2h/2a5F2TmKcCzjCDazVgRoX4= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA= @@ -3441,10 +3430,11 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76 h1:wDbc54qVQ+C5oQZ8Q5VlMbqEt2hrnev2bC/gIGL3Ksk= github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76/go.mod h1:iKyFMidsk/sVYONJRE372sJuX/QTRPacU7imPqqsu7g= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -3457,24 +3447,27 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/onsi/gomega v1.38.1/go.mod h1:LfcV8wZLvwcYRwPiJysphKAEsmcFnLMK/9c+PjvlX8g= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/open-policy-agent/opa v0.70.0/go.mod h1:Y/nm5NY0BX0BqjBriKUiV81sCl8XOjjvqQG7dXrggtI= github.com/open-policy-agent/opa v1.10.1/go.mod h1:7uPI3iRpOalJ0BhK6s1JALWPU9HvaV1XeBSSMZnr/PM= @@ -3558,13 +3551,13 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -3575,25 +3568,26 @@ github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJ github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/prometheus v0.51.0/go.mod h1:yv4MwOn3yHMQ6MZGHPg/U7Fcyqf+rxqiZfSur6myVtc= github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= @@ -3602,14 +3596,15 @@ github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuM github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -3619,12 +3614,12 @@ github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjI github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= @@ -3665,9 +3660,14 @@ github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE= github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/sigstore/rekor-tiles v0.1.11/go.mod h1:eGIeqASh52pgWpmp/j5KZDjmKdVwob7eTYskVVRCu5k= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.3/go.mod h1:2GIWuNvTRMvrzd0Nl8RNqxrt9H7X0OBStwOSzGYRjYw= @@ -3693,25 +3693,26 @@ github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJ github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/stathat/consistent v1.0.0 h1:ZFJ1QTRn8npNBKW065raSZ8xfOqhpb8vLOkfp4CcL/U= github.com/stathat/consistent v1.0.0/go.mod h1:uajTPbgSygZBJ+V+0mY7meZ8i0XAcZs7AQ6V121XSxw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= @@ -3723,10 +3724,10 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/streamnative/pulsarctl v0.5.0 h1:QnrdTUoITBEYFxcxmAhcuR5ooXeX9wfZovlta/rnrdI= github.com/streamnative/pulsarctl v0.5.0/go.mod h1:K14cqq4IHMzPBK1mCKXxhF1mhgAnc29VI/BzjHNW5Q8= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/substrait-io/substrait-go v0.4.2 h1:buDnjsb3qAqTaNbOR7VKmNgXf4lYQxWEcnSGUWBtmN8= @@ -3745,9 +3746,9 @@ github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOa github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvFMA= github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.9 h1:SswqJCmeN4B+9gEAi/5uqT0qpi1y2/2O47V/1hhGZT0= github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= -github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.865 h1:LcUqBlKC4j15LhT303yQDX/XxyHG4haEQqbHgZZA4SY= @@ -3816,12 +3817,14 @@ github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXST github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc= -github.com/vishvananda/netlink v1.3.1-0.20250303224720-0e7078ed04c8/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1-0.20250303224720-0e7078ed04c8/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= @@ -3862,6 +3865,7 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -3890,14 +3894,14 @@ go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8= go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ= -go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4= go.etcd.io/etcd/client/v2 v2.305.23/go.mod h1:Up9T9+5M3MMcCj/V0nDfadBERNMxIYf1tdycva1dXM4= +go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4= go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c= go.etcd.io/etcd/etcdctl/v3 v3.6.0/go.mod h1:ukAtyfIbiTajTDRfXruqUluVGvqcn/aGn0HEWdnzWC4= go.etcd.io/etcd/etcdutl/v3 v3.6.0/go.mod h1:gheEcr7WMMV9TN+TvXSxP9ixk8Bg5Lwp63uz1OANeKg= go.etcd.io/etcd/pkg/v3 v3.5.5/go.mod h1:6ksYFxttiUGzC2uxyqiyOEvhAiD0tuIqSZkX3TyPdaE= -go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGyx8rI= go.etcd.io/etcd/raft/v3 v3.5.23/go.mod h1:NJz9BGkhGvru47lIc1wL0QHsg5yvHTy6tUpEqM69ERM= +go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGyx8rI= go.etcd.io/etcd/server/v3 v3.5.5/go.mod h1:rZ95vDw/jrvsbj9XpTqPrTAB9/kzchVdhRirySPkUBc= go.etcd.io/etcd/tests/v3 v3.6.0/go.mod h1:wuyuwvXTF33++K6kQtpsMrbsISxCQZNbVGpFgx63E9w= go.etcd.io/etcd/v3 v3.6.0/go.mod h1:0sMPTfyOUZNFRYJEweFWFmr2vppoupl4gBiDF/IB7ng= @@ -4085,10 +4089,10 @@ go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42s go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.9.0/go.mod h1:1vKfU9rv61e9EVGthD1zNvUbiwPcimSsOPU9brfSHJg= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.9.0/go.mod h1:1vKfU9rv61e9EVGthD1zNvUbiwPcimSsOPU9brfSHJg= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= @@ -4101,10 +4105,10 @@ go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= @@ -4117,8 +4121,8 @@ go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -4132,9 +4136,6 @@ golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= @@ -4170,6 +4171,9 @@ golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvm golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -4222,10 +4226,6 @@ golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeap golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4= golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= -golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= -golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= -golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= @@ -4235,6 +4235,10 @@ golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78 golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= +golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -4252,15 +4256,6 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -4282,6 +4277,15 @@ golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -4332,13 +4336,6 @@ golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= @@ -4348,6 +4345,7 @@ golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= @@ -4367,6 +4365,7 @@ golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= @@ -4376,10 +4375,15 @@ golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwi golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -4404,11 +4408,6 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= @@ -4432,6 +4431,11 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -4441,14 +4445,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= @@ -4459,8 +4455,16 @@ golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -4545,12 +4549,6 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -4559,6 +4557,7 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -4569,6 +4568,7 @@ golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -4579,12 +4579,16 @@ golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= @@ -4604,14 +4608,6 @@ golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/ golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= @@ -4622,6 +4618,7 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= @@ -4632,6 +4629,7 @@ golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= @@ -4639,19 +4637,16 @@ golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -4672,12 +4667,21 @@ golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -4686,14 +4690,14 @@ golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180810170437-e96c4e24768d/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -4750,18 +4754,12 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= @@ -4770,12 +4768,14 @@ golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= @@ -4787,6 +4787,10 @@ golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= @@ -4803,71 +4807,22 @@ golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6f golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU= gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU= gonum.org/v1/plot v0.15.2 h1:Tlfh/jBk2tqjLZ4/P8ZIwGrLEWQSPDLRm/SNWKNXiGI= gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= @@ -4884,9 +4839,12 @@ google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2 google.golang.org/api v0.125.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.139.0/go.mod h1:CVagp6Eekz9CjGZ718Z+sloknzkDJE7Vc1Ckj9+viBk= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU= google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.150.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg= google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= google.golang.org/api v0.157.0/go.mod h1:+z4v4ufbZ1WEpld6yMGHyggs+PmAHiaLNj5ytP3N01g= @@ -4896,10 +4854,12 @@ google.golang.org/api v0.164.0/go.mod h1:2OatzO7ZDQsoS7IFf3rvsE17/TldiU3F/zxFHeq google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA= google.golang.org/api v0.167.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA= google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8= google.golang.org/api v0.176.1/go.mod h1:j2MaSDYcvYV1lkZ1+SMW4IeF90SrEyFA+tluDYWRrFg= google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= google.golang.org/api v0.178.0/go.mod h1:84/k2v8DFpDRebpGcooklv/lais3MEfqpaBLA12gl2U= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= google.golang.org/api v0.182.0/go.mod h1:cGhjy4caqA5yXRzEhkHI8Y9mfyC2VLTlER2l08xaqtM= google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ= @@ -4907,10 +4867,12 @@ google.golang.org/api v0.184.0/go.mod h1:CeDTtUEiYENAf8PPG5VZW2yNp2VM3VWbCeTioAZ google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk= google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag= google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.191.0/go.mod h1:tD5dsFGxFza0hnQveGfVk9QQYKcfp+VzgRqyXFxE0+E= google.golang.org/api v0.193.0/go.mod h1:Po3YMV1XZx+mTku3cfJrlIYR03wiGrCOsdpC67hjZvw= google.golang.org/api v0.196.0/go.mod h1:g9IL21uGkYgvQ5BZg6BAtoGJQIm8r6EgaAbpNey5wBE= google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= google.golang.org/api v0.205.0/go.mod h1:NrK1EMqO8Xk6l6QwRAmrXXg2v6dzukhlOyvkYtnvUuc= google.golang.org/api v0.210.0/go.mod h1:B9XDZGnx2NtyjzVkOVTGrFSAVZgPcbedzKg/gTLwqBs= @@ -4919,6 +4881,7 @@ google.golang.org/api v0.214.0/go.mod h1:bYPpLG8AyeMWwDU6NXoB00xC0DFkikVvd5Mfwox google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI= google.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY= google.golang.org/api v0.222.0/go.mod h1:efZia3nXpWELrwMlN5vyQrD4GmJN1Vw0x68Et3r+a9c= google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU= @@ -4934,6 +4897,7 @@ google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6c google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= google.golang.org/api v0.237.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.243.0/go.mod h1:GE4QtYfaybx1KmeHMdBnNnyLzBZCVihGBXAmJu/uUr8= google.golang.org/api v0.246.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8= google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= @@ -4941,6 +4905,46 @@ google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14w google.golang.org/api v0.250.0/go.mod h1:Y9Uup8bDLJJtMzJyQnu+rLRJLA0wn+wTtc6vTlOvfXo= google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= google.golang.org/api v0.260.0/go.mod h1:Shj1j0Phr/9sloYrKomICzdYgsSDImpTxME8rGLaZ/o= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -5483,6 +5487,7 @@ google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= @@ -5491,7 +5496,6 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -5610,13 +5614,13 @@ modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpN modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI= modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= -modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g= -modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= -modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= diff --git a/kubernetes/gateway-operator/go.mod b/kubernetes/gateway-operator/go.mod index bb9cb9c0eb..f16e154271 100644 --- a/kubernetes/gateway-operator/go.mod +++ b/kubernetes/gateway-operator/go.mod @@ -15,11 +15,13 @@ require ( gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.18.5 k8s.io/api v0.35.2 + k8s.io/apiextensions-apiserver v0.35.2 k8s.io/apimachinery v0.35.2 k8s.io/cli-runtime v0.33.3 k8s.io/client-go v0.35.2 sigs.k8s.io/controller-runtime v0.23.1 sigs.k8s.io/gateway-api v1.5.1 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -164,7 +166,6 @@ require ( google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/apiextensions-apiserver v0.35.2 // indirect k8s.io/apiserver v0.35.2 // indirect k8s.io/component-base v0.35.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect @@ -177,5 +178,4 @@ require ( sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/platform-api/README.md b/platform-api/README.md index 1cb53628cd..1f73b10511 100644 --- a/platform-api/README.md +++ b/platform-api/README.md @@ -6,7 +6,7 @@ Backend service that powers the API Platform portals, gateways, and automation f ### Prerequisites -Before using the Platform API, obtain a bearer token for authentication. In local JWT mode (default) you can generate a token using the configured `APIP_CP_AUTH_JWT_SECRET_KEY`. In IDP mode, obtain a token from your identity provider. +Before using the Platform API, obtain a bearer token for authentication. In `file` or `external_token` auth mode you can generate a token using the HMAC key configured at `platform_api.auth.jwt.secret_key`. In `idp` mode, obtain a token from your identity provider. See [Configuration](#configuration) below. ### Build and Run @@ -20,22 +20,36 @@ cd platform-api go run ./cmd/main.go ``` +`config/config.toml` is the local-development config, used with `platform_api.auth.mode = "file"` +(username/password login backed by the organization/user block in that file) — the same mode the +AI Workspace and Developer Portal quickstarts use, so it works out of the box with either, with no +env vars set. It's the one Platform API config shared by every quickstart (both docker-compose +setups mount it directly), so its admin user's scopes cover both the `ap:*` (AI Workspace / +platform-admin) and `dp:*` (Developer Portal) namespaces. Set `APIP_CP_ADMIN_USERNAME` / +`APIP_CP_ADMIN_PASSWORD_HASH` to pick your own login credentials (generate a hash with +`htpasswd -bnBC 12 "" | tr -d ':\n'`), or set `platform_api.auth.mode = "external_token"` +for locally-signed HMAC tokens with no local users — see +[`config/config-template.toml`](config/config-template.toml) for the full reference. + ### Database Configuration -Platform API supports `sqlite3` (default), `postgres`, and `sqlserver`. +Platform API supports `sqlite3` (default), `postgres`, and `sqlserver`. Configure the driver +under `[platform_api.database]` in your config file, e.g. for SQL Server: -```bash -# SQL Server example -export DATABASE_DRIVER=sqlserver -export DATABASE_HOST=sqlserver.example.internal -export DATABASE_PORT=1433 -export DATABASE_NAME=platform_api -export DATABASE_USER=sa -export DATABASE_PASSWORD='' -export DATABASE_SSL_MODE=disable +```toml +[platform_api.database] +driver = "sqlserver" +host = "sqlserver.example.internal" +port = "1433" +name = "platform_api" +username = "sa" +password = '{{ env "DB_PASSWORD" }}' # or '{{ file "/secrets/platform-api/db_password" }}' +ssl_mode = "disable" +``` +```bash cd platform-api -go run ./cmd/main.go +go run ./cmd/main.go -config config/config.toml ``` ### Step-by-Step Workflow @@ -230,120 +244,63 @@ The connected gateway will receive a deployment event via WebSocket: ## Configuration Configuration is read from a TOML config file (`-config `), layered over built-in -defaults. **Environment variables do not override config keys directly.** The only way an -environment variable affects a setting is through an explicit `{{ env "NAME" }}` interpolation -token placed in the config file, which is resolved at load time via `os.LookupEnv`; a field -with no token always takes its literal TOML value (or the built-in default). See "Providing -secrets via the config file" below. - -One variable is intentionally **not** prefixed: the shared `APIP_CONFIG_FILE_SOURCE_ALLOWLIST`. The `{{ env "NAME" }}` interpolation -tokens in the config file read the literal name via `os.LookupEnv` (independent of the koanf -prefix mechanism); the samples use the same `APIP_CP_`-prefixed names for one consistent -namespace — e.g. `{{ env "APIP_CP_ENCRYPTION_KEY" }}` (see "Providing secrets via the config -file" below). - -### Authentication - -Two authentication modes are supported. Exactly one should be active at a time. - -``` -APIP_CP_AUTH_IDP_ENABLED=false (default) → Local JWT mode (HMAC signature verification) -APIP_CP_AUTH_IDP_ENABLED=true → IDP mode (JWKS-based verification) -``` - -> `APIP_CP_ENCRYPTION_KEY` and `APIP_CP_AUTH_JWT_SECRET_KEY` are **required**; startup -> fails without them. TLS certificates are likewise required whenever the HTTPS -> listener is enabled — the server never generates a self-signed pair. - ---- - -#### Local JWT Mode (default) - -The server signs and validates HMAC login tokens using the key that `auth.jwt.secret_key` resolves to — a 32-byte value (64 hex chars or base64). This key is **required** at startup whenever local JWT or file-based auth is enabled and is never generated; a missing or malformed value fails startup. The sample config reads it from `{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}`. - -`auth.jwt.skip_validation` is a development-only switch that accepts bearer values without a signature check. It is honored **only in demo mode** — startup is rejected if it is `true` while `APIP_DEMO_MODE=false` — and it does **not** remove the required secret key above. - -| Variable | Default | Description | -|---|---|---| -| `APIP_CP_AUTH_JWT_SECRET_KEY` | _(empty)_ | HMAC key for signing/verifying login JWTs — 32-byte value (64 hex or base64; `openssl rand -hex 32`). **Required** whenever local JWT or file-based auth is enabled. | -| `APIP_CP_AUTH_JWT_ISSUER` | `platform-api` | Expected `iss` claim value | -| `APIP_CP_AUTH_JWT_SKIP_VALIDATION` | `false` | Skip signature verification — **development only**, honored solely in demo mode | - -Run locally. The config file supplies the `{{ env }}` tokens, so export the referenced variable and pass `-config`: -```bash -export APIP_CP_AUTH_JWT_SECRET_KEY="$(openssl rand -hex 32)" -go run ./cmd/main.go -config config/config.toml -``` - -To skip signature checks during local development, set `skip_validation = true` under `[auth.jwt]` in the config file (demo mode only) and run the same command. - -**Legacy variable names.** These unprefixed names are **no longer read** — environment -variables affect configuration only through `{{ env "…" }}` tokens (see above). Use the -current token variable name (or the config key directly) instead: - -| Old name | New name | -|---|---| -| `JWT_SECRET_KEY` | `APIP_CP_AUTH_JWT_SECRET_KEY` | -| `JWT_ISSUER` | `APIP_CP_AUTH_JWT_ISSUER` | -| `JWT_SKIP_VALIDATION` | `APIP_CP_AUTH_JWT_SKIP_VALIDATION` | -| `JWT_SKIP_PATHS` | `APIP_CP_AUTH_SKIP_PATHS` | - ---- +defaults. **There are no fixed, prescriptive environment variable names** — a key omitted from +the file simply falls back to its built-in default, a literal value in the file is used as-is, +and the only way an environment variable (or a mounted file) affects a setting is by writing an +explicit interpolation token as that key's value: -#### IDP Mode - -Tokens are validated against any standards-compliant identity provider (Thunder, Asgardeo, Keycloak, Azure AD, Okta, etc.) using its JWKS endpoint. Set `APIP_CP_AUTH_IDP_ENABLED=true` and supply at minimum `APIP_CP_AUTH_IDP_JWKS_URL` and `APIP_CP_AUTH_IDP_ISSUER`. - -| Variable | Default | Description | -|---|---|---| -| `APIP_CP_AUTH_IDP_ENABLED` | `false` | Set to `true` to activate IDP mode | -| `APIP_CP_AUTH_IDP_NAME` | _(empty)_ | Optional label shown in startup logs (e.g. `thunder`, `asgardeo`) | -| `APIP_CP_AUTH_IDP_JWKS_URL` | _(required)_ | IDP's JWKS endpoint for public key retrieval | -| `APIP_CP_AUTH_IDP_ISSUER` | _(required)_ | Accepted JWT issuer | -| `APIP_CP_AUTH_IDP_AUDIENCE` | _(empty)_ | Accepted JWT audience. When set, the token's `aud` claim must contain this value; empty skips the check | -| `APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME` | `organization` | JWT claim holding the org UUID for the active session | -| `APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_ORG_NAME_CLAIM_NAME` | `org_name` | JWT claim for the org display name | -| `APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_ORG_HANDLE_CLAIM_NAME` | `org_handle` | JWT claim for the org URL-safe handle | -| `APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_USER_ID_CLAIM_NAME` | `sub` | JWT claim used as the canonical user identifier | -| `APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_USERNAME_CLAIM_NAME` | `username` | JWT claim for the human-readable username | -| `APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_EMAIL_CLAIM_NAME` | `email` | JWT claim for the user's email address | -| `APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_SCOPE_CLAIM_NAME` | `scope` | JWT claim carrying granted OAuth2 scopes | -| `APIP_CP_AUTH_IDP_VALIDATION_MODE` | `scope` | Authorization mode: `scope` (validate scope claim directly) or `role` (expand IDP roles to platform roles) | -| `APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_ROLES_CLAIM_PATH` | _(empty)_ | Dot-notation path to the roles claim (e.g. `realm_access.roles`). Required when `APIP_CP_AUTH_IDP_VALIDATION_MODE=role` | -| `APIP_CP_AUTH_IDP_ROLE_MAPPINGS` | _(empty)_ | Comma-separated `idp-role=platform-role` pairs (e.g. `PLATFORM_ADMIN=admin,PLATFORM_DEV=developer`). When empty, IDP role values are used as-is | - -**Example — Asgardeo:** -```bash -export APIP_CP_AUTH_IDP_ENABLED=true -export APIP_CP_AUTH_IDP_NAME=asgardeo -export APIP_CP_AUTH_IDP_JWKS_URL=https://api.asgardeo.io/t//oauth2/jwks -export APIP_CP_AUTH_IDP_ISSUER=https://api.asgardeo.io/t//oauth2/token -export APIP_CP_AUTH_IDP_AUDIENCE= -export APIP_CP_AUTH_IDP_ORGANIZATION_CLAIM_NAME=organizationId -export APIP_CP_AUTH_IDP_VALIDATION_MODE=scope -export APIP_CP_AUTH_IDP_ROLES_CLAIM_PATH=scope +```toml +some_key = '{{ env "ANY_VAR_NAME" "optional-default" }}' # from an env var, with a fallback +some_key = '{{ env "ANY_VAR_NAME" }}' # from an env var, no fallback — unset fails config load +some_key = '{{ file "/secrets/platform-api/some-file" }}' # from a mounted file (preferred for secrets) ``` ---- +The name inside the token (`ANY_VAR_NAME`) is a free choice — it's read via `os.LookupEnv` at +load time and isn't tied to any specific naming scheme. [`config/config-template.toml`](config/config-template.toml) +is the authoritative reference: it lists every key the binary reads, each already wrapped in an +`{{ env }}` token using the `APIP_CP_*` naming convention as one consistent example — copy it and +edit the values, or replace the tokens with plain literals. `{{ file }}` reads are restricted to +an allowlisted directory (default `/etc/platform-api`, `/secrets/platform-api`) and fail closed: +a missing/empty required source, or a missing/disallowed/oversize file, aborts startup. -#### Skip Paths +### Key sections -Path prefixes listed here bypass authentication entirely. Used for internal gateway traffic and health checks. +All settings live under `[platform_api]` / `[platform_api.*]`. The main sections: -| Variable | Default | +| Section | Purpose | |---|---| -| `APIP_CP_AUTH_SKIP_PATHS` | `/health,/metrics,/api/internal/v1/ws/gateways/connect,...` | - -To extend the default list: -```bash -export APIP_CP_AUTH_SKIP_PATHS="/health,/metrics,/api/internal/v1/ws/gateways/connect,/my-custom-path" -``` - ---- - -### Role-Based Access Control (RBAC) - -Per-route scope checks are enforced when `APIP_CP_ENABLE_SCOPE_VALIDATION=true`. Five built-in platform roles exist: +| `[platform_api]` | resource paths | +| `[platform_api.logging]` | `level`, `format` | +| `[platform_api.security]` | `encryption_key` (**required** — at-rest AES-256 key, 32 bytes as hex or base64, never auto-generated) | +| `[platform_api.security.api_key]` | `hashing_algorithms` accepted for API key verification | +| `[platform_api.database]` | `driver` (`sqlite3` / `postgres` / `sqlserver`), connection fields, pool sizing | +| `[platform_api.auth]` | `mode` — one of `external_token`, `file`, or `idp`; `scope_validation`; `skip_paths` | +| `[platform_api.auth.jwt]` | Asymmetric (RS256) token settings: `issuer`, `public_key` (**required** — PEM RSA public key, verifies tokens), `private_key` (**required in `file` mode** — PEM RSA private key, signs login tokens), `token_ttl` | +| `[platform_api.auth.idp]` / `[platform_api.auth.claim_mappings]` | JWKS endpoint, issuer/audience, validation mode, and JWT claim-name mappings for `idp` mode | +| `[platform_api.auth.file.organization]` / `[[platform_api.auth.file.users]]` | Local org + username/password/scope entries for `file` mode | +| `[platform_api.server.http]` / `[platform_api.server.https]` | Listener enablement, ports, and (HTTPS) `cert_file` / `key_file` paths (certificates are always required for HTTPS — no self-signed fallback) | +| `[platform_api.server.timeouts]` | Read/write/idle timeouts | +| `[platform_api.server.cors]` | `allowed_origins` for credentialed cross-origin requests | +| `[platform_api.server.websocket]` | Gateway WebSocket connection limits and rate limiting | +| `[platform_api.deployments]` | Deployment caps and stuck-deployment timeout handling | +| `[platform_api.gateway]` | Gateway registration verification toggles | +| `[platform_api.event_hub]` | Multi-replica event delivery polling/retention | +| `[platform_api.webhook]` | Developer Portal webhook receiver: `enabled`, `secret` (required when enabled), signature/body limits | + +#### Authentication modes + +`platform_api.auth.mode` selects exactly one mode; only that mode's section is read: + +- **`external_token`** — verify locally-issued, asymmetrically-signed (RS256) JWTs (`[platform_api.auth.jwt]`); tokens are minted externally (e.g. by the Developer Portal) and signed with the matching RSA private key, verified here against `public_key`. Symmetric (HMAC) and unsigned (`none`) tokens are rejected. +- **`file`** — `external_token` plus local username/password login: the login endpoint authenticates against `[platform_api.auth.file]` and issues RS256 JWTs signed with `[platform_api.auth.jwt].private_key`, verified with the matching `public_key`. Used by the AI Workspace and Developer Portal quickstarts. +- **`idp`** — validate tokens against an external IDP's JWKS endpoint (Thunder, Asgardeo, Keycloak, Azure AD, Okta, etc.) via `[platform_api.auth.idp]`; `jwks_url` and `issuer` are required. + +`platform_api.auth.skip_paths` is a structured list (not a scalar), so it's edited directly in +the file rather than through a single token; setting it replaces the built-in default list. + +#### Role-Based Access Control (RBAC) + +Per-route scope checks are enforced when `platform_api.auth.scope_validation = true`. Five built-in platform roles exist: | Role | Persona | Access level | |---|---|---| @@ -353,91 +310,43 @@ Per-route scope checks are enforced when `APIP_CP_ENABLE_SCOPE_VALIDATION=true`. | `operator` | CI/CD service account | Deploy and undeploy operations only; cannot create resources or manage credentials | | `viewer` | Auditor | Read-only access to all resources | -| Variable | Default | Description | -|---|---|---| -| `APIP_CP_ENABLE_SCOPE_VALIDATION` | `false` | Set to `true` to enforce per-route scope/role checks | - -In **local JWT mode**, scopes are read directly from the `scope` claim in the token. -In **IDP mode with `APIP_CP_AUTH_IDP_VALIDATION_MODE=scope`**, scopes are read from the claim named by `APIP_CP_AUTH_IDP_SCOPE_CLAIM_NAME`. -In **IDP mode with `APIP_CP_AUTH_IDP_VALIDATION_MODE=role`**, IDP roles are resolved from `APIP_CP_AUTH_IDP_ROLES_CLAIM_PATH`, mapped via `APIP_CP_AUTH_IDP_ROLE_MAPPINGS`, and matched against the required roles for each route. +All three modes read identity fields — including scope — through the same +`[platform_api.auth.claim_mappings]` table (`scope` defaults to the `scope` claim); `file` mode's +login endpoint also signs the tokens it issues using these same claim names, so issuance and +validation never drift apart. In **`idp` mode**, `validation_mode` additionally controls whether +authorization uses the scope claim directly or expands IDP roles from `claim_mappings.roles` via +`role_mappings`. ---- +### Providing secrets via the config file -### Database - -| Variable | Default | Description | -|---|---|---| -| `APIP_CP_DATABASE_DRIVER` | `sqlite3` | `sqlite3` or `postgres` | -| `APIP_CP_DATABASE_DB_PATH` | `./data/api_platform.db` | SQLite file path (ignored for Postgres) | -| `APIP_CP_DATABASE_HOST` | `localhost` | Postgres host | -| `APIP_CP_DATABASE_PORT` | `5432` | Postgres port | -| `APIP_CP_DATABASE_NAME` | `platform_api` | Postgres database name | -| `APIP_CP_DATABASE_USER` | _(empty)_ | Postgres username | -| `APIP_CP_DATABASE_PASSWORD` | _(empty)_ | Postgres password | -| `APIP_CP_DATABASE_SSL_MODE` | `disable` | Postgres SSL mode (`disable`, `require`, `verify-full`) | -| `APIP_CP_DATABASE_EXECUTE_SCHEMA_DDL` | `true` | Set to `false` when the DB user lacks DDL privileges | - ---- - -### Encryption - -`APIP_CP_ENCRYPTION_KEY` protects all at-rest encryption (secrets, subscription tokens, WebSub HMAC secrets). It is **never auto-generated** — the operator must provide it. - -| Variable | Default | Description | -|---|---|---| -| `APIP_CP_ENCRYPTION_KEY` | _(empty)_ | **Required.** 32-byte AES-256 key as 64 hex chars or base64 (32 bytes). Generate with `openssl rand -hex 32`. Startup fails if missing or malformed. | - -#### Providing secrets via the config file (preferred over raw values) - -When the Platform API is configured from a TOML file, do **not** write raw key values into -it and do **not** hardcode them as literal env vars in a compose file. Reference each secret -(`APIP_CP_ENCRYPTION_KEY`, `APIP_CP_AUTH_JWT_SECRET_KEY`, `APIP_CP_DATABASE_PASSWORD`, -`APIP_CP_WEBHOOK_SECRET`, …) with an interpolation token that is resolved at startup, -preferring a mounted file: +Never write raw secret values into the config file, and never hardcode them as literals in a +compose file. Reference each secret (`security.encryption_key`, `auth.jwt.secret_key`, +`database.password`, `webhook.secret`, …) with an interpolation token, preferring a mounted file +over an env var: ```toml +[platform_api.security] encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' # from an env var # preferred — from a mounted secret file: # encryption_key = '{{ file "/secrets/platform-api/encryption_key" }}' ``` For the `{{ env }}` form, supply the value from a git-ignored env file rather than the shell or -the compose file — the samples keep secrets in `keys.env` and start the stack with -`docker compose --env-file keys.env up`, which the compose forwards into the container -via an `environment:` `${APIP_CP_…}` passthrough (never an `env_file:` block or a hardcoded value). - -`{{ file }}` reads are restricted to an allowlist (`/etc/platform-api`, `/secrets/platform-api`; -override with the shared `APIP_CONFIG_FILE_SOURCE_ALLOWLIST` env var). Resolution fails closed: -a missing/empty required env var, or a missing/disallowed/oversize file, aborts startup. +a hardcoded literal in the compose file — the samples keep secrets in `api-platform.env` and +mount it into the container via an `env_file:` entry (`format: raw`, since a bcrypt hash can +contain `$`, which must not be treated as compose interpolation): + +```yaml +services: + platform-api: + env_file: + - path: api-platform.env + required: true + format: raw +``` --- -### Other Settings - -| Variable | Default | Description | -|---|---|---| -| `LOG_LEVEL` | `DEBUG` | Log verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`) | -| `HTTPS_ENABLED` | `true` | Enable the TLS listener. Certificates are read from `HTTPS_CERT_DIR` (cert.pem / key.pem — required) | -| `HTTPS_PORT` | `9243` | Port for the TLS listener | -| `HTTPS_CERT_DIR` | `./data/certs` | Directory holding `cert.pem` / `key.pem` (used only when `HTTPS_ENABLED=true`) | -| `HTTP_ENABLED` | `false` | Enable the plain-HTTP listener. Use only behind a TLS-terminating ingress/sidecar or for internal traffic — never expose directly to untrusted networks | -| `HTTP_PORT` | `9080` | Port for the plain-HTTP listener | -| `TIMEOUTS_READ_HEADER` | `10s` | Max time to read request headers, on both listeners (`0` disables) | -| `TIMEOUTS_READ` | `60s` | Max time to read the whole request, including the body (`0` disables) | -| `TIMEOUTS_WRITE` | `120s` | Max time for handler execution plus response write (`0` disables) | -| `TIMEOUTS_IDLE` | `120s` | Max time a keep-alive connection may sit unused (`0` disables) | -| `DEPLOYMENTS_MAX_PER_API_GATEWAY` | `20` | Maximum deployments per API per gateway | -| `DEPLOYMENTS_TRANSITIONAL_STATUS_ENABLED` | `false` | Show `DEPLOYING`/`UNDEPLOYING` status before gateway ack | -| `ARTIFACT_LIMITS_MAX_LLM_PROVIDERS_PER_ORG` | _unlimited_ | Max LLM providers per organization (`0` or unset = unlimited) | -| `ARTIFACT_LIMITS_MAX_LLM_PROXIES_PER_ORG` | _unlimited_ | Max LLM proxies per organization (`0` or unset = unlimited) | -| `ARTIFACT_LIMITS_MAX_MCP_PROXIES_PER_ORG` | _unlimited_ | Max MCP proxies per organization (`0` or unset = unlimited) | -| `ARTIFACT_LIMITS_MAX_WEBSUB_APIS_PER_ORG` | _unlimited_ | Max WebSub APIs per organization (`0` or unset = unlimited) | -| `ARTIFACT_LIMITS_MAX_WEBBROKER_APIS_PER_ORG` | _unlimited_ | Max WebBroker APIs per organization (`0` or unset = unlimited) | -| `GATEWAY_ENABLE_VERSION_VERIFICATION` | `false` | Reject gateway connections with mismatched versions | -| `API_KEY_HASHING_ALGORITHMS` | `sha256` | Comma-separated hash algorithms for API key storage | - -> The legacy `PORT`, `TLS_ENABLED`, and `TLS_CERT_DIR` env vars are still honored and map onto the HTTPS listener (`HTTPS_PORT`, `HTTPS_ENABLED`, `HTTPS_CERT_DIR`). - ## Documentation See [spec/](spec/) for product, architecture, design, and implementation documentation. diff --git a/platform-api/cmd/main.go b/platform-api/cmd/main.go index dc2ecc949b..0b42a05c78 100644 --- a/platform-api/cmd/main.go +++ b/platform-api/cmd/main.go @@ -37,8 +37,8 @@ func main() { // Initialize logger logConfig := logger.Config{ - Level: cfg.LogLevel, - Format: cfg.LogFormat, + Level: cfg.Logging.Level, + Format: cfg.Logging.Format, } slogger := logger.NewLogger(logConfig) @@ -50,9 +50,9 @@ func main() { } slogger.Info("Starting server", - "http_enabled", cfg.HTTP.Enabled, "http_port", cfg.HTTP.Port, - "https_enabled", cfg.HTTPS.Enabled, "https_port", cfg.HTTPS.Port) - if err := srv.Start(cfg.HTTP, cfg.HTTPS, cfg.Timeouts); err != nil { + "http_enabled", cfg.Listeners.HTTP.Enabled, "http_port", cfg.Listeners.HTTP.Port, + "https_enabled", cfg.Listeners.HTTPS.Enabled, "https_port", cfg.Listeners.HTTPS.Port) + if err := srv.Start(cfg.Listeners, cfg.Listeners.Timeouts); err != nil { slogger.Error("Failed to start server", "error", err) os.Exit(1) } diff --git a/platform-api/config/config-template.toml b/platform-api/config/config-template.toml index d3535ee3ba..5b2815dab7 100644 --- a/platform-api/config/config-template.toml +++ b/platform-api/config/config-template.toml @@ -10,86 +10,115 @@ # Platform API configuration template # # Copy this file to your deployment's config location and edit the values. -# This file is read by the Platform API (Go binary). +# Read by the Platform API (Go binary). Lists EVERY key the Platform API +# reads, so it doubles as the configuration reference — every key below is +# already at its built-in default, so deleting a key just restores that +# default. Ships with distributions (e.g. the AI Workspace zip), copied at +# build time from here. # -# This template lists EVERY key the Platform API reads, so it doubles as the -# configuration reference. Every key is set to its built-in default unless the -# comment above it says otherwise; deleting a key simply restores its default. +# Every key below is a plain literal — edit the value directly. For secrets +# (encryption_key, admin credentials, the webhook secret) that means filling +# in a real value before deploying; an empty literal is a placeholder, not a +# safe default. For a value that should come from the environment or a +# mounted file instead, use an interpolation token: # -# This template is maintained here, next to the Platform API source, and is -# copied into distributions (e.g. the AI Workspace zip) at build time. +# key = '{{ env "APIP_CP_VAR" "default" }}' +# key = '{{ file "/secrets/platform-api/key" }}' # -# Precedence: environment variable (APIP_CP_ prefix) > this file > built-in default. +# Both fail closed at startup — a missing file, a file outside the allowed +# source directories (default /etc/platform-api and /secrets/platform-api; +# override with APIP_CONFIG_FILE_SOURCE_ALLOWLIST), or an unset variable with +# no default all refuse to start rather than run with an empty value. # -# QUICK START (file-based auth mode — no external IDP needed): +# Never write a secret as a raw literal in a file committed to version +# control or hardcode one in docker-compose.yaml. +# +# QUICK START (file auth mode — no external IDP needed): # 1. Copy this file to config-platform-api.toml. -# 2. Generate two 32-byte keys: openssl rand -hex 32 (one for APIP_CP_ENCRYPTION_KEY, -# one for APIP_CP_AUTH_JWT_SECRET_KEY). -# 3. Put both in `api-platform.env` file. -# The encryption_key / secret_key fields below read them via {{ env "..." }} -# tokens — never paste raw key values into this file. -# 4. Set [auth.file_based] enabled = true and configure users. +# 2. Set security.encryption_key: openssl rand -hex 32. +# 3. Generate an RS256 JWT keypair and mount it at the paths named by +# auth.jwt.public_key_file / private_key_file below: +# openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ +# -out jwt_private.pem +# openssl rsa -in jwt_private.pem -pubout -out jwt_public.pem +# 4. Set [platform_api.auth] mode = "file" and configure users. # 5. Run: docker compose up # -# Secrets are resolved at load time from environment variables ({{ env "..." }}) or -# mounted files ({{ file "..." }}, preferred in production). Do not hardcode secret -# values in this file or as literals in docker-compose.yaml. A {{ env }} / {{ file }} -# token whose source is unset fails config load — only reference sources that exist. -# -# OIDC mode: keep file_based disabled and set the [auth.idp] fields. +# OIDC mode: set [platform_api.auth] mode = "idp" and configure [platform_api.auth.idp]. # -------------------------------------------------------------------- # --------------------------------------------------------------------------- -# Server +# All Platform API settings live under [platform_api] / [platform_api.*]. # --------------------------------------------------------------------------- -log_level = "INFO" # DEBUG | INFO | WARN | ERROR - -# Log output encoding. Use "json" when shipping logs to an aggregator. -log_format = "text" # text | json +[platform_api] -# Scope-based authorization for API endpoints. When true, every request must -# carry the OAuth2 scope its endpoint declares. -# Set to false only to temporarily bypass during development. -enable_scope_validation = true - -# Paths to resources the binary loads at startup. The defaults are correct for -# the published container image; override only for a custom layout. +# Resource paths loaded at startup. Defaults match the published container +# image — override only for a custom layout. db_schema_path = "./internal/database/schema.sql" openapi_spec_path = "./resources/openapi.yaml" llm_template_definitions_path = "./resources/default-llm-provider-templates" +# Byte cap when fetching a remote OpenAPI spec by URL. <= 0 uses the +# built-in 5 MiB default. +openapi_spec_max_fetch_bytes = 5242880 + +[platform_api.logging] +# Level is matched case-insensitively; lowercase is canonical. +level = "info" # debug | info | warn | error + +# Log encoding. Use "json" when shipping logs to an aggregator. +format = "text" # text | json + # --------------------------------------------------------------------------- -# Encryption +# Security # --------------------------------------------------------------------------- -# Single 32-byte key (64 hex chars or base64) used for ALL at-rest encryption -# (secrets, subscription tokens, WebSub HMAC secrets). -# REQUIRED — the server refuses to start without it. Generate with: +[platform_api.security] +# Single 32-byte key (64 hex chars or base64) used for ALL at-rest +# encryption (secrets, subscription tokens, WebSub HMAC secrets). +# REQUIRED — an empty value fails config load. Generate with: # openssl rand -hex 32 -# Resolved from the APIP_CP_ENCRYPTION_KEY env var in `api-platform.env`. Files preferred: +# Prefer a mounted secret file in production: # encryption_key = '{{ file "/secrets/platform-api/encryption_key" }}' -encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' +encryption_key = "" + +[platform_api.security.api_key] +# Accepted API key hashing algorithms (comma-separated). sha256 is the +# default; add "sha512" to accept both. +hashing_algorithms = "sha256" # --------------------------------------------------------------------------- # Database # --------------------------------------------------------------------------- -[database] -driver = "sqlite3" # "sqlite3" or "postgres" +[platform_api.database] +driver = "sqlite3" # "sqlite3" | "postgres" | "postgresql" | "pgx" | "sqlserver" | "mssql" -# SQLite — ignored when driver = "postgres". Default: ./data/api_platform.db +# SQLite — ignored when driver = "postgres". path = "/app/data/api_platform.db" -# PostgreSQL — ignored when driver = "sqlite3"; set driver = "postgres" above to use. +# PostgreSQL/SQL Server — ignored when driver = "sqlite3". host, port, name, +# and user are required for every non-sqlite3 driver, or config load fails. host = "localhost" port = 5432 name = "platform_api" user = "platform_api" -# Resolve the password from a mounted secret file (preferred) or env var, e.g. +# Prefer a mounted secret file in production, e.g. # password = '{{ file "/secrets/platform-api/postgres_password" }}' -# The referenced file/env var must exist or config load fails. +# The "" default here is safe because sqlite3 needs no password; set a real +# value when driver is a non-sqlite3 database. password = "" ssl_mode = "disable" # "disable" | "require" | "verify-ca" | "verify-full" -# Connection pool — tune for production workloads +# CA cert used to verify the server's certificate. Required when ssl_mode +# is "verify-ca" or "verify-full"; ignored otherwise. +ssl_root_cert = "" + +# Client cert/key pair for mutual TLS (PostgreSQL only — SQL Server's driver +# has no client-certificate support). Both must be set together or both +# left empty. +ssl_cert = "" +ssl_key = "" + +# Connection pool — tune for production workloads. max_open_conns = 25 # maximum open connections max_idle_conns = 10 # maximum idle connections in the pool conn_max_lifetime = 300 # seconds before a connection is recycled @@ -97,16 +126,31 @@ conn_max_lifetime = 300 # seconds before a connection is recycled # --------------------------------------------------------------------------- # Authentication # -# Exactly one of jwt / idp / file_based drives request authentication; enabling -# the IDP alongside a local mode is rejected at startup. +# auth.mode selects exactly one mode: +# "external_token" — verify locally-issued, asymmetrically-signed (RS256) JWTs, +# minted externally (e.g. by the Developer Portal) and +# signed with the matching RSA private key; verified here +# against auth.jwt.public_key_file. +# "file" — external_token + local username/password login: the +# login endpoint authenticates from +# [platform_api.auth.file] and issues RS256 JWTs signed +# with auth.jwt.private_key_file. +# "idp" — validate tokens against an external IDP's JWKS +# ([platform_api.auth.idp]). +# Only the selected mode's section is read; the others are ignored. # --------------------------------------------------------------------------- -[auth] +[platform_api.auth] +mode = "file" + +# Enforce per-endpoint OAuth2 scopes on validated tokens. Set false only to +# temporarily bypass authorization during development. +scope_validation = true -# Paths that bypass the auth middleware entirely. The list below is the built-in -# default: the health/metrics probes, the file-based login endpoint, and the -# internal gateway routes (which authenticate with a gateway token instead). -# Setting this key REPLACES the default list — keep the entries you still need. -# Env var is APIP_CP_AUTH_SKIP_PATHS (comma-separated). +# Paths that bypass the auth middleware entirely. The list below is the +# built-in default: health/metrics probes, the file-based login endpoint, +# and internal gateway routes (which authenticate with a gateway token +# instead). Setting this key REPLACES the default list — keep the entries +# you still need. skip_paths = [ "/health", "/metrics", @@ -126,121 +170,125 @@ skip_paths = [ "/api/internal/v0.9/webhook/events", ] -# JWT (local HMAC) — the default for self-hosted deployments. -# Disable when delegating auth entirely to an external IDP. -# Resolved from APIP_CP_AUTH_JWT_SECRET_KEY in `api-platform.env` file; -# secret files preferred: secret_key = '{{ file "/secrets/platform-api/jwt_secret" }}' -[auth.jwt] -enabled = true -issuer = "platform-api" -# Signing key. REQUIRED — a 32-byte key (64 hex chars or base64); never generated. -secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' -# Skip signature verification. MUST stay false — the server refuses to start -# when signature validation is disabled. -skip_validation = false - -# IDP (JWKS-based) — enable instead of JWT when using Asgardeo, Keycloak, Auth0, etc. -# jwks_url and issuer are required when enabled = true. -[auth.idp] -enabled = false +# JWT claim name mappings — shared by all three auth modes ("idp" reads +# incoming claims by these names; "file" mode's login endpoint signs tokens +# using these names; "external_token" mode reads externally-minted tokens by +# these names too), so issuance and validation never drift apart. Each value +# is either a flat top-level claim name ("org_id") or a dot-separated path +# into a nested claim ("realm_access.org_id") — useful for IDPs like +# Keycloak that nest fields such as roles under realm_access.roles. +[platform_api.auth.claim_mappings] +organization = "organization" # claim carrying the org ID +org_name = "org_name" # claim carrying the org display name +org_handle = "org_handle" # claim carrying the org URL slug +user_id = "sub" # claim used as the user's unique ID +username = "username" +email = "email" +scope = "scope" # space-separated scope string +roles = "" # e.g. "realm_access.roles" (Keycloak) or "roles" (Asgardeo) + +# IDP (JWKS-based) — used when mode = "idp" (Asgardeo, Keycloak, Auth0, etc.). +# jwks_url and issuer are required in that mode. +[platform_api.auth.idp] name = "asgardeo" # friendly name for logging jwks_url = "https://accounts.example.com/oauth2/jwks" issuer = ["https://accounts.example.com"] # list of accepted issuers audience = [] # accepted "aud" values; empty = skip audience check # Authorization mode: "scope" (default) checks the scope claim; -# "role" checks a roles claim at claim_mappings.roles_claim_path. -validation_mode = "scope" -role_mappings_file = "" # path to a YAML file mapping IDP roles to platform scopes - -# JWT claim name mappings — configure to match your IDP's token structure. -[auth.idp.claim_mappings] -organization_claim_name = "organization" # claim carrying the org ID -org_name_claim_name = "org_name" # claim carrying the org display name -org_handle_claim_name = "org_handle" # claim carrying the org URL slug -user_id_claim_name = "sub" # claim used as the user's unique ID -username_claim_name = "username" -email_claim_name = "email" -scope_claim_name = "scope" # space-separated scope string -roles_claim_path = "" # JSON path to roles array, e.g. "realm_access.roles" - -# File-based auth — local username/password login. Ideal for initial / air-gapped -# setup; not recommended for production — prefer an IDP. -# When enabled, IDP is NOT used for Platform API authentication. -[auth.file_based] -enabled = true - -[auth.file_based.organization] -id = "default" # Required: organization handle (URL-safe slug) +# "role" checks the roles claim configured below at claim_mappings.roles. +validation_mode = "scope" +role_mappings = "" # path to a YAML file mapping IDP roles to platform scopes + +# File auth — local username/password login, used when mode = "file". Ideal +# for initial / air-gapped setup; not recommended for production — prefer +# an IDP. +[platform_api.auth.file.organization] +id = "default" # Required: organization handle (URL-safe slug) display_name = "Default" region = "us" # Platform organization UUID, emitted as the `organization` claim in issued -# tokens. Generated at startup when unset; pin it to keep the organization -# stable across fresh databases. +# tokens. Pin it to keep the organization stable across fresh databases. uuid = "99089a17-72e0-4dd8-a2f4-c8dfbb085295" -# Add one [[auth.file_based.users]] block per user. +# Add one [[platform_api.auth.file.users]] block per user. # Generate a bcrypt hash with: htpasswd -bnBC 12 "" | tr -d ':\n' -# The env-var equivalent (APIP_CP_AUTH_FILE_BASED_USERS) takes a JSON array instead. -[[auth.file_based.users]] -# The quickstart resolves these from api-platform.env (generated by setup.sh). -username = '{{ env "APIP_CP_ADMIN_USERNAME" }}' -password_hash = '{{ env "APIP_CP_ADMIN_PASSWORD_HASH" }}' +[[platform_api.auth.file.users]] +# REQUIRED — an empty username/password_hash fails config load rather than +# starting with a blank or guessable credential. +username = "" +password_hash = "" # Full scope set — trim to restrict permissions for this user. scopes = "ap:organization:manage ap:gateway:manage ap:gateway_custom_policy:manage ap:rest_api:manage ap:llm_provider:manage ap:llm_proxy:manage ap:mcp_proxy:manage ap:webbroker_api:manage ap:websub_api:manage ap:application:manage ap:subscription:manage ap:subscription_plan:manage ap:project:manage ap:llm_template:manage ap:devportal:manage ap:api_key:read ap:secret:manage" # Additional users — uncomment the WHOLE block (including the [[...]] header) # and replace the placeholder hash with a real bcrypt hash before use. -# [[auth.file_based.users]] +# [[platform_api.auth.file.users]] # username = "readonly" # password_hash = "$2a$12$" # scopes = "ap:organization:read ap:gateway:read ap:rest_api:read ap:llm_provider:read" +# JWT (local RS256) — used by "external_token" and "file" modes. Tokens are +# signed asymmetrically: "external_token" only verifies externally-minted tokens +# with the public key; "file" also signs the tokens its login endpoint issues +# with the private key. Symmetric (HMAC) and unsigned ("none") tokens are +# rejected. Keys are mounted PEM files, referenced here by path only — the +# server reads and parses them itself at startup/use, so the PEM content is +# never inlined into config (scripts/setup.sh generates the pair). Mount your +# keys at these paths, or point elsewhere by editing the path directly. +[platform_api.auth.jwt] +issuer = "platform-api" +# public_key_file is REQUIRED in every mode (verifies token signatures); +# private_key_file is REQUIRED only in "file" mode (signs login tokens) and +# must be the matching half of the pair. Neither is generated — a missing +# path, unreadable file, or malformed key fails config load. Both files must +# contain a PEM-encoded RSA key. +public_key_file = "/etc/platform-api/keys/jwt_public.pem" +private_key_file = "/etc/platform-api/keys/jwt_private.pem" +# Lifetime of tokens issued by the file-mode login endpoint (Go duration +# syntax). Not used for "external_token" tokens — their expiry is whatever +# "exp" claim the issuer set. +token_ttl = "1h" + # --------------------------------------------------------------------------- -# Listeners (HTTP + HTTPS) +# Server listeners (HTTP + HTTPS) # --------------------------------------------------------------------------- -# The plain-HTTP and TLS listeners are independent — enable either or both, each -# on its own port. Serve plain HTTP internally, HTTPS externally, or run both at -# once (e.g. to migrate clients between them without downtime). +# The plain-HTTP and HTTPS listeners are independent — enable either or both, +# each on its own port. Serve plain HTTP internally, HTTPS externally, or run +# both at once (e.g. to migrate clients between them without downtime). # # Defaults: the HTTPS listener is on at 9243; the plain-HTTP listener is off. # Enable the plain-HTTP listener only when a trusted upstream (ingress, -# service-mesh sidecar) terminates TLS, or for internal traffic — never expose -# it directly to untrusted networks. -# -# https.cert_dir must contain cert.pem and key.pem when https.enabled = true. -# Certificates are always required — there is no self-signed fallback. -# setup.sh generates a pair for the quickstart. +# service-mesh sidecar) terminates TLS, or for internal traffic — never +# expose it directly to untrusted networks. # -# Override with APIP_CP_HTTP_ENABLED / APIP_CP_HTTP_PORT and APIP_CP_HTTPS_ENABLED / -# APIP_CP_HTTPS_PORT / APIP_CP_HTTPS_CERT_DIR. Legacy TLS_ENABLED / TLS_CERT_DIR / -# PORT still map onto the HTTPS listener. -[http] +# cert_file / key_file must point at a certificate pair when +# server.https.enabled = true. Certificates are always required — there is +# no self-signed fallback. setup.sh generates a pair for the quickstart. +[platform_api.server.http] enabled = false -port = "9080" +port = 9080 -[https] -enabled = true -port = "9243" -cert_dir = "/app/data/certs" # default: ./data/certs +[platform_api.server.https] +enabled = true +port = 9243 +cert_file = "/app/data/certs/cert.pem" # default: ./data/certs/cert.pem +key_file = "/app/data/certs/key.pem" # default: ./data/certs/key.pem # --------------------------------------------------------------------------- # Listener timeouts # --------------------------------------------------------------------------- -# Bound the lifetime of a connection so a slow or idle peer cannot hold one open -# indefinitely (Slowloris). These apply to both listeners, which serve the same -# handler. Values are durations, e.g. "10s", "2m". +# Bound the lifetime of a connection so a slow or idle peer cannot hold one +# open indefinitely (Slowloris). These apply to both listeners, which serve +# the same handler. Values are durations, e.g. "10s", "2m". # # 0 disables a timeout (net/http semantics). Disabling read or read_header -# removes the Slowloris protection — only do so behind a proxy that enforces its -# own bounds. Keep `write` generous: it bounds handler execution, and some -# handlers proxy slow upstreams (LLM completions, deployments). +# removes the Slowloris protection — only do so behind a proxy that enforces +# its own bounds. Keep `write` generous: it bounds handler execution, and +# some handlers proxy slow upstreams (LLM completions, deployments). # # WebSocket routes are unaffected — the deadlines are cleared on upgrade. -# -# Override with APIP_CP_TIMEOUTS_READ_HEADER / APIP_CP_TIMEOUTS_READ / -# APIP_CP_TIMEOUTS_WRITE / APIP_CP_TIMEOUTS_IDLE. -[timeouts] +[platform_api.server.timeouts] read_header = "10s" read = "60s" write = "120s" @@ -249,70 +297,49 @@ idle = "120s" # --------------------------------------------------------------------------- # CORS # --------------------------------------------------------------------------- -# Origins allowed to make credentialed cross-origin requests, e.g. the Developer -# Portal and AI Workspace origins. Must never contain "*"; leave empty to -# disable cross-origin access (the default). Env var is -# APIP_CP_CORS_ALLOWED_ORIGINS (comma-separated). -[cors] -allowed_origins = [] # e.g. ["https://workspace.example.com", "https://devportal.example.com"] +# Origins allowed to make credentialed cross-origin requests, e.g. the +# Developer Portal and AI Workspace origins. Must never contain "*"; leave +# empty to disable cross-origin access (the default). +[platform_api.server.cors] +allowed_origins = "" # comma-separated, e.g. "https://workspace.example.com,https://devportal.example.com" # --------------------------------------------------------------------------- -# API Key +# WebSocket # --------------------------------------------------------------------------- -[api_key] -# Hashing algorithms accepted for API key verification. -# sha256 is the default; add "sha512" to accept both. -hashing_algorithms = ["sha256"] +[platform_api.server.websocket] +max_connections = 1000 # global WebSocket connection limit +connection_timeout = 30 # seconds before an idle connection is closed +rate_limit_per_min = 1000 # maximum messages per minute per connection +metrics_log_enabled = true # emit WebSocket metrics to the log +metrics_log_interval = 10 # seconds between metrics log lines # --------------------------------------------------------------------------- -# WebSocket +# Gateway # --------------------------------------------------------------------------- -[websocket] -max_connections = 1000 # global WebSocket connection limit -connection_timeout = 30 # seconds before an idle connection is closed -rate_limit_per_min = 1000 # maximum messages per minute per connection -max_connections_per_org = 3 # maximum concurrent WebSocket connections per org -metrics_log_enabled = true # emit WebSocket metrics to the log -metrics_log_interval = 10 # seconds between metrics log lines +[platform_api.gateway] +# Reject gateway registrations whose runtime version does not match the expected range. +enable_version_verification = false +# Reject gateways that report an unexpected functionality type. +enable_functionality_type_verification = false # --------------------------------------------------------------------------- # Deployments # --------------------------------------------------------------------------- -[deployments] -max_per_api_gateway = 20 # maximum API deployments per gateway -transitional_status_enabled = false # expose "pending" deployment states in responses +[platform_api.deployments] +max_per_api_gateway = 20 # maximum API deployments per gateway +transitional_status_enabled = false # expose "pending" deployment states in responses # Deployment timeout — mark stuck deployments as failed after timeout_duration seconds. timeout_enabled = true timeout_interval = 20 # seconds between timeout check sweeps timeout_duration = 60 # seconds before a stuck deployment is timed out -# --------------------------------------------------------------------------- -# Artifact limits -# --------------------------------------------------------------------------- -# Maximum number of each artifact kind an organization may create. -# A value <= 0 (the default) means unlimited. -[artifact_limits] -max_llm_providers_per_org = 0 -max_llm_proxies_per_org = 0 -max_mcp_proxies_per_org = 0 -max_websub_apis_per_org = 0 -max_webbroker_apis_per_org = 0 - -# --------------------------------------------------------------------------- -# Gateway -# --------------------------------------------------------------------------- -[gateway] -# Reject gateway registrations whose runtime version does not match the expected range. -enable_version_verification = false -# Reject gateways that report an unexpected functionality type. -enable_functionality_type_verification = false # --------------------------------------------------------------------------- # EventHub — multi-replica HA event delivery # --------------------------------------------------------------------------- # Values are durations, e.g. "3s", "10m", "1h". All must be positive. -[event_hub] +[platform_api.event_hub] poll_interval = "3s" # how often each replica polls for new events cleanup_interval = "10m" # how often delivered events are purged retention_period = "1h" # how long delivered events are retained @@ -320,19 +347,16 @@ retention_period = "1h" # how long delivered events are retained # --------------------------------------------------------------------------- # Webhook — control-plane webhook receiver # --------------------------------------------------------------------------- -# The Developer Portal delivers signed events (API key / subscription changes) -# to this endpoint. -[webhook] +# The Developer Portal delivers signed events (API key / subscription +# changes) to this endpoint. +[platform_api.webhook] enabled = false # HMAC-SHA256 shared secret used to verify request signatures. REQUIRED when -# enabled. Resolve via a token, e.g. secret = '{{ env "APIP_CP_WEBHOOK_SECRET" }}' -# — the referenced env var/file must exist or config load fails. +# enabled. The "" default below is only ever read while enabled = false. secret = "" -# PEM RSA private key used to decrypt encrypted_key fields. Required only for -# events that carry encrypted secrets (API key generate/regenerate). -private_key_path = "" -# Events with a different gateway_type are accepted as a no-op. -gateway_type = "wso2/api-platform" -signature_tolerance = "5m" # max age of a signed request (replay protection) -max_body_size = 1048576 # request body cap in bytes (1 MiB) -signature_header = "X-Devportal-Signature" # header carrying the "t=...,v1=..." signature +# PEM RSA private key used to decrypt encrypted_key fields. Required only +# for events that carry encrypted secrets (API key generate/regenerate). +private_key_path = "" +signature_tolerance = "5m" # max age of a signed request (replay protection) +max_body_size = 1048576 # request body cap in bytes (1 MiB) +signature_header = "X-Devportal-Signature" # header carrying the "t=...,v1=..." signature diff --git a/platform-api/config/config.go b/platform-api/config/config.go index 18d19e4b1b..128535a607 100644 --- a/platform-api/config/config.go +++ b/platform-api/config/config.go @@ -18,16 +18,20 @@ package config import ( + "crypto/rsa" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "log/slog" + "os" "reflect" + "strings" "sync" "time" "github.com/go-viper/mapstructure/v2" + "github.com/golang-jwt/jwt/v5" toml "github.com/knadh/koanf/parsers/toml/v2" "github.com/knadh/koanf/providers/confmap" "github.com/knadh/koanf/providers/file" @@ -45,7 +49,7 @@ type FileBasedUser struct { } // FileBasedUsers is a slice of FileBasedUser that can be decoded from a JSON string (env var) -// or from a TOML array of tables ([[auth.file_based.users]]). +// or from a TOML array of tables ([[auth.file.users]]). type FileBasedUsers []FileBasedUser // FileBasedOrg holds the single organization used in file-based auth mode. @@ -66,72 +70,101 @@ type FileBasedOrg struct { } // FileBased holds configuration for local username/password authentication. +// Active when Auth.Mode is AuthModeFile. type FileBased struct { - Enabled bool `koanf:"enabled"` Organization FileBasedOrg `koanf:"organization"` Users FileBasedUsers `koanf:"users"` } +// Logging holds logging configuration. +type Logging struct { + Level string `koanf:"level"` + Format string `koanf:"format"` +} + // Server holds the configuration parameters for the application. type Server struct { - LogLevel string `koanf:"log_level"` - LogFormat string `koanf:"log_format"` + Logging Logging `koanf:"logging"` DBSchemaPath string `koanf:"db_schema_path"` OpenAPISpecPath string `koanf:"openapi_spec_path"` LLMTemplateDefinitionsPath string `koanf:"llm_template_definitions_path"` OpenAPISpecMaxFetchBytes int64 `koanf:"openapi_spec_max_fetch_bytes"` - EncryptionKey string `koanf:"encryption_key"` - - Database Database `koanf:"database"` - Auth Auth `koanf:"auth"` - WebSocket WebSocket `koanf:"websocket"` - DefaultDevPortal DefaultDevPortal `koanf:"default_devportal"` - Deployments Deployments `koanf:"deployments"` - ArtifactLimits ArtifactLimits `koanf:"artifact_limits"` - HTTP HTTPListener `koanf:"http"` - HTTPS HTTPSListener `koanf:"https"` - Timeouts Timeouts `koanf:"timeouts"` - CORS CORS `koanf:"cors"` - APIKey APIKey `koanf:"api_key"` - Gateway Gateway `koanf:"gateway"` - EventHub EventHub `koanf:"event_hub"` - Webhook Webhook `koanf:"webhook"` - - EnableScopeValidation bool `koanf:"enable_scope_validation"` -} + Database Database `koanf:"database"` + Auth Auth `koanf:"auth"` + Deployments Deployments `koanf:"deployments"` + Listeners ServerListeners `koanf:"server"` + Security Security `koanf:"security"` + Gateway Gateway `koanf:"gateway"` + EventHub EventHub `koanf:"event_hub"` + Webhook Webhook `koanf:"webhook"` +} + +// Authentication modes selectable via auth.mode. Exactly one mode is active; +// modeling the choice as a single discriminator (rather than per-mode enabled +// flags) makes conflicting configurations inexpressible. +const ( + // AuthModeExternalToken verifies locally-issued, asymmetrically-signed JWTs + // (RS256) minted externally, e.g. by the Developer Portal. Verification uses + // the RSA public key in auth.jwt.public_key_file; symmetric (HMAC) and unsigned + // ("none") tokens are rejected. + AuthModeExternalToken = "external_token" + // AuthModeFile is AuthModeExternalToken plus local username/password login: the + // login endpoint authenticates users from auth.file and issues RS256 JWTs signed + // with the RSA private key in auth.jwt.private_key_file, verified with the matching + // auth.jwt.public_key_file. + AuthModeFile = "file" + // AuthModeIDP validates tokens against an external IDP's JWKS (auth.idp). + AuthModeIDP = "idp" +) // Auth groups all authentication-related configuration. type Auth struct { - SkipPaths []string `koanf:"skip_paths"` - IDP IDP `koanf:"idp"` - JWT JWT `koanf:"jwt"` - FileBased FileBased `koanf:"file_based"` -} - -// IDPClaimMappings holds JWT claim name mappings for an IDP. -type IDPClaimMappings struct { - OrganizationClaimName string `koanf:"organization_claim_name"` - OrgNameClaimName string `koanf:"org_name_claim_name"` - OrgHandleClaimName string `koanf:"org_handle_claim_name"` - UserIDClaimName string `koanf:"user_id_claim_name"` - UsernameClaimName string `koanf:"username_claim_name"` - EmailClaimName string `koanf:"email_claim_name"` - ScopeClaimName string `koanf:"scope_claim_name"` - RolesClaimPath string `koanf:"roles_claim_path"` -} - -// IDP holds configuration for JWKS-based identity providers. + // Mode selects the active authentication mode: "external_token", "file", or "idp". + Mode string `koanf:"mode"` + // ScopeValidation enforces per-endpoint OAuth2 scopes on validated tokens. + // Disable only to temporarily bypass authorization during development. + ScopeValidation bool `koanf:"scope_validation"` + SkipPaths []string `koanf:"skip_paths"` + IDP IDP `koanf:"idp"` + // JWT is shared by two modes — "external_token" mode only verifies + // externally-minted tokens with the public key, "file" mode both signs (with + // the private key) and verifies (with the public key) using the RSA key pair. + JWT JWT `koanf:"jwt"` + File FileBased `koanf:"file"` + // ClaimMappings names the JWT claims that carry each identity field. It is + // shared by all three auth modes: "idp" reads incoming claims by these + // names, "file" mode's login endpoint signs tokens using these names, and + // "external_token" mode reads externally-minted tokens by these names too + // — one mapping, so issuance and validation can never drift apart. Every + // field accepts either a flat top-level claim name ("org_id") or a + // dot-separated path into a nested claim ("realm_access.org_id") — see + // resolveClaimPath in internal/middleware/auth.go. + ClaimMappings ClaimMappings `koanf:"claim_mappings"` +} + +// ClaimMappings holds JWT claim name mappings, shared across all auth modes. +type ClaimMappings struct { + Organization string `koanf:"organization"` + OrgName string `koanf:"org_name"` + OrgHandle string `koanf:"org_handle"` + UserID string `koanf:"user_id"` + Username string `koanf:"username"` + Email string `koanf:"email"` + Scope string `koanf:"scope"` + Roles string `koanf:"roles"` +} + +// IDP holds configuration for JWKS-based identity providers. Active when +// Auth.Mode is AuthModeIDP. type IDP struct { - Enabled bool `koanf:"enabled"` - Name string `koanf:"name"` - JWKSUrl string `koanf:"jwks_url"` - Issuer []string `koanf:"issuer"` - Audience []string `koanf:"audience"` - ValidationMode string `koanf:"validation_mode"` - RoleMappingsFile string `koanf:"role_mappings_file"` - ClaimMappings IDPClaimMappings `koanf:"claim_mappings"` + Name string `koanf:"name"` + JWKSUrl string `koanf:"jwks_url"` + Issuer []string `koanf:"issuer"` + Audience []string `koanf:"audience"` + ValidationMode string `koanf:"validation_mode"` + RoleMappings string `koanf:"role_mappings"` } // EventHub holds EventHub-specific configuration for multi-replica HA event delivery. @@ -152,9 +185,6 @@ type Webhook struct { // PrivateKeyPath points to the PEM RSA private key used to decrypt encrypted_key fields. // Optional: required only for events that carry encrypted secrets (API key generate/regenerate). PrivateKeyPath string `koanf:"private_key_path"` - // GatewayType filters events meant for this platform type. Events with a different - // gateway_type are accepted as a no-op. - GatewayType string `koanf:"gateway_type"` // SignatureTolerance bounds how old a signed request may be (replay protection). SignatureTolerance time.Duration `koanf:"signature_tolerance"` // MaxBodySize caps the request body size in bytes. @@ -169,27 +199,35 @@ type Gateway struct { EnableFunctionalityTypeVerification bool `koanf:"enable_functionality_type_verification"` } -// HTTPListener and HTTPSListener model the two independent listeners, following -// the gateway router's http/https split (see RouterConfig.HTTPSEnabled / -// HTTPSPort in gateway-controller). Each is enabled independently and bound to -// its own port, so a deployment can serve plain HTTP internally, HTTPS -// externally, or both at once (e.g. to migrate clients between the two without -// downtime). +// ServerListeners models the [server] section: the two independent HTTP +// listeners (each enabled independently and bound to its own port, so a +// deployment can serve plain HTTP internally, HTTPS externally, or both at +// once to migrate clients between them without downtime), plus the +// cross-cutting settings — timeouts, CORS, WebSocket — that apply to +// whichever listener(s) are serving requests. +type ServerListeners struct { + HTTP HTTPListener `koanf:"http"` + HTTPS HTTPSListener `koanf:"https"` + Timeouts Timeouts `koanf:"timeouts"` + CORS CORS `koanf:"cors"` + WebSocket WebSocket `koanf:"websocket"` +} // HTTPListener configures the plain-HTTP listener. Enable it only when a trusted // upstream (ingress, service-mesh sidecar) terminates TLS, or for internal // cluster traffic; never expose it directly to untrusted networks. type HTTPListener struct { - Enabled bool `koanf:"enabled"` - Port string `koanf:"port"` + Enabled bool `koanf:"enabled"` + Port int `koanf:"port"` } -// HTTPSListener configures the TLS listener. CertDir must contain cert.pem and -// key.pem when Enabled is true; there is no self-signed fallback. +// HTTPSListener configures the TLS listener. CertFile and KeyFile must point at a +// certificate pair when Enabled is true; there is no self-signed fallback. type HTTPSListener struct { - Enabled bool `koanf:"enabled"` - Port string `koanf:"port"` - CertDir string `koanf:"cert_dir"` + Enabled bool `koanf:"enabled"` + Port int `koanf:"port"` + CertFile string `koanf:"cert_file"` + KeyFile string `koanf:"key_file"` } // Timeouts bounds the lifetime of a connection on both listeners, so a slow or @@ -224,22 +262,67 @@ type CORS struct { AllowedOrigins []string `koanf:"allowed_origins"` } -// JWT holds configuration for local HMAC JWT authentication. +// JWT holds configuration for local asymmetric (RS256) JWT authentication. +// Active when Auth.Mode is AuthModeExternalToken (verify-only, externally-minted +// tokens) or AuthModeFile (file mode also issues these tokens). Signature +// validation is always on and strictly asymmetric — symmetric (HMAC) and +// unsigned ("none") algorithms are rejected. +// +// TODO(pqc): migrate — RS256 is quantum-vulnerable. Move to an ML-DSA (FIPS 204) +// signature once a Go JWT library exposes it. See post-quantum-cryptography.md. type JWT struct { - Enabled bool `koanf:"enabled"` - SecretKey string `koanf:"secret_key"` - Issuer string `koanf:"issuer"` - SkipValidation bool `koanf:"skip_validation"` + // PublicKeyFile is the path to a mounted PEM-encoded RSA public key file, + // used to verify token signatures. Required in both "external_token" and + // "file" modes. The key is read from disk at the point of use rather than + // being interpolated into config at load time, so the PEM content is never + // held in the config struct. + PublicKeyFile string `koanf:"public_key_file"` + // PrivateKeyFile is the path to a mounted PEM-encoded RSA private key file, + // used to sign tokens. Required only in "file" mode, whose login endpoint + // mints tokens; unused (and not required) in verify-only "external_token" + // mode. Read from disk at the point of use, never cached as content. + PrivateKeyFile string `koanf:"private_key_file"` + Issuer string `koanf:"issuer"` + TokenTTL time.Duration `koanf:"token_ttl"` +} + +// LoadPublicKey reads and parses the PEM-encoded RSA public key from +// PublicKeyFile. The file is read fresh on every call rather than cached, +// so PublicKeyFile is a mounted-file path, never inlined PEM content. +func (j *JWT) LoadPublicKey() (*rsa.PublicKey, error) { + raw, err := os.ReadFile(j.PublicKeyFile) + if err != nil { + return nil, fmt.Errorf("failed to read auth.jwt.public_key_file %q: %w", j.PublicKeyFile, err) + } + pub, err := jwt.ParseRSAPublicKeyFromPEM(raw) + if err != nil { + return nil, fmt.Errorf("auth.jwt.public_key_file %q must be a PEM-encoded RSA public key: %w", j.PublicKeyFile, err) + } + return pub, nil +} + +// LoadPrivateKey reads and parses the PEM-encoded RSA private key from +// PrivateKeyFile. The file is read fresh on every call rather than cached, +// so PrivateKeyFile is a mounted-file path, never inlined PEM content. +func (j *JWT) LoadPrivateKey() (*rsa.PrivateKey, error) { + raw, err := os.ReadFile(j.PrivateKeyFile) + if err != nil { + return nil, fmt.Errorf("failed to read auth.jwt.private_key_file %q: %w", j.PrivateKeyFile, err) + } + priv, err := jwt.ParseRSAPrivateKeyFromPEM(raw) + if err != nil { + return nil, fmt.Errorf("auth.jwt.private_key_file %q must be a PEM-encoded RSA private key: %w", j.PrivateKeyFile, err) + } + return priv, nil } // WebSocket holds WebSocket-specific configuration. type WebSocket struct { - MaxConnections int `koanf:"max_connections"` - ConnectionTimeout int `koanf:"connection_timeout"` - RateLimitPerMin int `koanf:"rate_limit_per_min"` - MaxConnectionsPerOrg int `koanf:"max_connections_per_org"` - MetricsLogEnabled bool `koanf:"metrics_log_enabled"` - MetricsLogInterval int `koanf:"metrics_log_interval"` + MaxConnections int `koanf:"max_connections"` + ConnectionTimeout int `koanf:"connection_timeout"` + RateLimitPerMin int `koanf:"rate_limit_per_min"` + MetricsLogEnabled bool `koanf:"metrics_log_enabled"` + MetricsLogInterval int `koanf:"metrics_log_interval"` } // Database holds database-specific configuration. @@ -247,37 +330,25 @@ type Database struct { // Driver supports: sqlite3, postgres/postgresql/pgx, sqlserver/mssql. Driver string `koanf:"driver"` // Path is the file path for SQLite databases. - Path string `koanf:"path"` - Host string `koanf:"host"` - Port int `koanf:"port"` - Name string `koanf:"name"` - User string `koanf:"user"` - Password string `koanf:"password"` - SSLMode string `koanf:"ssl_mode"` + Path string `koanf:"path"` + Host string `koanf:"host"` + Port int `koanf:"port"` + Name string `koanf:"name"` + User string `koanf:"user"` + Password string `koanf:"password"` + SSLMode string `koanf:"ssl_mode"` + // SSLRootCert is the CA certificate file path used to verify the server's + // certificate. Required when SSLMode is "verify-ca" or "verify-full". + SSLRootCert string `koanf:"ssl_root_cert"` + // SSLCert and SSLKey are the client certificate/key pair used for mutual + // TLS. Optional; both must be set together or not at all. + SSLCert string `koanf:"ssl_cert"` + SSLKey string `koanf:"ssl_key"` MaxOpenConns int `koanf:"max_open_conns"` MaxIdleConns int `koanf:"max_idle_conns"` ConnMaxLifetime int `koanf:"conn_max_lifetime"` } -// DefaultDevPortal holds default DevPortal configuration for new organizations. -type DefaultDevPortal struct { - Enabled bool `koanf:"enabled"` - Name string `koanf:"name"` - Identifier string `koanf:"identifier"` - APIUrl string `koanf:"api_url"` - Hostname string `koanf:"hostname"` - APIKey string `koanf:"api_key"` - HeaderKeyName string `koanf:"header_key_name"` - Timeout int `koanf:"timeout"` - - RoleClaimName string `koanf:"role_claim_name"` - GroupsClaimName string `koanf:"groups_claim_name"` - OrganizationClaimName string `koanf:"organization_claim_name"` - AdminRole string `koanf:"admin_role"` - SubscriberRole string `koanf:"subscriber_role"` - SuperAdminRole string `koanf:"super_admin_role"` -} - // Deployments holds deployment-specific configuration. type Deployments struct { MaxPerAPIGateway int `koanf:"max_per_api_gateway"` @@ -287,29 +358,19 @@ type Deployments struct { TimeoutDuration int `koanf:"timeout_duration"` } -// ArtifactLimits holds the maximum number of each artifact kind an organization -// may create. Each limit is optional: a value <= 0 (the default) means unlimited, -// so organizations may create as many artifacts of that kind as they want. -type ArtifactLimits struct { - MaxLLMProvidersPerOrg int `koanf:"max_llm_providers_per_org"` - MaxLLMProxiesPerOrg int `koanf:"max_llm_proxies_per_org"` - MaxMCPProxiesPerOrg int `koanf:"max_mcp_proxies_per_org"` - MaxWebSubAPIsPerOrg int `koanf:"max_websub_apis_per_org"` - MaxWebBrokerAPIsPerOrg int `koanf:"max_webbroker_apis_per_org"` -} - -// LimitReached reports whether an organization currently holding currentCount -// artifacts of some kind has reached its configured limit. A limit <= 0 means -// unlimited, in which case this always returns false. -func LimitReached(currentCount, limit int) bool { - return limit > 0 && currentCount >= limit -} - // APIKey holds API key-specific configuration. type APIKey struct { HashingAlgorithms []string `koanf:"hashing_algorithms"` } +// Security holds cryptographic/secret-handling configuration. +type Security struct { + // EncryptionKey is the single 32-byte key used for ALL at-rest encryption + // (secrets, subscription tokens, WebSub HMAC secrets). + EncryptionKey string `koanf:"encryption_key"` + APIKey APIKey `koanf:"api_key"` +} + // package-level singleton. var ( configFilePath string @@ -335,11 +396,6 @@ func GetConfig() *Server { return settingInstance } -// EnvPrefix namespaces the environment variables that override configuration keys. -// The prefix is stripped before the remainder is mapped to a koanf key (see envToKoanfKey), -// e.g. APIP_CP_LOG_LEVEL -> log_level, APIP_CP_DATABASE_HOST -> database.host. -const EnvPrefix = "APIP_CP_" - // defaultFileSourceAllowlist is the platform-api's default set of directories that a // {{ file "..." }} config-interpolation token may read from. It can be overridden via // the shared APIP_CONFIG_FILE_SOURCE_ALLOWLIST env var (see configinterpolate.ResolveAllowlist). @@ -348,6 +404,12 @@ var defaultFileSourceAllowlist = []string{ "/secrets/platform-api", } +// platformAPIConfigKey is the top-level TOML table that all Platform API +// settings live under (e.g. [platform_api], [platform_api.database]). This +// namespacing lets a Platform API config file coexist with sibling services' +// sections in a shared deployment config. +const platformAPIConfigKey = "platform_api" + // LoadConfig loads configuration with priority: config file > defaults. // configPath may be empty — when omitted only env vars and defaults are used. func LoadConfig(configPath string) (*Server, error) { @@ -369,7 +431,7 @@ func LoadConfig(configPath string) (*Server, error) { return nil, err } - if err := k.UnmarshalWithConf("", cfg, koanf.UnmarshalConf{ + if err := k.UnmarshalWithConf(platformAPIConfigKey, cfg, koanf.UnmarshalConf{ DecoderConfig: &mapstructure.DecoderConfig{ TagName: "koanf", WeaklyTypedInput: true, @@ -387,12 +449,12 @@ func LoadConfig(configPath string) (*Server, error) { // Install the configured logger as the slog default so the warnings/info logs // emitted below (and any package-level slog.* call in this file) use the same // format as the rest of the application, instead of slog's default handler. - slog.SetDefault(logger.NewLogger(logger.Config{Level: cfg.LogLevel, Format: cfg.LogFormat})) + slog.SetDefault(logger.NewLogger(logger.Config{Level: cfg.Logging.Level, Format: cfg.Logging.Format})) - if err := validateTimeoutsConfig(&cfg.Timeouts); err != nil { + if err := validateLoggingConfig(cfg.Logging.Level, cfg.Logging.Format); err != nil { return nil, err } - if err := validateDefaultDevPortalConfig(&cfg.DefaultDevPortal); err != nil { + if err := validateTimeoutsConfig(&cfg.Listeners.Timeouts); err != nil { return nil, err } if err := validateDeploymentsConfig(&cfg.Deployments); err != nil { @@ -401,22 +463,22 @@ func LoadConfig(configPath string) (*Server, error) { if err := validateEventHubConfig(&cfg.EventHub); err != nil { return nil, err } - if err := validateIDPConfig(&cfg.Auth.IDP); err != nil { + if err := validateAuthConfig(&cfg.Auth); err != nil { return nil, err } if err := validateWebhookConfig(&cfg.Webhook); err != nil { return nil, err } - if err := validateFileBasedConfig(&cfg.Auth.FileBased); err != nil { + if err := validateEncryptionKey(cfg.Security.EncryptionKey); err != nil { return nil, err } - if err := validateAuthModeExclusivity(&cfg.Auth); err != nil { + if err := validateDatabaseConfig(&cfg.Database); err != nil { return nil, err } - if err := validateJWTConfig(&cfg.Auth.JWT, cfg.Auth.FileBased.Enabled); err != nil { + if err := validateListenersConfig(&cfg.Listeners); err != nil { return nil, err } - if err := validateEncryptionKey(cfg.EncryptionKey); err != nil { + if err := validateCORSConfig(&cfg.Listeners.CORS); err != nil { return nil, err } @@ -465,8 +527,8 @@ func valid32ByteKey(keyStr string) bool { return false } -// fileBasedUsersDecodeHook handles decoding AUTH_FILE_BASED_USERS from a JSON string -// (env var format) in addition to the native TOML array-of-tables format. +// fileBasedUsersDecodeHook handles decoding auth.file.users from a JSON string +// (e.g. a {{ env }} token) in addition to the native TOML array-of-tables format. func fileBasedUsersDecodeHook() mapstructure.DecodeHookFuncType { return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { if t != reflect.TypeOf(FileBasedUsers{}) { @@ -481,7 +543,7 @@ func fileBasedUsersDecodeHook() mapstructure.DecodeHookFuncType { } var users FileBasedUsers if err := json.Unmarshal([]byte(s), &users); err != nil { - return nil, fmt.Errorf("failed to parse AUTH_FILE_BASED_USERS as JSON: %w", err) + return nil, fmt.Errorf("failed to parse auth.file.users as JSON: %w", err) } return users, nil } @@ -495,10 +557,10 @@ func validateTimeoutsConfig(cfg *Timeouts) error { name string value time.Duration }{ - {"timeouts.read_header (TIMEOUTS_READ_HEADER)", cfg.ReadHeader}, - {"timeouts.read (TIMEOUTS_READ)", cfg.Read}, - {"timeouts.write (TIMEOUTS_WRITE)", cfg.Write}, - {"timeouts.idle (TIMEOUTS_IDLE)", cfg.Idle}, + {"server.timeouts.read_header", cfg.ReadHeader}, + {"server.timeouts.read", cfg.Read}, + {"server.timeouts.write", cfg.Write}, + {"server.timeouts.idle", cfg.Idle}, } { if f.value < 0 { return fmt.Errorf("%s must not be negative (got %s); use 0 to disable the timeout", f.name, f.value) @@ -506,131 +568,215 @@ func validateTimeoutsConfig(cfg *Timeouts) error { } if cfg.Read > 0 && cfg.ReadHeader > cfg.Read { return fmt.Errorf( - "timeouts.read_header (%s) must not exceed timeouts.read (%s): the header deadline would never be reached", + "server.timeouts.read_header (%s) must not exceed server.timeouts.read (%s): the header deadline would never be reached", cfg.ReadHeader, cfg.Read, ) } return nil } -func validateDefaultDevPortalConfig(cfg *DefaultDevPortal) error { - if !cfg.Enabled { - return nil +// validateAuthConfig validates the selected auth mode and the section that mode +// activates. Modes are mutually exclusive by construction: auth.mode is a single +// discriminator, so conflicting-mode configurations are inexpressible and only +// the active mode's section is validated. +func validateAuthConfig(auth *Auth) error { + switch auth.Mode { + case AuthModeExternalToken: + // Verify-only: a public key is sufficient (tokens are minted elsewhere). + return validateJWTConfig(&auth.JWT, false) + case AuthModeFile: + // File mode also mints tokens, so it additionally needs a signing key. + if err := validateJWTConfig(&auth.JWT, true); err != nil { + return err + } + // TokenTTL only matters in file mode: the login endpoint mints tokens + // itself here, whereas in plain "external_token" mode tokens are minted + // externally and their expiry is whatever "exp" claim the issuer set. + if auth.JWT.TokenTTL <= 0 { + return fmt.Errorf("Auth.JWT.TokenTTL must be a positive duration when auth.mode is %q "+ + "(set auth.jwt.token_ttl, e.g. \"8h\")", AuthModeFile) + } + return validateFileBasedConfig(&auth.File) + case AuthModeIDP: + return validateIDPConfig(&auth.IDP, &auth.ClaimMappings) + default: + return fmt.Errorf("auth.mode must be %q, %q, or %q (got %q)", AuthModeExternalToken, AuthModeFile, AuthModeIDP, auth.Mode) + } +} + +// validateJWTConfig verifies the local asymmetric JWT key material is present +// and readable. The RSA public key verifies token signatures and is required +// in both the "external_token" and "file" auth modes. When requireSigningKey +// is true (file mode, which mints tokens at its login endpoint) the RSA +// private key is also required and must form a matching pair with the public +// key. Keys are mounted files, read fresh here rather than cached: a missing +// path, an unreadable file, or malformed material all fail startup. Only +// asymmetric RSA keys are accepted, so symmetric (HMAC) verification is +// structurally impossible. +func validateJWTConfig(jwtCfg *JWT, requireSigningKey bool) error { + if jwtCfg.PublicKeyFile == "" { + return fmt.Errorf("Auth.JWT.PublicKeyFile is required when auth.mode is %q or %q "+ + "(set auth.jwt.public_key_file to the path of a mounted PEM-encoded RSA public key)", + AuthModeExternalToken, AuthModeFile) + } + pub, err := jwtCfg.LoadPublicKey() + if err != nil { + return fmt.Errorf("invalid Auth.JWT.PublicKeyFile: %w", err) } - if cfg.Name == "" { - return fmt.Errorf("default DevPortal is enabled but name is not configured") + + if !requireSigningKey { + return nil } - if cfg.Identifier == "" { - return fmt.Errorf("default DevPortal is enabled but identifier is not configured") + + if jwtCfg.PrivateKeyFile == "" { + return fmt.Errorf("Auth.JWT.PrivateKeyFile is required when auth.mode is %q "+ + "(set auth.jwt.private_key_file to the path of a mounted PEM-encoded RSA private key)", + AuthModeFile) } - if cfg.APIUrl == "" { - return fmt.Errorf("default DevPortal is enabled but api_url is not configured") + priv, err := jwtCfg.LoadPrivateKey() + if err != nil { + return fmt.Errorf("invalid Auth.JWT.PrivateKeyFile: %w", err) } - if cfg.Hostname == "" { - return fmt.Errorf("default DevPortal is enabled but hostname is not configured") + if !priv.PublicKey.Equal(pub) { + return fmt.Errorf("Auth.JWT.PrivateKeyFile and Auth.JWT.PublicKeyFile must be a matching RSA key pair") } - if cfg.APIKey == "" { - return fmt.Errorf("default DevPortal is enabled but api_key is not configured") + return nil +} + +// validateEncryptionKey verifies the at-rest encryption key. +// A missing or malformed key fails startup. +func validateEncryptionKey(key string) error { + if key == "" { + return fmt.Errorf("EncryptionKey is required (set encryption_key in config via " + + "{{ env }}/{{ file }}") } - if cfg.HeaderKeyName == "" { - return fmt.Errorf("default DevPortal header_key_name is not configured") + if !valid32ByteKey(key) { + return fmt.Errorf("invalid EncryptionKey: must be 64 hex characters or " + + "base64 decoding to 32 bytes") } return nil } -// validateAuthModeExclusivity enforces that IDP (JWKS) auth is not enabled -// alongside the local auth modes. When an IDP is configured every token must be -// validated against its JWKS; leaving local HMAC auth on would let the server -// silently validate (file-based) or accept (local JWT) tokens with the local -// secret instead, shadowing the IDP. So enabling the IDP requires consciously -// turning the local modes off. -func validateAuthModeExclusivity(auth *Auth) error { - if !auth.IDP.Enabled { - return nil - } - if auth.JWT.Enabled { - return fmt.Errorf("auth.idp.enabled=true and auth.jwt.enabled=true are mutually exclusive: " + - "set auth.jwt.enabled=false to delegate authentication to the IDP (tokens are validated against auth.idp.jwks_url)") +// validateLoggingConfig rejects a logging.level/logging.format typo at startup +// instead of silently falling back to logger.NewLogger's default (info/json), +// which would leave an operator's requested verbosity or encoding silently +// ignored. The level is matched case-insensitively (canonical form is lowercase). +func validateLoggingConfig(level, format string) error { + switch strings.ToLower(level) { + case "debug", "info", "warn", "warning", "error": + default: + return fmt.Errorf("logging.level must be one of \"debug\", \"info\", \"warn\", or \"error\" (got %q)", level) } - if auth.FileBased.Enabled { - return fmt.Errorf("auth.idp.enabled=true and auth.file_based.enabled=true are mutually exclusive: " + - "set auth.file_based.enabled=false to delegate authentication to the IDP (tokens are validated against auth.idp.jwks_url)") + switch strings.ToLower(format) { + case "text", "json": + default: + return fmt.Errorf("logging.format must be \"text\" or \"json\" (got %q)", format) } return nil } -// validateJWTConfig verifies the local HMAC JWT secret. The same secret signs and -// verifies the login tokens issued in file-based mode, so it is required whenever -// either JWT auth or file-based auth is enabled. The secret is never generated: a -// missing or malformed key fails startup. -func validateJWTConfig(jwt *JWT, fileBasedEnabled bool) error { - if !jwt.Enabled && !fileBasedEnabled { +// validateDatabaseConfig fails closed when the selected driver's required +// connection fields are missing, rather than surfacing an opaque driver-level +// connection error only once the server tries to open the database. +func validateDatabaseConfig(cfg *Database) error { + driver := strings.ToLower(cfg.Driver) + switch driver { + case "sqlite3", "postgres", "postgresql", "pgx", "sqlserver", "mssql": + default: + return fmt.Errorf("database.driver must be one of \"sqlite3\", \"postgres\", \"postgresql\", \"pgx\", "+ + "\"sqlserver\", or \"mssql\" (got %q)", cfg.Driver) + } + if driver == "sqlite3" { return nil } - if jwt.SecretKey == "" { - return fmt.Errorf("Auth.JWT.SecretKey is required when JWT or file-based authentication is enabled " + - "(set auth.jwt.secret_key in config via {{ env }}/{{ file }})") + if cfg.Host == "" { + return fmt.Errorf("database.host is required when database.driver is %q", cfg.Driver) + } + if cfg.Port <= 0 || cfg.Port > 65535 { + return fmt.Errorf("database.port must be between 1 and 65535 when database.driver is %q (got %d)", cfg.Driver, cfg.Port) + } + if cfg.Name == "" { + return fmt.Errorf("database.name is required when database.driver is %q", cfg.Driver) + } + if cfg.User == "" { + return fmt.Errorf("database.user is required when database.driver is %q", cfg.Driver) + } + switch cfg.SSLMode { + case "", "disable", "require", "verify-ca", "verify-full": + default: + return fmt.Errorf("database.ssl_mode must be \"disable\", \"require\", \"verify-ca\", or \"verify-full\" (got %q)", cfg.SSLMode) + } + if (cfg.SSLMode == "verify-ca" || cfg.SSLMode == "verify-full") && cfg.SSLRootCert == "" { + return fmt.Errorf("database.ssl_root_cert is required when database.ssl_mode is %q", cfg.SSLMode) } - if !valid32ByteKey(jwt.SecretKey) { - return fmt.Errorf("invalid Auth.JWT.SecretKey: must be 64 hex characters or base64 decoding to 32 bytes") + if (cfg.SSLCert == "") != (cfg.SSLKey == "") { + return fmt.Errorf("database.ssl_cert and database.ssl_key must both be set together, or both left empty") } return nil } -// validateEncryptionKey verifies the at-rest encryption key. -// A missing or malformed key fails startup. -func validateEncryptionKey(key string) error { - if key == "" { - return fmt.Errorf("EncryptionKey is required (set encryption_key in config via " + - "{{ env }}/{{ file }}") +// validateListenersConfig rejects an out-of-range port on either listener and +// a port collision when both listeners are enabled, rather than failing at +// bind time with a generic "address already in use" error. +func validateListenersConfig(l *ServerListeners) error { + if l.HTTP.Enabled && (l.HTTP.Port <= 0 || l.HTTP.Port > 65535) { + return fmt.Errorf("server.http.port must be between 1 and 65535 (got %d)", l.HTTP.Port) } - if !valid32ByteKey(key) { - return fmt.Errorf("invalid EncryptionKey: must be 64 hex characters or " + - "base64 decoding to 32 bytes") + if l.HTTPS.Enabled && (l.HTTPS.Port <= 0 || l.HTTPS.Port > 65535) { + return fmt.Errorf("server.https.port must be between 1 and 65535 (got %d)", l.HTTPS.Port) + } + if l.HTTP.Enabled && l.HTTPS.Enabled && l.HTTP.Port == l.HTTPS.Port { + return fmt.Errorf("server.http.port and server.https.port must differ when both listeners are enabled (both are %d)", l.HTTP.Port) } return nil } -func validateIDPConfig(idp *IDP) error { - if !idp.Enabled { - return nil +// validateCORSConfig rejects a wildcard origin: CORS.AllowedOrigins is used +// for credentialed cross-origin requests, and wildcard origins cannot be +// combined with credentials without opening a cross-tenant exploit path. +func validateCORSConfig(c *CORS) error { + for _, o := range c.AllowedOrigins { + if o == "*" { + return fmt.Errorf("cors.allowed_origins must not contain \"*\" (wildcard origins cannot be combined with credentialed requests)") + } } + return nil +} + +func validateIDPConfig(idp *IDP, claimMappings *ClaimMappings) error { if idp.JWKSUrl == "" { - return fmt.Errorf("auth.idp.enabled=true requires auth.idp.jwks_url to be configured") + return fmt.Errorf("auth.mode=%q requires auth.idp.jwks_url to be configured", AuthModeIDP) } if len(idp.Issuer) == 0 { - return fmt.Errorf("auth.idp.enabled=true requires auth.idp.issuer to be configured") + return fmt.Errorf("auth.mode=%q requires auth.idp.issuer to be configured", AuthModeIDP) } switch idp.ValidationMode { case "scope", "role": default: return fmt.Errorf("auth.idp.validation_mode must be \"scope\" or \"role\" (got %q)", idp.ValidationMode) } - if idp.ValidationMode == "role" && idp.ClaimMappings.RolesClaimPath == "" { - return fmt.Errorf("auth.idp.validation_mode=role requires auth.idp.claim_mappings.roles_claim_path to be configured") + if idp.ValidationMode == "role" && claimMappings.Roles == "" { + return fmt.Errorf("auth.idp.validation_mode=role requires auth.claim_mappings.roles to be configured") } return nil } func validateFileBasedConfig(cfg *FileBased) error { - if !cfg.Enabled { - return nil - } if cfg.Organization.ID == "" { - return fmt.Errorf("auth.file_based.enabled=true requires auth.file_based.organization.id to be configured") + return fmt.Errorf("auth.mode=%q requires auth.file.organization.id to be configured", AuthModeFile) } if cfg.Organization.DisplayName == "" { - return fmt.Errorf("auth.file_based.enabled=true requires auth.file_based.organization.display_name to be configured") + return fmt.Errorf("auth.mode=%q requires auth.file.organization.display_name to be configured", AuthModeFile) } if len(cfg.Users) == 0 { - return fmt.Errorf("auth.file_based.enabled=true requires at least one user in auth.file_based.users") + return fmt.Errorf("auth.mode=%q requires at least one user in auth.file.users", AuthModeFile) } for i, u := range cfg.Users { if u.Username == "" { - return fmt.Errorf("auth.file_based.users[%d]: username is required (set it in config via {{ env }}/{{ file }})", i) + return fmt.Errorf("auth.file.users[%d]: username is required (set it in config via {{ env }}/{{ file }})", i) } if u.PasswordHash == "" { - return fmt.Errorf("auth.file_based.users[%d] (%s): password_hash is required (set it in config via {{ env }}/{{ file }})", i, u.Username) + return fmt.Errorf("auth.file.users[%d] (%s): password_hash is required (set it in config via {{ env }}/{{ file }})", i, u.Username) } } return nil @@ -654,9 +800,6 @@ func validateWebhookConfig(w *Webhook) error { if w.SignatureHeader == "" { w.SignatureHeader = "X-Devportal-Signature" } - if w.GatewayType == "" { - w.GatewayType = "wso2/api-platform" - } return nil } diff --git a/platform-api/config/config.toml b/platform-api/config/config.toml index 375178d21b..165c0c6601 100644 --- a/platform-api/config/config.toml +++ b/platform-api/config/config.toml @@ -6,56 +6,34 @@ # in compliance with the License. You may obtain a copy of the # License at http://www.apache.org/licenses/LICENSE-2.0 # -------------------------------------------------------------------- -# -# Platform API configuration — quickstart (file-based auth) -# -# This file is read by the Platform API (Go binary). -# -# Default login: username = admin, password = admin -# Change the password hash before deploying to a shared environment. -# Generate a new hash: htpasswd -bnBC 12 "" | tr -d ':\n' -# -------------------------------------------------------------------- -# --------------------------------------------------------------------------- -# Server -# --------------------------------------------------------------------------- -log_level = "INFO" # DEBUG | INFO | WARN | ERROR -port = "9243" +[platform_api] -# Validate OAuth2 scopes on incoming requests. -enable_scope_validation = true +[platform_api.logging] +level = '{{ env "APIP_CP_LOGGING_LEVEL" "info" }}' -# --------------------------------------------------------------------------- -# Encryption -# --------------------------------------------------------------------------- -# Single 32-byte key (64 hex chars or base64) used for ALL at-rest encryption -# (secrets, subscription tokens, WebSub HMAC secrets) +[platform_api.security] encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' -# --------------------------------------------------------------------------- -# Database -# --------------------------------------------------------------------------- -[database] -driver = "sqlite3" # "sqlite3" or "postgres" -# path = "/app/data/api_platform.db" # SQLite file path (ignored for postgres) - -# --------------------------------------------------------------------------- -# Authentication -# --------------------------------------------------------------------------- - -# JWT (local HMAC) — issues signed tokens after file-based login. -[auth.jwt] -enabled = true -issuer = "platform-api" -secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' -skip_validation = false - -# IDP (JWKS-based) — disabled in quickstart mode. -# Enable and configure when using Asgardeo, Keycloak, Auth0, etc. -[auth.idp] -enabled = false -audience = [] - -# File-based auth — local username/password login. -[auth.file_based] -enabled = false +[platform_api.database] +driver = "sqlite3" +path = "./data/api_platform.db" + +[platform_api.auth] +mode = "file" +scope_validation = "true" + +[platform_api.auth.jwt] +issuer = "platform-api" +public_key_file = "/etc/platform-api/keys/jwt_public.pem" +private_key_file = "/etc/platform-api/keys/jwt_private.pem" + +[platform_api.auth.file.organization] +id = "default" +display_name = "Default" +region = "us" + +[[platform_api.auth.file.users]] +username = '{{ env "APIP_CP_ADMIN_USERNAME" "admin" }}' +password_hash = '{{ env "APIP_CP_ADMIN_PASSWORD_HASH" "$2y$10$U2yKMwGamGwDoMu0hRPT7u8nCuP8z/qxHFOKV6dhIxkJN9NJ0eVQ." }}' +scopes = "ap:organization:manage ap:gateway:manage ap:gateway_custom_policy:manage ap:rest_api:manage ap:llm_provider:manage ap:llm_proxy:manage ap:mcp_proxy:manage ap:webbroker_api:manage ap:websub_api:manage ap:application:manage ap:subscription:manage ap:subscription_plan:manage ap:project:manage ap:llm_template:manage ap:devportal:manage ap:git:read ap:api_key:read ap:secret:manage dp:org_read dp:org_create dp:org_update dp:org_manage dp:org_delete dp:org_content_read dp:org_content_write dp:org_content_manage dp:org_content_delete dp:api_read dp:api_create dp:api_update dp:api_manage dp:api_delete dp:api_content_read dp:api_content_write dp:api_content_manage dp:api_content_delete dp:mcp_read dp:mcp_create dp:mcp_update dp:mcp_manage dp:mcp_delete dp:mcp_content_read dp:mcp_content_create dp:mcp_content_update dp:mcp_content_manage dp:mcp_content_delete dp:api_key_create dp:api_key_read dp:api_key_update dp:api_key_manage dp:api_key_revoke dp:mcp_key_create dp:mcp_key_read dp:mcp_key_update dp:mcp_key_manage dp:mcp_key_revoke dp:api_workflow_create dp:api_workflow_read dp:api_workflow_update dp:api_workflow_delete dp:api_workflow_manage dp:app_create dp:app_read dp:app_update dp:app_manage dp:app_delete dp:app_key_create dp:app_key_manage dp:app_key_revoke dp:app_key_mapping_read dp:app_key_mapping_write dp:app_key_mapping_manage dp:subscription_create dp:subscription_read dp:subscription_update dp:subscription_manage dp:subscription_delete dp:sub_plan_create dp:sub_plan_read dp:sub_plan_update dp:sub_plan_manage dp:sub_plan_delete dp:km_create dp:km_read dp:km_update dp:km_manage dp:km_delete dp:view_create dp:view_read dp:view_update dp:view_manage dp:view_delete dp:label_create dp:label_read dp:label_update dp:label_manage dp:label_delete dp:webhook_subscriber_create dp:webhook_subscriber_read dp:webhook_subscriber_update dp:webhook_subscriber_delete dp:webhook_subscriber_manage dp:event_read dp:delivery_manage" diff --git a/platform-api/config/config_test.go b/platform-api/config/config_test.go index 635960df77..4556718e1d 100644 --- a/platform-api/config/config_test.go +++ b/platform-api/config/config_test.go @@ -19,6 +19,10 @@ package config import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "os" "path/filepath" "testing" @@ -28,22 +32,65 @@ import ( "github.com/stretchr/testify/require" ) -// Valid 32-byte keys encoded as 64 hex chars. -const ( - validInlineKey = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" - validJWTKey = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" +// validInlineKey is a valid 32-byte AES key encoded as 64 hex chars, used for the +// at-rest encryption key (still a symmetric key — unrelated to JWT signing). +const validInlineKey = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + +// RSA key-pair PEM fixtures for the asymmetric JWT config, generated once in init() +// and written to files, since JWT config now holds mounted-file paths rather than +// inline PEM content. validJWTPublicKeyFile/validJWTPrivateKeyFile form a matching +// pair; otherJWTPublicKeyFile is from a different pair, used to exercise the +// matching-pair validation. +var ( + validJWTPublicKeyFile string + validJWTPrivateKeyFile string + otherJWTPublicKeyFile string ) +// writePEMFile writes content to a fresh file under a package-lifetime temp +// directory and returns its path. +func writePEMFile(dir, name, content string) string { + path := filepath.Join(dir, name) + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + panic(err) + } + return path +} + +// genRSAKeyPEMs generates a fresh RSA key pair and returns its public key (PKIX PEM) +// and private key (PKCS1 PEM). +func genRSAKeyPEMs() (pubPEM, privPEM string) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + privPEM = string(pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + })) + pubDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + if err != nil { + panic(err) + } + pubPEM = string(pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubDER, + })) + return pubPEM, privPEM +} + // validKeysBase is a minimal config whose required secrets resolve from the -// APIP_CP_ENCRYPTION_KEY / APIP_CP_AUTH_JWT_SECRET_KEY env vars via {{ env }} +// APIP_CP_ENCRYPTION_KEY / APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE env vars via {{ env }} // interpolation. Environment variables reach config ONLY through these tokens now // (there is no direct env-key override), so tests must go through a config file. +// The default auth mode is "external_token", which needs only the verification +// public key. const validKeysBase = ` +[platform_api.security] encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' -[auth.jwt] -enabled = true -secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' +[platform_api.auth.jwt] +public_key_file = '{{ env "APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE" }}' ` // loadTOML writes toml to a temp config file and loads it through LoadConfig. @@ -60,7 +107,7 @@ func loadTOML(t *testing.T, toml string) (*Server, error) { func loadWithKeys(t *testing.T, extra string) (*Server, error) { t.Helper() t.Setenv("APIP_CP_ENCRYPTION_KEY", validInlineKey) - t.Setenv("APIP_CP_AUTH_JWT_SECRET_KEY", validJWTKey) + t.Setenv("APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE", validJWTPublicKeyFile) return loadTOML(t, validKeysBase+extra) } @@ -68,18 +115,17 @@ func loadWithKeys(t *testing.T, extra string) (*Server, error) { func TestLoadConfig_ValidKeys_Succeeds(t *testing.T) { cfg, err := loadWithKeys(t, "") require.NoError(t, err) - assert.Equal(t, validInlineKey, cfg.EncryptionKey) + assert.Equal(t, validInlineKey, cfg.Security.EncryptionKey) } // The encryption key is required and never generated — a config that omits it fails startup. func TestLoadConfig_MissingEncryptionKey_Errors(t *testing.T) { - t.Setenv("APIP_CP_AUTH_JWT_SECRET_KEY", validJWTKey) + t.Setenv("APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE", validJWTPublicKeyFile) - // Encryption key omitted entirely; the JWT secret resolves so the JWT check passes first. + // Encryption key omitted entirely; the JWT public key resolves so the JWT check passes first. _, err := loadTOML(t, ` -[auth.jwt] -enabled = true -secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' +[platform_api.auth.jwt] +public_key_file = '{{ env "APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE" }}' `) require.Error(t, err) assert.Contains(t, err.Error(), "EncryptionKey is required") @@ -87,46 +133,45 @@ secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' // A provided encryption key must be an AES-256-sized key (64 hex / base64→32 bytes). func TestLoadConfig_InvalidEncryptionKey_Errors(t *testing.T) { - t.Setenv("APIP_CP_AUTH_JWT_SECRET_KEY", validJWTKey) + t.Setenv("APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE", validJWTPublicKeyFile) _, err := loadTOML(t, ` +[platform_api.security] encryption_key = "not-a-valid-32-byte-key" -[auth.jwt] -enabled = true -secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' +[platform_api.auth.jwt] +public_key_file = '{{ env "APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE" }}' `) require.Error(t, err) assert.Contains(t, err.Error(), "invalid EncryptionKey") } -// The JWT secret is required (JWT auth is enabled) and never generated. -func TestLoadConfig_MissingJWTSecretKey_Errors(t *testing.T) { +// The JWT public key is required (default auth mode is "external_token") and never generated. +func TestLoadConfig_MissingJWTPublicKey_Errors(t *testing.T) { t.Setenv("APIP_CP_ENCRYPTION_KEY", validInlineKey) _, err := loadTOML(t, ` +[platform_api.security] encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' - -[auth.jwt] -enabled = true `) require.Error(t, err) - assert.Contains(t, err.Error(), "Auth.JWT.SecretKey is required") + assert.Contains(t, err.Error(), "Auth.JWT.PublicKeyFile is required") } -// A provided JWT secret must be an AES-256-sized key (64 hex / base64→32 bytes). -func TestLoadConfig_InvalidJWTSecretKey_Errors(t *testing.T) { +// A provided JWT public key file must contain a PEM-encoded RSA public key. +func TestLoadConfig_InvalidJWTPublicKey_Errors(t *testing.T) { t.Setenv("APIP_CP_ENCRYPTION_KEY", validInlineKey) + invalidKeyFile := writePEMFile(t.TempDir(), "invalid_public.pem", "not-a-valid-rsa-public-key") _, err := loadTOML(t, ` +[platform_api.security] encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' -[auth.jwt] -enabled = true -secret_key = "not-a-valid-32-byte-key" +[platform_api.auth.jwt] +public_key_file = "`+invalidKeyFile+`" `) require.Error(t, err) - assert.Contains(t, err.Error(), "invalid Auth.JWT.SecretKey") + assert.Contains(t, err.Error(), "invalid Auth.JWT.PublicKeyFile") } // --- valid32ByteKey unit coverage --- @@ -143,49 +188,113 @@ func TestValid32ByteKey(t *testing.T) { // Clear env vars that LoadConfig reads so each test starts from a known baseline and host // environment values don't leak into assertions. func init() { + // Generate the RSA key-pair fixtures used across the asymmetric JWT tests, and + // write them to files since JWT config now holds mounted-file paths, not + // inline PEM content. + validJWTPublicKey, validJWTPrivateKey := genRSAKeyPEMs() + otherJWTPublicKey, _ := genRSAKeyPEMs() + + keysDir, err := os.MkdirTemp("", "jwt-test-keys") + if err != nil { + panic(err) + } + validJWTPublicKeyFile = writePEMFile(keysDir, "valid_public.pem", validJWTPublicKey) + validJWTPrivateKeyFile = writePEMFile(keysDir, "valid_private.pem", validJWTPrivateKey) + otherJWTPublicKeyFile = writePEMFile(keysDir, "other_public.pem", otherJWTPublicKey) + for _, v := range []string{ "APIP_CP_ENCRYPTION_KEY", - "APIP_CP_AUTH_JWT_SECRET_KEY", + "APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE", + "APIP_CP_AUTH_JWT_PRIVATE_KEY_FILE", } { os.Unsetenv(v) } } -// validateAuthModeExclusivity: IDP (JWKS) auth must not be enabled alongside the -// local JWT or file-based modes — the server must fail fast so operators turn the -// local modes off consciously and all tokens are validated against the IDP JWKS. -func TestValidateAuthModeExclusivity(t *testing.T) { +// validateAuthConfig: auth.mode is a single discriminator, so exactly one mode +// is active and only that mode's section is validated. Unknown modes fail fast. +func TestValidateAuthConfig(t *testing.T) { tests := []struct { name string auth Auth - wantErr bool + wantErr string }{ { - name: "idp disabled — local modes allowed", - auth: Auth{IDP: IDP{Enabled: false}, JWT: JWT{Enabled: true}, FileBased: FileBased{Enabled: true}}, - wantErr: false, + name: "external_token mode with valid public key", + auth: Auth{Mode: AuthModeExternalToken, JWT: JWT{PublicKeyFile: validJWTPublicKeyFile}}, + }, + { + name: "external_token mode without public key", + auth: Auth{Mode: AuthModeExternalToken}, + wantErr: "Auth.JWT.PublicKeyFile is required", + }, + { + name: "file mode without private key", + auth: Auth{Mode: AuthModeFile, JWT: JWT{PublicKeyFile: validJWTPublicKeyFile, TokenTTL: time.Hour}}, + wantErr: "Auth.JWT.PrivateKeyFile is required", + }, + { + name: "file mode with mismatched key pair", + auth: Auth{Mode: AuthModeFile, JWT: JWT{ + PublicKeyFile: otherJWTPublicKeyFile, + PrivateKeyFile: validJWTPrivateKeyFile, + TokenTTL: time.Hour, + }}, + wantErr: "matching RSA key pair", + }, + { + name: "file mode without token_ttl", + auth: Auth{Mode: AuthModeFile, JWT: JWT{PublicKeyFile: validJWTPublicKeyFile, PrivateKeyFile: validJWTPrivateKeyFile}}, + wantErr: "Auth.JWT.TokenTTL must be a positive duration", + }, + { + name: "file mode requires org and users", + auth: Auth{Mode: AuthModeFile, JWT: JWT{PublicKeyFile: validJWTPublicKeyFile, PrivateKeyFile: validJWTPrivateKeyFile, TokenTTL: time.Hour}}, + // Default org fields are empty in a zero-value Auth — users check fires + // after the org checks. + wantErr: "auth.file.organization.id", + }, + { + name: "file mode fully configured", + auth: Auth{ + Mode: AuthModeFile, + JWT: JWT{PublicKeyFile: validJWTPublicKeyFile, PrivateKeyFile: validJWTPrivateKeyFile, TokenTTL: time.Hour}, + File: FileBased{ + Organization: FileBasedOrg{ID: "default", DisplayName: "Default"}, + Users: FileBasedUsers{{Username: "admin", PasswordHash: "$2a$12$hash"}}, + }, + }, + }, + { + name: "idp mode requires jwks_url", + auth: Auth{Mode: AuthModeIDP, IDP: IDP{ValidationMode: "scope"}}, + wantErr: "auth.idp.jwks_url", }, { - name: "idp only", - auth: Auth{IDP: IDP{Enabled: true}, JWT: JWT{Enabled: false}, FileBased: FileBased{Enabled: false}}, - wantErr: false, + name: "idp mode fully configured", + auth: Auth{Mode: AuthModeIDP, IDP: IDP{ + JWKSUrl: "https://idp.example.com/jwks", + Issuer: []string{"https://idp.example.com"}, + ValidationMode: "scope", + }}, }, { - name: "idp and jwt both enabled", - auth: Auth{IDP: IDP{Enabled: true}, JWT: JWT{Enabled: true}, FileBased: FileBased{Enabled: false}}, - wantErr: true, + name: "unknown mode rejected", + auth: Auth{Mode: "basic"}, + wantErr: "auth.mode must be", }, { - name: "idp and file_based both enabled", - auth: Auth{IDP: IDP{Enabled: true}, JWT: JWT{Enabled: false}, FileBased: FileBased{Enabled: true}}, - wantErr: true, + name: "empty mode rejected", + auth: Auth{}, + wantErr: "auth.mode must be", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validateAuthModeExclusivity(&tt.auth) - if tt.wantErr { - assert.Error(t, err) + err := validateAuthConfig(&tt.auth) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) } else { assert.NoError(t, err) } @@ -199,37 +308,40 @@ func TestValidateAuthModeExclusivity(t *testing.T) { func TestLoadConfig_HTTPSEnabled_DefaultsToTrue(t *testing.T) { cfg, err := loadWithKeys(t, "") require.NoError(t, err) - assert.True(t, cfg.HTTPS.Enabled, "https.enabled must default to true when unset") - assert.Equal(t, "9243", cfg.HTTPS.Port, "https.port must default to 9243") - assert.False(t, cfg.HTTP.Enabled, "http.enabled must default to false when unset") + assert.True(t, cfg.Listeners.HTTPS.Enabled, "server.https.enabled must default to true when unset") + assert.Equal(t, 9243, cfg.Listeners.HTTPS.Port, "server.https.port must default to 9243") + assert.False(t, cfg.Listeners.HTTP.Enabled, "server.http.enabled must default to false when unset") + assert.Equal(t, "./data/certs/cert.pem", cfg.Listeners.HTTPS.CertFile) + assert.Equal(t, "./data/certs/key.pem", cfg.Listeners.HTTPS.KeyFile) } // A {{ env }} token feeding a bool field must survive koanf's weakly-typed decode, // so an operator can disable the TLS listener by pointing the field at an env var. func TestLoadConfig_HTTPSEnabled_TokenDisables(t *testing.T) { - t.Setenv("APIP_CP_HTTPS_ENABLED", "false") + t.Setenv("APIP_CP_SERVER_HTTPS_ENABLED", "false") cfg, err := loadWithKeys(t, ` -[https] -enabled = '{{ env "APIP_CP_HTTPS_ENABLED" }}' +[platform_api.server.https] +enabled = '{{ env "APIP_CP_SERVER_HTTPS_ENABLED" }}' `) require.NoError(t, err) - assert.False(t, cfg.HTTPS.Enabled, "https.enabled from a {{ env }} token must decode to false") + assert.False(t, cfg.Listeners.HTTPS.Enabled, "server.https.enabled from a {{ env }} token must decode to false") } -// The plain-HTTP listener can be enabled independently on its own port via tokens. +// The plain-HTTP listener can be enabled independently on its own port via tokens; +// a numeric string from an env var must decode into the int port field. func TestLoadConfig_HTTPListener_TokenEnables(t *testing.T) { - t.Setenv("APIP_CP_HTTP_ENABLED", "true") - t.Setenv("APIP_CP_HTTP_PORT", "9080") + t.Setenv("APIP_CP_SERVER_HTTP_ENABLED", "true") + t.Setenv("APIP_CP_SERVER_HTTP_PORT", "9080") cfg, err := loadWithKeys(t, ` -[http] -enabled = '{{ env "APIP_CP_HTTP_ENABLED" }}' -port = '{{ env "APIP_CP_HTTP_PORT" }}' +[platform_api.server.http] +enabled = '{{ env "APIP_CP_SERVER_HTTP_ENABLED" }}' +port = '{{ env "APIP_CP_SERVER_HTTP_PORT" }}' `) require.NoError(t, err) - assert.True(t, cfg.HTTP.Enabled, "http.enabled from a {{ env }} token must decode to true") - assert.Equal(t, "9080", cfg.HTTP.Port) + assert.True(t, cfg.Listeners.HTTP.Enabled, "server.http.enabled from a {{ env }} token must decode to true") + assert.Equal(t, 9080, cfg.Listeners.HTTP.Port) } // Listener timeouts must be finite by default, so a deployment that never sets @@ -237,42 +349,42 @@ port = '{{ env "APIP_CP_HTTP_PORT" }}' func TestLoadConfig_Timeouts_DefaultToFiniteValues(t *testing.T) { cfg, err := loadWithKeys(t, "") require.NoError(t, err) - assert.Equal(t, 10*time.Second, cfg.Timeouts.ReadHeader) - assert.Equal(t, 60*time.Second, cfg.Timeouts.Read) - assert.Equal(t, 120*time.Second, cfg.Timeouts.Write) - assert.Equal(t, 120*time.Second, cfg.Timeouts.Idle) + assert.Equal(t, 10*time.Second, cfg.Listeners.Timeouts.ReadHeader) + assert.Equal(t, 60*time.Second, cfg.Listeners.Timeouts.Read) + assert.Equal(t, 120*time.Second, cfg.Listeners.Timeouts.Write) + assert.Equal(t, 120*time.Second, cfg.Listeners.Timeouts.Idle) } // Duration strings resolved from {{ env }} tokens must decode into time.Duration fields. func TestLoadConfig_Timeouts_TokenOverride(t *testing.T) { - t.Setenv("APIP_CP_TIMEOUTS_READ_HEADER", "5s") - t.Setenv("APIP_CP_TIMEOUTS_READ", "30s") - t.Setenv("APIP_CP_TIMEOUTS_WRITE", "2m") - t.Setenv("APIP_CP_TIMEOUTS_IDLE", "90s") + t.Setenv("APIP_CP_SERVER_TIMEOUTS_READ_HEADER", "5s") + t.Setenv("APIP_CP_SERVER_TIMEOUTS_READ", "30s") + t.Setenv("APIP_CP_SERVER_TIMEOUTS_WRITE", "2m") + t.Setenv("APIP_CP_SERVER_TIMEOUTS_IDLE", "90s") cfg, err := loadWithKeys(t, ` -[timeouts] -read_header = '{{ env "APIP_CP_TIMEOUTS_READ_HEADER" }}' -read = '{{ env "APIP_CP_TIMEOUTS_READ" }}' -write = '{{ env "APIP_CP_TIMEOUTS_WRITE" }}' -idle = '{{ env "APIP_CP_TIMEOUTS_IDLE" }}' +[platform_api.server.timeouts] +read_header = '{{ env "APIP_CP_SERVER_TIMEOUTS_READ_HEADER" }}' +read = '{{ env "APIP_CP_SERVER_TIMEOUTS_READ" }}' +write = '{{ env "APIP_CP_SERVER_TIMEOUTS_WRITE" }}' +idle = '{{ env "APIP_CP_SERVER_TIMEOUTS_IDLE" }}' `) require.NoError(t, err) - assert.Equal(t, 5*time.Second, cfg.Timeouts.ReadHeader) - assert.Equal(t, 30*time.Second, cfg.Timeouts.Read) - assert.Equal(t, 2*time.Minute, cfg.Timeouts.Write) - assert.Equal(t, 90*time.Second, cfg.Timeouts.Idle) + assert.Equal(t, 5*time.Second, cfg.Listeners.Timeouts.ReadHeader) + assert.Equal(t, 30*time.Second, cfg.Listeners.Timeouts.Read) + assert.Equal(t, 2*time.Minute, cfg.Listeners.Timeouts.Write) + assert.Equal(t, 90*time.Second, cfg.Listeners.Timeouts.Idle) } // 0 is the net/http "no timeout" sentinel and must be accepted as-is, rather // than being silently replaced by the default. func TestLoadConfig_Timeouts_ZeroDisablesTimeout(t *testing.T) { cfg, err := loadWithKeys(t, ` -[timeouts] +[platform_api.server.timeouts] write = "0s" `) require.NoError(t, err) - assert.Zero(t, cfg.Timeouts.Write, "timeouts.write = 0 must disable the write timeout") + assert.Zero(t, cfg.Listeners.Timeouts.Write, "server.timeouts.write = 0 must disable the write timeout") } // A negative duration would expire immediately and break every request; a @@ -281,7 +393,7 @@ write = "0s" func TestLoadConfig_Timeouts_RejectsInvalidValues(t *testing.T) { t.Run("negative", func(t *testing.T) { _, err := loadWithKeys(t, ` -[timeouts] +[platform_api.server.timeouts] read = "-1s" `) require.Error(t, err) @@ -290,7 +402,7 @@ read = "-1s" t.Run("read_header exceeds read", func(t *testing.T) { _, err := loadWithKeys(t, ` -[timeouts] +[platform_api.server.timeouts] read_header = "30s" read = "10s" `) diff --git a/platform-api/config/default_config.go b/platform-api/config/default_config.go index 7552606718..fd5bd0df1d 100644 --- a/platform-api/config/default_config.go +++ b/platform-api/config/default_config.go @@ -26,12 +26,13 @@ import ( // defaultConfig returns a Server with all default values. func defaultConfig() *Server { return &Server{ - LogLevel: "INFO", - LogFormat: "text", + Logging: Logging{ + Level: "info", + Format: "text", + }, DBSchemaPath: "./internal/database/schema.sql", OpenAPISpecPath: "./resources/openapi.yaml", LLMTemplateDefinitionsPath: "./resources/default-llm-provider-templates", - EnableScopeValidation: true, Database: Database{ Driver: "sqlite3", Path: "./data/api_platform.db", @@ -40,6 +41,10 @@ func defaultConfig() *Server { ConnMaxLifetime: 300, }, Auth: Auth{ + // Default mode verifies locally-issued, asymmetrically-signed (RS256) JWTs; + // the quickstart config selects "file" to add username/password login on top. + Mode: AuthModeExternalToken, + ScopeValidation: true, // SkipPaths bypasses JWT/IDP auth middleware. Paths below the health/metrics // probes are internal gateway routes authenticated via gateway token instead. SkipPaths: []string{ @@ -61,25 +66,22 @@ func defaultConfig() *Server { "/api/internal/" + constants.APIVersion + "/webhook/events", }, JWT: JWT{ - Enabled: true, - Issuer: "platform-api", - SkipValidation: true, + Issuer: "platform-api", + TokenTTL: time.Hour, }, IDP: IDP{ - Enabled: false, ValidationMode: "scope", - ClaimMappings: IDPClaimMappings{ - OrganizationClaimName: "organization", - OrgNameClaimName: "org_name", - OrgHandleClaimName: "org_handle", - UserIDClaimName: "sub", - UsernameClaimName: "username", - EmailClaimName: "email", - ScopeClaimName: "scope", - }, }, - FileBased: FileBased{ - Enabled: false, + ClaimMappings: ClaimMappings{ + Organization: "organization", + OrgName: "org_name", + OrgHandle: "org_handle", + UserID: "sub", + Username: "username", + Email: "email", + Scope: "scope", + }, + File: FileBased{ Organization: FileBasedOrg{ ID: "default", DisplayName: "Default", @@ -96,65 +98,48 @@ func defaultConfig() *Server { }, }, }, - WebSocket: WebSocket{ - MaxConnections: 1000, - ConnectionTimeout: 30, - RateLimitPerMin: 1000, - MaxConnectionsPerOrg: 3, - MetricsLogEnabled: true, - MetricsLogInterval: 10, - }, - DefaultDevPortal: DefaultDevPortal{ - Enabled: false, - Name: "Default DevPortal", - Identifier: "default", - APIUrl: "http://localhost:3001", - Hostname: "devportal.local", - APIKey: "default-api-key", - HeaderKeyName: "x-wso2-api-key", - Timeout: 10, - RoleClaimName: "roles", - GroupsClaimName: "groups", - OrganizationClaimName: "organizationID", - AdminRole: "admin", - SubscriberRole: "Internal/subscriber", - SuperAdminRole: "superAdmin", - }, Deployments: Deployments{ MaxPerAPIGateway: 20, TimeoutEnabled: true, TimeoutInterval: 20, TimeoutDuration: 60, }, - // ArtifactLimits are unlimited by default: every limit is left at its - // zero value, which LimitReached treats as "no limit". Operators can cap - // a specific artifact kind per organization by setting a positive value - // (config file key artifact_limits.max_* or env ARTIFACT_LIMITS_MAX_*). - ArtifactLimits: ArtifactLimits{}, // By default the HTTPS listener serves on 9243 and the plain-HTTP listener // is off — preserving the historical single-TLS-port behavior. Enable the - // HTTP listener (and/or move ports) via the [http] / [https] config or the - // HTTP_* / HTTPS_* env vars. - HTTP: HTTPListener{ - Enabled: false, - Port: "9080", - }, - HTTPS: HTTPSListener{ - Enabled: true, - Port: "9243", - CertDir: "./data/certs", - }, - // Finite by default so a slow or idle peer cannot hold a connection open - // indefinitely. Write is the loosest of the four because some handlers - // proxy slow upstreams (LLM completions, deployments). - Timeouts: Timeouts{ - ReadHeader: 10 * time.Second, - Read: 60 * time.Second, - Write: 120 * time.Second, - Idle: 120 * time.Second, + // HTTP listener (and/or move ports) via the [server.http] / [server.https] + // config sections. + Listeners: ServerListeners{ + HTTP: HTTPListener{ + Enabled: false, + Port: 9080, + }, + HTTPS: HTTPSListener{ + Enabled: true, + Port: 9243, + CertFile: "./data/certs/cert.pem", + KeyFile: "./data/certs/key.pem", + }, + // Finite by default so a slow or idle peer cannot hold a connection open + // indefinitely. Write is the loosest of the four because some handlers + // proxy slow upstreams (LLM completions, deployments). + Timeouts: Timeouts{ + ReadHeader: 10 * time.Second, + Read: 60 * time.Second, + Write: 120 * time.Second, + Idle: 120 * time.Second, + }, + WebSocket: WebSocket{ + MaxConnections: 1000, + ConnectionTimeout: 30, + RateLimitPerMin: 1000, + MetricsLogEnabled: true, + MetricsLogInterval: 10, + }, }, - APIKey: APIKey{ - HashingAlgorithms: []string{"sha256"}, + Security: Security{ + APIKey: APIKey{ + HashingAlgorithms: []string{"sha256"}, + }, }, EventHub: EventHub{ PollInterval: 3 * time.Second, @@ -163,7 +148,6 @@ func defaultConfig() *Server { }, Webhook: Webhook{ Enabled: false, - GatewayType: "wso2/api-platform", SignatureTolerance: 5 * time.Minute, MaxBodySize: 1 << 20, // 1 MiB SignatureHeader: "X-Devportal-Signature", diff --git a/platform-api/internal/apperror/catalog.go b/platform-api/internal/apperror/catalog.go index 313348bb84..f10c5fa564 100644 --- a/platform-api/internal/apperror/catalog.go +++ b/platform-api/internal/apperror/catalog.go @@ -77,14 +77,12 @@ var ( LLMProviderNotFound = def(CodeLLMProviderNotFound, http.StatusNotFound, "The specified LLM provider could not be found.") LLMProviderRefNotFound = def(CodeLLMProviderRefNotFound, http.StatusBadRequest, "The referenced LLM provider could not be found.") LLMProviderExists = def(CodeLLMProviderExists, http.StatusConflict, "An LLM provider with this ID already exists.") - LLMProviderLimitReached = def(CodeLLMProviderLimitReached, http.StatusConflict, "LLM provider limit reached for the organization.") LLMProviderAPIKeyNotFound = def(CodeLLMProviderAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") LLMProviderAPIKeyForbidden = def(CodeLLMProviderAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") LLMProviderDeploymentValidationFailed = def(CodeLLMProviderDeploymentValidationFailed, http.StatusBadRequest, "%s") LLMProxyNotFound = def(CodeLLMProxyNotFound, http.StatusNotFound, "The specified LLM proxy could not be found.") LLMProxyExists = def(CodeLLMProxyExists, http.StatusConflict, "An LLM proxy with this ID already exists.") - LLMProxyLimitReached = def(CodeLLMProxyLimitReached, http.StatusConflict, "LLM proxy limit reached for the organization.") LLMProxyAPIKeyNotFound = def(CodeLLMProxyAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") LLMProxyAPIKeyForbidden = def(CodeLLMProxyAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") LLMProxyDeploymentValidationFailed = def(CodeLLMProxyDeploymentValidationFailed, http.StatusBadRequest, "%s") @@ -132,7 +130,6 @@ var ( var ( MCPProxyNotFound = def(CodeMCPProxyNotFound, http.StatusNotFound, "The specified MCP proxy could not be found.") MCPProxyExists = def(CodeMCPProxyExists, http.StatusConflict, "An MCP proxy with this ID already exists.") - MCPProxyLimitReached = def(CodeMCPProxyLimitReached, http.StatusConflict, "MCP proxy limit reached for the organization.") MCPProxyDeploymentValidationFailed = def(CodeMCPProxyDeploymentValidationFailed, http.StatusBadRequest, "%s") ) @@ -191,12 +188,10 @@ var ( // WebSub / WebBroker API entries. var ( - WebSubAPINotFound = def(CodeWebSubAPINotFound, http.StatusNotFound, "The specified WebSub API could not be found.") - WebSubAPIExists = def(CodeWebSubAPIExists, http.StatusConflict, "A WebSub API with this ID already exists.") - WebSubAPILimitReached = def(CodeWebSubAPILimitReached, http.StatusConflict, "WebSub API limit reached for the organization.") - WebBrokerAPINotFound = def(CodeWebBrokerAPINotFound, http.StatusNotFound, "The specified WebBroker API could not be found.") - WebBrokerAPIExists = def(CodeWebBrokerAPIExists, http.StatusConflict, "A WebBroker API with this ID already exists.") - WebBrokerAPILimitReached = def(CodeWebBrokerAPILimitReached, http.StatusConflict, "WebBroker API limit reached for the organization.") + WebSubAPINotFound = def(CodeWebSubAPINotFound, http.StatusNotFound, "The specified WebSub API could not be found.") + WebSubAPIExists = def(CodeWebSubAPIExists, http.StatusConflict, "A WebSub API with this ID already exists.") + WebBrokerAPINotFound = def(CodeWebBrokerAPINotFound, http.StatusNotFound, "The specified WebBroker API could not be found.") + WebBrokerAPIExists = def(CodeWebBrokerAPIExists, http.StatusConflict, "A WebBroker API with this ID already exists.") ) // HMAC secret entries. The 32-character minimum is a fixed, publicly diff --git a/platform-api/internal/apperror/codes.go b/platform-api/internal/apperror/codes.go index 7e55f34055..106792987b 100644 --- a/platform-api/internal/apperror/codes.go +++ b/platform-api/internal/apperror/codes.go @@ -39,12 +39,10 @@ const ( const ( CodeLLMProviderNotFound = "LLM_PROVIDER_NOT_FOUND" CodeLLMProviderExists = "LLM_PROVIDER_EXISTS" - CodeLLMProviderLimitReached = "LLM_PROVIDER_LIMIT_REACHED" CodeLLMProviderAPIKeyNotFound = "LLM_PROVIDER_API_KEY_NOT_FOUND" CodeLLMProviderDeploymentValidationFailed = "LLM_PROVIDER_DEPLOYMENT_VALIDATION_FAILED" CodeLLMProxyNotFound = "LLM_PROXY_NOT_FOUND" CodeLLMProxyExists = "LLM_PROXY_EXISTS" - CodeLLMProxyLimitReached = "LLM_PROXY_LIMIT_REACHED" CodeLLMProxyAPIKeyNotFound = "LLM_PROXY_API_KEY_NOT_FOUND" CodeLLMProviderRefNotFound = "LLM_PROVIDER_REF_NOT_FOUND" CodeLLMProxyDeploymentValidationFailed = "LLM_PROXY_DEPLOYMENT_VALIDATION_FAILED" @@ -104,7 +102,6 @@ const ( const ( CodeMCPProxyNotFound = "MCP_PROXY_NOT_FOUND" CodeMCPProxyExists = "MCP_PROXY_EXISTS" - CodeMCPProxyLimitReached = "MCP_PROXY_LIMIT_REACHED" CodeMCPProxyDeploymentValidationFailed = "MCP_PROXY_DEPLOYMENT_VALIDATION_FAILED" ) @@ -182,12 +179,10 @@ const ( // WebSub and WebBroker API domain codes, raised by the event-gateway plugin // and by the two gateway-internal artifact lookups. const ( - CodeWebSubAPINotFound = "WEBSUB_API_NOT_FOUND" - CodeWebSubAPIExists = "WEBSUB_API_EXISTS" - CodeWebSubAPILimitReached = "WEBSUB_API_LIMIT_REACHED" - CodeWebBrokerAPINotFound = "WEBBROKER_API_NOT_FOUND" - CodeWebBrokerAPIExists = "WEBBROKER_API_EXISTS" - CodeWebBrokerAPILimitReached = "WEBBROKER_API_LIMIT_REACHED" + CodeWebSubAPINotFound = "WEBSUB_API_NOT_FOUND" + CodeWebSubAPIExists = "WEBSUB_API_EXISTS" + CodeWebBrokerAPINotFound = "WEBBROKER_API_NOT_FOUND" + CodeWebBrokerAPIExists = "WEBBROKER_API_EXISTS" ) // HMAC secret domain codes (WebSub subscriber callback signing secrets). diff --git a/platform-api/internal/constants/constants.go b/platform-api/internal/constants/constants.go index 9ad7a4f38a..d4eb9aa4c4 100644 --- a/platform-api/internal/constants/constants.go +++ b/platform-api/internal/constants/constants.go @@ -137,11 +137,6 @@ const ( DeploymentLimitBuffer = 100 ) -// Per-organization artifact creation limits are no longer hardcoded here. They are -// configured via config.ArtifactLimits (config file keys artifact_limits.max_* or -// env vars ARTIFACT_LIMITS_MAX_*) and default to unlimited. Enforcement uses -// config.LimitReached, which treats a limit <= 0 as "no limit". - // Gateway artifact apiVersion (the `apiVersion:` field on deployment artifacts). // GatewayApiVersionV1Alpha1 is the legacy value for gateways < 1.2.0 — use it only // in down-convert paths (gatewaytranslator) that must produce artifacts @@ -162,8 +157,8 @@ const ( // Custom Policy ManagedBy constants const ( - PolicyManagedByOrganization = "organization" - PolicyManagedByWSO2 = "wso2" + PolicyManagedByOrganization = "organization" + PolicyManagedByWSO2 = "wso2" PolicyManagedByLegacyCustomer = "customer" ) diff --git a/platform-api/internal/database/connection.go b/platform-api/internal/database/connection.go index 905cb45742..3c1dfd3221 100644 --- a/platform-api/internal/database/connection.go +++ b/platform-api/internal/database/connection.go @@ -111,6 +111,17 @@ func NewConnection(cfg *config.Database, slogger *slog.Logger) (*DB, error) { "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.Name, cfg.SSLMode, ) + // sslrootcert verifies the server certificate (verify-ca/verify-full); + // sslcert/sslkey present a client certificate for mutual TLS. + if cfg.SSLRootCert != "" { + dsn += fmt.Sprintf(" sslrootcert=%s", cfg.SSLRootCert) + } + if cfg.SSLCert != "" { + dsn += fmt.Sprintf(" sslcert=%s", cfg.SSLCert) + } + if cfg.SSLKey != "" { + dsn += fmt.Sprintf(" sslkey=%s", cfg.SSLKey) + } // Use pgx stdlib driver for PostgreSQL db, err = sql.Open(DriverPGX, dsn) @@ -582,6 +593,13 @@ func buildSQLServerDSN(cfg *config.Database) string { q.Set("database", cfg.Name) q.Set("encrypt", encrypt) q.Set("TrustServerCertificate", trustServerCertificate) + // certificate pins the CA used to verify the server certificate; only + // meaningful once TrustServerCertificate is false. go-mssqldb has no + // client-certificate (mutual TLS) support, so ssl_cert/ssl_key are not + // applicable to this driver. + if cfg.SSLRootCert != "" && trustServerCertificate == "false" { + q.Set("certificate", cfg.SSLRootCert) + } host := cfg.Host if host == "" { diff --git a/platform-api/internal/handler/auth_login.go b/platform-api/internal/handler/auth_login.go index aab14a85ee..970d07e0e3 100644 --- a/platform-api/internal/handler/auth_login.go +++ b/platform-api/internal/handler/auth_login.go @@ -66,7 +66,7 @@ func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) error { return apperror.ValidationFailed.New("username and password are required") } - fileBasedAuth := &h.cfg.Auth.FileBased + fileBasedAuth := &h.cfg.Auth.File var matched *config.FileBasedUser for i := range fileBasedAuth.Users { if fileBasedAuth.Users[i].Username == req.Username { @@ -86,21 +86,37 @@ func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) error { return apperror.Unauthorized.New().WithLogMessage("login failed: password mismatch") } - expiry := time.Now().Add(8 * time.Hour) + // Claim names come from auth.claim_mappings — the same mapping IDP mode + // reads incoming claims by — so a token this endpoint signs is readable by + // validateLocalJWT (and by any other consumer configured against the same + // mapping) without the two ever drifting apart. Mapped names are used as + // flat claim keys here; a dot-separated nested path (meant for reading + // externally-issued tokens) is not meaningful to sign against and is used + // as a literal flat key if configured that way. + cm := h.cfg.Auth.ClaimMappings + expiry := time.Now().Add(h.cfg.Auth.JWT.TokenTTL) claims := jwt.MapClaims{ - "sub": matched.Username, - "username": matched.Username, - "scope": matched.Scopes, - "organization": fileBasedAuth.Organization.UUID, - "org_name": fileBasedAuth.Organization.DisplayName, - "org_handle": fileBasedAuth.Organization.ID, - "iss": h.cfg.Auth.JWT.Issuer, - "exp": expiry.Unix(), - "iat": time.Now().Unix(), + "sub": matched.Username, + claimKey(cm.Username, "username"): matched.Username, + claimKey(cm.Scope, "scope"): matched.Scopes, + claimKey(cm.Organization, "organization"): fileBasedAuth.Organization.UUID, + claimKey(cm.OrgName, "org_name"): fileBasedAuth.Organization.DisplayName, + claimKey(cm.OrgHandle, "org_handle"): fileBasedAuth.Organization.ID, + "iss": h.cfg.Auth.JWT.Issuer, + "exp": expiry.Unix(), + "iat": time.Now().Unix(), } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signed, err := token.SignedString([]byte(h.cfg.Auth.JWT.SecretKey)) + // Sign asymmetrically with RS256 using the configured RSA private key, + // read fresh from its mounted file. Config validation (validateJWTConfig) + // guarantees the key file is readable and matches the verification public + // key, so a load error here is an internal fault. + privateKey, err := h.cfg.Auth.JWT.LoadPrivateKey() + if err != nil { + return apperror.Internal.Wrap(err).WithLogMessage("failed to load JWT signing key") + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + signed, err := token.SignedString(privateKey) if err != nil { return apperror.Internal.Wrap(err).WithLogMessage("failed to issue token") } @@ -111,3 +127,12 @@ func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) error { }) return nil } + +// claimKey returns name, falling back to def when the operator has left the +// corresponding auth.claim_mappings field unset. +func claimKey(name, def string) string { + if name == "" { + return def + } + return name +} diff --git a/platform-api/internal/handler/llm_provider_integration_test.go b/platform-api/internal/handler/llm_provider_integration_test.go index 789eb62cb5..e7b3596007 100644 --- a/platform-api/internal/handler/llm_provider_integration_test.go +++ b/platform-api/internal/handler/llm_provider_integration_test.go @@ -44,9 +44,8 @@ const provOrg = "org-prov-it-001" // setupLLMProviderEnv builds the real route -> handler -> service -> repository // stack over an in-memory SQLite DB, seeded with the shipped built-in templates -// and a test organization. maxProviders <= 0 means unlimited (see -// config.ArtifactLimits), letting individual tests exercise the limit-reached path. -func setupLLMProviderEnv(t *testing.T, maxProviders int) (http.Handler, func()) { +// and a test organization. +func setupLLMProviderEnv(t *testing.T) (http.Handler, func()) { t.Helper() dbPath := filepath.Join(t.TempDir(), "test.db") @@ -84,7 +83,7 @@ func setupLLMProviderEnv(t *testing.T, maxProviders int) (http.Handler, func()) } identityService := service.NewIdentityService(repository.NewUserIdentityMappingRepo(db)) - cfg := &config.Server{ArtifactLimits: config.ArtifactLimits{MaxLLMProvidersPerOrg: maxProviders}} + cfg := &config.Server{} providerService := service.NewLLMProviderService( providerRepo, templateRepo, orgRepo, seeder, @@ -146,7 +145,7 @@ func validProviderBody(idSuffix string) string { // ---- Auth ------------------------------------------------------------- func TestLLMProviderHTTP_CreateRequiresOrg_401(t *testing.T) { - r, cleanup := setupLLMProviderEnv(t, 0) + r, cleanup := setupLLMProviderEnv(t) defer cleanup() w := doProviderJSON(t, r, http.MethodPost, provBase, validProviderBody(""), false) @@ -165,7 +164,7 @@ func TestLLMProviderHTTP_CreateRequiresOrg_401(t *testing.T) { // ---- Create: validation errors ----------------------------------------- func TestLLMProviderHTTP_Create_InvalidBody_400(t *testing.T) { - r, cleanup := setupLLMProviderEnv(t, 0) + r, cleanup := setupLLMProviderEnv(t) defer cleanup() w := doProviderJSON(t, r, http.MethodPost, provBase, `not-json`, true) @@ -175,7 +174,7 @@ func TestLLMProviderHTTP_Create_InvalidBody_400(t *testing.T) { } func TestLLMProviderHTTP_Create_MissingRequiredFields_400(t *testing.T) { - r, cleanup := setupLLMProviderEnv(t, 0) + r, cleanup := setupLLMProviderEnv(t) defer cleanup() // Missing displayName/version/template entirely. @@ -190,7 +189,7 @@ func TestLLMProviderHTTP_Create_MissingRequiredFields_400(t *testing.T) { } func TestLLMProviderHTTP_Create_UnknownTemplate_400(t *testing.T) { - r, cleanup := setupLLMProviderEnv(t, 0) + r, cleanup := setupLLMProviderEnv(t) defer cleanup() body := `{ @@ -209,7 +208,7 @@ func TestLLMProviderHTTP_Create_UnknownTemplate_400(t *testing.T) { // ---- Create: conflicts -------------------------------------------------- func TestLLMProviderHTTP_Create_DuplicateHandle_409(t *testing.T) { - r, cleanup := setupLLMProviderEnv(t, 0) + r, cleanup := setupLLMProviderEnv(t) defer cleanup() body := `{ @@ -234,24 +233,10 @@ func TestLLMProviderHTTP_Create_DuplicateHandle_409(t *testing.T) { } } -func TestLLMProviderHTTP_Create_LimitReached_409(t *testing.T) { - r, cleanup := setupLLMProviderEnv(t, 1) // only one provider allowed for this org - defer cleanup() - - if w := doProviderJSON(t, r, http.MethodPost, provBase, validProviderBody("A"), true); w.Code != http.StatusCreated { - t.Fatalf("first create: expected 201, got %d: %s", w.Code, w.Body.String()) - } - - w := doProviderJSON(t, r, http.MethodPost, provBase, validProviderBody("B"), true) - if w.Code != http.StatusConflict { - t.Fatalf("limit reached: expected 409, got %d: %s", w.Code, w.Body.String()) - } -} - // ---- Create: secret placeholder validation ------------------------------ func TestLLMProviderHTTP_Create_MissingSecretRef_400(t *testing.T) { - r, cleanup := setupLLMProviderEnv(t, 0) + r, cleanup := setupLLMProviderEnv(t) defer cleanup() body := `{ @@ -275,7 +260,7 @@ func TestLLMProviderHTTP_Create_MissingSecretRef_400(t *testing.T) { // ---- Create: happy path (sanity check for the error tests above) ------- func TestLLMProviderHTTP_Create_Success(t *testing.T) { - r, cleanup := setupLLMProviderEnv(t, 0) + r, cleanup := setupLLMProviderEnv(t) defer cleanup() w := doProviderJSON(t, r, http.MethodPost, provBase, validProviderBody(""), true) diff --git a/platform-api/internal/handler/websocket.go b/platform-api/internal/handler/websocket.go index c41d42fc3e..e13c1e889e 100644 --- a/platform-api/internal/handler/websocket.go +++ b/platform-api/internal/handler/websocket.go @@ -102,16 +102,6 @@ func (h *WebSocketHandler) Connect(w http.ResponseWriter, r *http.Request) error WithLogMessage(fmt.Sprintf("WebSocket authentication failed from IP %s", clientIP)) } - // Check organization connection limit before upgrading to WebSocket - if !h.manager.CanAcceptOrgConnection(gateway.OrganizationID) { - stats := h.manager.GetOrgConnectionStats(gateway.OrganizationID) - h.manager.IncrementFailedConnections() - return apperror.TooManyRequests.New(fmt.Sprintf( - "Organization connection limit reached. Maximum allowed connections: %d", stats.MaxAllowed)). - WithLogMessage(fmt.Sprintf("organization connection limit exceeded for org %s (count=%d, max=%d)", - gateway.OrganizationID, stats.CurrentCount, stats.MaxAllowed)) - } - // Upgrade HTTP connection to WebSocket conn, err := h.upgrader.Upgrade(w, r, nil) if err != nil { @@ -129,29 +119,12 @@ func (h *WebSocketHandler) Connect(w http.ResponseWriter, r *http.Request) error h.slogger.Error("Connection registration failed", "gatewayID", gateway.ID, "orgID", gateway.OrganizationID, "error", err) h.manager.IncrementFailedConnections() - // Check if this is an org connection limit error - if orgLimitErr, ok := err.(*ws.OrgConnectionLimitError); ok { - errorMsg := map[string]interface{}{ - "type": "error", - "code": "ORG_CONNECTION_LIMIT_EXCEEDED", - "message": "Organization connection limit reached", - "currentCount": orgLimitErr.CurrentCount, - "maxAllowed": orgLimitErr.MaxAllowed, - } - if jsonErr, _ := json.Marshal(errorMsg); jsonErr != nil { - conn.WriteMessage(websocket.TextMessage, jsonErr) - } - h.slogger.Warn("Organization connection limit exceeded", "orgID", orgLimitErr.OrganizationID, - "count", orgLimitErr.CurrentCount, "max", orgLimitErr.MaxAllowed) - } else { - // Generic error - errorMsg := map[string]string{ - "type": "error", - "message": err.Error(), - } - if jsonErr, _ := json.Marshal(errorMsg); jsonErr != nil { - conn.WriteMessage(websocket.TextMessage, jsonErr) - } + errorMsg := map[string]string{ + "type": "error", + "message": err.Error(), + } + if jsonErr, _ := json.Marshal(errorMsg); jsonErr != nil { + conn.WriteMessage(websocket.TextMessage, jsonErr) } conn.Close() return nil diff --git a/platform-api/internal/integration/harness_test.go b/platform-api/internal/integration/harness_test.go index 9599d9fbf5..cb5c4583dd 100644 --- a/platform-api/internal/integration/harness_test.go +++ b/platform-api/internal/integration/harness_test.go @@ -50,7 +50,7 @@ func TestMain(m *testing.M) { if err != nil { panic(fmt.Sprintf("integration harness: create temp config: %v", err)) } - if _, err := fmt.Fprintf(f, "encryption_key = %q\n\n[auth.jwt]\nenabled = true\nsecret_key = %q\n", testKey, testKey); err != nil { + if _, err := fmt.Fprintf(f, "[platform_api.security]\nencryption_key = %q\n\n[platform_api.auth]\nmode = \"idp\"\n\n[platform_api.auth.idp]\njwks_url = \"https://example.invalid/jwks\"\nissuer = \"https://example.invalid\"\n", testKey); err != nil { panic(fmt.Sprintf("integration harness: write temp config: %v", err)) } _ = f.Close() diff --git a/platform-api/internal/middleware/auth.go b/platform-api/internal/middleware/auth.go index 3065504e5a..d43fbdfd83 100644 --- a/platform-api/internal/middleware/auth.go +++ b/platform-api/internal/middleware/auth.go @@ -19,6 +19,7 @@ package middleware import ( "context" + "crypto/rsa" "fmt" "log/slog" "net/http" @@ -64,15 +65,22 @@ type CustomClaims struct { // AuthConfig holds the configuration for the local JWT (non-IDP) authentication path. type AuthConfig struct { - SecretKey string - TokenIssuer string - SkipPaths []string - SkipValidation bool - OrganizationClaimName string -} - -// PlatformClaimNames holds the JWT claim names used to extract platform-specific values. -type PlatformClaimNames struct { + // PublicKey is the RSA public key used to verify token signatures (RS256). + // Only asymmetric verification is supported; symmetric (HMAC) and unsigned + // ("none") tokens are rejected. + PublicKey *rsa.PublicKey + TokenIssuer string + SkipPaths []string + SkipValidation bool + // ClaimMappings is the same claim-name mapping used by IDP mode + // (PlatformClaimsMiddleware) and by the file-mode login endpoint when it + // signs tokens — one mapping shared by issuance and validation. + ClaimMappings ClaimMappings +} + +// ClaimMappings holds the JWT claim names used to extract identity values, +// shared by the local-JWT (external_token/file) and IDP auth paths. +type ClaimMappings struct { OrganizationClaim string OrgNameClaim string OrgHandleClaim string @@ -163,11 +171,14 @@ func validateLocalJWT(r *http.Request, tokenString string, config AuthConfig) (* } } else { token, err := jwt.ParseWithClaims(tokenString, mapClaims, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + // Strictly enforce asymmetric RSA signatures. Rejecting non-RSA + // methods here blocks the "none" algorithm and the HMAC-with-public-key + // forgery where an attacker signs with the public key as an HMAC secret. + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected or forbidden signing method: %v", token.Header["alg"]) } - return []byte(config.SecretKey), nil - }) + return config.PublicKey, nil + }, jwt.WithValidMethods([]string{"RS256", "RS384", "RS512"})) if err != nil { return nil, fmt.Errorf("invalid token: %w", err) } @@ -184,7 +195,7 @@ func validateLocalJWT(r *http.Request, tokenString string, config AuthConfig) (* } } - orgClaimName := config.OrganizationClaimName + orgClaimName := config.ClaimMappings.OrganizationClaim if orgClaimName == "" { orgClaimName = "organization" } @@ -192,17 +203,19 @@ func validateLocalJWT(r *http.Request, tokenString string, config AuthConfig) (* if org == "" { return nil, fmt.Errorf("token missing required '%s' claim", orgClaimName) } + orgName := getStringClaim(mapClaims, config.ClaimMappings.OrgNameClaim) + orgHandle := getStringClaim(mapClaims, config.ClaimMappings.OrgHandleClaim) sub, _ := mapClaims["sub"].(string) - username := getStringClaim(mapClaims, "username") + username := getStringClaim(mapClaims, config.ClaimMappings.UsernameClaim) if username == "" { username = sub } claimsObj := &CustomClaims{ Organization: org, Username: username, - Email: getStringClaim(mapClaims, "email"), - Scope: getStringClaim(mapClaims, "scope"), + Email: getStringClaim(mapClaims, config.ClaimMappings.EmailClaim), + Scope: getStringClaim(mapClaims, config.ClaimMappings.ScopeClaim), Audience: audienceToString(mapClaims), JTI: getStringClaim(mapClaims, "jti"), RegisteredClaims: jwt.RegisteredClaims{ @@ -210,23 +223,27 @@ func validateLocalJWT(r *http.Request, tokenString string, config AuthConfig) (* }, } + platformRoles := resolvePlatformRoles(mapClaims, config.ClaimMappings.RolesClaimPath, config.ClaimMappings.RoleScopeMap) + ctx := r.Context() - ctx = context.WithValue(ctx, keyUserID, resolveUserID(mapClaims, "")) + ctx = context.WithValue(ctx, keyUserID, resolveUserID(mapClaims, config.ClaimMappings.UserIDClaim)) ctx = context.WithValue(ctx, keyUsername, claimsObj.Username) ctx = context.WithValue(ctx, keyEmail, claimsObj.Email) ctx = context.WithValue(ctx, keyFirstName, getStringClaim(mapClaims, "firstName")) ctx = context.WithValue(ctx, keyLastName, getStringClaim(mapClaims, "lastName")) ctx = context.WithValue(ctx, keyOrganization, org) + ctx = context.WithValue(ctx, keyOrgName, orgName) + ctx = context.WithValue(ctx, keyOrgHandle, orgHandle) ctx = context.WithValue(ctx, keyScope, claimsObj.Scope) ctx = context.WithValue(ctx, keyAudience, claimsObj.Audience) ctx = context.WithValue(ctx, keyClaims, claimsObj) - ctx = context.WithValue(ctx, keyPlatformRoles, []string{}) + ctx = context.WithValue(ctx, keyPlatformRoles, platformRoles) return r.WithContext(ctx), nil } // PlatformClaimsMiddleware extracts platform-specific values from the AuthContext set by // common/authenticators.AuthMiddleware (IDP mode) and populates per-key context entries. -func PlatformClaimsMiddleware(claimNames PlatformClaimNames) func(http.Handler) http.Handler { +func PlatformClaimsMiddleware(claimNames ClaimMappings) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authCtx, ok := authenticators.GetAuthContext(r) @@ -325,23 +342,34 @@ func resolvePlatformRoles(claims jwt.MapClaims, claimPath string, roleScopeMap m } func extractClaimByPath(claims jwt.MapClaims, path string) []string { - return extractByPath(map[string]interface{}(claims), path) + val, ok := resolveClaimPath(map[string]interface{}(claims), path) + if !ok { + return nil + } + return toStringSlice(val) } -func extractByPath(obj map[string]interface{}, path string) []string { +// resolveClaimPath walks a dot-separated path into nested claim objects and +// returns the raw value found there. A path with no "." is a single flat +// claim lookup, so every claim_mappings field — not just roles — can point at +// either a top-level claim ("org_id") or a nested one ("realm_access.org_id"). +func resolveClaimPath(obj map[string]interface{}, path string) (interface{}, bool) { + if path == "" { + return nil, false + } parts := strings.SplitN(path, ".", 2) val, ok := obj[parts[0]] if !ok { - return nil + return nil, false } if len(parts) == 1 { - return toStringSlice(val) + return val, true } nested, ok := val.(map[string]interface{}) if !ok { - return nil + return nil, false } - return extractByPath(nested, parts[1]) + return resolveClaimPath(nested, parts[1]) } func toStringSlice(val interface{}) []string { @@ -360,12 +388,19 @@ func toStringSlice(val interface{}) []string { return nil } +// getStringClaim resolves name as a claim path (see resolveClaimPath) and +// returns the value as a string. name may be a flat claim ("email") or a +// dot-separated path into a nested claim ("realm_access.email"). func getStringClaim(claims jwt.MapClaims, name string) string { if name == "" { return "" } - v, _ := claims[name].(string) - return v + val, ok := resolveClaimPath(map[string]interface{}(claims), name) + if !ok { + return "" + } + s, _ := val.(string) + return s } // resolveUserID returns the stable user identifier used for audit fields diff --git a/platform-api/internal/middleware/auth_role_extraction_test.go b/platform-api/internal/middleware/auth_role_extraction_test.go index 143261391e..065371857d 100644 --- a/platform-api/internal/middleware/auth_role_extraction_test.go +++ b/platform-api/internal/middleware/auth_role_extraction_test.go @@ -198,3 +198,76 @@ func TestResolvePlatformRoles(t *testing.T) { }) } } + +// TestGetStringClaim verifies that every claim_mappings field — not just +// roles — resolves a dot-separated path into a nested claim, not only a flat +// top-level claim name. +func TestGetStringClaim(t *testing.T) { + tests := []struct { + name string + claims jwt.MapClaims + path string + want string + }{ + { + name: "flat top-level claim", + claims: jwt.MapClaims{"org_id": "acme"}, + path: "org_id", + want: "acme", + }, + { + name: "nested path into a sub-object", + claims: jwt.MapClaims{ + "realm_access": map[string]interface{}{"org_id": "acme"}, + }, + path: "realm_access.org_id", + want: "acme", + }, + { + name: "deeply nested path", + claims: jwt.MapClaims{ + "a": map[string]interface{}{ + "b": map[string]interface{}{"email": "user@example.com"}, + }, + }, + path: "a.b.email", + want: "user@example.com", + }, + { + name: "missing claim returns empty string", + claims: jwt.MapClaims{"org_id": "acme"}, + path: "missing", + want: "", + }, + { + name: "path through a non-object value returns empty string", + claims: jwt.MapClaims{ + "org_id": "acme", + }, + path: "org_id.nested", + want: "", + }, + { + name: "empty path returns empty string", + claims: jwt.MapClaims{"org_id": "acme"}, + path: "", + want: "", + }, + { + name: "non-string value at path returns empty string", + claims: jwt.MapClaims{ + "org_id": []interface{}{"acme"}, + }, + path: "org_id", + want: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := getStringClaim(tc.claims, tc.path); got != tc.want { + t.Errorf("getStringClaim() = %q, want %q", got, tc.want) + } + }) + } +} diff --git a/platform-api/internal/repository/subscription_repository.go b/platform-api/internal/repository/subscription_repository.go index 9f5de319f4..619747dc62 100644 --- a/platform-api/internal/repository/subscription_repository.go +++ b/platform-api/internal/repository/subscription_repository.go @@ -52,11 +52,11 @@ func hashSubscriptionToken(token string) string { // The single configured EncryptionKey is used for all at-rest encryption. func getSubscriptionTokenEncryptionKey() ([]byte, error) { cfg := config.GetConfig() - if cfg.EncryptionKey == "" { + if cfg.Security.EncryptionKey == "" { return nil, fmt.Errorf("subscription token encryption requires EncryptionKey. Set encryption_key" + " in config via {{ env }}/{{ file }}") } - return utils.DeriveEncryptionKey(cfg.EncryptionKey) + return utils.DeriveEncryptionKey(cfg.Security.EncryptionKey) } // SubscriptionRepo implements SubscriptionRepository @@ -241,10 +241,10 @@ func (r *SubscriptionRepo) ListByFilters(orgUUID string, apiUUID *string, subscr // re-introduced for a migration if ever needed). func decryptionKeyCandidates() [][]byte { cfg := config.GetConfig() - if cfg.EncryptionKey == "" { + if cfg.Security.EncryptionKey == "" { return nil } - if k, err := utils.DeriveEncryptionKey(cfg.EncryptionKey); err == nil { + if k, err := utils.DeriveEncryptionKey(cfg.Security.EncryptionKey); err == nil { return [][]byte{k} } return nil diff --git a/platform-api/internal/server/server.go b/platform-api/internal/server/server.go index 39b49fb5a4..4aba8437ca 100644 --- a/platform-api/internal/server/server.go +++ b/platform-api/internal/server/server.go @@ -28,6 +28,7 @@ import ( "os/signal" "path/filepath" "slices" + "strconv" "strings" "syscall" "time" @@ -66,25 +67,20 @@ type Server struct { logger *slog.Logger } -// validateAuthConfig enforces auth requirements at startup. All checks are -// unconditional: there is no relaxed/demo mode. -func validateAuthConfig(cfg *config.Server) error { - if !cfg.Auth.FileBased.Enabled && !cfg.Auth.IDP.Enabled && !cfg.Auth.JWT.Enabled { - return fmt.Errorf("no authentication mode is enabled; set auth.file_based.enabled=true, auth.jwt.enabled=true, or auth.idp.enabled=true") - } - if cfg.Auth.JWT.Enabled && cfg.Auth.JWT.SkipValidation { - return fmt.Errorf("JWT signature validation cannot be skipped (auth.jwt.skip_validation=true / APIP_CP_AUTH_JWT_SKIP_VALIDATION=true); set it to false") - } - if slices.Contains(cfg.CORS.AllowedOrigins, "*") { - return fmt.Errorf("cors.allowed_origins (APIP_CP_CORS_ALLOWED_ORIGINS) must not contain \"*\"; list explicit origins, or leave it empty to disable cross-origin access") +// validateServerConfig enforces request-security requirements at startup that +// are not covered by config-load validation. All checks are unconditional: +// there is no relaxed/demo mode. +func validateServerConfig(cfg *config.Server) error { + if slices.Contains(cfg.Listeners.CORS.AllowedOrigins, "*") { + return fmt.Errorf("cors.allowed_origins must not contain \"*\"; list explicit origins, or leave it empty to disable cross-origin access") } return nil } // StartPlatformAPIServer creates a new server instance with all dependencies initialized func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, error) { - if err := validateAuthConfig(cfg); err != nil { - slogger.Error("Invalid auth configuration for production mode", "error", err) + if err := validateServerConfig(cfg); err != nil { + slogger.Error("Invalid server configuration", "error", err) return nil, err } @@ -133,8 +129,8 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, userIdentityMappingRepo := repository.NewUserIdentityMappingRepo(db) userOrgMappingRepo := repository.NewUserOrganizationMappingRepo(db) - // Seed the file-based organization on startup if file-based auth mode is enabled. - if cfg.Auth.FileBased.Enabled { + // Seed the file-based organization on startup if file auth mode is selected. + if cfg.Auth.Mode == config.AuthModeFile { if err := seedFileBasedOrg(cfg, orgRepo, slogger); err != nil { return nil, fmt.Errorf("failed to seed file-based organization: %w", err) } @@ -191,14 +187,13 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, // Initialize WebSocket manager first (needed for GatewayEventsService) wsConfig := websocket.ManagerConfig{ - MaxConnections: cfg.WebSocket.MaxConnections, - HeartbeatInterval: 20 * time.Second, - HeartbeatTimeout: time.Duration(cfg.WebSocket.ConnectionTimeout) * time.Second, - MaxConnectionsPerOrg: cfg.WebSocket.MaxConnectionsPerOrg, - MetricsLogEnabled: cfg.WebSocket.MetricsLogEnabled, - MetricsLogInterval: time.Duration(cfg.WebSocket.MetricsLogInterval) * time.Second, + MaxConnections: cfg.Listeners.WebSocket.MaxConnections, + HeartbeatInterval: 20 * time.Second, + HeartbeatTimeout: time.Duration(cfg.Listeners.WebSocket.ConnectionTimeout) * time.Second, + MetricsLogEnabled: cfg.Listeners.WebSocket.MetricsLogEnabled, + MetricsLogInterval: time.Duration(cfg.Listeners.WebSocket.MetricsLogInterval) * time.Second, } - wsManager := websocket.NewManager(wsConfig, gatewayRepo, slogger) + wsManager := websocket.NewManager(wsConfig, slogger) // Initialize EventHub for multi-replica HA event delivery. // Events published here are polled by all platform-api instances; each instance @@ -244,7 +239,7 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, subscriptionService := service.NewSubscriptionService(apiRepo, artifactRepo, subscriptionRepo, subscriptionPlanRepo, orgRepo, gatewayEventsService, auditRepo, slogger) subscriptionPlanService := service.NewSubscriptionPlanService(subscriptionPlanRepo, gatewayRepo, orgRepo, gatewayEventsService, auditRepo, slogger) internalGatewayService := service.NewGatewayInternalAPIService(apiRepo, subscriptionRepo, subscriptionPlanRepo, llmProviderRepo, llmProxyRepo, mcpProxyRepo, deploymentRepo, gatewayRepo, orgRepo, projectRepo, apiKeyRepo, artifactRepo, secretRepo, cfg, slogger) - apiKeyService := service.NewAPIKeyService(apiRepo, artifactRepo, apiKeyRepo, gatewayEventsService, auditRepo, cfg.APIKey.HashingAlgorithms, slogger) + apiKeyService := service.NewAPIKeyService(apiRepo, artifactRepo, apiKeyRepo, gatewayEventsService, auditRepo, cfg.Security.APIKey.HashingAlgorithms, slogger) deploymentService := service.NewDeploymentService(apiRepo, artifactRepo, deploymentRepo, gatewayRepo, orgRepo, apiKeyRepo, gatewayEventsService, auditRepo, apiUtil, cfg, slogger) llmTemplateService := service.NewLLMProviderTemplateService(llmTemplateRepo, auditRepo, identityService) llmProviderService := service.NewLLMProviderService(llmProviderRepo, llmTemplateRepo, orgRepo, llmTemplateSeeder, deploymentRepo, gatewayRepo, gatewayEventsService, slogger, auditRepo, cfg, identityService) @@ -253,7 +248,7 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, // The single configured encryption key (APIP_CP_ENCRYPTION_KEY) is used for all encrypted DB // columns (secrets, subscription tokens, WebSub HMAC secrets) - dbEncryptionKey := cfg.EncryptionKey + dbEncryptionKey := cfg.Security.EncryptionKey llmProviderDeploymentService := service.NewLLMProviderDeploymentService( llmProviderRepo, llmTemplateRepo, @@ -305,7 +300,7 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, ) // Initialize secret vault and service using the single configured encryption key. - secretKey, keyErr := utils.DeriveEncryptionKey(cfg.EncryptionKey) + secretKey, keyErr := utils.DeriveEncryptionKey(cfg.Security.EncryptionKey) if keyErr != nil { return nil, fmt.Errorf("invalid encryption key: %w", keyErr) } @@ -323,7 +318,7 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService, subscriptionPlanService, identityService, slogger) subscriptionPlanHandler := handler.NewSubscriptionPlanHandler(subscriptionPlanService, identityService, slogger) appHandler := handler.NewApplicationHandler(appService, identityService, slogger) - wsHandler := handler.NewWebSocketHandler(wsManager, gatewayService, deploymentService, cfg.WebSocket.RateLimitPerMin, slogger) + wsHandler := handler.NewWebSocketHandler(wsManager, gatewayService, deploymentService, cfg.Listeners.WebSocket.RateLimitPerMin, slogger) internalGatewayHandler := handler.NewGatewayInternalAPIHandler(gatewayService, internalGatewayService, artifactImportService, secretService, slogger) apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService, identityService, slogger) deploymentHandler := handler.NewDeploymentHandler(deploymentService, identityService, slogger) @@ -368,7 +363,7 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, return nil, err } - if !cfg.EnableScopeValidation { + if !cfg.Auth.ScopeValidation { slogger.Warn("scope validation is disabled — all authenticated requests will be allowed regardless of scope") } @@ -462,7 +457,7 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, slogger, ) webhookReceiver.RegisterRoutes(mux) - slogger.Info("Webhook receiver enabled", "path", webhook.RoutePath, "gatewayType", cfg.Webhook.GatewayType) + slogger.Info("Webhook receiver enabled", "path", webhook.RoutePath) } slogger.Info("Registered API routes successfully") @@ -473,7 +468,7 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, // Cross-origin access is disabled by default (empty AllowedOrigins fails closed in the // CORS middleware); operators must opt in explicitly via CORS.AllowedOrigins in config. - corsOrigins := cfg.CORS.AllowedOrigins + corsOrigins := cfg.Listeners.CORS.AllowedOrigins if len(corsOrigins) == 0 { slogger.Warn("cors.allowed_origins not set in config — cross-origin requests are disabled") } else if slices.Contains(corsOrigins, "*") { @@ -486,14 +481,19 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, AllowCredentials: true, })) - if cfg.Auth.FileBased.Enabled { - slogger.Info("Auth mode: file-based (HMAC-signed JWT)") + if cfg.Auth.Mode == config.AuthModeFile { + slogger.Info("Auth mode: file (local users, RS256-signed JWT)") slogger.Warn("file-based authentication is enabled — this is not recommended for production; please configure an IDP of your choice") + publicKey, err := cfg.Auth.JWT.LoadPublicKey() + if err != nil { + return nil, fmt.Errorf("failed to load auth.jwt.public_key_file: %w", err) + } chain = append(chain, middleware.LocalJWTAuthMiddleware(middleware.AuthConfig{ - SecretKey: cfg.Auth.JWT.SecretKey, + PublicKey: publicKey, TokenIssuer: cfg.Auth.JWT.Issuer, SkipPaths: cfg.Auth.SkipPaths, SkipValidation: false, + ClaimMappings: buildClaimMappings(cfg.Auth.ClaimMappings, roleScopeMap), })) } else { authenticator, err := buildAuthenticator(cfg, slogger, roleScopeMap) @@ -522,14 +522,13 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, // values are already in the context when scope checks run. chain = append(chain, middleware.ScopeEnforcer(scopeRegistry, middleware.ScopeEnforcerConfig{ ValidationMode: cfg.Auth.IDP.ValidationMode, - Enabled: cfg.EnableScopeValidation, + Enabled: cfg.Auth.ScopeValidation, })) slogger.Info("WebSocket manager initialized", - slog.Int("maxConnections", cfg.WebSocket.MaxConnections), - slog.Int("heartbeatTimeout", cfg.WebSocket.ConnectionTimeout), - slog.Int("rateLimitPerMin", cfg.WebSocket.RateLimitPerMin), - slog.Int("maxConnectionsPerOrg", cfg.WebSocket.MaxConnectionsPerOrg), + slog.Int("maxConnections", cfg.Listeners.WebSocket.MaxConnections), + slog.Int("heartbeatTimeout", cfg.Listeners.WebSocket.ConnectionTimeout), + slog.Int("rateLimitPerMin", cfg.Listeners.WebSocket.RateLimitPerMin), ) return &Server{ @@ -547,19 +546,40 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, }, nil } +// buildClaimMappings adapts the config-level claim name mapping (shared by all +// three auth modes) into the middleware package's ClaimMappings, attaching the +// resolved IDP-role-to-scope table alongside it. +func buildClaimMappings(cm config.ClaimMappings, roleScopeMap map[string][]string) middleware.ClaimMappings { + return middleware.ClaimMappings{ + OrganizationClaim: cm.Organization, + OrgNameClaim: cm.OrgName, + OrgHandleClaim: cm.OrgHandle, + UserIDClaim: cm.UserID, + UsernameClaim: cm.Username, + EmailClaim: cm.Email, + ScopeClaim: cm.Scope, + RolesClaimPath: cm.Roles, + RoleScopeMap: roleScopeMap, + } +} + // buildAuthenticator constructs an Authenticator from the server configuration. -// Only called when file-based auth is disabled. +// Only called when the auth mode is "external_token" or "idp" (file mode wires +// its own local-JWT middleware). func buildAuthenticator(cfg *config.Server, slogger *slog.Logger, roleScopeMap map[string][]string) (middleware.Authenticator, error) { - if !cfg.Auth.IDP.Enabled { - // validateAuthConfig already rejected skip_validation=true, so signatures - // are always verified here. - slogger.Info("JWT mode: HMAC signature validation enabled") + if cfg.Auth.Mode != config.AuthModeIDP { + slogger.Info("Auth mode: jwt (asymmetric RS256 signature validation enabled)") + publicKey, err := cfg.Auth.JWT.LoadPublicKey() + if err != nil { + return nil, fmt.Errorf("failed to load auth.jwt.public_key_file: %w", err) + } return middleware.NewJWTAuthenticator( middleware.LocalJWTAuthMiddleware(middleware.AuthConfig{ - SecretKey: cfg.Auth.JWT.SecretKey, + PublicKey: publicKey, TokenIssuer: cfg.Auth.JWT.Issuer, SkipPaths: cfg.Auth.SkipPaths, SkipValidation: false, + ClaimMappings: buildClaimMappings(cfg.Auth.ClaimMappings, roleScopeMap), }), ), nil } @@ -573,7 +593,7 @@ func buildAuthenticator(cfg *config.Server, slogger *slog.Logger, roleScopeMap m Enabled: true, IssuerURL: issuerURL, JWKSUrl: cfg.Auth.IDP.JWKSUrl, - ScopeClaim: cfg.Auth.IDP.ClaimMappings.ScopeClaimName, + ScopeClaim: cfg.Auth.ClaimMappings.Scope, } // Enforce audience validation only when at least one audience is configured. if len(cfg.Auth.IDP.Audience) > 0 { @@ -587,17 +607,7 @@ func buildAuthenticator(cfg *config.Server, slogger *slog.Logger, roleScopeMap m if err != nil { return nil, fmt.Errorf("failed to initialize IDP auth middleware: %w", err) } - claimsMiddleware := middleware.PlatformClaimsMiddleware(middleware.PlatformClaimNames{ - OrganizationClaim: cfg.Auth.IDP.ClaimMappings.OrganizationClaimName, - OrgNameClaim: cfg.Auth.IDP.ClaimMappings.OrgNameClaimName, - OrgHandleClaim: cfg.Auth.IDP.ClaimMappings.OrgHandleClaimName, - UserIDClaim: cfg.Auth.IDP.ClaimMappings.UserIDClaimName, - UsernameClaim: cfg.Auth.IDP.ClaimMappings.UsernameClaimName, - EmailClaim: cfg.Auth.IDP.ClaimMappings.EmailClaimName, - ScopeClaim: cfg.Auth.IDP.ClaimMappings.ScopeClaimName, - RolesClaimPath: cfg.Auth.IDP.ClaimMappings.RolesClaimPath, - RoleScopeMap: roleScopeMap, - }) + claimsMiddleware := middleware.PlatformClaimsMiddleware(buildClaimMappings(cfg.Auth.ClaimMappings, roleScopeMap)) idpLabel := cfg.Auth.IDP.Name if idpLabel == "" { @@ -618,18 +628,18 @@ func buildAuthenticator(cfg *config.Server, slogger *slog.Logger, roleScopeMap m // Returns nil when role mode is not active or no mapping file is configured, // which causes IDP role names to be used as-is as scope values (passthrough). func loadRoleScopeMap(cfg *config.Server, registry *middleware.ScopeRegistry, slogger *slog.Logger) (map[string][]string, error) { - if !cfg.Auth.IDP.Enabled || cfg.Auth.IDP.ValidationMode != "role" || cfg.Auth.IDP.RoleMappingsFile == "" { + if cfg.Auth.Mode != config.AuthModeIDP || cfg.Auth.IDP.ValidationMode != "role" || cfg.Auth.IDP.RoleMappings == "" { return nil, nil } - m, err := middleware.LoadRoleScopeMap(cfg.Auth.IDP.RoleMappingsFile) + m, err := middleware.LoadRoleScopeMap(cfg.Auth.IDP.RoleMappings) if err != nil { return nil, fmt.Errorf("failed to load role mappings file: %w", err) } if err := middleware.ValidateRoleScopeMap(m, registry); err != nil { return nil, fmt.Errorf("invalid roles.yaml: %w", err) } - slogger.Info("Loaded role-to-scope mapping", "path", cfg.Auth.IDP.RoleMappingsFile, "roles", len(m)) + slogger.Info("Loaded role-to-scope mapping", "path", cfg.Auth.IDP.RoleMappings, "roles", len(m)) return m, nil } @@ -639,20 +649,22 @@ func loadRoleScopeMap(cfg *config.Server, registry *middleware.ScopeRegistry, sl // there is no self-signed fallback; use the quickstart setup script (or your // own tooling) to generate a pair and mount it. func (s *Server) buildTLSConfig(httpsCfg config.HTTPSListener) (*tls.Config, error) { - certDir := httpsCfg.CertDir - certPath := filepath.Join(certDir, "cert.pem") - keyPath := filepath.Join(certDir, "key.pem") + certFile := httpsCfg.CertFile + keyFile := httpsCfg.KeyFile + if certFile == "" || keyFile == "" { + return nil, fmt.Errorf("HTTPS listener enabled but server.https.cert_file / server.https.key_file is not configured") + } - cert, err := tls.LoadX509KeyPair(certPath, keyPath) + cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, fmt.Errorf( - "failed to load TLS certificates from %q (cert.pem / key.pem): %w. "+ - "Mount certificates there, set APIP_CP_HTTPS_CERT_DIR to a directory containing cert.pem and key.pem, "+ - "or set APIP_CP_HTTPS_ENABLED=false to serve plain HTTP behind a TLS-terminating proxy", - certDir, err, + "failed to load TLS certificates (cert %q / key %q): %w. "+ + "Mount a certificate pair and point server.https.cert_file / key_file at it, "+ + "or set server.https.enabled=false to serve plain HTTP behind a TLS-terminating proxy", + certFile, keyFile, err, ) } - s.logger.Info("Using mounted certificates", "certDir", certDir) + s.logger.Info("Using mounted certificates", "certFile", certFile, "keyFile", keyFile) return &tls.Config{ Certificates: []tls.Certificate{cert}, @@ -667,21 +679,23 @@ func (s *Server) buildTLSConfig(httpsCfg config.HTTPSListener) (*tls.Config, err // // timeouts bounds connection lifetime on both listeners so a slow or idle peer // cannot hold one open indefinitely (Slowloris). It is validated at config load. -func (s *Server) Start(httpCfg config.HTTPListener, httpsCfg config.HTTPSListener, timeouts config.Timeouts) error { +func (s *Server) Start(listeners config.ServerListeners, timeouts config.Timeouts) error { + httpCfg := listeners.HTTP + httpsCfg := listeners.HTTPS if !httpCfg.Enabled && !httpsCfg.Enabled { s.logger.Error("No listeners enabled") - return fmt.Errorf("no listeners enabled: set http.enabled=true and/or https.enabled=true in config") + return fmt.Errorf("no listeners enabled: set server.http.enabled=true and/or server.https.enabled=true in config") } // Preflight: validate listener configuration and build the TLS config before // starting any listener, background job, or goroutine - if httpCfg.Enabled && httpCfg.Port == "" { - return fmt.Errorf("HTTP listener enabled but http.port is empty") + if httpCfg.Enabled && (httpCfg.Port <= 0 || httpCfg.Port > 65535) { + return fmt.Errorf("HTTP listener enabled but server.http.port is invalid (got %d)", httpCfg.Port) } var tlsConfig *tls.Config if httpsCfg.Enabled { - if httpsCfg.Port == "" { - return fmt.Errorf("HTTPS listener enabled but https.port is empty") + if httpsCfg.Port <= 0 || httpsCfg.Port > 65535 { + return fmt.Errorf("HTTPS listener enabled but server.https.port is invalid (got %d)", httpsCfg.Port) } var err error if tlsConfig, err = s.buildTLSConfig(httpsCfg); err != nil { @@ -709,11 +723,12 @@ func (s *Server) Start(httpCfg config.HTTPListener, httpsCfg config.HTTPSListene if httpCfg.Enabled { // Plain HTTP is only safe when something upstream terminates TLS, or for // internal traffic. Say so loudly. - s.logger.Warn("Plain-HTTP listener is enabled (http.enabled=true)" + - "terminate TLS at an ingress or service-mesh sidecar and never expose this listener " + - "directly to untrusted networks.") + s.logger.Warn("Plain-HTTP listener is enabled (server.http.enabled=true); " + + "terminate TLS at an ingress or service-mesh sidecar and never expose this listener " + + "directly to untrusted networks.") + httpPort := strconv.Itoa(httpCfg.Port) httpServer := &http.Server{ - Addr: ":" + httpCfg.Port, + Addr: ":" + httpPort, Handler: s.handler, ReadHeaderTimeout: timeouts.ReadHeader, ReadTimeout: timeouts.Read, @@ -721,7 +736,7 @@ func (s *Server) Start(httpCfg config.HTTPListener, httpsCfg config.HTTPSListene IdleTimeout: timeouts.Idle, } httpServers = append(httpServers, httpServer) - s.logger.Info("Starting HTTP listener", "address", "http://localhost:"+httpCfg.Port) + s.logger.Info("Starting HTTP listener", "address", "http://localhost:"+httpPort) go func() { errCh <- httpServer.ListenAndServe() }() @@ -729,8 +744,9 @@ func (s *Server) Start(httpCfg config.HTTPListener, httpsCfg config.HTTPSListene // TLS listener. if httpsCfg.Enabled { + httpsPort := strconv.Itoa(httpsCfg.Port) httpsServer := &http.Server{ - Addr: ":" + httpsCfg.Port, + Addr: ":" + httpsPort, Handler: s.handler, TLSConfig: tlsConfig, ReadHeaderTimeout: timeouts.ReadHeader, @@ -739,7 +755,7 @@ func (s *Server) Start(httpCfg config.HTTPListener, httpsCfg config.HTTPSListene IdleTimeout: timeouts.Idle, } httpServers = append(httpServers, httpsServer) - s.logger.Info("Starting HTTPS listener", "address", "https://localhost:"+httpsCfg.Port) + s.logger.Info("Starting HTTPS listener", "address", "https://localhost:"+httpsPort) go func() { errCh <- httpsServer.ListenAndServeTLS("", "") }() @@ -800,7 +816,7 @@ func (s *Server) GetMux() *http.ServeMux { // is stored back into cfg (Organization.UUID) so the login handler issues tokens // whose `organization` claim matches the value the organization resolver looks up. func seedFileBasedOrg(cfg *config.Server, orgRepo repository.OrganizationRepository, slogger *slog.Logger) error { - ba := &cfg.Auth.FileBased + ba := &cfg.Auth.File existing, err := orgRepo.GetOrganizationByHandle(ba.Organization.ID) if err != nil { diff --git a/platform-api/internal/server/server_tls_test.go b/platform-api/internal/server/server_tls_test.go index c555a058c2..82b9492a2a 100644 --- a/platform-api/internal/server/server_tls_test.go +++ b/platform-api/internal/server/server_tls_test.go @@ -41,21 +41,36 @@ func testServer() *Server { // HTTPS listener with no certificates: a fatal misconfiguration — certificates // are always required, there is no self-signed fallback. func TestBuildTLSConfig_MissingCert_Errors(t *testing.T) { + missingDir := filepath.Join(t.TempDir(), "does-not-exist") _, err := testServer().buildTLSConfig(config.HTTPSListener{ - Enabled: true, - CertDir: filepath.Join(t.TempDir(), "does-not-exist"), + Enabled: true, + CertFile: filepath.Join(missingDir, "cert.pem"), + KeyFile: filepath.Join(missingDir, "key.pem"), }) if err == nil { t.Fatal("expected an error when the HTTPS listener has no certificates") } } +// HTTPS listener with unset certificate paths: rejected before any file access. +func TestBuildTLSConfig_UnsetCertPaths_Errors(t *testing.T) { + _, err := testServer().buildTLSConfig(config.HTTPSListener{Enabled: true}) + if err == nil { + t.Fatal("expected an error when cert_file / key_file are not configured") + } +} + // HTTPS listener with a mounted certificate pair: loaded successfully. func TestBuildTLSConfig_MountedCert_Loads(t *testing.T) { certDir := t.TempDir() writeTestCertPair(t, certDir) - tlsConfig, err := testServer().buildTLSConfig(config.HTTPSListener{Enabled: true, Port: "9243", CertDir: certDir}) + tlsConfig, err := testServer().buildTLSConfig(config.HTTPSListener{ + Enabled: true, + Port: 9243, + CertFile: filepath.Join(certDir, "cert.pem"), + KeyFile: filepath.Join(certDir, "key.pem"), + }) if err != nil { t.Fatalf("expected mounted certificates to load, got %v", err) } diff --git a/platform-api/internal/service/llm.go b/platform-api/internal/service/llm.go index 96089b952f..ba2e66d500 100644 --- a/platform-api/internal/service/llm.go +++ b/platform-api/internal/service/llm.go @@ -907,14 +907,6 @@ func (s *LLMProviderService) Create(orgUUID, createdBy string, req *api.LLMProvi } } - providerCount, err := s.repo.Count(orgUUID) - if err != nil { - return nil, fmt.Errorf("failed to count providers: %w", err) - } - if err := validateLLMResourceLimit(providerCount, s.cfg.ArtifactLimits.MaxLLMProvidersPerOrg, apperror.LLMProviderLimitReached.New()); err != nil { - return nil, err - } - openapiSpec := utils.ValueOrEmpty(req.Openapi) if openapiSpec == "" { openapiSpec = resolveTemplateOpenAPISpec(context.Background(), tpl, openAPISpecFetchLimit(s.cfg), s.slogger) @@ -1398,14 +1390,6 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( } } - proxyCount, err := s.repo.Count(orgUUID) - if err != nil { - return nil, fmt.Errorf("failed to count proxies: %w", err) - } - if err := validateLLMResourceLimit(proxyCount, s.cfg.ArtifactLimits.MaxLLMProxiesPerOrg, apperror.LLMProxyLimitReached.New()); err != nil { - return nil, err - } - // Resolve any associated gateways up-front so they can be persisted within the // same transaction as the proxy create. associatedGateways, err := resolveAssociatedGateways(s.gatewayRepo, orgUUID, req.AssociatedGateways) @@ -1889,15 +1873,6 @@ func preserveUpstreamAuthCredential(existing, updated *model.UpstreamAuth) *mode return updated } -// validateLLMResourceLimit returns limitErr when the org has reached maxAllowed. -// A maxAllowed <= 0 means unlimited (see config.LimitReached), so it never errors. -func validateLLMResourceLimit(currentCount int, maxAllowed int, limitErr error) error { - if config.LimitReached(currentCount, maxAllowed) { - return limitErr - } - return nil -} - func mapExtractionIdentifierAPI(in *api.ExtractionIdentifier) *model.ExtractionIdentifier { if in == nil { return nil diff --git a/platform-api/internal/service/llm_test.go b/platform-api/internal/service/llm_test.go index 6657ce85aa..e292748ede 100644 --- a/platform-api/internal/service/llm_test.go +++ b/platform-api/internal/service/llm_test.go @@ -355,37 +355,6 @@ func TestMapProxyModelToAPI_DoesNotExposeProviderAuthValue(t *testing.T) { } } -func TestValidateLLMResourceLimit(t *testing.T) { - t.Run("below limit should pass", func(t *testing.T) { - err := validateLLMResourceLimit(4, 5, apperror.LLMProviderLimitReached.New()) - if err != nil { - t.Fatalf("expected no error below limit, got: %v", err) - } - }) - - t.Run("at limit should fail", func(t *testing.T) { - err := validateLLMResourceLimit(5, 5, apperror.LLMProviderLimitReached.New()) - if !apperror.LLMProviderLimitReached.Is(err) { - t.Fatalf("expected ErrLLMProviderLimitReached, got: %v", err) - } - }) - - t.Run("above limit should fail", func(t *testing.T) { - err := validateLLMResourceLimit(6, 5, apperror.LLMProxyLimitReached.New()) - if !apperror.LLMProxyLimitReached.Is(err) { - t.Fatalf("expected ErrLLMProxyLimitReached, got: %v", err) - } - }) - - t.Run("unlimited (limit <= 0) should always pass", func(t *testing.T) { - for _, limit := range []int{0, -1} { - if err := validateLLMResourceLimit(1_000_000, limit, apperror.LLMProviderLimitReached.New()); err != nil { - t.Fatalf("expected no error for unlimited (limit=%d), got: %v", limit, err) - } - } - }) -} - func TestGenerateLLMProviderDeploymentYAML_WithSecurityAPIKeyPolicy(t *testing.T) { trueValue := true diff --git a/platform-api/internal/service/mcp.go b/platform-api/internal/service/mcp.go index 141e34cc9c..e8a8e2b350 100644 --- a/platform-api/internal/service/mcp.go +++ b/platform-api/internal/service/mcp.go @@ -152,15 +152,6 @@ func (s *MCPProxyService) Create(orgUUID, createdBy string, req *api.MCPProxy) ( } req.Id = &handle - // Enforce the per-organization MCP proxy limit (unlimited when not configured). - proxyCount, err := s.repo.Count(orgUUID) - if err != nil { - return nil, fmt.Errorf("failed to count existing MCP proxies: %w", err) - } - if config.LimitReached(proxyCount, s.cfg.ArtifactLimits.MaxMCPProxiesPerOrg) { - return nil, apperror.MCPProxyLimitReached.New() - } - // Validate {{ secret "..." }} placeholders anywhere in the request — the // gateway-controller's template engine resolves placeholders generically // across the whole artifact (policies included), not just upstream.auth, diff --git a/platform-api/internal/webhook/envelope.go b/platform-api/internal/webhook/envelope.go index bd5e2987ac..b7ec40b382 100644 --- a/platform-api/internal/webhook/envelope.go +++ b/platform-api/internal/webhook/envelope.go @@ -66,7 +66,6 @@ type Envelope struct { // OrgID is the legacy flat org identifier, superseded by Org.RefID. DecodeEnvelope copies // Org.RefID into it when present, so downstream handlers can keep reading env.OrgID. OrgID string `json:"org_id"` - GatewayType string `json:"gateway_type"` AggregateType string `json:"aggregate_type"` AggregateID string `json:"aggregate_id"` SchemaVersion string `json:"schema_version"` diff --git a/platform-api/internal/webhook/receiver.go b/platform-api/internal/webhook/receiver.go index 4e75f65271..c67c39b3b7 100644 --- a/platform-api/internal/webhook/receiver.go +++ b/platform-api/internal/webhook/receiver.go @@ -133,7 +133,7 @@ func (r *Receiver) RegisterRoutes(mux *http.ServeMux) { } // ReceiveEvent runs the full webhook flow: size-limited read -> signature verify -> envelope -// decode/validate -> gateway_type filter -> idempotency -> dispatch -> mark processed. +// decode/validate -> idempotency -> dispatch -> mark processed. func (r *Receiver) ReceiveEvent(w http.ResponseWriter, req *http.Request) error { if !r.cfg.Enabled { return apperror.NotFound.New().WithLogMessage("webhook endpoint is disabled") @@ -175,14 +175,7 @@ func (r *Receiver) ReceiveEvent(w http.ResponseWriter, req *http.Request) error } log = log.With("orgId", env.OrgID) - // 4. gateway_type filter — events for other gateway types are a no-op (accepted, not processed). - if r.cfg.GatewayType != "" && env.GatewayType != "" && env.GatewayType != r.cfg.GatewayType { - log.Info("Webhook event for a different gateway_type; accepting as no-op", "eventGatewayType", env.GatewayType) - httputil.WriteJSON(w, http.StatusAccepted, map[string]string{"status": "ignored", "reason": "gateway_type mismatch"}) - return nil - } - - // 5. Dispatch to the matching handler. Duplicate (at-least-once) deliveries are made safe by + // 4. Dispatch to the matching handler. Duplicate (at-least-once) deliveries are made safe by // each handler being idempotent by domain identity, so no envelope-level dedup is needed. handle, ok := r.handlers[env.EventType] if !ok { diff --git a/platform-api/internal/websocket/errors.go b/platform-api/internal/websocket/errors.go deleted file mode 100644 index 5d8f619303..0000000000 --- a/platform-api/internal/websocket/errors.go +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package websocket - -import "fmt" - -// OrgConnectionLimitError is returned when an organization has reached its connection limit -type OrgConnectionLimitError struct { - OrganizationID string - CurrentCount int - MaxAllowed int -} - -func (e *OrgConnectionLimitError) Error() string { - return fmt.Sprintf("organization %s has reached maximum connection limit: %d/%d", - e.OrganizationID, e.CurrentCount, e.MaxAllowed) -} - -// IsOrgConnectionLimitError checks if an error is an OrgConnectionLimitError -func IsOrgConnectionLimitError(err error) bool { - _, ok := err.(*OrgConnectionLimitError) - return ok -} diff --git a/platform-api/internal/websocket/manager.go b/platform-api/internal/websocket/manager.go index 1509966e63..a654058d7a 100644 --- a/platform-api/internal/websocket/manager.go +++ b/platform-api/internal/websocket/manager.go @@ -25,8 +25,6 @@ import ( "sync/atomic" "time" - "github.com/wso2/api-platform/platform-api/internal/repository" - "github.com/google/uuid" ) @@ -57,12 +55,6 @@ type Manager struct { // heartbeatTimeout specifies when to consider a connection dead (default 30s) heartbeatTimeout time.Duration - // maxConnectionsPerOrg enforces per-organization connection limits - maxConnectionsPerOrg int - - // gatewayRepo provides access to gateway data for org-scoped connection counting - gatewayRepo repository.GatewayRepository - // slogger is the structured logger instance slogger *slog.Logger @@ -95,48 +87,38 @@ type Manager struct { // ManagerConfig contains configuration parameters for the connection manager type ManagerConfig struct { - MaxConnections int // Maximum concurrent connections (default 1000) - HeartbeatInterval time.Duration // Ping interval (default 20s) - HeartbeatTimeout time.Duration // Pong timeout (default 30s) - MaxConnectionsPerOrg int // Maximum connections per organization (default 3) - MetricsLogEnabled bool // Enable periodic metrics logging (default true) - MetricsLogInterval time.Duration // Interval between metrics log entries (default 10s) -} - -type OrgConnectionStats struct { - OrganizationID string `json:"organizationId"` - CurrentCount int `json:"currentCount"` - MaxAllowed int `json:"maxAllowed"` + MaxConnections int // Maximum concurrent connections (default 1000) + HeartbeatInterval time.Duration // Ping interval (default 20s) + HeartbeatTimeout time.Duration // Pong timeout (default 30s) + MetricsLogEnabled bool // Enable periodic metrics logging (default true) + MetricsLogInterval time.Duration // Interval between metrics log entries (default 10s) } // DefaultManagerConfig returns sensible default configuration values func DefaultManagerConfig() ManagerConfig { return ManagerConfig{ - MaxConnections: 1000, - HeartbeatInterval: 20 * time.Second, - HeartbeatTimeout: 30 * time.Second, - MaxConnectionsPerOrg: 3, - MetricsLogEnabled: true, - MetricsLogInterval: 10 * time.Second, + MaxConnections: 1000, + HeartbeatInterval: 20 * time.Second, + HeartbeatTimeout: 30 * time.Second, + MetricsLogEnabled: true, + MetricsLogInterval: 10 * time.Second, } } // NewManager creates a new connection manager with the provided configuration -func NewManager(config ManagerConfig, gatewayRepo repository.GatewayRepository, slogger *slog.Logger) *Manager { +func NewManager(config ManagerConfig, slogger *slog.Logger) *Manager { ctx, cancel := context.WithCancel(context.Background()) mgr := &Manager{ - connections: sync.Map{}, - connectionCount: 0, - maxConnections: config.MaxConnections, - heartbeatInterval: config.HeartbeatInterval, - heartbeatTimeout: config.HeartbeatTimeout, - maxConnectionsPerOrg: config.MaxConnectionsPerOrg, - gatewayRepo: gatewayRepo, - slogger: slogger, - shutdownCtx: ctx, - shutdownFn: cancel, - metricsLogEnabled: config.MetricsLogEnabled, - metricsLogInterval: config.MetricsLogInterval, + connections: sync.Map{}, + connectionCount: 0, + maxConnections: config.MaxConnections, + heartbeatInterval: config.HeartbeatInterval, + heartbeatTimeout: config.HeartbeatTimeout, + slogger: slogger, + shutdownCtx: ctx, + shutdownFn: cancel, + metricsLogEnabled: config.MetricsLogEnabled, + metricsLogInterval: config.MetricsLogInterval, } // Disable metrics logging if the interval is non-positive to prevent // time.NewTicker from panicking. @@ -177,16 +159,6 @@ func (m *Manager) SetConnectionHooks(onConnect, onDisconnect func(gatewayID stri func (m *Manager) Register(gatewayID string, transport Transport, authToken string, orgID string) (*Connection, error) { - // Check per-org limit first (count from main connections map) - orgCount := m.countOrgConnections(orgID) - if orgCount >= m.maxConnectionsPerOrg { - return nil, &OrgConnectionLimitError{ - OrganizationID: orgID, - CurrentCount: orgCount, - MaxAllowed: m.maxConnectionsPerOrg, - } - } - // Check global connection limit m.mu.Lock() if m.connectionCount >= m.maxConnections { @@ -212,7 +184,7 @@ func (m *Manager) Register(gatewayID string, transport Transport, authToken stri m.IncrementSuccessfulConnections() m.slogger.Info("Gateway connected", "gatewayID", gatewayID, "connectionID", connectionID, - "orgID", orgID, "totalConnections", m.GetConnectionCount(), "orgConnections", m.countOrgConnections(orgID)) + "orgID", orgID, "totalConnections", m.GetConnectionCount()) if m.onConnect != nil { m.onConnect(gatewayID) @@ -311,25 +283,6 @@ func (m *Manager) GetConnectionCount() int { return m.connectionCount } -// countOrgConnections counts the number of connections for a specific organization -// by fetching the org's gateways and only counting connections for those gateway IDs. -func (m *Manager) countOrgConnections(orgID string) int { - gateways, err := m.gatewayRepo.GetByOrganizationID(orgID) - if err != nil { - m.slogger.Error("Failed to fetch gateways for org", "orgID", orgID, "error", err) - return 0 - } - - count := 0 - for _, gw := range gateways { - if connsInterface, ok := m.connections.Load(gw.ID); ok { - conns := connsInterface.([]*Connection) - count += len(conns) - } - } - return count -} - // monitorHeartbeat periodically sends ping frames and detects connection death. // Runs in a background goroutine for each connection. // @@ -411,21 +364,6 @@ func (m *Manager) Shutdown() { m.slogger.Info("WebSocket manager shutdown complete") } -// GetOrgConnectionStats returns connection statistics for a specific organization -func (m *Manager) GetOrgConnectionStats(orgID string) OrgConnectionStats { - return OrgConnectionStats{ - OrganizationID: orgID, - CurrentCount: m.countOrgConnections(orgID), - MaxAllowed: m.maxConnectionsPerOrg, - } -} - -// CanAcceptOrgConnection checks if the organization can accept a new connection -// without actually adding it. Use this for pre-upgrade validation. -func (m *Manager) CanAcceptOrgConnection(orgID string) bool { - return m.countOrgConnections(orgID) < m.maxConnectionsPerOrg -} - type metricsPayload struct { From string `json:"from"` To string `json:"to"` diff --git a/platform-api/plugins/eventgateway/plugin.go b/platform-api/plugins/eventgateway/plugin.go index fdde3af64b..03a57bcb8e 100644 --- a/platform-api/plugins/eventgateway/plugin.go +++ b/platform-api/plugins/eventgateway/plugin.go @@ -278,19 +278,10 @@ func (p *EventGatewayPlugin) EnrichSubscription(orgID string, sub *api.Organizat if err != nil { return err } - // A limit <= 0 means unlimited: report only usage, leaving Limit/Remaining - // unset so consumers treat the WebSub API quota as uncapped. - quota := &api.OrganizationQuota{Used: count} - if limit := p.cfg.ArtifactLimits.MaxWebSubAPIsPerOrg; limit > 0 { - quota.Limit = intPtr(limit) - quota.Remaining = intPtr(max(limit-count, 0)) - } - sub.Quotas.WebsubApis = quota + sub.Quotas.WebsubApis = &api.OrganizationQuota{Used: count} return nil } -func intPtr(v int) *int { return &v } - // selectSchema returns the DDL appropriate for the current database driver. func (p *EventGatewayPlugin) selectSchema(db *database.DB) string { driver := strings.ToLower(db.Driver()) diff --git a/platform-api/plugins/eventgateway/service/webbroker_api.go b/platform-api/plugins/eventgateway/service/webbroker_api.go index c43d1be2f5..0cad990abb 100644 --- a/platform-api/plugins/eventgateway/service/webbroker_api.go +++ b/platform-api/plugins/eventgateway/service/webbroker_api.go @@ -126,15 +126,6 @@ func (s *WebBrokerAPIService) Create(orgUUID, createdBy string, req *api.WebBrok return nil, apperror.WebBrokerAPIExists.New() } - // Enforce the per-organization WebBroker API limit (unlimited when not configured). - count, err := s.repo.Count(orgUUID) - if err != nil { - return nil, fmt.Errorf("failed to count existing WebBroker APIs: %w", err) - } - if config.LimitReached(count, s.cfg.ArtifactLimits.MaxWebBrokerAPIsPerOrg) { - return nil, apperror.WebBrokerAPILimitReached.New() - } - transport := []string{"http", "https"} if req.Transport != nil && len(*req.Transport) > 0 { transport = make([]string, 0, len(*req.Transport)) diff --git a/platform-api/plugins/eventgateway/service/websub_api.go b/platform-api/plugins/eventgateway/service/websub_api.go index 205e5dfdf9..21cd1accdc 100644 --- a/platform-api/plugins/eventgateway/service/websub_api.go +++ b/platform-api/plugins/eventgateway/service/websub_api.go @@ -89,7 +89,6 @@ func (s *WebSubAPIService) toWebSubAPI(m *model.WebSubAPI) (*api.WebSubAPI, erro return resp, nil } - // Create creates a new WebSub API func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) (*api.WebSubAPI, error) { if req == nil { @@ -127,15 +126,6 @@ func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) return nil, apperror.WebSubAPIExists.New() } - // Enforce the per-organization WebSub API limit (unlimited when not configured). - count, err := s.repo.Count(orgUUID) - if err != nil { - return nil, fmt.Errorf("failed to count existing WebSub APIs: %w", err) - } - if config.LimitReached(count, s.cfg.ArtifactLimits.MaxWebSubAPIsPerOrg) { - return nil, apperror.WebSubAPILimitReached.New() - } - transport := []string{"http", "https"} if req.Transport != nil && len(*req.Transport) > 0 { transport = make([]string, 0, len(*req.Transport)) diff --git a/platform-api/resources/roles.yaml b/platform-api/resources/roles.yaml index 7e60e0fefa..a779b46e8b 100644 --- a/platform-api/resources/roles.yaml +++ b/platform-api/resources/roles.yaml @@ -1,14 +1,14 @@ # Role-to-scope mapping for IDP role mode (auth.idp.validation_mode: role). # # Each entry maps an IDP role name (as it appears in the token claim configured -# by auth.idp.roles_claim_path) to the list of platform scopes that role grants. +# by auth.idp.roles) to the list of platform scopes that role grants. # When a token carries multiple roles, the effective scopes are the union of all # role entries — most-permissive wins. # # Supported IDPs: -# Asgardeo — set roles_claim_path: roles -# Keycloak — set roles_claim_path: realm_access.roles (or resource_access..roles) -# Microsoft Entra ID — set roles_claim_path: roles +# Asgardeo — set roles: roles +# Keycloak — set roles: realm_access.roles (or resource_access..roles) +# Microsoft Entra ID — set roles: roles # # Scope convention: # ap::manage — full access to all actions on that resource (covers read/create/update/delete) diff --git a/portals/ai-workspace/.gitignore b/portals/ai-workspace/.gitignore index 97628a0377..25031c3f3e 100644 --- a/portals/ai-workspace/.gitignore +++ b/portals/ai-workspace/.gitignore @@ -6,3 +6,4 @@ target .vscode api-platform.env resources/certificates/ +resources/keys/ \ No newline at end of file diff --git a/portals/ai-workspace/Makefile b/portals/ai-workspace/Makefile index c2b16f45c1..a35a28e554 100644 --- a/portals/ai-workspace/Makefile +++ b/portals/ai-workspace/Makefile @@ -30,8 +30,8 @@ IMAGE_NAME := $(DOCKER_REGISTRY)/ai-workspace # BFF (Backend-for-Frontend) — Go server that serves the SPA, proxies all # browser→backend traffic, and owns authentication. BFF_DIR := bff -BFF_DEV_ADDR ?= :8081 -PLATFORM_API_URL ?= https://localhost:9243 +BFF_DEV_PORT ?= 8081 +CONTROL_PLANE_URL ?= https://localhost:9243 help: ## Show this help message @echo 'AI Workspace Build System (Version: $(VERSION))' @@ -55,17 +55,16 @@ bff-build: ## Build the BFF binary # thing that lets an environment variable reach a key, so a key absent from the file # cannot be set here. To test against an IDP, add APIP_AIW_AUTH_MODE=oidc and the # APIP_AIW_OIDC_* variables the [oidc] tokens name — see README.md. -bff-run: ## Run the BFF locally (proxies to PLATFORM_API_URL; pair with `make run`) +bff-run: ## Run the BFF locally (proxies to CONTROL_PLANE_URL; pair with `make run`) ./setup.sh --certs-only cd $(BFF_DIR) && \ - APIP_AIW_PLATFORM_API_URL=$(PLATFORM_API_URL) \ - APIP_AIW_PLATFORM_API_TLS_SKIP_VERIFY=true \ - APIP_AIW_TLS_CERT_FILE=../resources/certificates/cert.pem \ - APIP_AIW_TLS_KEY_FILE=../resources/certificates/key.pem \ - APIP_AIW_LISTEN_ADDR=$(BFF_DEV_ADDR) \ - APIP_AIW_STATIC_DIR=../dist \ - APIP_AIW_LOG_LEVEL=debug \ - go run . -config ../configs/config.toml + APIP_AIW_CONTROL_PLANE_URL=$(CONTROL_PLANE_URL) \ + APIP_AIW_CONTROL_PLANE_TLS_SKIP_VERIFY=true \ + APIP_AIW_SERVER_HTTPS_CERT_FILE=../resources/certificates/cert.pem \ + APIP_AIW_SERVER_HTTPS_KEY_FILE=../resources/certificates/key.pem \ + APIP_AIW_SERVER_HTTPS_PORT=$(BFF_DEV_PORT) \ + APIP_AIW_LOGGING_LEVEL=debug \ + go run . -config ../configs/config.toml -static-dir ../dist bff-test: ## Run BFF unit tests cd $(BFF_DIR) && GOWORK=off go test ./... @@ -198,7 +197,6 @@ endif dist: clean-dist ## Build standalone AI Workspace + Platform API distribution zip @echo "Building distribution $(DIST_NAME)..." @mkdir -p $(DIST_DIR)/configs $(DIST_DIR)/scripts $(DIST_DIR)/resources/certificates $(DIST_DIR)/resources/platform-api/db-scripts - @cp -R configs/. $(DIST_DIR)/configs/ @cp ../../platform-api/resources/roles.yaml $(DIST_DIR)/resources/roles.yaml ifeq ($(PLATFORM_API_FROM_TAG),true) @echo "Fetching platform-api db-scripts from tag $(PLATFORM_API_TAG)..." @@ -214,11 +212,24 @@ ifeq ($(PLATFORM_API_FROM_TAG),true) for f in $$files; do git -C ../.. show "$$tag:$$f" > "$$dest/$$(basename $$f)"; done; \ echo "✓ Fetched $$(echo "$$files" | wc -l | tr -d ' ') db-script(s) from $$tag" @git -C ../.. show "$(PLATFORM_API_TAG):platform-api/config/config-template.toml" \ - > $(DIST_DIR)/configs/config-platform-api-template.toml + > $(DIST_DIR)/.platform-api-config-template.toml + @git -C ../.. show "$(PLATFORM_API_TAG):platform-api/config/config.toml" \ + > $(DIST_DIR)/.platform-api-config.toml else @cp ../../platform-api/internal/database/schema.*.sql $(DIST_DIR)/resources/platform-api/db-scripts/ - @cp ../../platform-api/config/config-template.toml $(DIST_DIR)/configs/config-platform-api-template.toml + @cp ../../platform-api/config/config-template.toml $(DIST_DIR)/.platform-api-config-template.toml + @cp ../../platform-api/config/config.toml $(DIST_DIR)/.platform-api-config.toml endif + # Platform API's [platform_api.*] tables and AI Workspace's [ai_workspace.*] tables + # are deliberately namespaced so they can coexist in one file without collision (see + # configs/config-template.toml) — combine them into the single configs/config.toml + # that the sed below mounts into BOTH containers, so there is one file to edit for + # the whole standalone deployment instead of two kept in sync by hand. + @{ cat $(DIST_DIR)/.platform-api-config.toml; echo; echo; cat configs/config.toml; } \ + > $(DIST_DIR)/configs/config.toml + @{ cat $(DIST_DIR)/.platform-api-config-template.toml; echo; echo; cat configs/config-template.toml; } \ + > $(DIST_DIR)/configs/config-template.toml + @rm -f $(DIST_DIR)/.platform-api-config.toml $(DIST_DIR)/.platform-api-config-template.toml @printf '%s\n' \ '# Generated secrets — never commit these.' \ '*.env' \ @@ -226,7 +237,8 @@ endif > $(DIST_DIR)/.gitignore @cp setup.sh $(DIST_DIR)/scripts/setup.sh @chmod +x $(DIST_DIR)/scripts/setup.sh - @cp docker-compose.yaml $(DIST_DIR)/docker-compose.yaml + @sed 's#\.\./\.\./platform-api/config/config\.toml#./configs/config.toml#' \ + docker-compose.yaml > $(DIST_DIR)/docker-compose.yaml @cp distribution/README.md $(DIST_DIR)/README.md @sed -i.bak -E \ -e 's|^([[:space:]]*image:[[:space:]]*.*/ai-workspace):[^[:space:]]*|\1:$(DIST_VERSION)|' \ diff --git a/portals/ai-workspace/README.md b/portals/ai-workspace/README.md index c92836ed9d..8d08ea04fb 100644 --- a/portals/ai-workspace/README.md +++ b/portals/ai-workspace/README.md @@ -11,22 +11,22 @@ The AI Workspace is a React/Vite SPA served by a **Go BFF (Backend-for-Frontend) | Get running locally in 5 minutes | [QUICKSTART.md](QUICKSTART.md) | | Production setup (Asgardeo IDP) | [production/README.md](production/README.md) | | Full runtime configuration reference | [configs/config-template.toml](configs/config-template.toml) | -| Platform API configuration reference | [../../platform-api/config/config-template.toml](../../platform-api/config/config-template.toml) (copied into the distribution zip as `configs/config-platform-api-template.toml`) | +| Platform API configuration reference | [../../platform-api/config/config-template.toml](../../platform-api/config/config-template.toml) (merged into `configs/config-template.toml` / `configs/config.toml` in the distribution zip — see `make dist`) | --- ## Technology stack - **React** + **TypeScript** + **Vite** -- **Go BFF** — serves the SPA, reverse-proxies `/api/proxy/*` to the Platform API (injecting the session bearer token), and handles login/logout/session + the OIDC code flow +- **Go BFF** — serves the SPA, reverse-proxies `/proxy/*` to the Platform API (injecting the session bearer token), and handles login/logout/session + the OIDC code flow - **Docker Compose** — orchestrates AI Workspace + Platform API --- ## Auth modes -Controlled by `auth_mode` in `configs/config.toml` (whose shipped token also reads -`APIP_AIW_AUTH_MODE` from the environment): +Controlled by `mode` in `[ai_workspace.auth]` in `configs/config.toml` (whose shipped token also +reads `APIP_AIW_AUTH_MODE` from the environment): | Mode | When to use | |---|---| @@ -57,24 +57,29 @@ The shipped `config.toml` writes each key as an `{{ env }}` token, so a deployme them from the environment without editing the file: ```toml +[ai_workspace.logging] # The token names the variable; the second argument is the value used when it is unset. -log_level = '{{ env "APIP_AIW_LOG_LEVEL" "info" }}' +level = '{{ env "APIP_AIW_LOGGING_LEVEL" "info" }}' -[oidc] -# No default — an unset APIP_AIW_OIDC_CLIENT_SECRET fails startup rather than +[ai_workspace.auth.oidc] +# No default — an unset APIP_AIW_AUTH_OIDC_CLIENT_SECRET fails startup rather than # running with an empty credential. Keep the value in a git-ignored env file # (api-platform.env in the compose quickstart). -client_secret = '{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}' +client_secret = '{{ env "APIP_AIW_AUTH_OIDC_CLIENT_SECRET" }}' # Preferred in production — the secret never enters the environment at all. client_secret = '{{ file "/secrets/ai-workspace/oidc_client_secret" }}' ``` -The token is what reads the environment, so setting `APIP_AIW_LOG_LEVEL` does nothing unless -`log_level` is present in the file with its token. Keys are grouped into TOML tables -(`[platform_api]`, `[tls]`, `[session]`, `[cookie]`, `[oidc]`), and by convention a token names the -key's table path uppercased with underscores (`[oidc] client_id` → `APIP_AIW_OIDC_CLIENT_ID`) — but -a token may name any variable. +The token is what reads the environment, so setting `APIP_AIW_LOGGING_LEVEL` does nothing unless +`level` is present in the file with its token. All AI Workspace settings live under +`[ai_workspace]` — the same namespacing convention the Platform API uses for its own +`[platform_api]` table, so a shared config.toml can hold both services' sections without their +keys colliding. Keys are grouped into TOML tables under it +(`[ai_workspace.logging]`, `[ai_workspace.control_plane]`, `[ai_workspace.server.https]`, `[ai_workspace.session]`, +`[ai_workspace.auth]`, `[ai_workspace.auth.oidc]`), and by convention a token names the key's path +under `[ai_workspace]` uppercased with underscores (`[ai_workspace.auth.oidc] client_id` → +`APIP_AIW_AUTH_OIDC_CLIENT_ID`) — but a token may name any variable. All available options are documented in [configs/config-template.toml](configs/config-template.toml). @@ -87,8 +92,9 @@ All available options are documented in portals/ai-workspace/ ├── configs/ │ ├── config-template.toml # AI Workspace config reference -│ ├── config.toml # Active config (gitignored in prod) -│ └── config-platform-api.toml # Active Platform API config +│ └── config.toml # Active config (gitignored in prod) +│ # docker-compose.yaml mounts ../../platform-api/config/config.toml +│ # directly for the Platform API sidecar — no per-portal copy here. ├── production/ │ └── README.md # Production setup guide (Asgardeo) ├── bff/ # Go BFF — serves SPA, proxy, auth @@ -140,13 +146,13 @@ npm run dev ``` This starts the AI Workspace frontend in development mode. -Update platform-api/config/config.toml and set the following configuration: +`platform-api/config/config.toml` already defaults to `[auth] mode = "file"`, so users configured +under `[auth.file]` can log in without any changes. To confirm or set it explicitly: ```bash -[auth.file_based] -enabled = true +[auth] +mode = "file" ``` -This enables file-based authentication, allowing users configured in the file-based authentication settings to log in. Terminal 2: ```bash @@ -161,9 +167,9 @@ cd portals/ai-workspace make bff-run # serves /api/* on https://localhost:8081, proxies to the Platform API ``` The Vite dev server proxies `/api` and `/runtime-config.js` to the BFF, so the browser -talks only to the app origin (same topology as production). Set `PLATFORM_API_URL` if the +talks only to the app origin (same topology as production). Set `CONTROL_PLANE_URL` if the Platform API is not on `https://localhost:9243` — `make bff-run` forwards it to the BFF as -`APIP_AIW_PLATFORM_API_URL`, the variable the `[platform_api] url` token names, so the two +`APIP_AIW_CONTROL_PLANE_URL`, the variable the `[ai_workspace.control_plane] url` token names, so the two names are the same setting. Ensure all three services are running before accessing the application. @@ -181,7 +187,7 @@ you supply your IDP's issuer, JWKS URL and confidential-client credentials. By default the stack runs in `basic` (file-based) auth mode. In OIDC mode the **BFF is a confidential client**: it runs the authorization-code + PKCE flow back-channel, holds the client secret and tokens in a server-side session, and injects the access token on every -`/api/proxy/*` call. The browser never sees a token or the secret. +`/proxy/*` call. The browser never sees a token or the secret. The flow touches **two** components and both must agree on the IDP: @@ -251,9 +257,10 @@ stays out of the file, referenced there by an interpolation token: ```toml # portals/ai-workspace/configs/config.toml -auth_mode = "oidc" +[ai_workspace.auth] +mode = "oidc" -[oidc] +[ai_workspace.auth.oidc] authority = "https://idp.example.com/oauth2/token" # Asgardeo: https://api.asgardeo.io/t/acme/oauth2/token client_id = "" redirect_url = "https://localhost:5380/api/auth/callback" @@ -261,7 +268,7 @@ post_logout_redirect_url = "https://localhost:5380/login" # The secret's *value* stays out of the file — this token pulls it in from the # environment at startup. Without the key here, the variable is never read. -client_secret = '{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}' +client_secret = '{{ env "APIP_AIW_AUTH_OIDC_CLIENT_SECRET" }}' ``` The secret's value is **not** written into the config file. Put it — along with the Platform API's @@ -270,18 +277,16 @@ IDP settings — in `api-platform.env` next to `docker-compose.yaml` (git-ignore ```bash # portals/ai-workspace/api-platform.env — append below the setup.sh-generated keys -APIP_AIW_OIDC_CLIENT_SECRET= # read by the token above -APIP_CP_AUTH_IDP_ENABLED=true -APIP_CP_AUTH_JWT_ENABLED=false # required: mutually exclusive with the IDP -APIP_CP_AUTH_FILE_BASED_ENABLED=false # required: mutually exclusive with the IDP +APIP_AIW_AUTH_OIDC_CLIENT_SECRET= # read by the token above +APIP_CP_AUTH_MODE=idp APIP_CP_AUTH_IDP_JWKS_URL=https://idp.example.com/oauth2/jwks APIP_CP_AUTH_IDP_ISSUER=https://idp.example.com/oauth2/token APIP_CP_AUTH_IDP_AUDIENCE= # optional; omit to skip the aud check # Set only if your IDP names the org claims differently (defaults: # organization / org_name / org_handle): -# APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME=org_id -# APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_ORG_NAME_CLAIM_NAME=org_name -# APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_ORG_HANDLE_CLAIM_NAME=org_handle +# APIP_CP_AUTH_CLAIM_ORGANIZATION=org_id +# APIP_CP_AUTH_CLAIM_ORG_NAME=org_name +# APIP_CP_AUTH_CLAIM_ORG_HANDLE=org_handle ``` Then start the stack: @@ -291,15 +296,14 @@ docker compose up -d ``` In production, prefer mounting the secret as a file and referencing it with -`[oidc] client_secret = '{{ file "/secrets/ai-workspace/oidc_client_secret" }}'` — the value then +`[ai_workspace.auth.oidc] client_secret = '{{ file "/secrets/ai-workspace/oidc_client_secret" }}'` — the value then never enters the environment at all. -(Skipping this section and leaving `auth_mode = "basic"`, the default, keeps the file-based +(Skipping this section and leaving `[ai_workspace.auth] mode = "basic"`, the default, keeps the file-based quickstart instead — the admin user generated by `setup.sh` — with no IDP involved.) -> The Platform API auth modes are mutually exclusive: enabling the IDP while local JWT -> or file-based auth is also on is rejected at startup — keep the -> `APIP_CP_AUTH_JWT_ENABLED=false` and `APIP_CP_AUTH_FILE_BASED_ENABLED=false` lines in `api-platform.env`. +> `platform_api.auth.mode` selects exactly one Platform API auth mode — set it to `idp` so the +> Platform API validates tokens against Asgardeo's JWKS instead of local JWT or file-based auth. #### Option 2 (local `make bff-run`) @@ -307,50 +311,46 @@ Running the BFF and Platform API directly (no compose) means no pre-wiring, so c sides by hand. Edit `platform-api/config/config.toml` so the Platform API validates the Asgardeo token — -remember the three auth modes are mutually exclusive, so turn the local ones off: +`auth.mode` selects exactly one mode, so set it to `idp`: ```toml -[auth.jwt] -enabled = false # required: mutually exclusive with the IDP +[auth] +mode = "idp" [auth.idp] -enabled = true jwks_url = "https://api.asgardeo.io/t//oauth2/jwks" issuer = ["https://api.asgardeo.io/t//oauth2/token"] audience = [""] # match Asgardeo's aud, or [] to skip the check # Asgardeo emits org_id (not the default "organization") — these overrides are required. -[auth.idp.claim_mappings] -organization_claim_name = "org_id" -org_name_claim_name = "org_name" -org_handle_claim_name = "org_handle" - -[auth.file_based] -enabled = false # required: mutually exclusive with the IDP +[auth.claim_mappings] +organization = "org_id" +org_name = "org_name" +org_handle = "org_handle" ``` Then export the BFF settings and start it. `make bff-run` runs the BFF against [`configs/config.toml`](configs/config.toml) — the same file the container mounts, passed with `-config` — whose keys are all `{{ env }}` tokens naming the variables below, so exporting one -sets its key (`APIP_AIW_OIDC_CLIENT_ID` → `[oidc] client_id`). A key absent from that file is not +sets its key (`APIP_AIW_AUTH_OIDC_CLIENT_ID` → `[ai_workspace.auth.oidc] client_id`). A key absent from that file is not settable this way; add it there first. Point the BFF at the locally published Platform API port — the `platform-api` compose hostname does **not** resolve outside the compose network: -> `[oidc] authority` is Asgardeo's **token base** — the BFF appends +> `[ai_workspace.auth.oidc] authority` is Asgardeo's **token base** — the BFF appends > `/.well-known/openid-configuration` itself, so do **not** include the discovery suffix. ```bash cd portals/ai-workspace -export APIP_AIW_PLATFORM_API_URL=https://localhost:9243 # NOT https://platform-api:9243 when run locally -export APIP_AIW_PLATFORM_API_TLS_SKIP_VERIFY=true +export APIP_AIW_CONTROL_PLANE_URL=https://localhost:9243 # NOT https://platform-api:9243 when run locally +export APIP_AIW_CONTROL_PLANE_TLS_SKIP_VERIFY=true export APIP_AIW_AUTH_MODE=oidc -export APIP_AIW_OIDC_AUTHORITY=https://api.asgardeo.io/t//oauth2/token -export APIP_AIW_OIDC_CLIENT_ID= -export APIP_AIW_OIDC_CLIENT_SECRET= -export APIP_AIW_OIDC_REDIRECT_URL=https://localhost:5380/api/auth/callback -export APIP_AIW_OIDC_POST_LOGOUT_REDIRECT_URL=https://localhost:5380/login +export APIP_AIW_AUTH_OIDC_AUTHORITY=https://api.asgardeo.io/t//oauth2/token +export APIP_AIW_AUTH_OIDC_CLIENT_ID= +export APIP_AIW_AUTH_OIDC_CLIENT_SECRET= +export APIP_AIW_AUTH_OIDC_REDIRECT_URL=https://localhost:5380/api/auth/callback +export APIP_AIW_AUTH_OIDC_POST_LOGOUT_REDIRECT_URL=https://localhost:5380/login # Keep `offline_access` — without it the IDP issues no refresh token and the BFF cannot silently renew the session. -export APIP_AIW_OIDC_SCOPE="openid profile email offline_access ap:organization:manage ap:gateway:manage ap:rest_api:manage ..." +export APIP_AIW_AUTH_OIDC_SCOPE="openid profile email offline_access ap:organization:manage ap:gateway:manage ap:rest_api:manage ..." make bff-run ``` @@ -362,13 +362,13 @@ failures, by symptom: | Symptom | Cause | Fix | |---|---|---| | `unauthorized_client` / *"not authorized to use the requested grant type"* | App registered as SPA, or Code/Refresh grant not enabled | Recreate as Standard-Based OIDC app; enable **Code** + **Refresh Token** (step 1) | -| Platform API exits at startup with *"auth.idp.enabled=true and auth.jwt.enabled=true are mutually exclusive"* | Local auth left on alongside the IDP | Compose: set `APIP_CP_AUTH_JWT_ENABLED=false` + `APIP_CP_AUTH_FILE_BASED_ENABLED=false` in `api-platform.env` (step 3, Option 1). Local: set `auth.jwt.enabled=false` and `auth.file_based.enabled=false` (step 3, Option 2) | -| `502` + `dial tcp: lookup platform-api: no such host` | BFF run locally but `[platform_api] url` points at the compose hostname | Set `APIP_AIW_PLATFORM_API_URL=https://localhost:9243` (step 3, Option 2) | +| Platform API exits at startup with *`auth.mode must be "external_token", "file", or "idp"`* | `auth.mode` is unset or misspelled | Compose: set `APIP_CP_AUTH_MODE=idp` in `api-platform.env` (step 3, Option 1). Local: set `[auth] mode = "idp"` in `config.toml` (step 3, Option 2) | +| `502` + `dial tcp: lookup platform-api: no such host` | BFF run locally but `[ai_workspace.control_plane] url` points at the compose hostname | Set `APIP_AIW_CONTROL_PLANE_URL=https://localhost:9243` (step 3, Option 2) | | Proxied calls return `authentication_failed` | Platform API still on local JWT/file-based, validating the IDP token with the wrong validator | Switch it to the IDP — compose: set the `APIP_CP_AUTH_IDP_*` keys in `api-platform.env` (step 3, Option 1); local: enable `[auth.idp]` (step 3, Option 2) | | Proxied calls return `authentication_failed`, Platform API logs `token contains an invalid number of segments` | IDP is issuing **opaque** access tokens — the BFF forwards the access token and the Platform API can only validate a **JWT** via JWKS | Set **Access Token Type = JWT** on the app's Protocol tab (step 1) and re-login | | Login works, then proxied calls return `403` | Access token lacks `ap:*` scopes, or Platform API IDP/claim mapping wrong | Grant `ap:*` scopes to the user (step 2); check `[auth.idp]` issuer/JWKS/claim mappings | | User shows as a UUID and email is blank in the UI | Token carries no name/email claims — the BFF falls back to the `sub` (user UUID) | Release the `given_name` (or `name`/`preferred_username`) and `email` claims to the app and ensure the user has those attributes set; the `profile` and `email` scopes must be granted (both are in the default request) | -| Logged out as soon as the access token expires; never silently refreshed | IDP returned no refresh token — `offline_access` scope missing from the request, or not permitted for the app | Keep `offline_access` in `[oidc] scope` (it's in the default); allow it for the app in the IDP (step 1) | +| Logged out as soon as the access token expires; never silently refreshed | IDP returned no refresh token — `offline_access` scope missing from the request, or not permitted for the app | Keep `offline_access` in `[ai_workspace.auth.oidc] scope` (it's in the default); allow it for the app in the IDP (step 1) | | Refresh fails minutes after login | **Refresh Token** grant not enabled on the app | Enable it on the Protocol tab (step 1) | ## Session lifetime & token refresh @@ -457,7 +457,7 @@ resources/certificates/ docker-compose mounts this directory read-only into both containers (`/etc/platform-api/tls` and `/etc/ai-workspace/tls`). The same cert also serves as the CA bundle -the BFF trusts for the upstream platform-api hop, referenced by `[platform_api] ca_file` in +the BFF trusts for the upstream platform-api hop, referenced by `[ai_workspace.control_plane] ca_file` in `configs/config.toml`. Then restart the stack: `docker compose up -d --force-recreate` (a plain `docker @@ -475,8 +475,9 @@ credentials, and self-signed certificates); for production, provide real values: | Requirement | Quickstart (`setup.sh`) | Production | |---|---|---| -| **Platform API** — `APIP_CP_ENCRYPTION_KEY`, `APIP_CP_AUTH_JWT_SECRET_KEY` | Generated into `api-platform.env` | Manage as real secrets; prefer mounting files and referencing them with `{{ file "..." }}` in the config TOML | +| **Platform API** — `APIP_CP_ENCRYPTION_KEY` | Generated into `api-platform.env` | Manage as real secrets; prefer mounting files and referencing them with `{{ file "..." }}` in the config TOML | +| **Platform API** — RS256 JWT signing keypair | Generated as PEM files in `resources/keys/` (`jwt_private.pem` / `jwt_public.pem`), read by `config.toml` via `{{ file }}` | Mount your own RSA keypair at the same paths; rotate as real signing keys | | **Platform API** — admin credentials | Generated into `api-platform.env` (bcrypt hash); password printed once | Use OIDC (`auth.idp`) instead of file-based auth | | **TLS certificates (both services)** | One self-signed pair in `resources/certificates/`, shared by both services | Certificates from your CA (same file names), or terminate TLS at an ingress and disable the listeners' TLS | -| **Upstream trust (BFF → Platform API)** | The generated shared cert is mounted as a CA bundle (`[platform_api] ca_file`) | Point `ca_file` at your CA bundle; never use `tls_skip_verify` in production | +| **Upstream trust (BFF → Platform API)** | The generated shared cert is mounted as a CA bundle (`[ai_workspace.control_plane] ca_file`) | Point `ca_file` at your CA bundle; never use `tls_skip_verify` in production | | **Auth** | File-based (basic) auth with the generated admin user | OIDC — follow [Testing with an IDP locally](#testing-with-an-idp-locally) and [production/README.md](production/README.md) | diff --git a/portals/ai-workspace/VERSION b/portals/ai-workspace/VERSION index 12fc46a001..d07947ac0f 100644 --- a/portals/ai-workspace/VERSION +++ b/portals/ai-workspace/VERSION @@ -1 +1 @@ -1.0.0-alpha2-SNAPSHOT +1.0.0-beta-SNAPSHOT diff --git a/portals/ai-workspace/bff/go.mod b/portals/ai-workspace/bff/go.mod index 0b5eb197d6..c405405cc3 100644 --- a/portals/ai-workspace/bff/go.mod +++ b/portals/ai-workspace/bff/go.mod @@ -2,7 +2,23 @@ module ai-workspace-bff go 1.26.5 -require github.com/wso2/api-platform/common v0.0.0 +require ( + github.com/go-viper/mapstructure/v2 v2.5.0 + github.com/knadh/koanf/parsers/toml/v2 v2.2.0 + github.com/knadh/koanf/providers/confmap v1.0.0 + github.com/knadh/koanf/providers/file v1.2.1 + github.com/knadh/koanf/v2 v2.3.2 + github.com/wso2/api-platform/common v0.0.0 +) + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + golang.org/x/sys v0.47.0 // indirect +) replace github.com/wso2/api-platform/common => ../../../common diff --git a/portals/ai-workspace/bff/go.sum b/portals/ai-workspace/bff/go.sum index 5d19fde925..6aad93e97c 100644 --- a/portals/ai-workspace/bff/go.sum +++ b/portals/ai-workspace/bff/go.sum @@ -1,8 +1,30 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/toml/v2 v2.2.0 h1:2nV7tHYJ5OZy2BynQ4mOJ6k5bDqbbCzRERLUKBytz3A= +github.com/knadh/koanf/parsers/toml/v2 v2.2.0/go.mod h1:JpjTeK1Ge1hVX0wbof5DMCuDBriR8bWgeQP98eeOZpI= +github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= +github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= +github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= +github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= +github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4= +github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/portals/ai-workspace/bff/internal/auth/filebased.go b/portals/ai-workspace/bff/internal/auth/filebased.go index 046cc5c46e..931170b56e 100644 --- a/portals/ai-workspace/bff/internal/auth/filebased.go +++ b/portals/ai-workspace/bff/internal/auth/filebased.go @@ -38,6 +38,7 @@ type FileBased struct { client *http.Client loginURL string absTTL time.Duration + mapping session.ClaimMapping } type fileBasedLoginResponse struct { @@ -46,12 +47,17 @@ type fileBasedLoginResponse struct { } // NewFileBased builds a file-based authenticator. platformBaseURL is the Platform -// API origin (e.g. https://platform-api:9243); loginPath is its login route. -func NewFileBased(client *http.Client, platformBaseURL, loginPath string, absTTL time.Duration) *FileBased { +// API origin (e.g. https://platform-api:9243); portalBasePath is its portal route +// prefix (e.g. /api/portal/v0.9), under which /auth/login lives. mapping is the +// same claim mapping used for OIDC — the Platform API's file-based login endpoint +// signs its JWTs using these same mapped claim names, so an operator who +// customizes the claim names must not need to configure them twice. +func NewFileBased(client *http.Client, platformBaseURL, portalBasePath string, absTTL time.Duration, mapping session.ClaimMapping) *FileBased { return &FileBased{ client: client, - loginURL: strings.TrimRight(platformBaseURL, "/") + loginPath, + loginURL: strings.TrimRight(platformBaseURL, "/") + strings.TrimRight(portalBasePath, "/") + "/auth/login", absTTL: absTTL, + mapping: mapping, } } @@ -110,6 +116,6 @@ func (f *FileBased) Login(ctx context.Context, username, password string) (*sess AccessToken: body.Token, AccessExpiry: accessExpiry, AbsoluteExpiry: abs, - User: session.UserFromClaims(claims, nil, session.DefaultClaimMapping()), + User: session.UserFromClaims(claims, nil, f.mapping), }, nil } diff --git a/portals/ai-workspace/bff/internal/config/config.go b/portals/ai-workspace/bff/internal/config/config.go index 7a7da9d43a..f120d60856 100644 --- a/portals/ai-workspace/bff/internal/config/config.go +++ b/portals/ai-workspace/bff/internal/config/config.go @@ -16,135 +16,170 @@ // Package config loads BFF configuration from config.toml, resolving its // {{ env }} / {{ file }} interpolation tokens through the shared configinterpolate -// library. The file is the only source: a key takes its value from the environment or -// a mounted secret file exactly when its token says so. Browser-safe keys are surfaced -// to the SPA as APIP_AIW_* runtime config. The BFF never validates tokens, so there are -// no signing keys here — only the IDP client credentials needed to perform the OAuth2 -// code exchange. +// library and unmarshalling the result into a nested struct via koanf — the same +// loading stack the Gateway and Platform API use. The file is the only source: a key +// takes its value from the environment or a mounted secret file exactly when its token +// says so. Browser-safe keys are surfaced to the SPA as APIP_AIW_* runtime config. The +// BFF never validates tokens, so there are no signing keys here — only the IDP client +// credentials needed to perform the OAuth2 code exchange. package config import ( "fmt" "log/slog" "net/url" + "strconv" "strings" "time" + + "github.com/go-viper/mapstructure/v2" + "github.com/knadh/koanf/v2" ) -// Config is the fully-resolved BFF configuration. +// Config is the fully-resolved BFF configuration. Its shape mirrors the +// [ai_workspace.*] tables in config.toml, so koanf unmarshals straight into it — the +// same pattern the Platform API uses. Keys the BFF does not consume (browser-only +// values the SPA reads) are deliberately not modeled here; they flow to RuntimeConfig +// straight from the parsed config, gated by browserSafeKeys (see runtime_config.go). type Config struct { - // Listener - Addr string // host:port to listen on, e.g. ":5380" - StaticDir string // directory containing the built SPA (index.html + assets) - - // Logging - LogLevel string // "debug" | "info" | "warn" | "error" (default "info") - LogFormat string // "text" | "json" (default "text") - - // TLS for the BFF listener - TLS TLSConfig - - // Upstream Platform API - PlatformAPI PlatformAPIConfig + Server ServerConfig `koanf:"server"` + Logging LoggingConfig `koanf:"logging"` + ControlPlane ControlPlaneConfig `koanf:"control_plane"` + Session SessionConfig `koanf:"session"` + Auth AuthConfig `koanf:"auth"` - // Same-origin reverse-proxy prefix the SPA calls (stripped before forwarding) - ProxyPrefix string - - // Session / cookie - Session SessionConfig - Cookie CookieConfig + // Cookie attributes are fixed implementation details of the session mechanism, not + // deployment config; Load sets them. RuntimeConfig is assembled after load. + Cookie CookieConfig `koanf:"-"` + RuntimeConfig map[string]string `koanf:"-"` +} - // CSRF - CSRFHeader string // custom header required on state-mutating requests +// ServerConfig is [ai_workspace.server] — the single listener, following the +// platform-wide [server.https] shape. Domain is the one browser-safe key here (served +// to the SPA as APIP_AIW_SERVER_DOMAIN); everything else stays server-side. +type ServerConfig struct { + StaticDir string `koanf:"static_dir"` // directory containing the built SPA (index.html + assets) + // Domain is shown in the browser address bar (host:port, or just host for port + // 80/443). Browser-safe — see browserSafeKeys in runtime_config.go. + Domain string `koanf:"domain"` + // Enabled makes the BFF terminate TLS itself, presenting the certificate and + // decrypting inbound TLS; set it false only when a trusted upstream (ingress, + // service-mesh sidecar) terminates TLS and forwards plain HTTP, in which case the + // listener serves plain HTTP on the same port and no certificate is read or + // required. CertFile/KeyFile are required when Enabled — there is no self-signed + // fallback. + Enabled bool `koanf:"enabled"` + Port int `koanf:"port"` + CertFile string `koanf:"cert_file"` + KeyFile string `koanf:"key_file"` +} - // Auth - AuthMode string // "basic" | "oidc" — informs the SPA which login UX to show - OIDC OIDCConfig +// Addr is the listener address, ":" + port (e.g. ":5380"). The listener always binds +// all interfaces, so there is no host to configure. +func (c *Config) Addr() string { + return ":" + strconv.Itoa(c.Server.Port) +} - // Runtime config surfaced to the SPA (window.__RUNTIME_CONFIG__) - RuntimeConfig map[string]string +// LoggingConfig is [ai_workspace.logging]. Level/Format are this process's own logs; +// browser_debug is browser-only and not modeled here — it reaches the SPA through +// RuntimeConfig. Level and Format are matched case-insensitively (lowercased in Load). +type LoggingConfig struct { + Level string `koanf:"level"` // debug | info | warn | error (default "info") + Format string `koanf:"format"` // text | json (default "text") } -// PlatformAPIConfig groups everything about the upstream Platform API hop: where -// it is, and how its TLS certificate is trusted. -type PlatformAPIConfig struct { +// ControlPlaneConfig is [ai_workspace.control_plane]: everything about the upstream +// Platform API hop — where it is, how its TLS certificate is trusted, and the +// same-origin prefix the SPA calls. +type ControlPlaneConfig struct { // URL is the base URL, e.g. https://platform-api:9243. Its http/https scheme is // the single source of truth for whether the outbound hop uses TLS — there is // deliberately no separate boolean, since that could contradict the URL. - URL string - // CAFile is a PEM bundle to trust for the upstream's TLS certificate. It is - // appended to the system roots rather than replacing them, so public CAs keep - // working; leaving it empty simply uses the OS trust store on its own. Set it to - // accept a private/self-signed Platform API cert with verification still on. - // Ignored when TLSSkipVerify is true. - CAFile string - // TLSSkipVerify disables upstream certificate verification entirely. - // Last-resort escape hatch for dev/demo only; prefer CAFile. - TLSSkipVerify bool - // LoginPath is the file-based login path on the Platform API. - LoginPath string + URL string `koanf:"url"` + // CAFile is a PEM bundle to trust for the upstream's TLS certificate, appended to + // the system roots rather than replacing them. Ignored when TLSSkipVerify is true. + CAFile string `koanf:"ca_file"` + // TLSSkipVerify disables upstream certificate verification entirely. Last-resort + // escape hatch for dev/demo only; prefer CAFile. + TLSSkipVerify bool `koanf:"tls_skip_verify"` + // PortalBasePath is the Platform API's portal route prefix (e.g. /api/portal/v0.9), + // used to build paths for BFF-initiated calls (file-based login today). + PortalBasePath string `koanf:"portal_base_path"` + // ProxyPrefix is the same-origin reverse-proxy prefix the SPA calls; it is stripped + // before forwarding upstream, so the browser only ever talks to the app origin. + ProxyPrefix string `koanf:"proxy_prefix"` } -// TLSConfig controls whether the BFF listener serves HTTPS directly or sits -// behind a component that terminates TLS on its behalf. -type TLSConfig struct { - // TerminateTLS makes the BFF serve HTTPS on its own listener: it presents the - // certificate and decrypts inbound TLS itself. Defaults to true (config key - // [tls] enabled). Set to false only when a trusted upstream (ingress, - // service-mesh sidecar) terminates TLS and forwards plain HTTP to the BFF; no - // certificate is then read, generated, or required. - TerminateTLS bool - CertFile string - KeyFile string +// SessionConfig is [ai_workspace.session]: server-side session lifetime. +type SessionConfig struct { + Store string `koanf:"store"` // "memory" (default) | "redis" (future) + IdleTimeout time.Duration `koanf:"idle_timeout"` // sliding idle window + AbsoluteTTL time.Duration `koanf:"absolute_ttl"` // hard cap regardless of activity / token exp } -// SessionConfig controls server-side session lifetime. -type SessionConfig struct { - Store string // "memory" (default) | "redis" (future) - IdleTimeout time.Duration // sliding idle window - AbsoluteTTL time.Duration // hard cap regardless of activity / token exp +// AuthConfig is [ai_workspace.auth]: the login mode and the claim/OIDC settings. +type AuthConfig struct { + Mode string `koanf:"mode"` // "basic" | "oidc" — informs the SPA which login UX to show + OIDC OIDCConfig `koanf:"oidc"` + ClaimMappings ClaimMappingConfig `koanf:"claim_mappings"` +} + +// OIDCConfig is [ai_workspace.auth.oidc]: the confidential-client settings. The client +// secret lives only here on the BFF and is never emitted to the browser. Enabled is +// both a config key and derived — Load ORs it with (auth.mode == "oidc"). +type OIDCConfig struct { + Enabled bool `koanf:"enabled"` + Issuer string `koanf:"authority"` // discovery base; {issuer}/.well-known/openid-configuration + ClientID string `koanf:"client_id"` + ClientSecret string `koanf:"client_secret"` + RedirectURL string `koanf:"redirect_url"` // must equal the IDP-registered redirect, points at /api/auth/callback + PostLogoutRedirectURL string `koanf:"post_logout_redirect_url"` + Scopes string `koanf:"scope"` // space-separated +} + +// ClaimMappingConfig is [ai_workspace.auth.claim_mappings]: which claim names the BFF +// reads for each user/org field. It mirrors the Platform API's [auth.claim_mappings] +// key for key, and the two must agree. It is a sibling of [auth.oidc], not nested +// inside it, because it applies to BOTH auth modes — OIDC tokens from the configured +// IDP, and the HMAC JWTs the Platform API's file-based login endpoint signs with these +// same mapped claim names. The same keys drive the BFF's session mapping and the SPA's +// runtime config, so one config entry keeps both layers in sync. +type ClaimMappingConfig struct { + Username string `koanf:"username"` + Email string `koanf:"email"` + Roles string `koanf:"roles"` + Scope string `koanf:"scope"` + OrgID string `koanf:"organization"` + OrgName string `koanf:"org_name"` + OrgHandle string `koanf:"org_handle"` } -// CookieConfig controls the session cookie attributes. +// CookieConfig controls the session cookie attributes. Not user-configurable: these +// are implementation details of the BFF's session mechanism, not a deployment concern. +// The BFF always terminates TLS (or sits behind a proxy that does), so Secure is +// unconditionally true; there is no supported plain-HTTP deployment that would need it +// false. type CookieConfig struct { Name string Secure bool SameSite string // "lax" | "strict" | "none" } -// OIDCConfig holds the confidential-client settings. The client secret lives -// only here on the BFF and is never emitted to the browser. -type OIDCConfig struct { - Enabled bool - Issuer string // discovery base; {issuer}/.well-known/openid-configuration - ClientID string - ClientSecret string - RedirectURL string // must equal the IDP-registered redirect, points at /api/auth/callback - PostLogoutRedirectURL string - Scopes string // space-separated +// cookieName is the session cookie's name. +const cookieName = "_ai_workspace_session" - // Claims maps which token claim names carry each user/org field. Override per - // IDP when the defaults don't match (e.g. the display name lands on "sub"). - Claims ClaimMappingConfig -} - -// ClaimMappingConfig configures which claim names the BFF reads for each -// user/org field from the OIDC tokens. Empty fields fall back to built-in -// defaults in the session package. -type ClaimMappingConfig struct { - Username string - Email string - Role string - Scope string - OrgID string - OrgName string - OrgHandle string -} +// CSRFHeaderName is the header the SPA must set on every state-mutating request, and +// the BFF checks for on the way in (see server/middleware.go requireCSRF). It is a +// fixed contract between the BFF and the SPA it ships, not a deployment concern — an +// operator changing it on one side without the other would silently break CSRF +// protection, so it is a constant rather than a config key. The SPA's copy lives in +// src/config.env.ts CSRF_HEADER and must be kept in sync with this value. +const CSRFHeaderName = "X-Requested-By" // defaultOIDCScopes is the full set of scopes the BFF requests in OIDC mode so a // logged-in user's access token carries every ap:* permission the Platform API // authorizes against. The IDP must still have these scopes registered and granted -// to the user, otherwise it drops the ungranted ones. Override with the [oidc] scope +// to the user, otherwise it drops the ungranted ones. Override with the [auth.oidc] scope // config key to request a narrower set. // // offline_access is required: without it most IDPs (Asgardeo, WSO2 IS, Okta, @@ -193,163 +228,131 @@ const defaultOIDCScopes = "openid profile email offline_access" + const DefaultConfigFile = "/etc/ai-workspace/config.toml" // Load resolves configuration from the config.toml at path, or from the mounted -// DefaultConfigFile when path is empty. The file's {{ env }} / {{ file }} tokens are -// expanded first, so any key — the OIDC client secret in particular — can be pulled -// from an environment variable or a mounted secret file instead of being written in -// the clear. A key not present in the file falls back to the default below. +// DefaultConfigFile when path is empty. It loads defaults, overlays the file (with its +// {{ env }} / {{ file }} tokens expanded), normalizes derived fields, then validates — +// so any key, the OIDC client secret in particular, can be pulled from an environment +// variable or a mounted secret file instead of being written in the clear, and a key +// not present in the file falls back to its default. func Load(path string) (*Config, error) { if path == "" { path = DefaultConfigFile } - s, err := loadSettings(path) + k, err := loadConfigKoanf(path) if err != nil { return nil, err } - authMode := strings.ToLower(s.get("auth_mode", "basic")) - // A typo'd mode must not silently degrade to basic auth: any value other than - // "oidc" would leave OIDC.Enabled false and hand the SPA an unknown login UX. - if authMode != "basic" && authMode != "oidc" { - return nil, fmt.Errorf("invalid auth_mode %q: must be \"basic\" or \"oidc\"", authMode) + // Defaults first, then overlay the file. WeaklyTypedInput lets a {{ env }} token's + // string value decode into the typed field (e.g. "5380" -> int, "true" -> bool); + // a value that cannot be coerced (e.g. enabled = "maybe") fails startup here rather + // than being silently dropped. + cfg := defaultConfig() + if err := k.UnmarshalWithConf("", cfg, koanf.UnmarshalConf{ + DecoderConfig: &mapstructure.DecoderConfig{ + TagName: "koanf", + WeaklyTypedInput: true, + Result: cfg, + DecodeHook: mapstructure.StringToTimeDurationHookFunc(), + }, + }); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) } - // Parse typed values up front so a malformed one fails startup instead of - // being silently replaced with the default. - tlsEnabled, err := s.getbool("tls.enabled", true) - if err != nil { - return nil, err - } - platformTLSSkipVerify, err := s.getbool("platform_api.tls_skip_verify", false) - if err != nil { + cfg.normalize() + if err := cfg.validate(); err != nil { return nil, err } - idleTimeout, err := s.getdur("session.idle_timeout", 30*time.Minute) - if err != nil { - return nil, err - } - absoluteTTL, err := s.getdur("session.absolute_ttl", 8*time.Hour) - if err != nil { - return nil, err + + cfg.RuntimeConfig = buildRuntimeConfig(cfg, k) + return cfg, nil +} + +// normalize resolves the derived fields that are not a straight copy of a config key: +// case-folding (level/format/mode), trimming trailing slashes off URLs/prefixes, the +// oidc-mode-implies-enabled rule, and the fixed cookie attributes. +func (c *Config) normalize() { + c.Logging.Level = strings.ToLower(c.Logging.Level) + c.Logging.Format = strings.ToLower(c.Logging.Format) + c.Auth.Mode = strings.ToLower(c.Auth.Mode) + + c.ControlPlane.URL = strings.TrimRight(c.ControlPlane.URL, "/") + c.ControlPlane.PortalBasePath = strings.TrimRight(c.ControlPlane.PortalBasePath, "/") + c.ControlPlane.ProxyPrefix = strings.TrimRight(c.ControlPlane.ProxyPrefix, "/") + c.Auth.OIDC.Issuer = strings.TrimRight(c.Auth.OIDC.Issuer, "/") + + // oidc mode implies the client is enabled even if the explicit flag is unset, so a + // typo'd mode cannot silently degrade to basic auth. + c.Auth.OIDC.Enabled = c.Auth.OIDC.Enabled || c.Auth.Mode == "oidc" + + c.Cookie = CookieConfig{Name: cookieName, Secure: true, SameSite: "lax"} +} + +// validate fails startup on any value that would otherwise surface as a confusing +// runtime error (a bad port, an empty upstream URL, an incomplete OIDC set) and warns +// on security-relevant downgrades. +func (c *Config) validate() error { + // A typo'd mode must not silently degrade to basic auth: any value other than + // "oidc" would leave OIDC.Enabled false and hand the SPA an unknown login UX. + if c.Auth.Mode != "basic" && c.Auth.Mode != "oidc" { + return fmt.Errorf("invalid [auth] mode %q: must be \"basic\" or \"oidc\"", c.Auth.Mode) } - cookieSecure, err := s.getbool("cookie.secure", true) - if err != nil { - return nil, err + if c.Server.Port < 1 || c.Server.Port > 65535 { + return fmt.Errorf("[server] port must be between 1 and 65535, got %d", c.Server.Port) } - oidcEnabled, err := s.getbool("oidc.enabled", false) - if err != nil { - return nil, err + // Every session duration is a lifetime, where <= 0 is never meaningful. + if c.Session.IdleTimeout <= 0 { + return fmt.Errorf("[session] idle_timeout must be positive, got %s", c.Session.IdleTimeout) } - - cfg := &Config{ - Addr: s.get("listen_addr", ":5380"), - StaticDir: s.get("static_dir", "/app"), - LogLevel: strings.ToLower(s.get("log_level", "info")), - LogFormat: strings.ToLower(s.get("log_format", "text")), - TLS: TLSConfig{ - TerminateTLS: tlsEnabled, - // Convention matches the container's mount path. A certificate pair is - // required there whenever TerminateTLS is on. - CertFile: s.get("tls.cert_file", "/etc/ai-workspace/tls/cert.pem"), - KeyFile: s.get("tls.key_file", "/etc/ai-workspace/tls/key.pem"), - }, - PlatformAPI: PlatformAPIConfig{ - URL: strings.TrimRight(s.get("platform_api.url", ""), "/"), - CAFile: s.get("platform_api.ca_file", ""), - TLSSkipVerify: platformTLSSkipVerify, - LoginPath: s.get("platform_api.login_path", "/api/portal/v0.9/auth/login"), - }, - ProxyPrefix: strings.TrimRight(s.get("proxy_prefix", "/api/proxy"), "/"), - Session: SessionConfig{ - Store: s.get("session.store", "memory"), - IdleTimeout: idleTimeout, - AbsoluteTTL: absoluteTTL, - }, - Cookie: CookieConfig{ - Name: s.get("cookie.name", "_ai_workspace_session"), - Secure: cookieSecure, - SameSite: strings.ToLower(s.get("cookie.samesite", "lax")), - }, - CSRFHeader: s.get("csrf_header", "X-Requested-By"), - AuthMode: authMode, - OIDC: OIDCConfig{ - Enabled: authMode == "oidc" || oidcEnabled, - Issuer: strings.TrimRight(s.get("oidc.authority", ""), "/"), - ClientID: s.get("oidc.client_id", ""), - // Never write the secret as a literal. Point the key at an environment - // variable with '{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}', or — preferably — - // at a mounted file with '{{ file "/secrets/ai-workspace/oidc_client_secret" }}'. - ClientSecret: s.get("oidc.client_secret", ""), - RedirectURL: s.get("oidc.redirect_url", ""), - // Empty by default: LogoutURL() forwards this as post_logout_redirect_uri, - // which IDPs require to be an absolute, pre-registered URL. A relative - // default would produce an invalid logout request, so leave it unset - // unless an absolute URL is explicitly configured. - PostLogoutRedirectURL: s.get("oidc.post_logout_redirect_url", ""), - Scopes: s.get("oidc.scope", defaultOIDCScopes), - // [oidc.claim_mappings] deliberately mirrors the Platform API's - // [auth.idp.claim_mappings], key for key: both describe the same IDP token, - // and they must agree, so they are named the same on both sides. - // - // The same keys drive the BFF's session mapping and the SPA's runtime - // config, so one config entry keeps both layers in sync. - Claims: ClaimMappingConfig{ - Username: s.get("oidc.claim_mappings.username_claim_name", "username"), - Email: s.get("oidc.claim_mappings.email_claim_name", "email"), - Role: s.get("oidc.claim_mappings.role_claim_name", "platform_role"), - Scope: s.get("oidc.claim_mappings.scope_claim_name", "scope"), - OrgID: s.get("oidc.claim_mappings.organization_claim_name", "org_id"), - OrgName: s.get("oidc.claim_mappings.org_name_claim_name", "org_name"), - OrgHandle: s.get("oidc.claim_mappings.org_handle_claim_name", "org_handle"), - }, - }, + if c.Session.AbsoluteTTL <= 0 { + return fmt.Errorf("[session] absolute_ttl must be positive, got %s", c.Session.AbsoluteTTL) } - if cfg.PlatformAPI.URL == "" { - return nil, fmt.Errorf("[platform_api] url is required: set it in config.toml, " + + if c.ControlPlane.URL == "" { + return fmt.Errorf("[control_plane] url is required: set it in config.toml, " + "either as a literal or via an {{ env }} / {{ file }} token") } // The scheme is the single source of truth for the outbound TLS decision, so a - // missing/typo'd scheme must fail at startup rather than surface as an opaque - // dial error on the first proxied request. - u, err := url.Parse(cfg.PlatformAPI.URL) + // missing/typo'd scheme must fail at startup rather than surface as an opaque dial + // error on the first proxied request. + u, err := url.Parse(c.ControlPlane.URL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { - return nil, fmt.Errorf("[platform_api] url must be an absolute http:// or https:// URL, got %q", cfg.PlatformAPI.URL) + return fmt.Errorf("[control_plane] url must be an absolute http:// or https:// URL, got %q", c.ControlPlane.URL) } // Trust knobs only apply to an https upstream; flag them on a plain-http URL so a // mistaken belief that TLS is in effect is caught early. if u.Scheme == "http" { - if cfg.PlatformAPI.CAFile != "" || cfg.PlatformAPI.TLSSkipVerify { - return nil, fmt.Errorf("[platform_api] ca_file / tls_skip_verify are set but [platform_api] url is http:// (no TLS on the upstream hop)") + if c.ControlPlane.CAFile != "" || c.ControlPlane.TLSSkipVerify { + return fmt.Errorf("[control_plane] ca_file / tls_skip_verify are set but [control_plane] url is http:// (no TLS on the upstream hop)") } } - // Skipping verification is a security downgrade; say so loudly and point at - // the supported alternative. - if u.Scheme == "https" && cfg.PlatformAPI.TLSSkipVerify { - slog.Warn("[platform_api] tls_skip_verify = true — upstream certificate verification is DISABLED. " + - "Trust the upstream certificate with [platform_api] ca_file instead.") + // Skipping verification is a security downgrade; say so loudly and point at the + // supported alternative. + if u.Scheme == "https" && c.ControlPlane.TLSSkipVerify { + slog.Warn("[control_plane] tls_skip_verify = true — upstream certificate verification is DISABLED. " + + "Trust the upstream certificate with [control_plane] ca_file instead.") } - if cfg.OIDC.Enabled { - if cfg.OIDC.Issuer == "" || cfg.OIDC.ClientID == "" || cfg.OIDC.ClientSecret == "" || cfg.OIDC.RedirectURL == "" { - return nil, fmt.Errorf("OIDC mode requires [oidc] authority, client_id, client_secret and redirect_url") + + if c.Auth.OIDC.Enabled { + if c.Auth.OIDC.Issuer == "" || c.Auth.OIDC.ClientID == "" || c.Auth.OIDC.ClientSecret == "" || c.Auth.OIDC.RedirectURL == "" { + return fmt.Errorf("OIDC mode requires [auth.oidc] authority, client_id, client_secret and redirect_url") } } // Empty is fine (the key is optional), but a relative value would be forwarded as an // invalid post_logout_redirect_uri and only fail at logout time — catch it here. - if cfg.OIDC.PostLogoutRedirectURL != "" { - u, err := url.Parse(cfg.OIDC.PostLogoutRedirectURL) + if c.Auth.OIDC.PostLogoutRedirectURL != "" { + u, err := url.Parse(c.Auth.OIDC.PostLogoutRedirectURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { - return nil, fmt.Errorf("[oidc] post_logout_redirect_url must be an absolute http:// or https:// URL, got %q", - cfg.OIDC.PostLogoutRedirectURL) + return fmt.Errorf("[auth.oidc] post_logout_redirect_url must be an absolute http:// or https:// URL, got %q", + c.Auth.OIDC.PostLogoutRedirectURL) } } // Basic (file-based) auth is supported for quickstart deployments but is not // recommended for production; point operators at OIDC. - if !cfg.OIDC.Enabled { + if !c.Auth.OIDC.Enabled { slog.Warn("basic (file-based) auth is enabled — this is not recommended for production; " + - "configure OIDC (set auth_mode = \"oidc\" and [oidc] authority, client_id, client_secret, redirect_url)") + "configure OIDC (set [auth] mode = \"oidc\" and [auth.oidc] authority, client_id, client_secret, redirect_url)") } - cfg.RuntimeConfig = buildRuntimeConfig(cfg, s) - return cfg, nil + return nil } diff --git a/portals/ai-workspace/bff/internal/config/config_test.go b/portals/ai-workspace/bff/internal/config/config_test.go index 06be844bc3..7fc80be5b8 100644 --- a/portals/ai-workspace/bff/internal/config/config_test.go +++ b/portals/ai-workspace/bff/internal/config/config_test.go @@ -37,9 +37,10 @@ func writeConfig(t *testing.T, body string) string { // A literal config.toml value is used as written. func TestLoad_ConfigFileValue(t *testing.T) { cfgPath := writeConfig(t, ` -log_level = "warn" +[ai_workspace.logging] +level = "warn" -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" `) @@ -47,11 +48,11 @@ url = "https://platform-api:9243" if err != nil { t.Fatalf("Load() error = %v", err) } - if cfg.PlatformAPI.URL != "https://platform-api:9243" { - t.Errorf("PlatformAPI.URL = %q, want the config.toml value", cfg.PlatformAPI.URL) + if cfg.ControlPlane.URL != "https://platform-api:9243" { + t.Errorf("ControlPlane.URL = %q, want the config.toml value", cfg.ControlPlane.URL) } - if cfg.LogLevel != "warn" { - t.Errorf("LogLevel = %q, want %q", cfg.LogLevel, "warn") + if cfg.Logging.Level != "warn" { + t.Errorf("LogLevel = %q, want %q", cfg.Logging.Level, "warn") } } @@ -59,24 +60,25 @@ url = "https://platform-api:9243" // supplies the variable's value, and its default applies when the variable is unset. func TestLoad_EnvTokenSuppliesValueAndDefault(t *testing.T) { cfgPath := writeConfig(t, ` -log_level = '{{ env "APIP_AIW_LOG_LEVEL" "info" }}' -log_format = '{{ env "APIP_AIW_LOG_FORMAT" "text" }}' +[ai_workspace.logging] +level = '{{ env "APIP_AIW_LOGGING_LEVEL" "info" }}' +format = '{{ env "APIP_AIW_LOGGING_FORMAT" "text" }}' -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" `) - t.Setenv("APIP_AIW_LOG_LEVEL", "debug") // named by the token - // APIP_AIW_LOG_FORMAT is left unset, so the token's default stands. + t.Setenv("APIP_AIW_LOGGING_LEVEL", "debug") // named by the token + // APIP_AIW_LOGGING_FORMAT is left unset, so the token's default stands. cfg, err := Load(cfgPath) if err != nil { t.Fatalf("Load() error = %v", err) } - if cfg.LogLevel != "debug" { - t.Errorf("LogLevel = %q, want %q (the token's variable is set)", cfg.LogLevel, "debug") + if cfg.Logging.Level != "debug" { + t.Errorf("LogLevel = %q, want %q (the token's variable is set)", cfg.Logging.Level, "debug") } - if cfg.LogFormat != "text" { - t.Errorf("LogFormat = %q, want the token default %q", cfg.LogFormat, "text") + if cfg.Logging.Format != "text" { + t.Errorf("LogFormat = %q, want the token default %q", cfg.Logging.Format, "text") } } @@ -85,20 +87,21 @@ url = "https://platform-api:9243" // pulls a value in from the environment. func TestLoad_EnvVarWithoutTokenIsIgnored(t *testing.T) { cfgPath := writeConfig(t, ` -log_level = "warn" +[ai_workspace.logging] +level = "warn" -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" `) - t.Setenv("APIP_AIW_LOG_LEVEL", "debug") + t.Setenv("APIP_AIW_LOGGING_LEVEL", "debug") cfg, err := Load(cfgPath) if err != nil { t.Fatalf("Load() error = %v", err) } - if cfg.LogLevel != "warn" { + if cfg.Logging.Level != "warn" { t.Errorf("LogLevel = %q, want the config.toml literal %q — an env var must not override a key with no token", - cfg.LogLevel, "warn") + cfg.Logging.Level, "warn") } } @@ -106,12 +109,13 @@ url = "https://platform-api:9243" // variable — the APIP_AIW_ prefix is a convention, not a requirement. func TestLoad_InterpolatesEnvToken(t *testing.T) { cfgPath := writeConfig(t, ` -auth_mode = "oidc" +[ai_workspace.auth] +mode = "oidc" -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" -[oidc] +[ai_workspace.auth.oidc] authority = "https://idp.example.com" client_id = "client-id" client_secret = '{{ env "CUSTOM_SECRET_VAR" }}' @@ -123,8 +127,8 @@ redirect_url = "https://localhost:5380/api/auth/callback" if err != nil { t.Fatalf("Load() error = %v", err) } - if cfg.OIDC.ClientSecret != "s3cr3t" { - t.Errorf("OIDC.ClientSecret = %q, want the value resolved from the env token", cfg.OIDC.ClientSecret) + if cfg.Auth.OIDC.ClientSecret != "s3cr3t" { + t.Errorf("OIDC.ClientSecret = %q, want the value resolved from the env token", cfg.Auth.OIDC.ClientSecret) } } @@ -137,12 +141,13 @@ func TestLoad_InterpolatesFileToken(t *testing.T) { t.Setenv("APIP_CONFIG_FILE_SOURCE_ALLOWLIST", secretDir) cfgPath := writeConfig(t, ` -auth_mode = "oidc" +[ai_workspace.auth] +mode = "oidc" -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" -[oidc] +[ai_workspace.auth.oidc] authority = "https://idp.example.com" client_id = "client-id" client_secret = '{{ file "`+filepath.Join(secretDir, "oidc_client_secret")+`" }}' @@ -154,8 +159,8 @@ redirect_url = "https://localhost:5380/api/auth/callback" t.Fatalf("Load() error = %v", err) } // The trailing newline every secret file ends with must be trimmed. - if cfg.OIDC.ClientSecret != "from-file" { - t.Errorf("OIDC.ClientSecret = %q, want %q", cfg.OIDC.ClientSecret, "from-file") + if cfg.Auth.OIDC.ClientSecret != "from-file" { + t.Errorf("OIDC.ClientSecret = %q, want %q", cfg.Auth.OIDC.ClientSecret, "from-file") } } @@ -169,10 +174,10 @@ func TestLoad_FileTokenOutsideAllowlist_Errors(t *testing.T) { t.Setenv("APIP_CONFIG_FILE_SOURCE_ALLOWLIST", t.TempDir()) // a different directory cfgPath := writeConfig(t, ` -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" -[oidc] +[ai_workspace.auth.oidc] client_secret = '{{ file "`+outside+`" }}' `) @@ -185,10 +190,10 @@ client_secret = '{{ file "`+outside+`" }}' // yield an empty secret. func TestLoad_MissingEnvToken_Errors(t *testing.T) { cfgPath := writeConfig(t, ` -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" -[oidc] +[ai_workspace.auth.oidc] client_secret = '{{ env "CUSTOM_SECRET_VAR" }}' `) t.Setenv("CUSTOM_SECRET_VAR", "") @@ -203,15 +208,15 @@ client_secret = '{{ env "CUSTOM_SECRET_VAR" }}' } // The upstream URL is mandatory — the BFF has nothing to proxy to without it. -func TestLoad_MissingPlatformAPIURL_Errors(t *testing.T) { - cfgPath := writeConfig(t, `log_level = "info"`) +func TestLoad_MissingControlPlaneURL_Errors(t *testing.T) { + cfgPath := writeConfig(t, "[ai_workspace.server]\ndomain = \"localhost:5380\"") _, err := Load(cfgPath) if err == nil { - t.Fatal("Load() succeeded, want an error when [platform_api] url is unset") + t.Fatal("Load() succeeded, want an error when [control_plane] url is unset") } - if !strings.Contains(err.Error(), "[platform_api] url") { - t.Errorf("error = %v, want it to name [platform_api] url", err) + if !strings.Contains(err.Error(), "[control_plane] url") { + t.Errorf("error = %v, want it to name [control_plane] url", err) } } @@ -219,13 +224,16 @@ func TestLoad_MissingPlatformAPIURL_Errors(t *testing.T) { // and OIDC client credentials must never appear in it. func TestLoad_RuntimeConfigExcludesServerSideKeys(t *testing.T) { cfgPath := writeConfig(t, ` -auth_mode = "oidc" +[ai_workspace.server] domain = "localhost:5380" -[platform_api] +[ai_workspace.auth] +mode = "oidc" + +[ai_workspace.control_plane] url = "https://platform-api:9243" -[oidc] +[ai_workspace.auth.oidc] authority = "https://idp.example.com" client_id = "client-id" client_secret = "s3cr3t" @@ -237,15 +245,15 @@ redirect_url = "https://localhost:5380/api/auth/callback" t.Fatalf("Load() error = %v", err) } - if got := cfg.RuntimeConfig["APIP_AIW_DOMAIN"]; got != "localhost:5380" { - t.Errorf("APIP_AIW_DOMAIN = %q, want the browser-safe domain to be surfaced", got) + if got := cfg.RuntimeConfig["APIP_AIW_SERVER_DOMAIN"]; got != "localhost:5380" { + t.Errorf("APIP_AIW_SERVER_DOMAIN = %q, want the browser-safe domain to be surfaced", got) } for _, v := range cfg.RuntimeConfig { if strings.Contains(v, "s3cr3t") || strings.Contains(v, "platform-api:9243") { t.Errorf("runtime config leaked a server-side value: %q", v) } } - for _, key := range []string{"APIP_AIW_OIDC_CLIENT_SECRET", "APIP_AIW_OIDC_CLIENT_ID", "APIP_AIW_OIDC_AUTHORITY"} { + for _, key := range []string{"APIP_AIW_AUTH_OIDC_CLIENT_SECRET", "APIP_AIW_AUTH_OIDC_CLIENT_ID", "APIP_AIW_AUTH_OIDC_AUTHORITY"} { if _, ok := cfg.RuntimeConfig[key]; ok { t.Errorf("runtime config must not contain %s — the BFF owns the OIDC handshake", key) } @@ -257,9 +265,10 @@ redirect_url = "https://localhost:5380/api/auth/callback" // browser. func TestLoad_BrowserSafeKeyUsesSameName(t *testing.T) { cfgPath := writeConfig(t, ` +[ai_workspace] moesif_web_url = "https://moesif.example.com" -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" `) @@ -276,19 +285,20 @@ url = "https://platform-api:9243" // under that same name, exactly as if it had been written as a literal. func TestLoad_BrowserSafeKeyFromEnvToken(t *testing.T) { cfgPath := writeConfig(t, ` -domain = '{{ env "APIP_AIW_DOMAIN" "localhost:5380" }}' +[ai_workspace.server] +domain = '{{ env "APIP_AIW_SERVER_DOMAIN" "localhost:5380" }}' -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" `) - t.Setenv("APIP_AIW_DOMAIN", "app.example.com") + t.Setenv("APIP_AIW_SERVER_DOMAIN", "app.example.com") cfg, err := Load(cfgPath) if err != nil { t.Fatalf("Load() error = %v", err) } - if got := cfg.RuntimeConfig["APIP_AIW_DOMAIN"]; got != "app.example.com" { - t.Errorf("APIP_AIW_DOMAIN = %q, want the token-resolved value to reach the browser", got) + if got := cfg.RuntimeConfig["APIP_AIW_SERVER_DOMAIN"]; got != "app.example.com" { + t.Errorf("APIP_AIW_SERVER_DOMAIN = %q, want the token-resolved value to reach the browser", got) } } @@ -296,13 +306,13 @@ url = "https://platform-api:9243" // string, but a plain literal is naturally typed. Both forms must reach the same value. func TestLoad_BareTOMLScalars(t *testing.T) { cfgPath := writeConfig(t, ` -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" -[cookie] -secure = false +[ai_workspace.server] +enabled = false -[session] +[ai_workspace.session] absolute_ttl = "2h" `) @@ -310,8 +320,8 @@ absolute_ttl = "2h" if err != nil { t.Fatalf("Load() error = %v", err) } - if cfg.Cookie.Secure { - t.Error("Cookie.Secure = true, want false from the bare TOML boolean") + if cfg.Server.Enabled { + t.Error("Server.Enabled = true, want false from the bare TOML boolean") } if cfg.Session.AbsoluteTTL != 2*time.Hour { t.Errorf("Session.AbsoluteTTL = %s, want 2h", cfg.Session.AbsoluteTTL) @@ -319,18 +329,19 @@ absolute_ttl = "2h" } // A key in a table must not collide with the same key in another table — they are -// distinct dotted paths, so [tls] enabled and [oidc] enabled are independent. +// distinct dotted paths, so [server] enabled and [auth.oidc] enabled are independent. func TestLoad_SameKeyInDifferentTables(t *testing.T) { cfgPath := writeConfig(t, ` -auth_mode = "oidc" +[ai_workspace.auth] +mode = "oidc" -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" -[tls] +[ai_workspace.server] enabled = false -[oidc] +[ai_workspace.auth.oidc] enabled = true authority = "https://idp.example.com" client_id = "client-id" @@ -342,59 +353,61 @@ redirect_url = "https://localhost:5380/api/auth/callback" if err != nil { t.Fatalf("Load() error = %v", err) } - if cfg.TLS.TerminateTLS { - t.Error("TLS.TerminateTLS = true, want false — [tls] enabled must not read [oidc] enabled") + if cfg.Server.Enabled { + t.Error("Server.Enabled = true, want false — [server] enabled must not read [auth.oidc] enabled") } - if !cfg.OIDC.Enabled { + if !cfg.Auth.OIDC.Enabled { t.Error("OIDC.Enabled = false, want true") } } -// [oidc.claim_mappings] mirrors the Platform API's [auth.idp.claim_mappings] key for -// key. The BFF reads those keys for its own session mapping, and the browser-safe ones -// reach the SPA under the matching APIP_AIW_OIDC_CLAIM_MAPPINGS_* names that -// src/config.env.ts looks up. -func TestLoad_ClaimMappingsMirrorPlatformAPI(t *testing.T) { +// [auth.claim_mappings] mirrors the Platform API's [auth.claim_mappings] key for +// key, and is shared by both auth modes (not nested under [auth.oidc]): OIDC +// tokens from the configured IDP, and the HMAC JWTs the Platform API's +// file-based login endpoint signs using these same mapped claim names. The +// browser-safe ones reach the SPA under the matching +// APIP_AIW_AUTH_CLAIM_MAPPINGS_* names that src/config.env.ts looks up. +func TestLoad_ClaimMappingsMirrorControlPlane(t *testing.T) { cfgPath := writeConfig(t, ` -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" -[oidc.claim_mappings] -organization_claim_name = "org_uuid" -username_claim_name = "given_name" -role_claim_name = "roles" +[ai_workspace.auth.claim_mappings] +organization = "org_uuid" +username = "given_name" +roles = "roles" `) cfg, err := Load(cfgPath) if err != nil { t.Fatalf("Load() error = %v", err) } - if cfg.OIDC.Claims.OrgID != "org_uuid" { - t.Errorf("Claims.OrgID = %q, want %q from organization_claim_name", cfg.OIDC.Claims.OrgID, "org_uuid") + if cfg.Auth.ClaimMappings.OrgID != "org_uuid" { + t.Errorf("Claims.OrgID = %q, want %q from organization", cfg.Auth.ClaimMappings.OrgID, "org_uuid") } - if cfg.OIDC.Claims.Role != "roles" { - t.Errorf("Claims.Role = %q, want %q from role_claim_name", cfg.OIDC.Claims.Role, "roles") + if cfg.Auth.ClaimMappings.Roles != "roles" { + t.Errorf("Claims.Roles = %q, want %q from roles", cfg.Auth.ClaimMappings.Roles, "roles") } - if got := cfg.RuntimeConfig["APIP_AIW_OIDC_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME"]; got != "org_uuid" { - t.Errorf("runtime APIP_AIW_OIDC_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME = %q, want %q", got, "org_uuid") + if got := cfg.RuntimeConfig["APIP_AIW_AUTH_CLAIM_MAPPINGS_ORGANIZATION"]; got != "org_uuid" { + t.Errorf("runtime APIP_AIW_AUTH_CLAIM_MAPPINGS_ORGANIZATION = %q, want %q", got, "org_uuid") } - if got := cfg.RuntimeConfig["APIP_AIW_OIDC_CLAIM_MAPPINGS_USERNAME_CLAIM_NAME"]; got != "given_name" { - t.Errorf("runtime APIP_AIW_OIDC_CLAIM_MAPPINGS_USERNAME_CLAIM_NAME = %q, want %q", got, "given_name") + if got := cfg.RuntimeConfig["APIP_AIW_AUTH_CLAIM_MAPPINGS_USERNAME"]; got != "given_name" { + t.Errorf("runtime APIP_AIW_AUTH_CLAIM_MAPPINGS_USERNAME = %q, want %q", got, "given_name") } - // role_claim_name drives the BFF's session mapping only — it must not be published. - if _, ok := cfg.RuntimeConfig["APIP_AIW_OIDC_CLAIM_MAPPINGS_ROLE_CLAIM_NAME"]; ok { - t.Error("role_claim_name must not reach the browser — it is not in the browser-safe allowlist") + // roles drives the BFF's session mapping only — it must not be published. + if _, ok := cfg.RuntimeConfig["APIP_AIW_AUTH_CLAIM_MAPPINGS_ROLES"]; ok { + t.Error("roles must not reach the browser — it is not in the browser-safe allowlist") } } // A malformed boolean must fail startup rather than fall back to the default. func TestLoad_InvalidBool_Errors(t *testing.T) { cfgPath := writeConfig(t, ` -[platform_api] +[ai_workspace.control_plane] url = "https://platform-api:9243" -[cookie] -secure = "maybe" +[ai_workspace.server] +enabled = "maybe" `) if _, err := Load(cfgPath); err == nil { diff --git a/portals/ai-workspace/bff/internal/config/default_config.go b/portals/ai-workspace/bff/internal/config/default_config.go new file mode 100644 index 0000000000..5f2fccc947 --- /dev/null +++ b/portals/ai-workspace/bff/internal/config/default_config.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package config + +import "time" + +// defaultConfig returns a Config with all built-in defaults. Load overlays the parsed +// config.toml on top of it, so any key absent from the file keeps the value here. The +// required keys (control_plane.url) have no meaningful default and are enforced by +// Config.validate instead. Cookie and RuntimeConfig are populated by Load, not here. +func defaultConfig() *Config { + return &Config{ + Server: ServerConfig{ + StaticDir: "/app", + Domain: "localhost:5380", + Enabled: true, + Port: 5380, + // Convention matches the container's mount path. A certificate pair is + // required there whenever the listener terminates TLS. + CertFile: "/etc/ai-workspace/tls/cert.pem", + KeyFile: "/etc/ai-workspace/tls/key.pem", + }, + Logging: LoggingConfig{ + Level: "info", + Format: "text", + }, + ControlPlane: ControlPlaneConfig{ + PortalBasePath: "/api/portal/v0.9", + ProxyPrefix: "/proxy", + }, + Session: SessionConfig{ + Store: "memory", + IdleTimeout: 30 * time.Minute, + AbsoluteTTL: 8 * time.Hour, + }, + Auth: AuthConfig{ + Mode: "basic", + OIDC: OIDCConfig{ + Scopes: defaultOIDCScopes, + }, + // Defaults mirror the Platform API's own claim_mappings defaults so the two + // agree out of the box; override on both sides together when an IDP uses + // different claim names (e.g. Asgardeo's "org_id"). + ClaimMappings: ClaimMappingConfig{ + Username: "username", + Email: "email", + Roles: "roles", + Scope: "scope", + OrgID: "organization", + OrgName: "org_name", + OrgHandle: "org_handle", + }, + }, + } +} diff --git a/portals/ai-workspace/bff/internal/config/runtime_config.go b/portals/ai-workspace/bff/internal/config/runtime_config.go index 1a32ebd62a..da8d5971ae 100644 --- a/portals/ai-workspace/bff/internal/config/runtime_config.go +++ b/portals/ai-workspace/bff/internal/config/runtime_config.go @@ -16,33 +16,39 @@ package config -import "strings" +import ( + "fmt" + "strings" + + "github.com/knadh/koanf/v2" +) // browserSafeKeys is the allowlist of config keys the SPA may read from // window.__RUNTIME_CONFIG__. It is an allowlist, not a filter: only these keys ever // reach the browser, so a new server-side key (a secret, an upstream URL, a cookie // setting) cannot leak into the page merely by being added to config.toml. // -// [oidc] client_secret / client_id / authority are deliberately absent — the BFF +// [auth.oidc] client_secret / client_id / authority are deliberately absent — the BFF // performs the whole OIDC handshake, so the SPA needs no client identity. var browserSafeKeys = []string{ - // Identity of the deployment. auth_mode is not listed: buildRuntimeConfig - // always emits it from the parsed cfg.AuthMode instead. - "domain", + // Identity of the deployment. auth.mode is not listed: buildRuntimeConfig + // always emits it from the parsed cfg.Auth.Mode instead. + "server.domain", "default_org_region", - "controlplane_host", - "platform_gateway_versions", - "csrf_header", - "debug", + "gateway.controlplane_host", + "gateway.platform_gateway_versions", + "logging.browser_debug", // Claim names the SPA displays user/org identity from. The keys mirror the - // Platform API's [auth.idp.claim_mappings] exactly — same claim, same name. - "oidc.scope", - "oidc.claim_mappings.username_claim_name", - "oidc.claim_mappings.email_claim_name", - "oidc.claim_mappings.organization_claim_name", - "oidc.claim_mappings.org_name_claim_name", - "oidc.claim_mappings.org_handle_claim_name", + // Platform API's [auth.claim_mappings] exactly — same claim, same name. Shared + // by both auth modes, so it lives under [auth.claim_mappings], not nested in + // [auth.oidc]. + "auth.oidc.scope", + "auth.claim_mappings.username", + "auth.claim_mappings.email", + "auth.claim_mappings.organization", + "auth.claim_mappings.org_name", + "auth.claim_mappings.org_handle", // External links and SPA-only endpoints "dev_portal_base_url", @@ -53,8 +59,8 @@ var browserSafeKeys = []string{ } // runtimeKey converts a config key into the name the SPA reads: APIP_AIW_ + the key's -// dotted path uppercased, with dots as underscores ("oidc.scope" -> -// APIP_AIW_OIDC_SCOPE). It is the same spelling the key's {{ env }} token +// dotted path uppercased, with dots as underscores ("auth.oidc.scope" -> +// APIP_AIW_AUTH_OIDC_SCOPE). It is the same spelling the key's {{ env }} token // conventionally names, so a value has one name across config.toml, the environment, // Vite's import.meta.env, and window.__RUNTIME_CONFIG__. func runtimeKey(configKey string) string { @@ -64,18 +70,29 @@ func runtimeKey(configKey string) string { // buildRuntimeConfig collects the browser-safe values the SPA reads from // window.__RUNTIME_CONFIG__, then forces the API base URLs onto the same-origin // proxy prefix so the browser only ever talks to the BFF. -func buildRuntimeConfig(cfg *Config, s settings) map[string]string { +// +// Values are read straight from the parsed config (k, rooted at [ai_workspace]), not +// from the resolved Config struct, so only keys actually present in config.toml are +// surfaced — a code default is used internally by the BFF but never pushed to the +// browser. Most browser-safe keys (domain, gateway.*, moesif_*, dev_portal_base_url, +// ...) are browser-only and not modeled on Config at all; this is where they flow out. +func buildRuntimeConfig(cfg *Config, k *koanf.Koanf) map[string]string { out := make(map[string]string, len(browserSafeKeys)+3) for _, key := range browserSafeKeys { - if v, ok := s[key]; ok && v != "" { + if !k.Exists(key) { + continue + } + // Stringify so a value written bare (a bool/int, e.g. logging.browser_debug) + // reaches the SPA the same as a quoted one. + if v := fmt.Sprint(k.Get(key)); v != "" { out[runtimeKey(key)] = v } } // Force the SPA's API base URLs through the BFF same-origin proxy. - out[runtimeKey("platform_api_base_url")] = cfg.ProxyPrefix + "/api/v0.9" - out[runtimeKey("portal_api_base_url")] = cfg.ProxyPrefix + "/api/portal/v0.9" - out[runtimeKey("auth_mode")] = cfg.AuthMode + out[runtimeKey("platform_api_base_url")] = cfg.ControlPlane.ProxyPrefix + "/api/v0.9" + out[runtimeKey("portal_api_base_url")] = cfg.ControlPlane.ProxyPrefix + "/api/portal/v0.9" + out[runtimeKey("auth_mode")] = cfg.Auth.Mode return out } diff --git a/portals/ai-workspace/bff/internal/config/settings.go b/portals/ai-workspace/bff/internal/config/settings.go index 1a8b1527e3..8b3632b9a7 100644 --- a/portals/ai-workspace/bff/internal/config/settings.go +++ b/portals/ai-workspace/bff/internal/config/settings.go @@ -20,10 +20,11 @@ import ( "fmt" "log/slog" "os" - "strconv" - "strings" - "time" + tomlparser "github.com/knadh/koanf/parsers/toml/v2" + "github.com/knadh/koanf/providers/confmap" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" "github.com/wso2/api-platform/common/configinterpolate" ) @@ -33,13 +34,21 @@ import ( // It is a naming convention, not a binding: the environment reaches the config only // through the {{ env "NAME" }} tokens written in config.toml, which name the variable // explicitly. By convention a key's variable is its dotted path uppercased, with dots -// as underscores, prefixed — so [oidc] client_id ships as -// '{{ env "APIP_AIW_OIDC_CLIENT_ID" }}'. A token may name any variable, and a key with -// no token cannot be set from the environment at all. +// as underscores, prefixed — so [auth.oidc] client_id ships as +// '{{ env "APIP_AIW_AUTH_OIDC_CLIENT_ID" }}'. A token may name any variable, and a key +// with no token cannot be set from the environment at all. // // The prefix also namespaces the runtime config the SPA reads (see runtimeKey). const EnvPrefix = "APIP_AIW_" +// aiWorkspaceConfigKey is the top-level TOML table all AI Workspace settings live +// under (e.g. [ai_workspace], [ai_workspace.control_plane]). It mirrors the Platform +// API's platformAPIConfigKey: this namespacing lets an AI Workspace config file +// coexist with sibling services' sections ([platform_api], ...) in a shared +// deployment config. loadConfigKoanf cuts to this table, so every key is resolved +// relative to [ai_workspace] and sibling tables are ignored. +const aiWorkspaceConfigKey = "ai_workspace" + // defaultFileSourceAllowlist is the AI Workspace's default set of directories a // {{ file "..." }} token may read from. Overridable via the shared // APIP_CONFIG_FILE_SOURCE_ALLOWLIST env var (see configinterpolate.ResolveAllowlist). @@ -48,34 +57,34 @@ var defaultFileSourceAllowlist = []string{ "/secrets/ai-workspace", } -// settings is the fully-resolved configuration: the config.toml values with every -// {{ env }} / {{ file }} token expanded, flattened to dotted paths. A key is its -// table path joined with dots — [platform_api] url becomes "platform_api.url" — and a -// key outside any table keeps its bare name ("domain"). -type settings map[string]string - -// loadSettings reads config.toml and expands its interpolation tokens. config.toml is -// the only source of configuration: there is no implicit environment overlay, so a -// value comes from the environment exactly when the key's token asks for it, e.g. +// loadConfigKoanf reads config.toml, expands its {{ env }} / {{ file }} interpolation +// tokens, and returns a koanf instance rooted at the [ai_workspace] subtree — the same +// koanf-based loading stack the Gateway and Platform API use. // -// log_level = '{{ env "APIP_AIW_LOG_LEVEL" "info" }}' -// -// One mechanism therefore covers both ordinary settings and secrets, and every source -// a value can come from is visible in the file itself rather than implied by a naming -// rule. Interpolation fails closed: an unset {{ env }} variable with no default, or an +// config.toml is the only source of configuration: there is no implicit environment +// overlay, so a value comes from the environment exactly when the key's token asks for +// it. Interpolation fails closed — an unset {{ env }} variable with no default, or an // unreadable/disallowed {{ file }} path, aborts startup rather than silently yielding // an empty credential. // -// A missing config.toml is not an error — the built-in defaults still apply — but the -// required keys (platform_api_url) then have no value, so Load fails on them. -func loadSettings(tomlPath string) (settings, error) { - raw, err := parseTOML(tomlPath) - if err != nil { - return nil, err +// A missing config.toml is not an error: the returned instance is empty, so every key +// falls back to defaultConfig() and Load fails only on the required ones. +func loadConfigKoanf(tomlPath string) (*koanf.Koanf, error) { + k := koanf.New(".") + + // Stat first so a missing file stays a no-op (defaults apply) rather than a koanf + // load error; anything else (e.g. a permission problem) is surfaced. + if _, statErr := os.Stat(tomlPath); statErr == nil { + if err := k.Load(file.Provider(tomlPath), tomlparser.Parser()); err != nil { + return nil, fmt.Errorf("failed to parse config file %q: %w", tomlPath, err) + } + } else if !os.IsNotExist(statErr) { + return nil, fmt.Errorf("failed to read config file %q: %w", tomlPath, statErr) } - // Expand walks the whole tree, so a token works at any depth. - expanded, stats, err := configinterpolate.Expand(raw, configinterpolate.Options{ + // Expand tokens across the whole tree, so a token works at any depth. Shared with + // the Platform API via configinterpolate, operating on koanf's raw nested map. + expanded, stats, err := configinterpolate.Expand(k.Raw(), configinterpolate.Options{ FileAllowlist: configinterpolate.ResolveAllowlist(defaultFileSourceAllowlist), }) if err != nil { @@ -89,87 +98,11 @@ func loadSettings(tomlPath string) (settings, error) { slog.Int("fields", stats.Fields)) } - s := settings{} - flatten(s, "", expanded) - return s, nil -} - -// parseTOML decodes the config file with the stdlib subset parser (see toml.go). -// A missing file yields an empty tree rather than an error, leaving every key on -// its default — Load then fails on the required ones. -func parseTOML(path string) (map[string]any, error) { - data, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return map[string]any{}, nil - } - return nil, fmt.Errorf("failed to read config file %q: %w", path, err) - } - - raw, err := parseTOMLSubset(data) - if err != nil { - return nil, fmt.Errorf("failed to parse config file %q: %w", path, err) - } - return raw, nil -} - -// flatten collapses the decoded tree into dotted keys ([oidc] client_id -> -// "oidc.client_id"), stringifying scalars so a value may be written either quoted or -// bare — tls.enabled = true and tls.enabled = "true" both reach getbool as "true". -// Arrays have no config key and are rejected by the parser before reaching here. -func flatten(dst settings, prefix string, tree map[string]any) { - for k, v := range tree { - key := strings.ToLower(k) - if prefix != "" { - key = prefix + "." + key - } - switch val := v.(type) { - case map[string]any: - flatten(dst, key, val) - case string: - dst[key] = val - case bool, int64, float64: - dst[key] = fmt.Sprint(val) - } - } -} - -// get returns the value for key, or def when it is unset or empty. -func (s settings) get(key, def string) string { - if v, ok := s[key]; ok && v != "" { - return v - } - return def -} - -// getbool parses key as a boolean. A malformed value fails startup rather than -// being silently replaced by the default. -func (s settings) getbool(key string, def bool) (bool, error) { - v, ok := s[key] - if !ok || v == "" { - return def, nil - } - b, err := strconv.ParseBool(strings.TrimSpace(v)) - if err != nil { - return false, fmt.Errorf("invalid boolean for %s=%q: %w", key, v, err) - } - return b, nil -} - -// getdur parses key as a Go duration. A malformed, zero, or negative value fails -// startup — every duration setting is a lifetime or timeout, where <= 0 is never -// meaningful. -func (s settings) getdur(key string, def time.Duration) (time.Duration, error) { - v, ok := s[key] - if !ok || v == "" { - return def, nil - } - d, err := time.ParseDuration(strings.TrimSpace(v)) - if err != nil { - return 0, fmt.Errorf("invalid duration for %s=%q: %w", key, v, err) - } - if d <= 0 { - return 0, fmt.Errorf("invalid duration for %s=%q: must be positive", key, v) + // Reload the expanded map into a fresh instance so no un-interpolated leaf survives, + // then cut to the [ai_workspace] subtree. + out := koanf.New(".") + if err := out.Load(confmap.Provider(expanded, "."), nil); err != nil { + return nil, fmt.Errorf("failed to reload interpolated config: %w", err) } - return d, nil + return out.Cut(aiWorkspaceConfigKey), nil } diff --git a/portals/ai-workspace/bff/internal/config/shipped_config_test.go b/portals/ai-workspace/bff/internal/config/shipped_config_test.go index 5ed0f5d512..3ad6871b39 100644 --- a/portals/ai-workspace/bff/internal/config/shipped_config_test.go +++ b/portals/ai-workspace/bff/internal/config/shipped_config_test.go @@ -17,7 +17,9 @@ package config import ( + "os" "path/filepath" + "strings" "testing" ) @@ -30,58 +32,60 @@ var ( ) // The shipped config.toml must load with nothing set in the environment — that is the -// quickstart: `docker compose up` with no .env beyond the two secrets. Its [oidc] keys -// default to empty, which must leave OIDC off rather than fail startup. +// quickstart: `docker compose up` with no .env beyond the two secrets. It ships without +// an [ai_workspace.auth.oidc] table at all, which must leave OIDC off rather than fail startup. func TestShippedConfig_QuickstartLoadsWithNoEnv(t *testing.T) { cfg, err := Load(quickstartConfig) if err != nil { t.Fatalf("Load(configs/config.toml) error = %v — the shipped quickstart config must load with an empty environment", err) } - if cfg.AuthMode != "basic" { - t.Errorf("AuthMode = %q, want %q", cfg.AuthMode, "basic") + if cfg.Auth.Mode != "basic" { + t.Errorf("AuthMode = %q, want %q", cfg.Auth.Mode, "basic") } - // The empty [oidc] defaults must not switch the OIDC client on, or Load would - // demand an authority/client_id/client_secret the quickstart has no use for. - if cfg.OIDC.Enabled { - t.Error("OIDC.Enabled = true, want false — the empty [oidc] defaults must leave basic mode intact") + // The missing [ai_workspace.auth.oidc] table must not switch the OIDC client on, or + // Load would demand an authority/client_id/client_secret the quickstart has no use for. + if cfg.Auth.OIDC.Enabled { + t.Error("OIDC.Enabled = true, want false — the empty [ai_workspace.auth.oidc] defaults must leave basic mode intact") } // The defaults in the file are the container's: the port it publishes and the SPA // baked into the image. - if cfg.Addr != ":5380" { - t.Errorf("Addr = %q, want the container default %q", cfg.Addr, ":5380") + if cfg.Addr() != ":5380" { + t.Errorf("Addr = %q, want the container default %q", cfg.Addr(), ":5380") } - if cfg.StaticDir != "/app" { - t.Errorf("StaticDir = %q, want the container default %q", cfg.StaticDir, "/app") + if cfg.Server.StaticDir != "/app" { + t.Errorf("StaticDir = %q, want the container default %q", cfg.Server.StaticDir, "/app") } - if cfg.PlatformAPI.URL != "https://platform-api:9243" { - t.Errorf("PlatformAPI.URL = %q, want the compose hostname", cfg.PlatformAPI.URL) + if cfg.ControlPlane.URL != "https://platform-api:9243" { + t.Errorf("ControlPlane.URL = %q, want the compose hostname", cfg.ControlPlane.URL) } // The quickstart trusts the setup.sh-generated platform-api certificate via // ca_file (mounted by docker-compose); verification stays on by default. - if cfg.PlatformAPI.TLSSkipVerify { - t.Error("PlatformAPI.TLSSkipVerify = true, want false — the quickstart trusts the upstream via ca_file, not by skipping verification") + if cfg.ControlPlane.TLSSkipVerify { + t.Error("ControlPlane.TLSSkipVerify = true, want false — the quickstart trusts the upstream via ca_file, not by skipping verification") } - // docker-compose no longer injects APIP_AIW_PLATFORM_API_CA_FILE — the default + // docker-compose no longer injects APIP_AIW_CONTROL_PLANE_CA_FILE — the default // must be the path the compose file mounts the certificate at. The shared // self-signed cert.pem is its own CA. - if cfg.PlatformAPI.CAFile != "/etc/ai-workspace/tls/cert.pem" { - t.Errorf("PlatformAPI.CAFile = %q, want the docker-compose mount path %q", cfg.PlatformAPI.CAFile, "/etc/ai-workspace/tls/cert.pem") + if cfg.ControlPlane.CAFile != "/etc/ai-workspace/tls/cert.pem" { + t.Errorf("ControlPlane.CAFile = %q, want the docker-compose mount path %q", cfg.ControlPlane.CAFile, "/etc/ai-workspace/tls/cert.pem") } } // `make bff-run` loads this same file and points the container-shaped defaults at the // developer's machine, purely through the {{ env }} tokens in it. A token is the only // way an environment variable reaches a key, so dropping one of these keys from -// config.toml would silently strip the override and leave the BFF on :5380 serving -// /app. This test pins the Makefile's contract with the file. +// config.toml would silently strip the override and leave the BFF on :5380. This test +// pins the Makefile's contract with the file. static_dir is NOT among these: the +// shipped config has no token for it (the container always serves /app), so +// `make bff-run` retargets it with the -static-dir CLI flag in main.go instead — +// outside config.Load, and so outside this test. func TestShippedConfig_MakeBffRunOverrides(t *testing.T) { // Exactly the variables the bff-run target sets. - t.Setenv("APIP_AIW_PLATFORM_API_URL", "https://localhost:9243") - t.Setenv("APIP_AIW_PLATFORM_API_TLS_SKIP_VERIFY", "true") - t.Setenv("APIP_AIW_LISTEN_ADDR", ":8081") - t.Setenv("APIP_AIW_STATIC_DIR", "../dist") - t.Setenv("APIP_AIW_LOG_LEVEL", "debug") + t.Setenv("APIP_AIW_CONTROL_PLANE_URL", "https://localhost:9243") + t.Setenv("APIP_AIW_CONTROL_PLANE_TLS_SKIP_VERIFY", "true") + t.Setenv("APIP_AIW_SERVER_HTTPS_PORT", "8081") + t.Setenv("APIP_AIW_LOGGING_LEVEL", "debug") cfg, err := Load(quickstartConfig) if err != nil { @@ -89,10 +93,9 @@ func TestShippedConfig_MakeBffRunOverrides(t *testing.T) { } for _, tc := range []struct{ name, got, want string }{ - {"Addr", cfg.Addr, ":8081"}, - {"StaticDir", cfg.StaticDir, "../dist"}, - {"PlatformAPI.URL", cfg.PlatformAPI.URL, "https://localhost:9243"}, - {"LogLevel", cfg.LogLevel, "debug"}, + {"Addr", cfg.Addr(), ":8081"}, + {"ControlPlane.URL", cfg.ControlPlane.URL, "https://localhost:9243"}, + {"LogLevel", cfg.Logging.Level, "debug"}, } { if tc.got != tc.want { t.Errorf("%s = %q, want %q — `make bff-run` sets this variable, so configs/config.toml must carry the matching {{ env }} token", @@ -101,59 +104,63 @@ func TestShippedConfig_MakeBffRunOverrides(t *testing.T) { } } -// The [oidc] tokens in the shipped config are what let `make bff-run` — and a compose -// deployment — turn on OIDC from the environment alone, without editing the file. -func TestShippedConfig_OIDCFromEnvAlone(t *testing.T) { - t.Setenv("APIP_AIW_AUTH_MODE", "oidc") - t.Setenv("APIP_AIW_OIDC_AUTHORITY", "https://idp.example.com") - t.Setenv("APIP_AIW_OIDC_CLIENT_ID", "client-id") - t.Setenv("APIP_AIW_OIDC_CLIENT_SECRET", "s3cr3t") - t.Setenv("APIP_AIW_OIDC_REDIRECT_URL", "https://localhost:5380/api/auth/callback") +// configs/config.toml is deliberately minimal and ships without an +// [ai_workspace.auth.oidc] table — the quickstart only ever runs in basic-auth mode. +// configs/config-template.toml is the file a real deployment copies from, and its +// [ai_workspace.auth.oidc] holds plain-literal placeholder values (mode = "basic", +// an empty client_secret) rather than {{ env }} tokens, so OIDC is not settable from +// the environment alone — a real deployment copies the template to config.toml and +// edits [ai_workspace.auth] mode plus [ai_workspace.auth.oidc] directly. This +// exercises that edited shape still loads correctly and keeps OIDC credentials off +// the browser-safe runtime config. +func TestShippedConfig_TemplateEditedForOIDC(t *testing.T) { + templateBytes, err := os.ReadFile(templateConfig) + if err != nil { + t.Fatalf("read %s: %v", templateConfig, err) + } + edited := strings.NewReplacer( + `mode = "basic"`, `mode = "oidc"`, + `authority = "https://accounts.example.com"`, `authority = "https://idp.example.com"`, + `client_id = "your-client-id"`, `client_id = "client-id"`, + `client_secret = ""`, `client_secret = "s3cr3t"`, + ).Replace(string(templateBytes)) - cfg, err := Load(quickstartConfig) + path := filepath.Join(t.TempDir(), "config.toml") + if err := os.WriteFile(path, []byte(edited), 0o600); err != nil { + t.Fatalf("write edited template: %v", err) + } + + cfg, err := Load(path) if err != nil { - t.Fatalf("Load(configs/config.toml) error = %v — the [oidc] tokens must make OIDC settable from the environment", err) + t.Fatalf("Load(edited template) error = %v", err) } - if !cfg.OIDC.Enabled { - t.Fatal("OIDC.Enabled = false, want true — auth_mode = oidc must enable the client") + if !cfg.Auth.OIDC.Enabled { + t.Fatal("OIDC.Enabled = false, want true — mode = oidc must enable the client") } - if cfg.OIDC.ClientSecret != "s3cr3t" { - t.Errorf("OIDC.ClientSecret = %q, want the value from APIP_AIW_OIDC_CLIENT_SECRET", cfg.OIDC.ClientSecret) + if cfg.Auth.OIDC.ClientSecret != "s3cr3t" { + t.Errorf("OIDC.ClientSecret = %q, want %q", cfg.Auth.OIDC.ClientSecret, "s3cr3t") } // Whatever the mode, the client credentials stay server-side. - for _, key := range []string{"APIP_AIW_OIDC_CLIENT_SECRET", "APIP_AIW_OIDC_CLIENT_ID", "APIP_AIW_OIDC_AUTHORITY"} { + for _, key := range []string{"APIP_AIW_AUTH_OIDC_CLIENT_SECRET", "APIP_AIW_AUTH_OIDC_CLIENT_ID", "APIP_AIW_AUTH_OIDC_AUTHORITY"} { if _, ok := cfg.RuntimeConfig[key]; ok { t.Errorf("runtime config must not contain %s — the BFF owns the OIDC handshake", key) } } } -// config-template.toml is copied to config.toml by users, so it must stay loadable. -// Its client_secret token deliberately has no default — an unset variable fails startup -// rather than running OIDC with an empty credential — so the variable is set here. +// config-template.toml is copied to config.toml by users, so it must stay loadable +// as shipped — OIDC off, basic auth, an unset (empty) client_secret. func TestShippedConfig_TemplateLoads(t *testing.T) { - t.Setenv("APIP_AIW_OIDC_CLIENT_SECRET", "s3cr3t") - cfg, err := Load(templateConfig) if err != nil { t.Fatalf("Load(configs/config-template.toml) error = %v — the template must remain a loadable config", err) } - if cfg.AuthMode != "basic" { - t.Errorf("AuthMode = %q, want %q", cfg.AuthMode, "basic") + if cfg.Auth.Mode != "basic" { + t.Errorf("AuthMode = %q, want %q", cfg.Auth.Mode, "basic") } // Unlike the quickstart file, the template defaults to verifying the upstream // certificate — it is the starting point for a real deployment. - if cfg.PlatformAPI.TLSSkipVerify { - t.Error("PlatformAPI.TLSSkipVerify = true, want false — the template must default to a verified upstream") - } -} - -// The template's client_secret has no default on purpose: leaving the variable unset -// must abort startup, not resolve to an empty credential. -func TestShippedConfig_TemplateFailsClosedOnMissingSecret(t *testing.T) { - t.Setenv("APIP_AIW_OIDC_CLIENT_SECRET", "") - - if _, err := Load(templateConfig); err == nil { - t.Fatal("Load(configs/config-template.toml) succeeded with APIP_AIW_OIDC_CLIENT_SECRET unset, want an error — the token has no default and must fail closed") + if cfg.ControlPlane.TLSSkipVerify { + t.Error("ControlPlane.TLSSkipVerify = true, want false — the template must default to a verified upstream") } } diff --git a/portals/ai-workspace/bff/internal/config/toml.go b/portals/ai-workspace/bff/internal/config/toml.go deleted file mode 100644 index f27e97d3b8..0000000000 --- a/portals/ai-workspace/bff/internal/config/toml.go +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the - * License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package config - -import ( - "fmt" - "strconv" - "strings" - "unicode" - "unicode/utf8" -) - -// This file implements the TOML subset the AI Workspace config uses, on the -// standard library alone. The full grammar is deliberately not supported: the -// config format is scalars in (possibly nested) tables — see flatten — so the -// parser accepts exactly that and fails closed on everything else. Rejecting an -// unsupported construct with a line-numbered error preserves integrity: a config -// can never be silently misread, only refused. -// -// Accepted per line (after comment/whitespace handling): -// - [table] and [nested.table] headers with bare-key segments -// - key = 'literal string' (no escapes, as TOML defines) -// - key = "basic string" (TOML escape sequences) -// - key = true | false | integer | float -// -// Rejected: arrays, inline tables, arrays of tables [[x]], multi-line strings, -// dotted or quoted keys, dates/times, and duplicate keys or table redefinitions. - -// bareKeyValid reports whether s is a valid TOML bare key (A-Za-z0-9_-, non-empty). -func bareKeyValid(s string) bool { - if s == "" { - return false - } - for _, r := range s { - if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-') { - return false - } - } - return true -} - -// tomlParser carries the document state: the root tree, the table the current -// header points at, and the set of explicitly declared headers and assigned keys -// (both used only for duplicate detection). -type tomlParser struct { - root map[string]any - current map[string]any - headers map[string]bool // explicitly declared [table] paths - keys map[string]bool // fully-qualified assigned keys - prefix string // dotted path of the current table ("" at root) -} - -// parseTOMLSubset parses data and returns the nested tree parseTOML used to get -// from go-toml: tables as map[string]any, scalars as string / bool / int64 / float64. -func parseTOMLSubset(data []byte) (map[string]any, error) { - p := &tomlParser{ - root: map[string]any{}, - headers: map[string]bool{}, - keys: map[string]bool{}, - } - p.current = p.root - - for n, line := range strings.Split(string(data), "\n") { - if err := p.parseLine(strings.TrimSpace(strings.TrimSuffix(line, "\r"))); err != nil { - return nil, fmt.Errorf("line %d: %w", n+1, err) - } - } - return p.root, nil -} - -func (p *tomlParser) parseLine(line string) error { - if line == "" || strings.HasPrefix(line, "#") { - return nil - } - if strings.HasPrefix(line, "[") { - return p.parseHeader(line) - } - return p.parseKeyValue(line) -} - -// parseHeader handles [table] and [a.b] lines, creating intermediate tables. -func (p *tomlParser) parseHeader(line string) error { - if strings.HasPrefix(line, "[[") { - return fmt.Errorf("arrays of tables ([[...]]) are not supported") - } - end := strings.IndexByte(line, ']') - if end < 0 { - return fmt.Errorf("unterminated table header %q", line) - } - if rest := strings.TrimSpace(line[end+1:]); rest != "" && !strings.HasPrefix(rest, "#") { - return fmt.Errorf("unexpected content after table header: %q", rest) - } - - path := strings.TrimSpace(line[1:end]) - if p.headers[path] { - return fmt.Errorf("table [%s] is defined twice", path) - } - table := p.root - for _, seg := range strings.Split(path, ".") { - seg = strings.TrimSpace(seg) - if !bareKeyValid(seg) { - return fmt.Errorf("invalid table name segment %q in [%s]", seg, path) - } - switch child := table[seg].(type) { - case nil: - next := map[string]any{} - table[seg] = next - table = next - case map[string]any: - table = child - default: - return fmt.Errorf("[%s] conflicts with key %q which already holds a value", path, seg) - } - } - p.headers[path] = true - p.current = table - p.prefix = path - return nil -} - -// parseKeyValue handles a `key = value` line inside the current table. -func (p *tomlParser) parseKeyValue(line string) error { - eq := strings.IndexByte(line, '=') - if eq < 0 { - return fmt.Errorf("expected key = value, got %q", line) - } - key := strings.TrimSpace(line[:eq]) - if !bareKeyValid(key) { - return fmt.Errorf("invalid key %q (only bare keys are supported)", key) - } - - qualified := key - if p.prefix != "" { - qualified = p.prefix + "." + key - } - if p.keys[qualified] { - return fmt.Errorf("key %q is set twice", qualified) - } - if _, isTable := p.current[key].(map[string]any); isTable { - return fmt.Errorf("key %q conflicts with table [%s]", key, qualified) - } - - value, rest, err := parseValue(strings.TrimSpace(line[eq+1:])) - if err != nil { - return fmt.Errorf("value for key %q: %w", qualified, err) - } - if rest = strings.TrimSpace(rest); rest != "" && !strings.HasPrefix(rest, "#") { - return fmt.Errorf("unexpected content after value for key %q: %q", qualified, rest) - } - - p.keys[qualified] = true - p.current[key] = value - return nil -} - -// parseValue parses one scalar at the start of s and returns it with the unparsed -// remainder (trailing comments are the caller's to validate). -func parseValue(s string) (any, string, error) { - switch { - case s == "": - return nil, "", fmt.Errorf("missing value") - case strings.HasPrefix(s, "'''"), strings.HasPrefix(s, `"""`): - return nil, "", fmt.Errorf("multi-line strings are not supported") - case s[0] == '\'': - end := strings.IndexByte(s[1:], '\'') - if end < 0 { - return nil, "", fmt.Errorf("unterminated literal string") - } - return s[1 : 1+end], s[2+end:], nil - case s[0] == '"': - return parseBasicString(s) - case s[0] == '[': - return nil, "", fmt.Errorf("arrays are not supported") - case s[0] == '{': - return nil, "", fmt.Errorf("inline tables are not supported") - } - - // Bare scalar: the token runs to the first whitespace or comment. - token := s - if i := strings.IndexAny(s, " \t#"); i >= 0 { - token, s = s[:i], s[i:] - } else { - s = "" - } - switch token { - case "true": - return true, s, nil - case "false": - return false, s, nil - } - // TOML permits underscore separators between digits (1_000). - numeric := strings.ReplaceAll(token, "_", "") - if i, err := strconv.ParseInt(numeric, 10, 64); err == nil { - return i, s, nil - } - if f, err := strconv.ParseFloat(numeric, 64); err == nil { - return f, s, nil - } - return nil, "", fmt.Errorf("unsupported value %q (expected a string, boolean, or number)", token) -} - -// parseBasicString decodes a double-quoted TOML basic string with its escape -// sequences, returning the value and the remainder after the closing quote. -func parseBasicString(s string) (string, string, error) { - var b strings.Builder - i := 1 // past the opening quote - for i < len(s) { - c := s[i] - switch c { - case '"': - return b.String(), s[i+1:], nil - case '\\': - if i+1 >= len(s) { - return "", "", fmt.Errorf("unterminated escape sequence") - } - i++ - switch e := s[i]; e { - case 'b': - b.WriteByte('\b') - case 't': - b.WriteByte('\t') - case 'n': - b.WriteByte('\n') - case 'f': - b.WriteByte('\f') - case 'r': - b.WriteByte('\r') - case '"': - b.WriteByte('"') - case '\\': - b.WriteByte('\\') - case 'u', 'U': - width := 4 - if e == 'U' { - width = 8 - } - if i+width >= len(s) { - return "", "", fmt.Errorf(`truncated \%c escape`, e) - } - code, err := strconv.ParseUint(s[i+1:i+1+width], 16, 32) - if err != nil { - return "", "", fmt.Errorf(`invalid \%c escape: %v`, e, err) - } - r := rune(code) - if !utf8.ValidRune(r) || unicode.Is(unicode.Cs, r) { - return "", "", fmt.Errorf(`\%c escape is not a valid Unicode scalar value`, e) - } - b.WriteRune(r) - i += width - default: - return "", "", fmt.Errorf(`invalid escape sequence \%c`, e) - } - default: - b.WriteByte(c) - } - i++ - } - return "", "", fmt.Errorf("unterminated basic string") -} diff --git a/portals/ai-workspace/bff/internal/config/toml_test.go b/portals/ai-workspace/bff/internal/config/toml_test.go deleted file mode 100644 index e5017c18f7..0000000000 --- a/portals/ai-workspace/bff/internal/config/toml_test.go +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the - * License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package config - -import ( - "reflect" - "strings" - "testing" -) - -// The parser accepts the full shape the shipped configs use: root keys, nested -// tables, both string quoting styles, scalars, comments, and CRLF endings. -func TestParseTOMLSubset(t *testing.T) { - doc := strings.ReplaceAll(`# top comment -domain = "app.example.com" # trailing comment -debug = true -retries = 1_000 -ratio = 0.5 - -[platform_api] -url = 'https://api.example.com' - -[oidc] -client_secret = '{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}' -greeting = "line1\nline2 \"quoted\" é" - -[oidc.claim_mappings] -organization_claim_name = "org_id" -`, "\n", "\r\n") - - got, err := parseTOMLSubset([]byte(doc)) - if err != nil { - t.Fatalf("parseTOMLSubset() error = %v", err) - } - want := map[string]any{ - "domain": "app.example.com", - "debug": true, - "retries": int64(1000), - "ratio": 0.5, - "platform_api": map[string]any{ - "url": "https://api.example.com", - }, - "oidc": map[string]any{ - "client_secret": `{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}`, - "greeting": "line1\nline2 \"quoted\" é", - "claim_mappings": map[string]any{ - "organization_claim_name": "org_id", - }, - }, - } - if !reflect.DeepEqual(got, want) { - t.Errorf("parseTOMLSubset() = %#v, want %#v", got, want) - } -} - -// A parent table may be declared after its child ([a.b] then [a]) — TOML allows -// it, and the template's table ordering must not become load-order sensitive. -func TestParseTOMLSubsetParentAfterChild(t *testing.T) { - got, err := parseTOMLSubset([]byte("[a.b]\nx = 1\n[a]\ny = 2\n")) - if err != nil { - t.Fatalf("parseTOMLSubset() error = %v", err) - } - want := map[string]any{"a": map[string]any{"b": map[string]any{"x": int64(1)}, "y": int64(2)}} - if !reflect.DeepEqual(got, want) { - t.Errorf("parseTOMLSubset() = %#v, want %#v", got, want) - } -} - -// Every unsupported or malformed construct must be an error, never a silent -// misread — the parser's whole safety argument is that it fails closed. -func TestParseTOMLSubsetRejects(t *testing.T) { - cases := map[string]string{ - "array value": `skip_paths = ["/health"]`, - "inline table": `oidc = { client_id = "x" }`, - "array of tables": "[[servers]]\nname = \"a\"", - "multiline basic": "s = \"\"\"\nx\n\"\"\"", - "multiline literal": "s = '''\nx\n'''", - "dotted key": `oidc.client_id = "x"`, - "quoted key": `"my key" = "x"`, - "duplicate key": "a = 1\na = 2", - "duplicate table": "[t]\n[t]", - "key vs table": "[oidc.claim_mappings]\nx = 1\n[oidc]\nclaim_mappings = 1", - "table vs key": "oidc = 1\n[oidc]", - "unterminated string": `s = "abc`, - "unterminated literal": `s = 'abc`, - "unterminated header": "[oidc", - "bad escape": `s = "a\qb"`, - "bad unicode escape": `s = "\uZZZZ"`, - "surrogate escape": `s = "\uD800"`, - "bare garbage value": "a = maybe", - "trailing garbage": `a = "x" y`, - "missing value": "a =", - "no equals": "just a line", - "datetime": "a = 2026-07-13T00:00:00Z", - } - for name, doc := range cases { - if _, err := parseTOMLSubset([]byte(doc)); err == nil { - t.Errorf("%s: parseTOMLSubset(%q) succeeded, want error", name, doc) - } - } -} diff --git a/portals/ai-workspace/bff/internal/proxy/reverse_proxy.go b/portals/ai-workspace/bff/internal/proxy/reverse_proxy.go index a7aebe6529..0893db9333 100644 --- a/portals/ai-workspace/bff/internal/proxy/reverse_proxy.go +++ b/portals/ai-workspace/bff/internal/proxy/reverse_proxy.go @@ -46,7 +46,7 @@ func tokenFromContext(r *http.Request) string { } // ReverseProxy builds an httputil.ReverseProxy targeting the Platform API. -// prefix (e.g. "/api/proxy") is stripped from the path before forwarding. +// prefix (e.g. "/proxy") is stripped from the path before forwarding. func ReverseProxy(target *url.URL, prefix string, transport http.RoundTripper) *httputil.ReverseProxy { rp := &httputil.ReverseProxy{ Transport: transport, diff --git a/portals/ai-workspace/bff/internal/proxy/reverse_proxy_test.go b/portals/ai-workspace/bff/internal/proxy/reverse_proxy_test.go index 4202b5ef45..574db23c2b 100644 --- a/portals/ai-workspace/bff/internal/proxy/reverse_proxy_test.go +++ b/portals/ai-workspace/bff/internal/proxy/reverse_proxy_test.go @@ -39,9 +39,9 @@ func TestReverseProxy_InjectsBearerStripsCookieAndPrefix(t *testing.T) { defer backend.Close() target, _ := url.Parse(backend.URL) - rp := ReverseProxy(target, "/api/proxy", backend.Client().Transport) + rp := ReverseProxy(target, "/proxy", backend.Client().Transport) - req := httptest.NewRequest(http.MethodGet, "/api/proxy/api/v0.9/projects", nil) + req := httptest.NewRequest(http.MethodGet, "/proxy/api/v0.9/projects", nil) req.Header.Set("Cookie", "_ai_workspace_session=secret-session-id") req = WithToken(req, "injected-bearer-token") @@ -73,9 +73,9 @@ func TestReverseProxy_NoTokenNoAuthHeader(t *testing.T) { defer backend.Close() target, _ := url.Parse(backend.URL) - rp := ReverseProxy(target, "/api/proxy", backend.Client().Transport) + rp := ReverseProxy(target, "/proxy", backend.Client().Transport) - req := httptest.NewRequest(http.MethodGet, "/api/proxy/health", nil) + req := httptest.NewRequest(http.MethodGet, "/proxy/health", nil) req.Header.Set("Authorization", "Bearer should-be-removed") rec := httptest.NewRecorder() rp.ServeHTTP(rec, req) diff --git a/portals/ai-workspace/bff/internal/proxy/transport.go b/portals/ai-workspace/bff/internal/proxy/transport.go index 806413302d..ba3c7cc353 100644 --- a/portals/ai-workspace/bff/internal/proxy/transport.go +++ b/portals/ai-workspace/bff/internal/proxy/transport.go @@ -75,14 +75,14 @@ func NewTransport(opts TLSClientOptions) (*http.Transport, error) { func caPool(path string) (*x509.CertPool, error) { pem, err := os.ReadFile(path) if err != nil { - return nil, fmt.Errorf("read platform_api_ca_file %q: %w", path, err) + return nil, fmt.Errorf("read control_plane_ca_file %q: %w", path, err) } pool, err := x509.SystemCertPool() if err != nil || pool == nil { pool = x509.NewCertPool() } if !pool.AppendCertsFromPEM(pem) { - return nil, fmt.Errorf("no valid certificates in platform_api_ca_file %q", path) + return nil, fmt.Errorf("no valid certificates in control_plane_ca_file %q", path) } return pool, nil } diff --git a/portals/ai-workspace/bff/internal/server/composite_handlers.go b/portals/ai-workspace/bff/internal/server/composite_handlers.go index f9f642c311..b354cd9bb8 100644 --- a/portals/ai-workspace/bff/internal/server/composite_handlers.go +++ b/portals/ai-workspace/bff/internal/server/composite_handlers.go @@ -67,7 +67,7 @@ func extractSecretHandles(body []byte) []string { // platformDo performs a single authenticated request against the Platform API, // returning the raw response. The caller is responsible for closing the body. func (s *Server) platformDo(ctx context.Context, jwt, method, path string, header http.Header, body []byte) (*http.Response, error) { - url := strings.TrimRight(s.cfg.PlatformAPI.URL, "/") + path + url := strings.TrimRight(s.cfg.ControlPlane.URL, "/") + path var reqBody io.Reader if len(body) > 0 { reqBody = bytes.NewReader(body) diff --git a/portals/ai-workspace/bff/internal/server/composite_handlers_test.go b/portals/ai-workspace/bff/internal/server/composite_handlers_test.go index 9719b1665e..c2f11d3d3b 100644 --- a/portals/ai-workspace/bff/internal/server/composite_handlers_test.go +++ b/portals/ai-workspace/bff/internal/server/composite_handlers_test.go @@ -101,15 +101,13 @@ func buildTestServer(t *testing.T, platformURL, jwt string) (*Server, *httptest. } cfg := &config.Config{ - PlatformAPI: config.PlatformAPIConfig{URL: platformURL}, - ProxyPrefix: "/api/proxy", - Cookie: config.CookieConfig{Name: "_ai_workspace_session"}, - CSRFHeader: "X-Requested-By", + ControlPlane: config.ControlPlaneConfig{URL: platformURL, ProxyPrefix: "/proxy"}, + Cookie: config.CookieConfig{Name: "_ai_workspace_session"}, } s := &Server{ cfg: cfg, - proxy: proxy.ReverseProxy(mustParseURL(platformURL), cfg.ProxyPrefix, transport), + proxy: proxy.ReverseProxy(mustParseURL(platformURL), cfg.ControlPlane.ProxyPrefix, transport), refreshLocks: make(map[string]*refreshLock), } @@ -140,7 +138,7 @@ type recordedRequest struct { auth string } -func fakePlatformAPI(t *testing.T, responses map[string]struct { +func fakeControlPlane(t *testing.T, responses map[string]struct { status int body string }) (*httptest.Server, *[]recordedRequest) { @@ -168,7 +166,7 @@ func fakePlatformAPI(t *testing.T, responses map[string]struct { } func TestHandleCreateWithSecretCompensation_Success(t *testing.T) { - platform, calls := fakePlatformAPI(t, map[string]struct { + platform, calls := fakeControlPlane(t, map[string]struct { status int body string }{ @@ -239,7 +237,7 @@ func TestHandleCreateWithSecretCompensation_ProviderFailTriggersDelete(t *testin } func TestHandleCreateWithSecretCompensation_NoSecretNoDelete(t *testing.T) { - platform, calls := fakePlatformAPI(t, map[string]struct { + platform, calls := fakeControlPlane(t, map[string]struct { status int body string }{ @@ -269,11 +267,10 @@ func TestHandleCreateWithSecretCompensation_NoSecretNoDelete(t *testing.T) { } func TestHandleCreateWithSecretCompensation_Unauthenticated(t *testing.T) { - platform, _ := fakePlatformAPI(t, nil) + platform, _ := fakeControlPlane(t, nil) cfg := &config.Config{ - PlatformAPI: config.PlatformAPIConfig{URL: platform.URL}, - ProxyPrefix: "/api/proxy", - Cookie: config.CookieConfig{Name: "_ai_workspace_session"}, + ControlPlane: config.ControlPlaneConfig{URL: platform.URL, ProxyPrefix: "/proxy"}, + Cookie: config.CookieConfig{Name: "_ai_workspace_session"}, } transport, err := proxy.NewTransport(proxy.TLSClientOptions{SkipVerify: true}) if err != nil { @@ -281,7 +278,7 @@ func TestHandleCreateWithSecretCompensation_Unauthenticated(t *testing.T) { } s := &Server{ cfg: cfg, - proxy: proxy.ReverseProxy(mustParseURL(platform.URL), cfg.ProxyPrefix, transport), + proxy: proxy.ReverseProxy(mustParseURL(platform.URL), cfg.ControlPlane.ProxyPrefix, transport), refreshLocks: make(map[string]*refreshLock), } diff --git a/portals/ai-workspace/bff/internal/server/handlers.go b/portals/ai-workspace/bff/internal/server/handlers.go index abf3fa708f..a54bf73438 100644 --- a/portals/ai-workspace/bff/internal/server/handlers.go +++ b/portals/ai-workspace/bff/internal/server/handlers.go @@ -178,7 +178,7 @@ func (s *Server) handleOIDCCallback(w http.ResponseWriter, r *http.Request) { // Reverse proxy // --------------------------------------------------------------------------- -// handleProxy (/api/proxy/*) — take the JWT straight from the cookie and forward +// handleProxy (/proxy/*) — take the JWT straight from the cookie and forward // it upstream. No server-side lookup is involved unless the token is an OIDC // access token that is near expiry and must be refreshed. func (s *Server) handleProxy(w http.ResponseWriter, r *http.Request) { @@ -255,7 +255,7 @@ func (s *Server) userFromToken(ctx context.Context, jwt string) session.User { } return s.oidc.UserFromAccessToken(jwt) } - return session.UserFromClaims(session.DecodeJWTClaims(jwt), nil, session.DefaultClaimMapping()) + return session.UserFromClaims(session.DecodeJWTClaims(jwt), nil, s.claims) } // putRefreshState stores the OIDC refresh/id tokens keyed by the access JWT so diff --git a/portals/ai-workspace/bff/internal/server/middleware.go b/portals/ai-workspace/bff/internal/server/middleware.go index 0c5e7376d5..878ed46957 100644 --- a/portals/ai-workspace/bff/internal/server/middleware.go +++ b/portals/ai-workspace/bff/internal/server/middleware.go @@ -22,6 +22,8 @@ import ( "log/slog" "net/http" "time" + + "ai-workspace-bff/internal/config" ) // chain applies middlewares in order (outermost first). @@ -81,7 +83,7 @@ func (s *Server) requireCSRF(next http.Handler) http.Handler { case http.MethodGet, http.MethodHead, http.MethodOptions: // Safe methods: no CSRF token required. default: - if r.Header.Get(s.cfg.CSRFHeader) == "" { + if r.Header.Get(config.CSRFHeaderName) == "" { writeErrorJSON(w, http.StatusForbidden, "MISSING_CSRF_HEADER", "missing CSRF header") return } diff --git a/portals/ai-workspace/bff/internal/server/middleware_test.go b/portals/ai-workspace/bff/internal/server/middleware_test.go index c442729c4c..190bfe210e 100644 --- a/portals/ai-workspace/bff/internal/server/middleware_test.go +++ b/portals/ai-workspace/bff/internal/server/middleware_test.go @@ -25,7 +25,7 @@ import ( ) func csrfTestServer() *Server { - return &Server{cfg: &config.Config{CSRFHeader: "X-Requested-By"}} + return &Server{cfg: &config.Config{}} } func TestRequireCSRF(t *testing.T) { @@ -49,7 +49,7 @@ func TestRequireCSRF(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - req := httptest.NewRequest(tc.method, "/api/proxy/x", nil) + req := httptest.NewRequest(tc.method, "/proxy/x", nil) if tc.header { req.Header.Set("X-Requested-By", "ai-workspace") } diff --git a/portals/ai-workspace/bff/internal/server/routes.go b/portals/ai-workspace/bff/internal/server/routes.go index a86884bf8f..c9c25e14bb 100644 --- a/portals/ai-workspace/bff/internal/server/routes.go +++ b/portals/ai-workspace/bff/internal/server/routes.go @@ -43,10 +43,10 @@ func (s *Server) routes() http.Handler { // Same-origin reverse proxy to the Platform API. The proxy's Rewrite hook // strips the prefix before forwarding, so we register the subtree directly. - mux.HandleFunc(s.cfg.ProxyPrefix+"/", s.handleProxy) + mux.HandleFunc(s.cfg.ControlPlane.ProxyPrefix+"/", s.handleProxy) // SPA static files + client-side routing fallback (must be last). - mux.Handle("/", spaHandler(s.cfg.StaticDir)) + mux.Handle("/", spaHandler(s.cfg.Server.StaticDir)) return chain(mux, recoverPanic, diff --git a/portals/ai-workspace/bff/internal/server/server.go b/portals/ai-workspace/bff/internal/server/server.go index 3ee6fdf565..3c97d88c53 100644 --- a/portals/ai-workspace/bff/internal/server/server.go +++ b/portals/ai-workspace/bff/internal/server/server.go @@ -46,6 +46,7 @@ type refreshLock struct { // Server holds the BFF dependencies and HTTP handler. type Server struct { cfg *config.Config + claims session.ClaimMapping store session.Store fileBased *auth.FileBased oidc *auth.OIDC @@ -60,36 +61,42 @@ type Server struct { // session store, the file-based authenticator, and (when enabled) the OIDC // authenticator — discovering the IDP endpoints up front. func New(ctx context.Context, cfg *config.Config) (*Server, error) { - target, err := url.Parse(cfg.PlatformAPI.URL) + target, err := url.Parse(cfg.ControlPlane.URL) if err != nil { return nil, err } transport, err := proxy.NewTransport(proxy.TLSClientOptions{ - CAFile: cfg.PlatformAPI.CAFile, - SkipVerify: cfg.PlatformAPI.TLSSkipVerify, + CAFile: cfg.ControlPlane.CAFile, + SkipVerify: cfg.ControlPlane.TLSSkipVerify, }) if err != nil { return nil, err } upstream := &http.Client{Transport: transport, Timeout: 60 * time.Second} + // Shared by both auth modes: OIDC tokens from the configured IDP, and the HMAC + // JWTs the Platform API's file-based login endpoint signs with the same mapped + // claim names. Building it once keeps the two readers from drifting apart. + claims := buildClaimMapping(cfg.Auth.ClaimMappings) + s := &Server{ cfg: cfg, - fileBased: auth.NewFileBased(upstream, cfg.PlatformAPI.URL, cfg.PlatformAPI.LoginPath, cfg.Session.AbsoluteTTL), - proxy: proxy.ReverseProxy(target, cfg.ProxyPrefix, transport), + claims: claims, + fileBased: auth.NewFileBased(upstream, cfg.ControlPlane.URL, cfg.ControlPlane.PortalBasePath, cfg.Session.AbsoluteTTL, claims), + proxy: proxy.ReverseProxy(target, cfg.ControlPlane.ProxyPrefix, transport), refreshLocks: make(map[string]*refreshLock), } - if cfg.OIDC.Enabled { + if cfg.Auth.OIDC.Enabled { // The session store exists only to hold OIDC refresh/id tokens for renewal. // File-based sessions are fully self-contained in the cookie JWT. s.store = session.NewMemoryStore() o, err := auth.NewOIDC( ctx, upstream, - cfg.OIDC.Issuer, cfg.OIDC.ClientID, cfg.OIDC.ClientSecret, - cfg.OIDC.RedirectURL, cfg.OIDC.PostLogoutRedirectURL, cfg.OIDC.Scopes, - oidcClaimMapping(cfg.OIDC.Claims), cfg.Session.AbsoluteTTL, + cfg.Auth.OIDC.Issuer, cfg.Auth.OIDC.ClientID, cfg.Auth.OIDC.ClientSecret, + cfg.Auth.OIDC.RedirectURL, cfg.Auth.OIDC.PostLogoutRedirectURL, cfg.Auth.OIDC.Scopes, + claims, cfg.Session.AbsoluteTTL, ) if err != nil { return nil, err @@ -116,11 +123,11 @@ func (s *Server) Close() error { return nil } -// oidcClaimMapping builds the claim mapping for OIDC tokens from config. Each -// field overrides the session-package default only when set, so an operator can -// point a single claim (e.g. the display name) at the right key via the -// OIDC_CLAIM_* env vars without re-specifying the rest. -func oidcClaimMapping(c config.ClaimMappingConfig) session.ClaimMapping { +// buildClaimMapping builds the claim mapping shared by both auth modes from +// config. Each field overrides the session-package default only when set, so an +// operator can point a single claim (e.g. the display name) at the right key via +// the CLAIM_MAPPINGS_* env vars without re-specifying the rest. +func buildClaimMapping(c config.ClaimMappingConfig) session.ClaimMapping { m := session.DefaultClaimMapping() if c.Username != "" { m.Username = c.Username @@ -128,8 +135,8 @@ func oidcClaimMapping(c config.ClaimMappingConfig) session.ClaimMapping { if c.Email != "" { m.Email = c.Email } - if c.Role != "" { - m.Role = c.Role + if c.Roles != "" { + m.Roles = c.Roles } if c.Scope != "" { m.Scope = c.Scope diff --git a/portals/ai-workspace/bff/internal/session/claims.go b/portals/ai-workspace/bff/internal/session/claims.go index b3d439ffb4..05dd4d3832 100644 --- a/portals/ai-workspace/bff/internal/session/claims.go +++ b/portals/ai-workspace/bff/internal/session/claims.go @@ -33,21 +33,21 @@ import ( type ClaimMapping struct { Username string Email string - Role string + Roles string Scope string OrgID string OrgName string OrgHandle string } -// DefaultClaimMapping returns the mapping used for file-based tokens (and as the -// fallback for OIDC). For OIDC, callers may override the org/user keys to match -// the IDP. +// DefaultClaimMapping returns the built-in fallback mapping, used whenever a +// config.ClaimMappingConfig field is left unset — for both file-based and OIDC +// tokens. Callers may override individual keys to match a specific IDP. func DefaultClaimMapping() ClaimMapping { return ClaimMapping{ Username: "username", Email: "email", - Role: "platform_role", + Roles: "roles", Scope: "scope", OrgID: "organization", OrgName: "org_name", @@ -119,7 +119,7 @@ func UserFromClaims(claims, idClaims map[string]any, m ClaimMapping) User { u := User{ Name: first(get(m.Username), get(m.Email), get("sub")), Email: get(m.Email), - Role: strClaim(claims, m.Role), + Role: strClaim(claims, m.Roles), Scopes: scopes(claims, m.Scope), } diff --git a/portals/ai-workspace/bff/main.go b/portals/ai-workspace/bff/main.go index d32fc53584..4140355b7e 100644 --- a/portals/ai-workspace/bff/main.go +++ b/portals/ai-workspace/bff/main.go @@ -91,6 +91,12 @@ func main() { // The container reads its mounted config.toml, so -config is only needed to run // the BFF outside one (see `make bff-run`). configFile := flag.String("config", config.DefaultConfigFile, "path to config.toml") + // The shipped config.toml has no {{ env }} token for static_dir — the container + // always serves the SPA baked in at /app, so there's nothing for an operator to + // override. `make bff-run` is the one caller that needs a different directory + // (the locally-built ../dist), so it's a dev-only CLI flag rather than a config + // key, keeping the shipped config free of a token no deployment ever sets. + staticDir := flag.String("static-dir", "", "override the directory serving the built SPA (local dev only, e.g. `make bff-run`)") flag.Parse() cfg, err := config.Load(*configFile) @@ -99,7 +105,10 @@ func main() { slog.Error("configuration error", "err", err) os.Exit(1) } - slog.SetDefault(logger.NewLogger(logger.Config{Level: cfg.LogLevel, Format: cfg.LogFormat})) + if *staticDir != "" { + cfg.Server.StaticDir = *staticDir + } + slog.SetDefault(logger.NewLogger(logger.Config{Level: cfg.Logging.Level, Format: cfg.Logging.Format})) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -112,7 +121,7 @@ func main() { defer srv.Close() httpServer := &http.Server{ - Addr: cfg.Addr, + Addr: cfg.Addr(), Handler: srv.Handler(), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 60 * time.Second, @@ -120,7 +129,7 @@ func main() { IdleTimeout: 120 * time.Second, } - tlsConfig, err := buildTLS(cfg.TLS) + tlsConfig, err := buildTLS(cfg.Server) if err != nil { slog.Error("failed to set up TLS", "err", err) os.Exit(1) @@ -128,13 +137,13 @@ func main() { httpServer.TLSConfig = tlsConfig go func() { - url := portalURL(cfg.Addr, tlsConfig != nil) + url := portalURL(cfg.Addr(), tlsConfig != nil) slog.Info("AI Workspace BFF started", - "addr", cfg.Addr, + "addr", cfg.Addr(), "url", url, - "auth_mode", cfg.AuthMode, - "platform_api", cfg.PlatformAPI.URL, - "oidc_enabled", cfg.OIDC.Enabled, + "auth_mode", cfg.Auth.Mode, + "control_plane", cfg.ControlPlane.URL, + "oidc_enabled", cfg.Auth.OIDC.Enabled, ) printStartedMarker(url) var serveErr error @@ -165,10 +174,10 @@ func main() { // disabled no certificate is read or required. Otherwise a mounted cert/key pair // is required — there is no self-signed fallback; use the quickstart setup // script (or your own tooling) to generate a pair and mount it. -func buildTLS(c config.TLSConfig) (*tls.Config, error) { - if !c.TerminateTLS { +func buildTLS(c config.ServerConfig) (*tls.Config, error) { + if !c.Enabled { // Plain HTTP is only safe when something upstream terminates TLS. - slog.Warn("TLS: disabled ([tls] enabled = false) — serving plain HTTP. " + + slog.Warn("TLS: disabled ([server] enabled = false) — serving plain HTTP. " + "Terminate TLS at an ingress or service-mesh sidecar and " + "never expose this listener directly to untrusted networks.") return nil, nil @@ -180,8 +189,8 @@ func buildTLS(c config.TLSConfig) (*tls.Config, error) { } if !fileExists(c.CertFile) { return nil, fmt.Errorf("TLS is enabled but no certificate is mounted: "+ - "set [tls] cert_file (%q) and key_file (%q) to existing files, "+ - "or set [tls] enabled = false to serve plain HTTP behind a TLS-terminating proxy", c.CertFile, c.KeyFile) + "set [server] cert_file (%q) and key_file (%q) to existing files, "+ + "or set [server] enabled = false to serve plain HTTP behind a TLS-terminating proxy", c.CertFile, c.KeyFile) } cert, err := tlsutil.CertFromFiles(c.CertFile, c.KeyFile) if err != nil { diff --git a/portals/ai-workspace/configs/config-platform-api.toml b/portals/ai-workspace/configs/config-platform-api.toml deleted file mode 100644 index 449e8dd3f5..0000000000 --- a/portals/ai-workspace/configs/config-platform-api.toml +++ /dev/null @@ -1,46 +0,0 @@ -# -------------------------------------------------------------------- -# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). -# -# WSO2 LLC. licenses this file to you under the Apache License, -# Version 2.0 (the "License"); you may not use this file except -# in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -------------------------------------------------------------------- -# Platform API configuration — quickstart (file-based auth). See README.md. - -log_level = '{{ env "APIP_CP_LOG_LEVEL" "INFO" }}' # DEBUG | INFO | WARN | ERROR -enable_scope_validation = '{{ env "APIP_CP_ENABLE_SCOPE_VALIDATION" "true" }}' - -encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' # encryption_key = '{{ file "/secrets/platform-api/encryption_key" }}' - -[https] -enabled = '{{ env "APIP_CP_HTTPS_ENABLED" "true" }}' -port = '{{ env "APIP_CP_HTTPS_PORT" "9243" }}' -cert_dir = "/etc/platform-api/tls" - -[database] -driver = '{{ env "APIP_CP_DATABASE_DRIVER" "sqlite3" }}' # "sqlite3" or "postgres" -path = "/app/data/api_platform.db" # SQLite file path (ignored for postgres) - - -[auth.jwt] -enabled = '{{ env "APIP_CP_AUTH_JWT_ENABLED" "true" }}' -issuer = '{{ env "APIP_CP_AUTH_JWT_ISSUER" "platform-api" }}' -secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' # secret_key = '{{ file "/secrets/platform-api/jwt_secret" }}' -skip_validation = '{{ env "APIP_CP_AUTH_JWT_SKIP_VALIDATION" "false" }}' # verify JWT signature - -[auth.idp] -enabled = '{{ env "APIP_CP_AUTH_IDP_ENABLED" "false" }}' - -[auth.file_based] -enabled = '{{ env "APIP_CP_AUTH_FILE_BASED_ENABLED" "true" }}' - -[auth.file_based.organization] -id = '{{ env "APIP_CP_AUTH_FILE_BASED_ORGANIZATION_ID" "default" }}' -display_name = '{{ env "APIP_CP_AUTH_FILE_BASED_ORGANIZATION_DISPLAY_NAME" "Default" }}' -region = '{{ env "APIP_CP_AUTH_FILE_BASED_ORGANIZATION_REGION" "us" }}' - -[[auth.file_based.users]] -username = '{{ env "APIP_CP_ADMIN_USERNAME" }}' -password_hash = '{{ env "APIP_CP_ADMIN_PASSWORD_HASH" }}' -scopes = '{{ env "APIP_CP_ADMIN_SCOPES" "ap:organization:manage ap:gateway:manage ap:gateway_custom_policy:manage ap:rest_api:manage ap:llm_provider:manage ap:llm_proxy:manage ap:mcp_proxy:manage ap:application:manage ap:subscription:manage ap:subscription_plan:manage ap:project:manage ap:llm_template:manage ap:devportal:manage ap:api_key:read ap:secret:manage" }}' diff --git a/portals/ai-workspace/configs/config-template.toml b/portals/ai-workspace/configs/config-template.toml index ff4d8470f7..433caffc04 100644 --- a/portals/ai-workspace/configs/config-template.toml +++ b/portals/ai-workspace/configs/config-template.toml @@ -9,9 +9,12 @@ # # AI Workspace configuration template # -# Copy this file to config.toml and edit the values for your deployment. -# This file is read by the AI Workspace container at startup, and is the only source of -# configuration. +# Copy this file to config.toml and edit the values for your deployment. It is the +# only source of configuration, read by the AI Workspace container at startup. +# +# All settings live under [ai_workspace] / [ai_workspace.*], mirroring the Platform +# API's own [platform_api] / [platform_api.*] tables so one config.toml can hold both +# services side by side without their tables colliding. # # HOW VALUES ARE RESOLVED # Each value is an interpolation token, resolved once at startup: @@ -19,51 +22,41 @@ # key = '{{ env "APIP_AIW_KEY" "default" }}' # └── environment variable └── value used when the variable is unset # -# So a key written this way can be set by its environment variable — no edit to this -# file required. The token is what reads the environment: there is no implicit override, -# so a key written as a plain literal (key = "value"), or left commented out below, -# ignores the variable entirely. Uncomment the key first to make it settable that way. -# -# By convention the variable is the key's table path, uppercased, dots as underscores, -# prefixed with APIP_AIW_: +# The token is what reads the environment — a plain literal or a commented-out key +# ignores the variable entirely. By convention the variable name is the key's table +# path under [ai_workspace], uppercased with underscores, prefixed APIP_AIW_: # -# [platform_api] url -> APIP_AIW_PLATFORM_API_URL -# [oidc] client_id -> APIP_AIW_OIDC_CLIENT_ID -# domain (no table) -> APIP_AIW_DOMAIN +# [ai_workspace] domain -> APIP_AIW_DOMAIN +# [ai_workspace.server.https] port -> APIP_AIW_SERVER_HTTPS_PORT +# [ai_workspace.auth.oidc] client_id -> APIP_AIW_AUTH_OIDC_CLIENT_ID # -# To source a value from a mounted file instead of the environment — the right -# choice for secrets — swap the token: +# For secrets, source from a mounted file instead of the environment: # # key = '{{ file "/secrets/ai-workspace/key" }}' # -# A {{ file }} token is required and fails closed: if the file is missing, or lives -# outside the allowed source directories (/etc/ai-workspace and /secrets/ai-workspace -# by default; override with APIP_CONFIG_FILE_SOURCE_ALLOWLIST), the server refuses to -# start rather than run with an empty value. An {{ env }} token with no default -# behaves the same way. +# Both {{ file }} and a required {{ env }} (no default) fail closed at startup — a +# missing file, a file outside the allowed source directories (/etc/ai-workspace and +# /secrets/ai-workspace; override with APIP_CONFIG_FILE_SOURCE_ALLOWLIST), or an unset +# required variable all refuse to start rather than run with an empty value. # -# Never write a secret as a raw literal in this file, and never hardcode one in -# docker-compose.yaml. +# Never write a secret as a raw literal here or hardcode one in docker-compose.yaml. # # KEY ORDER -# Within every table, keys are ordered so the ones that decide whether the rest apply -# come first: -# -# 1. the gate — enabled, or the mode that selects this table (auth_mode) -# 2. required identity/connection keys (url, authority, client_id, client_secret) -# 3. optional behaviour, in the order you would tune it -# 4. TLS/trust and other hardening keys last -# +# Within each table: 1) the gate (enabled/mode), 2) required identity/connection keys +# (url, authority, client_id, client_secret), 3) optional behaviour, 4) TLS/trust last. # Keep new keys in that order rather than appending to the end of a table. # # QUICK START (basic-auth mode): # 1. Copy this file to config.toml. -# 2. Set auth_mode = "basic" (uses the users defined in config-platform-api.toml). +# 2. Set [ai_workspace.auth] mode = "basic" (uses the users defined in +# [platform_api.auth.file.users] in the same config.toml). # 3. Run: docker compose up # -# OIDC mode: set auth_mode = "oidc" and fill in the [oidc] table below. +# OIDC mode: set [ai_workspace.auth] mode = "oidc" and fill in [ai_workspace.auth.oidc]. # -------------------------------------------------------------------- +[ai_workspace] + # ==================================================================== # Deployment identity # @@ -71,206 +64,189 @@ # window.__RUNTIME_CONFIG__. Never put a credential among them. # ==================================================================== -# Authentication mode for the UI: "basic" (local users) or "oidc" (external IDP). -# Choosing "oidc" enables the [oidc] table below. -auth_mode = '{{ env "APIP_AIW_AUTH_MODE" "basic" }}' - -# Domain shown in the browser address bar (host:port, or just host for port 80/443). -domain = '{{ env "APIP_AIW_DOMAIN" "localhost:5380" }}' - -# Control-plane host — the host:port that deployed gateways use to reach the Platform -# API. Shown in gateway setup instructions (keys.env, helm values). Must be the -# externally reachable address, not a relative/proxy path. -controlplane_host = '{{ env "APIP_AIW_CONTROLPLANE_HOST" "localhost:9243" }}' - # Default region assigned to new organizations on first login. -default_org_region = '{{ env "APIP_AIW_DEFAULT_ORG_REGION" "us" }}' - -# Gateway versions offered in the create-gateway version selector (JSON array string). -# Each entry: version (helm chart minor), latestVersion (image/chart tag shown to the -# user and used in the helm --version flag), channel ("STS" | "LTS"). The first entry -# is the default selection. -platform_gateway_versions = '{{ env "APIP_AIW_PLATFORM_GATEWAY_VERSIONS" "[{\"version\":\"1.2\",\"latestVersion\":\"v1.2.0-alpha2\",\"channel\":\"STS\"}]" }}' - -# Verbose logging in the browser console. -debug = '{{ env "APIP_AIW_DEBUG" "false" }}' +default_org_region = "us" -# ==================================================================== +# --------------------------------------------------------------------------- # Server # -# Everything from here down configures the AI Workspace server itself. These values -# stay server-side and are never sent to the browser (the [oidc] claim names being the -# one exception — the SPA needs them to display user/org identity). +# Everything in this table configures the AI Workspace server itself. These values +# stay server-side and are never sent to the browser, with two exceptions: domain +# (the SPA needs it to display the deployment's own address) and the +# [ai_workspace.auth.oidc] claim names (the SPA needs them to display user/org identity). # # All keys are optional: the value in each token's default position is the built-in # default. -# ==================================================================== +# --------------------------------------------------------------------------- + +# Note: the directory serving the built SPA (index.html + assets) is not +# configurable here — the container always serves the copy baked in at /app. +# Local dev (`make bff-run`) points the BFF at a different build via the +# -static-dir CLI flag instead, since no deployment ever needs to override it. + +# The single listener follows the platform-wide [server.https] shape. The listener +# always binds all interfaces (there is no host to configure), and the container +# publishes this port. +[ai_workspace.server] + +# Domain shown in the browser address bar (host:port, or just host for port 80/443). +# Browser-safe — served to the SPA as APIP_AIW_SERVER_DOMAIN. +domain = "localhost:5380" -# Address the server listens on (host:port). The container publishes this port. -listen_addr = '{{ env "APIP_AIW_LISTEN_ADDR" ":5380" }}' +# Port the server listens on. +port = 5380 -# Directory holding the built SPA (index.html + assets). -static_dir = '{{ env "APIP_AIW_STATIC_DIR" "/app" }}' +# Paths to a mounted TLS certificate and key. REQUIRED when enabled = true; setup.sh +# generates a pair for the quickstart. +cert_file = "/etc/ai-workspace/tls/cert.pem" +key_file = "/etc/ai-workspace/tls/key.pem" -# Same-origin prefix the SPA calls; it is stripped before forwarding upstream. The -# browser's API base URL is derived from this, so the browser only ever talks to the -# app origin and never hits the platform-api certificate directly. -proxy_prefix = '{{ env "APIP_AIW_PROXY_PREFIX" "/api/proxy" }}' -# Custom header required on state-mutating requests (CSRF protection). -csrf_header = '{{ env "APIP_AIW_CSRF_HEADER" "X-Requested-By" }}' +# --------------------------------------------------------------------------- +# Logging. level/format are this process's own logs; browser_debug is +# browser-safe — served to the SPA to control verbose console logging there. +# Level is matched case-insensitively; lowercase is canonical. +# --------------------------------------------------------------------------- +[ai_workspace.logging] -log_level = '{{ env "APIP_AIW_LOG_LEVEL" "info" }}' # debug | info | warn | error -log_format = '{{ env "APIP_AIW_LOG_FORMAT" "text" }}' # text | json +level = "info" # debug | info | warn | error +format = "text" # text | json +browser_debug = "false" # --------------------------------------------------------------------------- -# Upstream Platform API — the server-to-server hop the BFF proxies to. +# Upstream Platform API (control plane) — the server-to-server hop the BFF proxies to. # --------------------------------------------------------------------------- -[platform_api] +[ai_workspace.control_plane] -# Base URL used to reach the Platform API. REQUIRED. In docker compose this is the -# compose hostname; running locally, use localhost. Its http/https scheme decides -# whether the upstream hop uses TLS. -url = '{{ env "APIP_AIW_PLATFORM_API_URL" "https://platform-api:9243" }}' +# Base URL to reach the Platform API. REQUIRED. Use the compose hostname in docker +# compose, or localhost when running locally. The scheme decides if the hop uses TLS. +url = "https://platform-api:9243" -# File-based login path on the Platform API (basic-auth mode). -login_path = '{{ env "APIP_AIW_PLATFORM_API_LOGIN_PATH" "/api/portal/v0.9/auth/login" }}' +# Same-origin prefix the SPA calls; stripped before forwarding upstream, so the +# browser only ever talks to the app origin and never sees the platform-api cert. +proxy_prefix = "/proxy" + +# Platform API's portal route prefix. BFF-initiated calls (e.g. file-based login) are +# built as this base path plus the route's own suffix, e.g. base + "/auth/login". +portal_base_path = "/api/portal/v0.9" # --- Trust for the upstream's TLS certificate --- -# Accept the Platform API's self-signed certificate without verification (local -# development only) — prefer trusting the certificate with ca_file instead. -tls_skip_verify = '{{ env "APIP_AIW_PLATFORM_API_TLS_SKIP_VERIFY" "false" }}' +# Accept the Platform API's self-signed certificate without verification (local dev +# only) — prefer ca_file instead. +tls_skip_verify = "false" -# PEM bundle to trust for the upstream's TLS certificate. Appended to the system -# roots, so public CAs keep working. -ca_file = '{{ env "APIP_AIW_PLATFORM_API_CA_FILE" "" }}' +# PEM bundle to trust for the upstream's TLS certificate, appended to system roots. +ca_file = "" # --------------------------------------------------------------------------- -# TLS on the AI Workspace listener. +# Gateway deployment info — browser-safe values the SPA shows in gateway setup +# instructions. Unlike [ai_workspace.control_plane] (the BFF's own hop), this is what +# an externally deployed gateway needs to reach the Platform API itself. # --------------------------------------------------------------------------- -[tls] +[ai_workspace.gateway] + +# host:port that deployed gateways use to reach the Platform API, shown in setup +# instructions (keys.env, helm values). Must be externally reachable, not a proxy path. +controlplane_host = "localhost:9243" -# Terminate TLS on this listener. Set false only when a trusted upstream (ingress -# controller, service-mesh sidecar) terminates TLS for you — no certificate is then -# read, generated, or required. -enabled = '{{ env "APIP_AIW_TLS_ENABLED" "true" }}' +# Gateway versions offered in the create-gateway version selector (JSON array string). +# Each entry: version (helm chart minor), latestVersion (image/chart tag, used in the +# helm --version flag), channel ("STS" | "LTS"). The first entry is the default. +platform_gateway_versions = "[{\"version\":\"1.2\",\"latestVersion\":\"v1.2.0-alpha2\",\"channel\":\"STS\"}]" -# Paths to a mounted TLS certificate and key. REQUIRED when enabled = true — -# there is no self-signed fallback; setup.sh generates a pair for the quickstart. -cert_file = '{{ env "APIP_AIW_TLS_CERT_FILE" "/etc/ai-workspace/tls/cert.pem" }}' -key_file = '{{ env "APIP_AIW_TLS_KEY_FILE" "/etc/ai-workspace/tls/key.pem" }}' # --------------------------------------------------------------------------- # Server-side session lifetime. # --------------------------------------------------------------------------- -[session] +[ai_workspace.session] # Session store. Only "memory" is supported today ("redis" is future). -store = '{{ env "APIP_AIW_SESSION_STORE" "memory" }}' +store = "memory" # Sliding idle window (Go duration, e.g. "30m"). Reserved — not enforced yet. -idle_timeout = '{{ env "APIP_AIW_SESSION_IDLE_TIMEOUT" "30m" }}' +idle_timeout = "30m" # Hard cap on session lifetime regardless of activity/refresh (Go duration). -absolute_ttl = '{{ env "APIP_AIW_SESSION_ABSOLUTE_TTL" "8h" }}' +absolute_ttl = "8h" # --------------------------------------------------------------------------- -# Session cookie attributes. +# Auth — chooses the login UX and gates the OIDC client below. # --------------------------------------------------------------------------- -[cookie] +[ai_workspace.auth] + +# Authentication mode for the UI: "basic" (local users) or "oidc" (external IDP). +# Choosing "oidc" enables the [ai_workspace.auth.oidc] table below. +mode = "basic" -name = '{{ env "APIP_AIW_COOKIE_NAME" "_ai_workspace_session" }}' -# Mark the session cookie Secure (HTTPS-only). Set false only for local HTTP. -secure = '{{ env "APIP_AIW_COOKIE_SECURE" "true" }}' +# --------------------------------------------------------------------------- +# JWT claim name mappings — which token claim carries each user/org field. Applies to +# BOTH auth modes (a sibling of [ai_workspace.auth.oidc], not nested in it): in basic +# mode the Platform API signs JWTs using these same claim names, so the two services +# must agree. Mirrors [platform_api.auth.claim_mappings] in the same config.toml +# key for key — override only if your IDP uses different claim names. +# --------------------------------------------------------------------------- +[ai_workspace.auth.claim_mappings] -# SameSite policy: "lax" | "strict" | "none". -samesite = '{{ env "APIP_AIW_COOKIE_SAMESITE" "lax" }}' +organization = "organization" # claim carrying the org ID +org_name = "org_name" # org display name +org_handle = "org_handle" # org URL slug +username = "username" +email = "email" +scope = "scope" # space-separated scope string +roles = "roles" # --------------------------------------------------------------------------- -# OIDC — set auth_mode = "oidc" above to enable. +# OIDC — set [ai_workspace.auth] mode = "oidc" above to enable. # # The BFF is a confidential client: it performs the whole handshake, and the client # credentials never reach the browser. # --------------------------------------------------------------------------- -[oidc] +[ai_workspace.auth.oidc] -# Force the OIDC client on independently of auth_mode. Setting auth_mode = "oidc" -# already enables it, so this is rarely needed. -enabled = '{{ env "APIP_AIW_OIDC_ENABLED" "false" }}' +# Force the OIDC client on independently of mode. Rarely needed — mode = "oidc" +# above already enables it. +enabled = "false" # Issuer URL — endpoints are auto-discovered from /.well-known/openid-configuration. -authority = '{{ env "APIP_AIW_OIDC_AUTHORITY" "https://accounts.example.com" }}' +authority = "https://accounts.example.com" # Client ID registered in your IDP. -client_id = '{{ env "APIP_AIW_OIDC_CLIENT_ID" "your-client-id" }}' +client_id = "your-client-id" -# The confidential-client secret. Required in OIDC mode, and deliberately has no -# default: an unset variable fails startup rather than running with an empty credential. -# Prefer the {{ file }} form in production — the value then never enters the -# environment at all. -client_secret = '{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}' +# Confidential-client secret. REQUIRED in OIDC mode with no default — an unset +# variable fails startup. Prefer the {{ file }} form in production. +client_secret = "" -# The callback the IDP redirects to after login — a server-side callback, not an SPA -# route. Must equal the redirect URI registered on the IDP application. -redirect_url = '{{ env "APIP_AIW_OIDC_REDIRECT_URL" "https://localhost:5380/api/auth/callback" }}' +# Server-side callback the IDP redirects to after login (not an SPA route). Must +# match the redirect URI registered on the IDP application. +redirect_url = "https://localhost:5380/api/auth/callback" # Absolute URL the IDP redirects to after logout (must be pre-registered there). -post_logout_redirect_url = '{{ env "APIP_AIW_OIDC_POST_LOGOUT_REDIRECT_URL" "https://localhost:5380/login" }}' +post_logout_redirect_url = "https://localhost:5380/login" -# Scopes requested at login (space-separated). Leave APIP_AIW_OIDC_SCOPE unset to -# request the built-in full ap:* set (recommended). Override only to request a -# narrower set — and keep offline_access in any override, or token refresh breaks. -scope = '{{ env "APIP_AIW_OIDC_SCOPE" "" }}' - - -# --------------------------------------------------------------------------- -# JWT claim name mappings — which token claim carries each user/org field. -# -# This table mirrors [auth.idp.claim_mappings] in config-platform-api.toml key for key, -# and the two must agree: both services read the same claims out of the same token. -# The variables line up one-to-one as well: -# -# APIP_AIW_OIDC_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME (AI Workspace) -# APIP_CP_AUTH_IDP_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME (Platform API) -# -# Override only if your IDP uses claim names other than the defaults shown. -# -# Must be the last table under [oidc]: in TOML, a sub-table header ends the parent -# table's key section, so any plain [oidc] key placed below it would land here instead. -# --------------------------------------------------------------------------- -[oidc.claim_mappings] - -organization_claim_name = '{{ env "APIP_AIW_OIDC_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME" "org_id" }}' # claim carrying the org ID -org_name_claim_name = '{{ env "APIP_AIW_OIDC_CLAIM_MAPPINGS_ORG_NAME_CLAIM_NAME" "org_name" }}' # org display name -org_handle_claim_name = '{{ env "APIP_AIW_OIDC_CLAIM_MAPPINGS_ORG_HANDLE_CLAIM_NAME" "org_handle" }}' # org URL slug -username_claim_name = '{{ env "APIP_AIW_OIDC_CLAIM_MAPPINGS_USERNAME_CLAIM_NAME" "username" }}' -email_claim_name = '{{ env "APIP_AIW_OIDC_CLAIM_MAPPINGS_EMAIL_CLAIM_NAME" "email" }}' -scope_claim_name = '{{ env "APIP_AIW_OIDC_CLAIM_MAPPINGS_SCOPE_CLAIM_NAME" "scope" }}' # space-separated scope string -role_claim_name = '{{ env "APIP_AIW_OIDC_CLAIM_MAPPINGS_ROLE_CLAIM_NAME" "platform_role" }}' +# Scopes requested at login (space-separated). Defaults to the full ap:* set the +# Platform API authorizes against (recommended) — trim only what you need to +# restrict, and always keep offline_access or token refresh breaks. +scope = "openid profile email offline_access ap:organization:read ap:organization:manage ap:organization:subscription:read ap:project:read ap:project:create ap:project:update ap:project:delete ap:project:manage ap:application:read ap:application:create ap:application:update ap:application:delete ap:application:manage ap:application:api_key:read ap:application:api_key:create ap:application:api_key:delete ap:application:api_key:manage ap:application:association:read ap:application:association:create ap:application:association:delete ap:application:association:manage ap:application:association:api_key:read ap:gateway:read ap:gateway:create ap:gateway:update ap:gateway:delete ap:gateway:manage ap:gateway:token:read ap:gateway:token:create ap:gateway:token:delete ap:gateway:token:manage ap:gateway_custom_policy:read ap:gateway_custom_policy:create ap:gateway_custom_policy:delete ap:gateway_custom_policy:manage ap:gateway:artifact:read ap:gateway:manifest:read ap:rest_api:read ap:rest_api:create ap:rest_api:update ap:rest_api:delete ap:rest_api:manage ap:rest_api:import ap:rest_api:gateway:read ap:rest_api:gateway:create ap:rest_api:gateway:manage ap:rest_api:deployment:read ap:rest_api:deployment:create ap:rest_api:deployment:delete ap:rest_api:deployment:manage ap:rest_api:deployment:undeploy ap:rest_api:deployment:restore ap:rest_api:api_key:read ap:rest_api:api_key:create ap:rest_api:api_key:update ap:rest_api:api_key:delete ap:rest_api:api_key:manage ap:rest_api:publication:read ap:rest_api:publication:create ap:rest_api:publication:delete ap:devportal:read ap:devportal:create ap:devportal:update ap:devportal:delete ap:devportal:manage ap:subscription:read ap:subscription:create ap:subscription:update ap:subscription:delete ap:subscription:manage ap:subscription_plan:read ap:subscription_plan:create ap:subscription_plan:update ap:subscription_plan:delete ap:subscription_plan:manage ap:llm_template:read ap:llm_template:create ap:llm_template:update ap:llm_template:delete ap:llm_template:manage ap:llm_provider:read ap:llm_provider:create ap:llm_provider:update ap:llm_provider:delete ap:llm_provider:manage ap:llm_provider:api_key:read ap:llm_provider:api_key:create ap:llm_provider:api_key:delete ap:llm_provider:api_key:manage ap:llm_provider:deployment:read ap:llm_provider:deployment:create ap:llm_provider:deployment:delete ap:llm_provider:deployment:manage ap:llm_provider:deployment:undeploy ap:llm_provider:deployment:restore ap:llm_proxy:read ap:llm_proxy:create ap:llm_proxy:update ap:llm_proxy:delete ap:llm_proxy:manage ap:llm_proxy:api_key:read ap:llm_proxy:api_key:create ap:llm_proxy:api_key:delete ap:llm_proxy:api_key:manage ap:llm_proxy:deployment:read ap:llm_proxy:deployment:create ap:llm_proxy:deployment:delete ap:llm_proxy:deployment:manage ap:llm_proxy:deployment:undeploy ap:llm_proxy:deployment:restore ap:mcp_proxy:read ap:mcp_proxy:create ap:mcp_proxy:update ap:mcp_proxy:delete ap:mcp_proxy:manage ap:mcp_proxy:deployment:read ap:mcp_proxy:deployment:create ap:mcp_proxy:deployment:delete ap:mcp_proxy:deployment:manage ap:mcp_proxy:deployment:undeploy ap:mcp_proxy:deployment:restore ap:websub_api:read ap:websub_api:create ap:websub_api:update ap:websub_api:delete ap:websub_api:manage ap:websub_api:api_key:read ap:websub_api:api_key:create ap:websub_api:api_key:delete ap:websub_api:api_key:manage ap:websub_api:api_key:update ap:websub_api:deployment:read ap:websub_api:deployment:create ap:websub_api:deployment:delete ap:websub_api:deployment:manage ap:websub_api:deployment:undeploy ap:websub_api:deployment:restore ap:websub_api:publication:read ap:websub_api:publication:create ap:websub_api:publication:delete ap:webbroker_api:read ap:webbroker_api:create ap:webbroker_api:update ap:webbroker_api:delete ap:webbroker_api:manage ap:webbroker_api:api_key:read ap:webbroker_api:api_key:create ap:webbroker_api:api_key:delete ap:webbroker_api:api_key:manage ap:webbroker_api:api_key:update ap:webbroker_api:deployment:read ap:webbroker_api:deployment:create ap:webbroker_api:deployment:delete ap:webbroker_api:deployment:manage ap:webbroker_api:deployment:undeploy ap:webbroker_api:deployment:restore ap:webbroker_api:publication:read ap:webbroker_api:publication:create ap:webbroker_api:publication:delete ap:secret:read ap:secret:create ap:secret:update ap:secret:delete ap:secret:manage ap:git:read" # ==================================================================== # Settings with no config key # -# The path to this file cannot be a key in it: the server reads its mount, -# /etc/ai-workspace/config.toml, unless -config names another path -# (`bff -config ../configs/config.toml`, as `make bff-run` does). -# -# The variables below are read straight from the environment by the server itself, not -# through a token, because each is needed before (or independently of) the config file. -# They are not config keys, so they have no entry above. +# Read straight from the environment by the server itself (not through a token), +# since each is needed before or independently of the config file. # ==================================================================== # # APIP_CONFIG_FILE_SOURCE_ALLOWLIST # Comma-separated directories a {{ file "..." }} token may -# read from. Replaces (not extends) the defaults -# /etc/ai-workspace and /secrets/ai-workspace. Shared by -# every component in the platform. +# read from. Replaces the defaults /etc/ai-workspace and +# /secrets/ai-workspace. Shared by every component in the +# platform. diff --git a/portals/ai-workspace/configs/config.toml b/portals/ai-workspace/configs/config.toml index 443b511e38..28d4aed9ff 100644 --- a/portals/ai-workspace/configs/config.toml +++ b/portals/ai-workspace/configs/config.toml @@ -1,34 +1,34 @@ -# -------------------------------------------------------------------- -# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). -# -# WSO2 LLC. licenses this file to you under the Apache License, -# Version 2.0 (the "License"); you may not use this file except -# in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -------------------------------------------------------------------- -# AI Workspace configuration (quickstart). See README.md → "Configuration". -# See configs/config-template.toml for the full set of available settings. - -auth_mode = '{{ env "APIP_AIW_AUTH_MODE" "basic" }}' -domain = '{{ env "APIP_AIW_DOMAIN" "localhost:5380" }}' -controlplane_host = '{{ env "APIP_AIW_CONTROLPLANE_HOST" "host.docker.internal:9243" }}' -default_org_region = '{{ env "APIP_AIW_DEFAULT_ORG_REGION" "us" }}' -platform_gateway_versions = '{{ env "APIP_AIW_PLATFORM_GATEWAY_VERSIONS" "[{\"version\":\"1.2\",\"latestVersion\":\"v1.2.0-alpha2\",\"channel\":\"STS\"}]" }}' - -listen_addr = '{{ env "APIP_AIW_LISTEN_ADDR" ":5380" }}' -static_dir = '{{ env "APIP_AIW_STATIC_DIR" "/app" }}' -log_level = '{{ env "APIP_AIW_LOG_LEVEL" "info" }}' # debug | info | warn | error - - -[platform_api] - -url = '{{ env "APIP_AIW_PLATFORM_API_URL" "https://platform-api:9243" }}' -ca_file = "/etc/ai-workspace/tls/cert.pem" -tls_skip_verify = '{{ env "APIP_AIW_PLATFORM_API_TLS_SKIP_VERIFY" "false" }}' +[ai_workspace] + +default_org_region = "us" -[tls] +[ai_workspace.server] -enabled = '{{ env "APIP_AIW_TLS_ENABLED" "true" }}' +domain = '{{ env "APIP_AIW_SERVER_DOMAIN" "localhost:5380" }}' +port = 5380 cert_file = "/etc/ai-workspace/tls/cert.pem" key_file = "/etc/ai-workspace/tls/key.pem" + + +[ai_workspace.logging] + +level = '{{ env "APIP_AIW_LOGGING_LEVEL" "info" }}' # debug | info | warn | error + + +[ai_workspace.control_plane] + +url = '{{ env "APIP_AIW_CONTROL_PLANE_URL" "https://platform-api:9243" }}' +tls_skip_verify = '{{ env "APIP_AIW_CONTROL_PLANE_TLS_SKIP_VERIFY" "false" }}' +ca_file = "/etc/ai-workspace/tls/cert.pem" + + +[ai_workspace.gateway] + +controlplane_host = '{{ env "APIP_AIW_GATEWAY_CONTROLPLANE_HOST" "host.docker.internal:9243" }}' +platform_gateway_versions = "[{\"version\":\"1.2\",\"latestVersion\":\"v1.2.0-alpha2\",\"channel\":\"STS\"}]" + + +[ai_workspace.auth] + +mode = '{{ env "APIP_AIW_AUTH_MODE" "basic" }}' diff --git a/portals/ai-workspace/cypress/e2e/001-providers/001-provider-and-proxy.cy.js b/portals/ai-workspace/cypress/e2e/001-providers/001-provider-and-proxy.cy.js index 48fd91ec3b..540d31920f 100644 --- a/portals/ai-workspace/cypress/e2e/001-providers/001-provider-and-proxy.cy.js +++ b/portals/ai-workspace/cypress/e2e/001-providers/001-provider-and-proxy.cy.js @@ -39,7 +39,7 @@ describe('AI Workspace - OpenAI provider and proxy lifecycle', () => { cy.login(); cy.request({ method: 'POST', - url: '/api/proxy/api/portal/v0.9/auth/login', + url: '/proxy/api/portal/v0.9/auth/login', form: true, body: { username: Cypress.env('ADMIN_USER'), @@ -52,7 +52,7 @@ describe('AI Workspace - OpenAI provider and proxy lifecycle', () => { expect(authToken).to.not.equal(''); return cy.request({ - url: '/api/proxy/api/v0.9/organizations', + url: '/proxy/api/v0.9/organizations', headers: { Authorization: `Bearer ${authToken}`, }, @@ -212,7 +212,7 @@ function deleteLinkedProxies(authToken, organizationId, providerId) { return cy.wrap(null); } return requestWithAuth(authToken, { - url: `/api/proxy/api/v0.9/llm-providers/${encodeURIComponent(providerId)}/llm-proxies?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-providers/${encodeURIComponent(providerId)}/llm-proxies?organizationId=${encodeURIComponent(organizationId)}`, failOnStatusCode: false, }).then((response) => { expect(response.status).to.be.oneOf([200, 404]); @@ -229,7 +229,7 @@ function deleteLinkedProxies(authToken, organizationId, providerId) { return cy.wrap(proxies).each((proxy) => requestWithAuth(authToken, { method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-proxies/${encodeURIComponent(proxy.id)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-proxies/${encodeURIComponent(proxy.id)}?organizationId=${encodeURIComponent(organizationId)}`, failOnStatusCode: false, }).then((deleteResponse) => { expect(deleteResponse.status).to.be.oneOf([200, 204, 404]); @@ -240,7 +240,7 @@ function deleteLinkedProxies(authToken, organizationId, providerId) { function deleteProjectByName(authToken, targetProjectName, fallbackProjectName) { return requestWithAuth(authToken, { - url: '/api/proxy/api/v0.9/projects', + url: '/proxy/api/v0.9/projects', }).then((response) => { expect(response.status).to.eq(200); @@ -266,7 +266,7 @@ function deleteProjectByName(authToken, targetProjectName, fallbackProjectName) function ensureFallbackProject(authToken, fallbackProjectName) { return requestWithAuth(authToken, { method: 'POST', - url: '/api/proxy/api/v0.9/projects', + url: '/proxy/api/v0.9/projects', body: { displayName: fallbackProjectName, description: 'Reserved project to satisfy E2E cleanup invariants.', @@ -280,7 +280,7 @@ function ensureFallbackProject(authToken, fallbackProjectName) { function deleteProject(authToken, projectId) { return requestWithAuth(authToken, { method: 'DELETE', - url: `/api/proxy/api/v0.9/projects/${encodeURIComponent(projectId)}`, + url: `/proxy/api/v0.9/projects/${encodeURIComponent(projectId)}`, failOnStatusCode: false, }).then((response) => { expect(response.status).to.be.oneOf([200, 204, 404]); @@ -293,7 +293,7 @@ function deleteProvider(authToken, organizationId, providerId) { } return requestWithAuth(authToken, { method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-providers/${encodeURIComponent(providerId)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-providers/${encodeURIComponent(providerId)}?organizationId=${encodeURIComponent(organizationId)}`, failOnStatusCode: false, }).then((response) => { expect(response.status).to.be.oneOf([200, 204, 404]); diff --git a/portals/ai-workspace/cypress/e2e/001-providers/002-provider-secret-management.cy.js b/portals/ai-workspace/cypress/e2e/001-providers/002-provider-secret-management.cy.js index 21667a043d..a22b461a6a 100644 --- a/portals/ai-workspace/cypress/e2e/001-providers/002-provider-secret-management.cy.js +++ b/portals/ai-workspace/cypress/e2e/001-providers/002-provider-secret-management.cy.js @@ -88,7 +88,7 @@ describe('AI Workspace — LLM provider secret management (create flow)', () => cy.login(); cy.request({ method: 'POST', - url: '/api/proxy/api/portal/v0.9/auth/login', + url: '/proxy/api/portal/v0.9/auth/login', form: true, body: { username: Cypress.env('ADMIN_USER'), @@ -100,7 +100,7 @@ describe('AI Workspace — LLM provider secret management (create flow)', () => authToken = response.body?.token ?? ''; expect(authToken).to.not.equal(''); return cy.request({ - url: '/api/proxy/api/v0.9/organizations', + url: '/proxy/api/v0.9/organizations', headers: { Authorization: `Bearer ${authToken}` }, }); }) @@ -115,7 +115,7 @@ describe('AI Workspace — LLM provider secret management (create flow)', () => if (!authToken || !organizationId || !createdProviderId) return; cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-providers/${encodeURIComponent(createdProviderId)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-providers/${encodeURIComponent(createdProviderId)}?organizationId=${encodeURIComponent(organizationId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -174,7 +174,7 @@ describe('AI Workspace — LLM provider secret management (create flow)', () => // Pre-create the secret via API so there's a real secret backing the placeholder. cy.request({ method: 'POST', - url: '/api/proxy/api/v0.9/secrets', + url: '/proxy/api/v0.9/secrets', headers: { Authorization: `Bearer ${authToken}` }, form: true, body: { @@ -266,7 +266,7 @@ describe('AI Workspace — LLM provider secret management (create flow)', () => cy.request({ method: 'POST', - url: '/api/proxy/api/v0.9/secrets', + url: '/proxy/api/v0.9/secrets', headers: { Authorization: `Bearer ${authToken}` }, form: true, body: { @@ -284,7 +284,7 @@ describe('AI Workspace — LLM provider secret management (create flow)', () => // accumulates many secrets across the suite and the list defaults to // limit=25, which can miss the one just created). cy.request({ - url: `/api/proxy/api/v0.9/secrets/${encodeURIComponent(handle)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/secrets/${encodeURIComponent(handle)}?organizationId=${encodeURIComponent(organizationId)}`, headers: { Authorization: `Bearer ${authToken}` }, }).then((r) => { expect(r.status).to.eq(200); @@ -297,7 +297,7 @@ describe('AI Workspace — LLM provider secret management (create flow)', () => cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/secrets/${encodeURIComponent(handle)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/secrets/${encodeURIComponent(handle)}?organizationId=${encodeURIComponent(organizationId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -327,7 +327,7 @@ describe('AI Workspace — LLM provider secret management (update flow)', () => cy.request({ method: 'POST', - url: '/api/proxy/api/portal/v0.9/auth/login', + url: '/proxy/api/portal/v0.9/auth/login', form: true, body: { username: Cypress.env('ADMIN_USER'), @@ -337,7 +337,7 @@ describe('AI Workspace — LLM provider secret management (update flow)', () => cy.then(() => cy.request({ - url: '/api/proxy/api/v0.9/organizations', + url: '/proxy/api/v0.9/organizations', headers: { Authorization: `Bearer ${authToken}` }, }) ).then((r) => { organizationId = r.body?.list?.[0]?.id ?? ''; }); @@ -380,7 +380,7 @@ describe('AI Workspace — LLM provider secret management (update flow)', () => if (authToken && organizationId && providerId) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-providers/${encodeURIComponent(providerId)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-providers/${encodeURIComponent(providerId)}?organizationId=${encodeURIComponent(organizationId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -451,7 +451,7 @@ describe('AI Workspace — LLM provider secret management (update flow)', () => // cy.request() bypasses cy.intercept(), so this won't affect secretCallCount below. cy.request({ method: 'POST', - url: '/api/proxy/api/v0.9/secrets', + url: '/proxy/api/v0.9/secrets', headers: { Authorization: `Bearer ${authToken}` }, form: true, body: { diff --git a/portals/ai-workspace/cypress/e2e/001-providers/003-llm-proxy-secret-management.cy.js b/portals/ai-workspace/cypress/e2e/001-providers/003-llm-proxy-secret-management.cy.js index 3f7a9403ed..28512c8905 100644 --- a/portals/ai-workspace/cypress/e2e/001-providers/003-llm-proxy-secret-management.cy.js +++ b/portals/ai-workspace/cypress/e2e/001-providers/003-llm-proxy-secret-management.cy.js @@ -66,7 +66,7 @@ function loginAndFetchAuthContext(setAuthToken, setOrganizationId) { .then((r) => { setAuthToken(r.body.accessToken); return cy.request({ - url: '/api/proxy/api/v0.9/organizations', + url: '/proxy/api/v0.9/organizations', headers: { Authorization: `Bearer ${r.body.accessToken}` }, }); }) @@ -144,7 +144,7 @@ describe('AI Workspace — LLM proxy secret management (create flow)', () => { if (authToken && organizationId && createdProxyId) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-proxies/${encodeURIComponent(createdProxyId)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-proxies/${encodeURIComponent(createdProxyId)}?organizationId=${encodeURIComponent(organizationId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -152,14 +152,14 @@ describe('AI Workspace — LLM proxy secret management (create flow)', () => { if (authToken && organizationId && createdProviderId) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-providers/${encodeURIComponent(createdProviderId)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-providers/${encodeURIComponent(createdProviderId)}?organizationId=${encodeURIComponent(organizationId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); } if (authToken) { cy.request({ - url: '/api/proxy/api/v0.9/projects', + url: '/proxy/api/v0.9/projects', headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }).then((r) => { @@ -167,7 +167,7 @@ describe('AI Workspace — LLM proxy secret management (create flow)', () => { if (proj?.id) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/projects/${encodeURIComponent(proj.id)}`, + url: `/proxy/api/v0.9/projects/${encodeURIComponent(proj.id)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -220,7 +220,7 @@ describe('AI Workspace — LLM proxy secret management (create flow)', () => { const existingHandle = `${toSlug(proxyName)}-provider-api-key`; cy.request({ method: 'POST', - url: '/api/proxy/api/v0.9/secrets', + url: '/proxy/api/v0.9/secrets', headers: { Authorization: `Bearer ${authToken}` }, form: true, body: { @@ -345,7 +345,7 @@ describe('AI Workspace — LLM proxy secret management (update flow)', () => { if (authToken && organizationId && proxyId) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-proxies/${encodeURIComponent(proxyId)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-proxies/${encodeURIComponent(proxyId)}?organizationId=${encodeURIComponent(organizationId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -354,14 +354,14 @@ describe('AI Workspace — LLM proxy secret management (update flow)', () => { if (authToken && organizationId && createdProviderId) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-providers/${encodeURIComponent(createdProviderId)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-providers/${encodeURIComponent(createdProviderId)}?organizationId=${encodeURIComponent(organizationId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); } if (authToken) { cy.request({ - url: '/api/proxy/api/v0.9/projects', + url: '/proxy/api/v0.9/projects', headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }).then((r) => { @@ -369,7 +369,7 @@ describe('AI Workspace — LLM proxy secret management (update flow)', () => { if (proj?.id) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/projects/${encodeURIComponent(proj.id)}`, + url: `/proxy/api/v0.9/projects/${encodeURIComponent(proj.id)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -421,7 +421,7 @@ describe('AI Workspace — LLM proxy secret management (update flow)', () => { cy.wrap(null).then(() => { expect(initialSecretHandle, 'captured the initial secret handle in beforeEach').to.not.equal(''); cy.request({ - url: `/api/proxy/api/v0.9/secrets/${encodeURIComponent(initialSecretHandle)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/secrets/${encodeURIComponent(initialSecretHandle)}?organizationId=${encodeURIComponent(organizationId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }).then((r) => { @@ -439,7 +439,7 @@ describe('AI Workspace — LLM proxy secret management (update flow)', () => { cy.request({ method: 'POST', - url: '/api/proxy/api/v0.9/secrets', + url: '/proxy/api/v0.9/secrets', headers: { Authorization: `Bearer ${authToken}` }, form: true, body: { diff --git a/portals/ai-workspace/cypress/e2e/002-mcp-proxies/002-mcp-proxy-sample-url.cy.js b/portals/ai-workspace/cypress/e2e/002-mcp-proxies/002-mcp-proxy-sample-url.cy.js index d280db6ad9..27d090489e 100644 --- a/portals/ai-workspace/cypress/e2e/002-mcp-proxies/002-mcp-proxy-sample-url.cy.js +++ b/portals/ai-workspace/cypress/e2e/002-mcp-proxies/002-mcp-proxy-sample-url.cy.js @@ -30,7 +30,7 @@ describe('AI Workspace - MCP proxy sample URL lifecycle', () => { // even when the UI flow fails before its inline cleanup runs. cy.request({ method: 'POST', - url: '/api/proxy/api/portal/v0.9/auth/login', + url: '/proxy/api/portal/v0.9/auth/login', form: true, body: { username: Cypress.env('ADMIN_USER'), @@ -191,7 +191,7 @@ function toSlug(value) { function sweepE2EMCPProxies(authToken) { const headers = { Authorization: `Bearer ${authToken}` }; cy.request({ - url: '/api/proxy/api/v0.9/projects', + url: '/proxy/api/v0.9/projects', headers, failOnStatusCode: false, }).then((response) => { @@ -200,7 +200,7 @@ function sweepE2EMCPProxies(authToken) { projects.forEach((project) => { if (!project.id) return; cy.request({ - url: `/api/proxy/api/v0.9/mcp-proxies?projectId=${encodeURIComponent(project.id)}&limit=100&offset=0`, + url: `/proxy/api/v0.9/mcp-proxies?projectId=${encodeURIComponent(project.id)}&limit=100&offset=0`, headers, failOnStatusCode: false, }).then((listResponse) => { @@ -214,7 +214,7 @@ function sweepE2EMCPProxies(authToken) { if (!proxy.id) return; cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/mcp-proxies/${encodeURIComponent(proxy.id)}`, + url: `/proxy/api/v0.9/mcp-proxies/${encodeURIComponent(proxy.id)}`, headers, failOnStatusCode: false, }); @@ -227,7 +227,7 @@ function sweepE2EMCPProxies(authToken) { // Look up a project by its human-readable displayName and delete it by handle. function deleteProjectByName(authToken, targetName) { cy.request({ - url: '/api/proxy/api/v0.9/projects', + url: '/proxy/api/v0.9/projects', headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }).then((response) => { @@ -238,7 +238,7 @@ function deleteProjectByName(authToken, targetName) { if (!target?.id) return; cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/projects/${encodeURIComponent(target.id)}`, + url: `/proxy/api/v0.9/projects/${encodeURIComponent(target.id)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); diff --git a/portals/ai-workspace/cypress/e2e/002-mcp-proxies/003-mcp-secret-management.cy.js b/portals/ai-workspace/cypress/e2e/002-mcp-proxies/003-mcp-secret-management.cy.js index eebd41af1b..2f74db8dc3 100644 --- a/portals/ai-workspace/cypress/e2e/002-mcp-proxies/003-mcp-secret-management.cy.js +++ b/portals/ai-workspace/cypress/e2e/002-mcp-proxies/003-mcp-secret-management.cy.js @@ -45,7 +45,7 @@ describe('AI Workspace — MCP server secret management', () => { cy.login(); cy.request({ method: 'POST', - url: '/api/proxy/api/portal/v0.9/auth/login', + url: '/proxy/api/portal/v0.9/auth/login', form: true, body: { username: Cypress.env('ADMIN_USER'), @@ -56,7 +56,7 @@ describe('AI Workspace — MCP server secret management', () => { expect(response.status).to.eq(200); authToken = response.body?.token ?? ''; return cy.request({ - url: '/api/proxy/api/v0.9/organizations', + url: '/proxy/api/v0.9/organizations', headers: { Authorization: `Bearer ${authToken}` }, }); }) @@ -70,7 +70,7 @@ describe('AI Workspace — MCP server secret management', () => { if (authToken && organizationId && createdServerId) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/mcp-proxies/${encodeURIComponent(createdServerId)}`, + url: `/proxy/api/v0.9/mcp-proxies/${encodeURIComponent(createdServerId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -182,7 +182,7 @@ describe('AI Workspace — MCP server secret management', () => { const existingHandle = `${serverId}-auth`; cy.request({ method: 'POST', - url: '/api/proxy/api/v0.9/secrets', + url: '/proxy/api/v0.9/secrets', headers: { Authorization: `Bearer ${authToken}`, }, @@ -446,7 +446,7 @@ describe('AI Workspace — MCP server secret management', () => { function deleteProjectByName(authToken, targetName, fallbackName) { if (!authToken) return; cy.request({ - url: '/api/proxy/api/v0.9/projects', + url: '/proxy/api/v0.9/projects', headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }).then((response) => { @@ -458,7 +458,7 @@ function deleteProjectByName(authToken, targetName, fallbackName) { if (projects.length <= 1) { cy.request({ method: 'POST', - url: '/api/proxy/api/v0.9/projects', + url: '/proxy/api/v0.9/projects', headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json', @@ -468,7 +468,7 @@ function deleteProjectByName(authToken, targetName, fallbackName) { }).then(() => { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/projects/${encodeURIComponent(target.id)}`, + url: `/proxy/api/v0.9/projects/${encodeURIComponent(target.id)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -476,7 +476,7 @@ function deleteProjectByName(authToken, targetName, fallbackName) { } else { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/projects/${encodeURIComponent(target.id)}`, + url: `/proxy/api/v0.9/projects/${encodeURIComponent(target.id)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); diff --git a/portals/ai-workspace/cypress/e2e/002-mcp-proxies/004-mcp-secret-management-update.cy.js b/portals/ai-workspace/cypress/e2e/002-mcp-proxies/004-mcp-secret-management-update.cy.js index fe46e596eb..ba6af8916c 100644 --- a/portals/ai-workspace/cypress/e2e/002-mcp-proxies/004-mcp-secret-management-update.cy.js +++ b/portals/ai-workspace/cypress/e2e/002-mcp-proxies/004-mcp-secret-management-update.cy.js @@ -53,7 +53,7 @@ describe('AI Workspace — MCP server secret management (update / policy-save fl cy.request({ method: 'POST', - url: '/api/proxy/api/portal/v0.9/auth/login', + url: '/proxy/api/portal/v0.9/auth/login', form: true, body: { username: Cypress.env('ADMIN_USER'), @@ -63,7 +63,7 @@ describe('AI Workspace — MCP server secret management (update / policy-save fl cy.then(() => cy.request({ - url: '/api/proxy/api/v0.9/organizations', + url: '/proxy/api/v0.9/organizations', headers: { Authorization: `Bearer ${authToken}` }, }) ).then((r) => { organizationId = r.body?.list?.[0]?.id ?? ''; }); @@ -150,7 +150,7 @@ describe('AI Workspace — MCP server secret management (update / policy-save fl if (createdServerId && authToken) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/mcp-proxies/${encodeURIComponent(createdServerId)}`, + url: `/proxy/api/v0.9/mcp-proxies/${encodeURIComponent(createdServerId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -159,7 +159,7 @@ describe('AI Workspace — MCP server secret management (update / policy-save fl if (createdProjectId && authToken) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/projects/${encodeURIComponent(createdProjectId)}`, + url: `/proxy/api/v0.9/projects/${encodeURIComponent(createdProjectId)}`, headers: { Authorization: `Bearer ${authToken}` }, failOnStatusCode: false, }); @@ -228,7 +228,7 @@ describe('AI Workspace — MCP server secret management (update / no-auth server cy.request({ method: 'POST', - url: '/api/proxy/api/portal/v0.9/auth/login', + url: '/proxy/api/portal/v0.9/auth/login', form: true, body: { username: Cypress.env('ADMIN_USER'), @@ -238,7 +238,7 @@ describe('AI Workspace — MCP server secret management (update / no-auth server cy.then(() => cy.request({ - url: '/api/proxy/api/v0.9/organizations', + url: '/proxy/api/v0.9/organizations', headers: { Authorization: `Bearer ${authToken2}` }, }) ).then((r) => { organizationId2 = r.body?.list?.[0]?.id ?? ''; }); @@ -309,7 +309,7 @@ describe('AI Workspace — MCP server secret management (update / no-auth server if (createdServerId2 && authToken2) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/mcp-proxies/${encodeURIComponent(createdServerId2)}`, + url: `/proxy/api/v0.9/mcp-proxies/${encodeURIComponent(createdServerId2)}`, headers: { Authorization: `Bearer ${authToken2}` }, failOnStatusCode: false, }); @@ -318,7 +318,7 @@ describe('AI Workspace — MCP server secret management (update / no-auth server if (createdProjectId2 && authToken2) { cy.request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/projects/${encodeURIComponent(createdProjectId2)}`, + url: `/proxy/api/v0.9/projects/${encodeURIComponent(createdProjectId2)}`, headers: { Authorization: `Bearer ${authToken2}` }, failOnStatusCode: false, }); diff --git a/portals/ai-workspace/cypress/e2e/005-provider-templates/005-custom-provider-template.cy.js b/portals/ai-workspace/cypress/e2e/005-provider-templates/005-custom-provider-template.cy.js index 503b2f92cf..ed66ffc1b8 100644 --- a/portals/ai-workspace/cypress/e2e/005-provider-templates/005-custom-provider-template.cy.js +++ b/portals/ai-workspace/cypress/e2e/005-provider-templates/005-custom-provider-template.cy.js @@ -37,7 +37,7 @@ describe('AI Workspace - Custom LLM provider template lifecycle', () => { cy.intercept('POST', /\/llm-providers(\?|$)/).as('createProvider'); cy.request({ method: 'POST', - url: '/api/proxy/api/portal/v0.9/auth/login', + url: '/proxy/api/portal/v0.9/auth/login', form: true, body: { username: Cypress.env('ADMIN_USER'), @@ -50,7 +50,7 @@ describe('AI Workspace - Custom LLM provider template lifecycle', () => { expect(authToken).to.not.equal(''); return cy.request({ - url: '/api/proxy/api/v0.9/organizations', + url: '/proxy/api/v0.9/organizations', headers: { Authorization: `Bearer ${authToken}`, }, @@ -226,7 +226,7 @@ describe('AI Workspace - Custom LLM provider template lifecycle', () => { function waitForProviderGone(authToken, organizationId, providerId) { return requestWithAuth(authToken, { - url: `/api/proxy/api/v0.9/llm-providers?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-providers?organizationId=${encodeURIComponent(organizationId)}`, }).then((res) => { const stillThere = (res.body?.list ?? []).some((p) => p.id === providerId); if (stillThere) { @@ -243,7 +243,7 @@ function deleteProvider(authToken, organizationId, targetProviderId) { } return requestWithAuth(authToken, { method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-providers/${encodeURIComponent(targetProviderId)}?organizationId=${encodeURIComponent(organizationId)}`, + url: `/proxy/api/v0.9/llm-providers/${encodeURIComponent(targetProviderId)}?organizationId=${encodeURIComponent(organizationId)}`, failOnStatusCode: false, }).then((response) => { expect(response.status).to.be.oneOf([200, 204, 404]); @@ -253,7 +253,7 @@ function deleteProvider(authToken, organizationId, targetProviderId) { function deleteTemplateVersion(authToken, organizationId, templateId) { return requestWithAuth(authToken, { method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-provider-templates/${encodeURIComponent(templateId)}`, + url: `/proxy/api/v0.9/llm-provider-templates/${encodeURIComponent(templateId)}`, failOnStatusCode: false, }).then((response) => { if (response.status === 409) { diff --git a/portals/ai-workspace/cypress/support/commands.js b/portals/ai-workspace/cypress/support/commands.js index 83b1d2c20f..eac49695d8 100644 --- a/portals/ai-workspace/cypress/support/commands.js +++ b/portals/ai-workspace/cypress/support/commands.js @@ -77,7 +77,7 @@ Cypress.Commands.add('sweepE2EProviders', (authToken, organizationId) => { cy .request({ method: 'GET', - url: `/api/proxy/api/v0.9/llm-providers?organizationId=${encodeURIComponent(orgId)}&limit=${PAGE_SIZE}&offset=${offset}`, + url: `/proxy/api/v0.9/llm-providers?organizationId=${encodeURIComponent(orgId)}&limit=${PAGE_SIZE}&offset=${offset}`, headers: headersFor(token), failOnStatusCode: false, }) @@ -103,7 +103,7 @@ Cypress.Commands.add('sweepE2EProviders', (authToken, organizationId) => { cy .request({ method: 'GET', - url: `/api/proxy/api/v0.9/llm-providers/${encodeURIComponent(providerId)}/llm-proxies?organizationId=${encodeURIComponent(orgId)}`, + url: `/proxy/api/v0.9/llm-providers/${encodeURIComponent(providerId)}/llm-proxies?organizationId=${encodeURIComponent(orgId)}`, headers: headersFor(token), failOnStatusCode: false, }) @@ -115,7 +115,7 @@ Cypress.Commands.add('sweepE2EProviders', (authToken, organizationId) => { cy .request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-proxies/${encodeURIComponent(proxy.id)}?organizationId=${encodeURIComponent(orgId)}`, + url: `/proxy/api/v0.9/llm-proxies/${encodeURIComponent(proxy.id)}?organizationId=${encodeURIComponent(orgId)}`, headers: headersFor(token), failOnStatusCode: false, }) @@ -133,7 +133,7 @@ Cypress.Commands.add('sweepE2EProviders', (authToken, organizationId) => { cy .request({ method: 'DELETE', - url: `/api/proxy/api/v0.9/llm-providers/${encodeURIComponent(provider.id)}?organizationId=${encodeURIComponent(orgId)}`, + url: `/proxy/api/v0.9/llm-providers/${encodeURIComponent(provider.id)}?organizationId=${encodeURIComponent(orgId)}`, headers: headersFor(token), failOnStatusCode: false, }) @@ -153,7 +153,7 @@ Cypress.Commands.add('sweepE2EProviders', (authToken, organizationId) => { return cy .request({ method: 'POST', - url: '/api/proxy/api/portal/v0.9/auth/login', + url: '/proxy/api/portal/v0.9/auth/login', form: true, body: { username: Cypress.env('ADMIN_USER'), @@ -167,7 +167,7 @@ Cypress.Commands.add('sweepE2EProviders', (authToken, organizationId) => { if (!token) return; return cy .request({ - url: '/api/proxy/api/v0.9/organizations', + url: '/proxy/api/v0.9/organizations', headers: { Authorization: `Bearer ${token}` }, failOnStatusCode: false, }) diff --git a/portals/ai-workspace/distribution/README.md b/portals/ai-workspace/distribution/README.md index 4bd7313901..2cc99c4605 100644 --- a/portals/ai-workspace/distribution/README.md +++ b/portals/ai-workspace/distribution/README.md @@ -11,10 +11,10 @@ wso2apip-ai-workspace-/ ├── scripts/ │ └── setup.sh # One-time TLS + secrets provisioning ├── configs/ -│ ├── config.toml # AI Workspace active configuration -│ ├── config-template.toml # AI Workspace full configuration reference -│ ├── config-platform-api.toml # Platform API active configuration -│ └── config-platform-api-template.toml # Platform API full configuration reference +│ ├── config.toml # Active configuration for BOTH services — +│ │ # [platform_api.*] and [ai_workspace.*] +│ │ # tables side by side in one file +│ └── config-template.toml # Full configuration reference for both └── resources/ ├── roles.yaml # Platform API role definitions └── platform-api/ @@ -40,7 +40,8 @@ docker compose up -d | Output | Contents | |---|---| -| `api-platform.env` (git-ignored) | `APIP_CP_ENCRYPTION_KEY` (at-rest encryption), `APIP_CP_AUTH_JWT_SECRET_KEY` (signs login JWTs), `APIP_CP_ADMIN_USERNAME`, `APIP_CP_ADMIN_PASSWORD_HASH` (bcrypt) | +| `api-platform.env` (git-ignored) | `APIP_CP_ENCRYPTION_KEY` (at-rest encryption), `APIP_CP_ADMIN_USERNAME`, `APIP_CP_ADMIN_PASSWORD_HASH` (bcrypt) | +| `resources/keys/jwt_private.pem` + `jwt_public.pem` (git-ignored) | RS256 keypair signing/verifying login JWTs; read by `config.toml` via `{{ file }}` | | `resources/certificates/cert.pem` + `key.pem` | Self-signed TLS pair shared by both services (SAN: `localhost`, `platform-api`, `ai-workspace`) | The admin password is generated and printed once by `setup.sh` — it is not stored anywhere; only its bcrypt hash lands in `api-platform.env`. Re-running `setup.sh` keeps existing files; pass `--force` to rotate keys and credentials, or `--certs-only` to (re)generate just the TLS pair. `ADMIN_USERNAME` / `ADMIN_PASSWORD` environment variables skip the interactive prompts (used by CI to pin known test credentials). @@ -67,39 +68,41 @@ Open the AI Workspace in a browser at `https://localhost:5380` and log in with t ## Configuration -Edit `configs/config.toml` for AI Workspace settings and `configs/config-platform-api.toml` for Platform API settings. Both are read directly by the running containers — no rebuild required, just restart the affected service. +Both services read their settings from the single `configs/config.toml` — Platform API's `[platform_api.*]` tables and AI Workspace's `[ai_workspace.*]` tables live side by side in the same file (each service reads only its own top-level table and ignores the other's), and `docker-compose.yaml` mounts that one file into both containers. Edit it and restart the affected service — no rebuild required. -Each config TOML writes its values as `'{{ env "..." }}'` tokens, so a key can be set from the environment without editing the file — the token names the variable, by convention the key uppercased and prefixed with `APIP_AIW_` (AI Workspace) or `APIP_CP_` (Platform API), e.g. `APIP_AIW_LOG_LEVEL`, `APIP_CP_DATABASE_HOST`. A key with no token is not settable from the environment: uncomment or add it in the TOML first. To source a value from a mounted file instead — the right choice for secrets — swap the token for `'{{ file "/secrets/..." }}'`. Never write a secret as a raw literal in either file. +Each config key writes its value as a `'{{ env "..." }}'` token, so it can be set from the environment without editing the file — the token names the variable, by convention the key's path uppercased and prefixed `APIP_AIW_` (AI Workspace) or `APIP_CP_` (Platform API), e.g. `APIP_AIW_LOGGING_LEVEL`, `APIP_CP_DATABASE_HOST`. A key with no token is not settable from the environment: uncomment or add it in the TOML first. To source a value from a mounted file instead — the right choice for secrets — swap the token for `'{{ file "/secrets/..." }}'`. Never write a secret as a raw literal. -Environment overrides go in `api-platform.env` (git-ignored; loaded into both containers via `env_file`, format `raw`, since the bcrypt password hash contains `$`, which must not be treated as a compose interpolation variable). This is also where OIDC mode's `APIP_AIW_OIDC_CLIENT_SECRET` belongs — it's the only file compose passes into the containers, so a separate `.env` alongside it would never reach the app. +Environment overrides go in `api-platform.env` (git-ignored; loaded into both containers via `env_file`, format `raw`, since the bcrypt password hash contains `$`, which must not be treated as a compose interpolation variable). This is also where OIDC mode's `APIP_AIW_AUTH_OIDC_CLIENT_SECRET` belongs — it's the only file compose passes into the containers, so a separate `.env` alongside it would never reach the app. -### AI Workspace (`configs/config.toml`) +### AI Workspace (`[ai_workspace.*]`) -| Setting | Description | Default | -|---------|-------------|---------| -| `domain` | Host and port shown in the browser address bar | `localhost:5380` | -| `auth_mode` | `basic` (file-based quickstart) or `oidc` (external IDP) | `basic` | -| `controlplane_host` | Address gateways use to reach the Platform API | `host.docker.internal:9243` | -| `platform_gateway_versions` | Gateway versions shown in the create-gateway selector | _(current release)_ | -| `[platform_api].url` | Base URL of the upstream Platform API hop | `https://platform-api:9243` | -| `[platform_api].ca_file` | PEM bundle trusted for the upstream's TLS cert (appended to system roots). Fixed to the mounted path — not env-overridable; edit the TOML if you change the volume mount in `docker-compose.yaml` | `/etc/ai-workspace/tls/cert.pem` | -| `[platform_api].tls_skip_verify` | Skip upstream cert verification — local dev only | `false` | -| `[tls].cert_file` / `key_file` | Listener certificate pair — required when `[tls].enabled` is `true`. Fixed to the mounted path, same as `ca_file` above | `/etc/ai-workspace/tls/{cert,key}.pem` | -| `[oidc].*` | Used only when `auth_mode = "oidc"` — see [OIDC](#oidc-production) below | _(empty)_ | +| Setting | Description | +|---------|-------------| +| `[ai_workspace] default_org_region` | Default region assigned to new organizations on first login | +| `[ai_workspace.server] domain` | Host and port shown in the browser address bar | +| `[ai_workspace.auth] mode` | `basic` (file-based quickstart) or `oidc` (external IDP) | +| `[ai_workspace.control_plane].url` | Base URL of the upstream Platform API hop | +| `[ai_workspace.control_plane].ca_file` | PEM bundle trusted for the upstream's TLS cert (appended to system roots). Fixed to the mounted path — not env-overridable; edit the TOML if you change the volume mount in `docker-compose.yaml` | +| `[ai_workspace.control_plane].tls_skip_verify` | Skip upstream cert verification — local dev only | +| `[ai_workspace.gateway].controlplane_host` | Address gateways use to reach the Platform API | +| `[ai_workspace.gateway].platform_gateway_versions` | Gateway versions shown in the create-gateway selector | +| `[ai_workspace.server].cert_file` / `key_file` | Listener certificate pair. Fixed to the mounted path, same as `ca_file` above | +| `[ai_workspace.auth.oidc].*` | Used only when `[ai_workspace.auth] mode = "oidc"` — see [OIDC](#oidc-production) below | -### Platform API (`configs/config-platform-api.toml`) +### Platform API (`[platform_api.*]`) -| Setting | Description | Default | -|---------|-------------|---------| -| `log_level` | Log level (`DEBUG`, `INFO`, `WARN`, `ERROR`) | `INFO` | -| `encryption_key` | Single 32-byte key (64 hex chars or base64) used for all at-rest encryption (secrets, subscription tokens, WebSub HMAC secrets). Generate with `openssl rand -hex 32` | _(from `setup.sh`)_ | -| `[database].driver` | `sqlite3` or `postgres` | `sqlite3` | -| `[auth.jwt].secret_key` | 32-byte HMAC key signing login JWTs | _(from `setup.sh`)_ | -| `[auth.idp]` | JWKS-based IDP auth — disabled in quickstart mode; enable for Asgardeo, Keycloak, Auth0, etc. | disabled | -| `[auth.file_based.users]` | Local user credentials (change the password hash before sharing) | admin, generated by `setup.sh` | -| `[https]` | Listener on `:9243`; `cert_dir` holds `cert.pem`/`key.pem` | `/etc/platform-api/tls` | +| Setting | Description | +|---------|-------------| +| `[platform_api.logging].level` | Log level (`debug`, `info`, `warn`, `error`; matched case-insensitively) | +| `[platform_api.security].encryption_key` | Single 32-byte key (64 hex chars or base64) used for all at-rest encryption (secrets, subscription tokens, WebSub HMAC secrets). Generate with `openssl rand -hex 32` | +| `[platform_api.database].driver` | `sqlite3` or `postgres` | +| `[platform_api.auth].mode` | `file` (quickstart default), `external_token`, or `idp` — selects exactly one auth mode | +| `[platform_api.auth.jwt].public_key_file` / `private_key_file` | RS256 (asymmetric) PEM keys; `public_key_file` verifies every token, `private_key_file` signs login JWTs in `file` mode. Read via `{{ file }}` — HMAC and unsigned tokens are rejected | +| `[platform_api.auth.idp]` | JWKS-based IDP auth — active when `mode = "idp"`; configure for Asgardeo, Keycloak, Auth0, etc. | +| `[platform_api.auth.file.users]` | Local user credentials, active when `mode = "file"` (change the password hash before sharing) | +| `[platform_api.server.https]` | Listener on `:9243`; `cert_file`/`key_file` point at `cert.pem`/`key.pem` | -See `configs/config-template.toml` and `configs/config-platform-api-template.toml` for a fully-commented reference of every available setting. +Each key's default value is written inline in `configs/config-template.toml` — a fully-commented reference of every available setting and its default for both services, so defaults are not restated here. ## Authentication Modes @@ -111,28 +114,28 @@ The admin user is generated by `setup.sh` (see [Quick Start](#quick-start)). To htpasswd -bnBC 10 "" NEW_PASSWORD | tr -d ':\n' ``` -Replace the `password_hash` value in `configs/config-platform-api.toml` before starting. +Replace the `password_hash` value under `[platform_api.auth.file.users]` in `configs/config.toml` before starting. ### OIDC (production) To delegate login to an external OIDC-compliant provider (Asgardeo, Keycloak, Auth0, etc.) instead of file-based auth, both services need to be reconfigured — the AI Workspace to send users to the IDP, and the Platform API to trust the tokens it issues. 1. Register a **confidential** OIDC application in your IDP with redirect URL `https:///api/auth/callback` (use `https://localhost:5380/api/auth/callback` for local development), a post-logout redirect URL, and enable the **Authorization Code** and **Refresh Token** grants. -2. **AI Workspace** (`configs/config.toml`): set `auth_mode = "oidc"`. Every `[oidc]` key except `scope` defaults to empty and the server refuses to start in OIDC mode until each is set — either directly in the TOML or via its `APIP_AIW_OIDC_*` token in `api-platform.env`: +2. **AI Workspace** (`configs/config.toml`): set `[ai_workspace.auth] mode = "oidc"`. Every `[ai_workspace.auth.oidc]` key except `scope` defaults to empty and the server refuses to start in OIDC mode until each is set — either directly in the TOML or via its `APIP_AIW_AUTH_OIDC_*` token in `api-platform.env`: ```bash - APIP_AIW_OIDC_AUTHORITY=https://idp.example.com - APIP_AIW_OIDC_CLIENT_ID= - APIP_AIW_OIDC_CLIENT_SECRET= - APIP_AIW_OIDC_REDIRECT_URL=https:///api/auth/callback - APIP_AIW_OIDC_POST_LOGOUT_REDIRECT_URL=https:///login + APIP_AIW_AUTH_OIDC_AUTHORITY=https://idp.example.com + APIP_AIW_AUTH_OIDC_CLIENT_ID= + APIP_AIW_AUTH_OIDC_CLIENT_SECRET= + APIP_AIW_AUTH_OIDC_REDIRECT_URL=https:///api/auth/callback + APIP_AIW_AUTH_OIDC_POST_LOGOUT_REDIRECT_URL=https:///login ``` - Leaving `APIP_AIW_OIDC_SCOPE` unset requests the full `ap:*` scope set. + Leaving `APIP_AIW_AUTH_OIDC_SCOPE` unset requests the full `ap:*` scope set. -3. **Platform API** (`configs/config-platform-api.toml`): the `[auth.idp]` fields have no env-var tokens in the quickstart file, so edit the TOML directly — set `enabled = true` and fill in `jwks_url` and `issuer` for your IDP. Then set `[auth.file_based].enabled = false`: while file-based auth is enabled it takes priority, and the IDP is not used regardless of `[auth.idp]`. Align `[auth.idp.claim_mappings]` with `[oidc.claim_mappings]` in `configs/config.toml` — both services must read the same claims out of the same token. +3. **Platform API** (`[platform_api.*]` tables in `configs/config.toml`): the `[platform_api.auth.idp]` fields have no env-var tokens in the quickstart file, so edit the TOML directly — set `[platform_api.auth] mode = "idp"` and fill in `jwks_url` and `issuer` for your IDP. `mode` selects exactly one auth mode, so switching to `"idp"` stops the file-based login endpoint from being used. Align `[platform_api.auth.claim_mappings]` with `[ai_workspace.auth.claim_mappings]` — both services must read the same claims out of the same token. -See `configs/config-template.toml` and `configs/config-platform-api-template.toml` for the full, per-field reference, and the [WSO2 API Platform documentation](https://wso2.com/api-platform/docs/) (AI Workspace section) for a full OIDC setup walkthrough including Asgardeo scope registration. +See `configs/config-template.toml` for the full, per-field reference, and the [WSO2 API Platform documentation](https://wso2.com/api-platform/docs/) (AI Workspace section) for a full OIDC setup walkthrough including Asgardeo scope registration. ## Custom TLS Certificates @@ -144,10 +147,10 @@ docker compose up -d ## Database -The Platform API uses **SQLite** by default (data persisted in a Docker volume). To switch to PostgreSQL, update `configs/config-platform-api.toml`: +The Platform API uses **SQLite** by default (data persisted in a Docker volume). To switch to PostgreSQL, update `[platform_api.database]` in `configs/config.toml`: ```toml -[database] +[platform_api.database] driver = "postgres" host = "your-db-host" port = 5432 diff --git a/portals/ai-workspace/docker-compose.yaml b/portals/ai-workspace/docker-compose.yaml index de0c628dd8..fd3f130018 100644 --- a/portals/ai-workspace/docker-compose.yaml +++ b/portals/ai-workspace/docker-compose.yaml @@ -11,18 +11,19 @@ services: platform-api: - image: ghcr.io/wso2/api-platform/platform-api:0.11.0 + image: ghcr.io/wso2/api-platform/platform-api:0.13.0-SNAPSHOT container_name: platform-api restart: unless-stopped - command: ["-config", "/etc/platform-api/config-platform-api.toml"] + command: ["-config", "/etc/platform-api/config.toml"] env_file: - path: api-platform.env required: true format: raw volumes: - - ./configs/config-platform-api.toml:/etc/platform-api/config-platform-api.toml:ro + - ../../platform-api/config/config.toml:/etc/platform-api/config.toml:ro - platform-api-data:/app/data - - ./resources/certificates:/etc/platform-api/tls:ro + - ./resources/certificates:/app/data/certs:ro + - ./resources/keys:/etc/platform-api/keys:ro ports: - "9243:9243" healthcheck: @@ -33,7 +34,7 @@ services: retries: 3 ai-workspace: - image: ghcr.io/wso2/api-platform/ai-workspace:1.0.0-alpha2-SNAPSHOT + image: ghcr.io/wso2/api-platform/ai-workspace:1.0.0-beta-SNAPSHOT container_name: ai-workspace restart: unless-stopped depends_on: diff --git a/portals/ai-workspace/production/README.md b/portals/ai-workspace/production/README.md index 3e3899c5d6..c065366389 100644 --- a/portals/ai-workspace/production/README.md +++ b/portals/ai-workspace/production/README.md @@ -83,35 +83,31 @@ In each sub-organization: ## 2. Platform API Configuration -The Platform API reads its configuration from `config-platform-api.toml` (mounted at -`/etc/platform-api/config.toml` in the container). Open `configs/config-platform-api.toml` -and update the `[auth.idp]` section for production: +The Platform API reads its configuration from `../../platform-api/config/config.toml` +(mounted at `/etc/platform-api/config.toml` in the container — every portal's +docker-compose.yaml mounts this one file directly, no per-portal copy). Open it +and update the `[platform_api.auth]` section for production: > **Note:** Asgardeo uses `org_id` as the JWT claim for the organization UUID. The Platform > API defaults to `organization`, so the claim name overrides below are required. ```toml -# Disable local JWT auth when delegating entirely to an external IDP. -[auth.jwt] -enabled = false +# Select "idp" mode to delegate entirely to an external IDP. +[platform_api.auth] +mode = "idp" -# Enable JWKS-based IDP authentication. -[auth.idp] -enabled = true +# JWKS-based IDP authentication. +[platform_api.auth.idp] name = "asgardeo" jwks_url = "https://api.asgardeo.io/t//oauth2/jwks" issuer = ["https://api.asgardeo.io/t//oauth2/token"] audience = [""] # Client ID from Asgardeo Protocol tab # Asgardeo-specific claim name overrides. -[auth.idp.claim_mappings] -organization_claim_name = "org_id" -org_name_claim_name = "org_name" -org_handle_claim_name = "org_handle" - -# Disable file-based auth in production. -[auth.file_based] -enabled = false +[platform_api.auth.claim_mappings] +organization = "org_id" +org_name = "org_name" +org_handle = "org_handle" ``` Optional overrides (defaults shown): @@ -120,11 +116,11 @@ Optional overrides (defaults shown): [auth.idp] validation_mode = "scope" # or "role" for role-based auth -[auth.idp.claim_mappings] -user_id_claim_name = "sub" -username_claim_name = "username" -email_claim_name = "email" -scope_claim_name = "scope" +[auth.claim_mappings] +user_id = "sub" +username = "username" +email = "email" +scope = "scope" ``` --- @@ -138,28 +134,31 @@ tables; a value comes from the environment only through an `{{ env }}` token (se Open `configs/config.toml` and fill in the values for your deployment: ```toml +[ai_workspace] # Host shown in the browser address bar. domain = "" # e.g. app.example.com -# Set to "oidc" for production (Asgardeo or any OIDC-compliant IDP). -auth_mode = "oidc" +# Default region assigned to new organizations on first login. +default_org_region = "us" + +[ai_workspace.control_plane] +# The upstream the BFF proxies to, server-to-server. An origin, not a base path — the +# browser never uses it: the SPA calls the same-origin proxy prefix and the BFF forwards. +url = "https://" +[ai_workspace.gateway] # Externally reachable host:port that deployed gateways use to reach the Platform API. controlplane_host = "" -# Default region assigned to new organizations on first login. -default_org_region = "us" - # Available gateway versions shown in the create-gateway version selector (JSON array string). # Each entry: version (helm chart minor), latestVersion (image/chart tag), channel ("STS" | "LTS"). platform_gateway_versions = '[{"version":"1.2","latestVersion":"v1.2.0-alpha2","channel":"STS"}]' -[platform_api] -# The upstream the BFF proxies to, server-to-server. An origin, not a base path — the -# browser never uses it: the SPA calls the same-origin proxy prefix and the BFF forwards. -url = "https://" +[ai_workspace.auth] +# Set to "oidc" for production (Asgardeo or any OIDC-compliant IDP). +mode = "oidc" -[oidc] +[ai_workspace.auth.oidc] # Issuer URL — the BFF auto-discovers OIDC endpoints from # {authority}/.well-known/openid-configuration. authority = "https://api.asgardeo.io/t//oauth2/token" @@ -167,12 +166,13 @@ authority = "https://api.asgardeo.io/t//oauth2/token" # Client ID of the AI Workspace confidential application (from the IDP Protocol tab). client_id = "" -# JWT claim name mappings — this table mirrors [auth.idp.claim_mappings] in the Platform -# API config (section 2) key for key, and the two must agree. -[oidc.claim_mappings] -organization_claim_name = "org_id" -org_name_claim_name = "org_name" -org_handle_claim_name = "org_handle" +# JWT claim name mappings — this table mirrors [platform_api.auth.claim_mappings] in +# the Platform API config (section 2) key for key, and the two must agree. A sibling +# of [ai_workspace.auth.oidc], not nested in it: applies to both auth modes. +[ai_workspace.auth.claim_mappings] +organization = "org_id" +org_name = "org_name" +org_handle = "org_handle" ``` The redirect URLs are ordinary `config.toml` keys. The **client secret is never written into the @@ -180,7 +180,7 @@ file** — it is referenced with an interpolation token resolved at startup. In as a secret file (a Docker/Kubernetes secret) so the value never enters the environment at all: ```toml -[oidc] +[ai_workspace.auth.oidc] # BFF callback registered in the IDP (section 1.2) — NOT the SPA /signin route. redirect_url = "https:///api/auth/callback" post_logout_redirect_url = "https:///login" @@ -199,9 +199,9 @@ Mount the secret at that path, e.g. in `docker-compose.yaml`: Resolution fails closed: a missing or unreadable secret file aborts startup rather than yielding an empty credential. `{{ file }}` paths must live under `/etc/ai-workspace` or `/secrets/ai-workspace` (override with `APIP_CONFIG_FILE_SOURCE_ALLOWLIST`). For a simpler local setup, swap the token for -`'{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}'` and keep the value in the git-ignored `api-platform.env`. +`'{{ env "APIP_AIW_AUTH_OIDC_CLIENT_SECRET" }}'` and keep the value in the git-ignored `api-platform.env`. -> `[oidc] redirect_url` must exactly match the authorized redirect URL registered in the IDP +> `[ai_workspace.auth.oidc] redirect_url` must exactly match the authorized redirect URL registered in the IDP > application (section 1.2). The BFF, not the browser, completes the code exchange. ### Setting config.toml keys from the environment @@ -211,31 +211,20 @@ layer. A key takes its value from the environment when it is written as an `{{ e names the variable explicitly: ```toml -[oidc] -client_id = '{{ env "APIP_AIW_OIDC_CLIENT_ID" "" }}' +[ai_workspace.auth.oidc] +client_id = '{{ env "APIP_AIW_AUTH_OIDC_CLIENT_ID" "" }}' # ^ variable read at startup ^ used when it is unset ``` -Setting `APIP_AIW_OIDC_CLIENT_ID` in a `docker run -e` or a Kubernetes `env:` block then sets -`[oidc] client_id`, with no edit to the file. Setting it while the key is absent from the file, or +Setting `APIP_AIW_AUTH_OIDC_CLIENT_ID` in a `docker run -e` or a Kubernetes `env:` block then sets +`[ai_workspace.auth.oidc] client_id`, with no edit to the file. Setting it while the key is absent from the file, or written as a plain literal, does nothing. The shipped `config.toml` already writes its keys this way, naming each variable by the same -convention: the key's table path uppercased, dots as underscores, prefixed with **`APIP_AIW_`** (the -Platform API uses `APIP_CP_`, the Developer Portal `APIP_DP_`). - -| config.toml key | Variable named by its shipped token | -|--------------------------------------|------------------------------------------| -| `domain` | `APIP_AIW_DOMAIN` | -| `auth_mode` | `APIP_AIW_AUTH_MODE` | -| `controlplane_host` | `APIP_AIW_CONTROLPLANE_HOST` | -| `log_level` | `APIP_AIW_LOG_LEVEL` | -| `[platform_api] url` | `APIP_AIW_PLATFORM_API_URL` | -| `[oidc] authority` | `APIP_AIW_OIDC_AUTHORITY` | -| `[oidc] client_id` | `APIP_AIW_OIDC_CLIENT_ID` | -| `[oidc] client_secret` | `APIP_AIW_OIDC_CLIENT_SECRET` | -| `[oidc] redirect_url` | `APIP_AIW_OIDC_REDIRECT_URL` | -| `[oidc.claim_mappings] organization_claim_name` | `APIP_AIW_OIDC_CLAIM_MAPPINGS_ORGANIZATION_CLAIM_NAME` | +convention: the key's path under `[ai_workspace]` uppercased, dots as underscores, prefixed with +**`APIP_AIW_`** (the Platform API uses `APIP_CP_`, the Developer Portal `APIP_DP_`). Every key's +exact token — and the default it falls back to when the variable is unset — is written inline in +`configs/config-template.toml`; that file is the source of truth, so it is not restated here. A token may name any variable, not only the conventional one — that is what lets a key read a secret that already exists under its own name. For credentials, prefer a mounted secret file @@ -249,8 +238,9 @@ There is no demo mode: startup checks are always on for **both** the `platform-a `ai-workspace` services and fail fast when a requirement is missing. For production, replace the quickstart's `setup.sh` outputs with real values: use OIDC (sections 1–3) instead of the generated file-based admin user, mount TLS certificates from your CA instead of the generated -self-signed pairs, and manage `APIP_CP_ENCRYPTION_KEY` / `APIP_CP_AUTH_JWT_SECRET_KEY` as -stable, real secrets (prefer `{{ file }}` tokens over environment variables). +self-signed pairs, and manage `APIP_CP_ENCRYPTION_KEY` and the RS256 JWT signing keypair +(`resources/keys/jwt_private.pem` / `jwt_public.pem`, read via `{{ file }}`) as stable, real +secrets (prefer `{{ file }}` tokens over environment variables). See [Production hardening](../README.md#production-hardening) in the main README for the full checklist of what each service requires. diff --git a/portals/ai-workspace/setup.sh b/portals/ai-workspace/setup.sh index 4d1eec6d43..0741ba5b84 100755 --- a/portals/ai-workspace/setup.sh +++ b/portals/ai-workspace/setup.sh @@ -15,6 +15,9 @@ cd "$(dirname "$0")" ENV_FILE="api-platform.env" CERTS_DIR="resources/certificates" +# RS256 JWT keypair (PEM). Mounted into the platform-api container at +# /etc/platform-api/keys and read by config.toml via {{ file }}. +KEYS_DIR="resources/keys" FORCE=false CERTS_ONLY=false @@ -105,8 +108,28 @@ ADMIN_PASSWORD="${ADMIN_PASSWORD:-$GENERATED_PASSWORD}" log "Generating secrets into $ENV_FILE ..." ENCRYPTION_KEY="$(openssl rand -hex 32)" log " - APIP_CP_ENCRYPTION_KEY generated" -JWT_SECRET_KEY="$(openssl rand -hex 32)" -log " - APIP_CP_AUTH_JWT_SECRET_KEY generated" + +log "Provisioning Platform API JWT signing keypair (RS256) ..." +# Tokens are signed asymmetrically (RS256), not with a shared HMAC secret. The +# Platform API mints login tokens with the RSA private key and verifies every +# token with the matching public key. A PEM key is multi-line and does not +# survive an env file (one KEY=VALUE per line), so — like the TLS cert above — +# the keypair is written to files and read by config.toml via {{ file }}: +# config.toml -> public_key/private_key = '{{ file "/etc/platform-api/keys/jwt_*.pem" }}' +# resources/keys is mounted into the platform-api container at +# /etc/platform-api/keys (see docker-compose.yaml), which is on the Platform +# API's {{ file }} allowlist. +mkdir -p "$KEYS_DIR" +# PKCS#8 private key + matching SPKI public key — the PEM encodings golang-jwt's +# ParseRSAPrivateKeyFromPEM / ParseRSAPublicKeyFromPEM accept. +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + -out "$KEYS_DIR/jwt_private.pem" 2>/dev/null +openssl rsa -in "$KEYS_DIR/jwt_private.pem" -pubout \ + -out "$KEYS_DIR/jwt_public.pem" 2>/dev/null +# 644 (not 600) so the container's non-root user can read a host-owned file, +# matching the TLS key.pem above. +chmod 644 "$KEYS_DIR/jwt_private.pem" "$KEYS_DIR/jwt_public.pem" +log " - RS256 JWT keypair generated at $KEYS_DIR" log "Provisioning admin credentials ..." ADMIN_PASSWORD_HASH="$(bcrypt_hash "$ADMIN_PASSWORD")" @@ -117,7 +140,6 @@ cat > "$ENV_FILE" <( - 'APIP_AIW_PLATFORM_GATEWAY_VERSIONS', + 'APIP_AIW_GATEWAY_PLATFORM_GATEWAY_VERSIONS', [ { version: '1.2', latestVersion: 'v1.2.0-alpha2', channel: 'STS' } ] @@ -153,7 +165,7 @@ export const POLICY_HUB_WEB_URL = getEnvOrDefault( ); // Platform API base URL. Defaults to a relative path routed same-origin through the -// BFF reverse proxy (/api/proxy/* → Platform API) so the browser only ever talks to +// BFF reverse proxy (/proxy/* → Platform API) so the browser only ever talks to // the app origin, never holds a token, and never sees the platform-api self-signed cert. // Overrides should normally point at another BFF proxy base. Pointing this at the // Platform API directly bypasses the BFF session: the browser holds no token in this @@ -161,7 +173,7 @@ export const POLICY_HUB_WEB_URL = getEnvOrDefault( // path to attach credentials to those calls. export const PLATFORM_API_BASE_URL = getEnvOrDefault( 'APIP_AIW_PLATFORM_API_BASE_URL', - '/api/proxy/api/v0.9' + '/proxy/api/v0.9' ); // Base URL for BFF composite endpoints. These are handled directly by the BFF @@ -172,27 +184,18 @@ export const BFF_COMPOSITE_BASE_URL = '/api/bff'; // Control-plane host shown in gateway setup instructions (host:port). // Distinct from PLATFORM_API_BASE_URL which may be a relative nginx proxy path. export const CONTROLPLANE_HOST = getEnvOrDefault( - 'APIP_AIW_CONTROLPLANE_HOST', + 'APIP_AIW_GATEWAY_CONTROLPLANE_HOST', 'host.docker.internal:9243' ); export const PORTAL_API_BASE_URL = getEnvOrDefault( 'APIP_AIW_PORTAL_API_BASE_URL', - '/api/proxy/api/portal/v0.9' + '/proxy/api/portal/v0.9' ); // CSRF header sent on all BFF requests. Cross-site attackers cannot set a custom // header (CORS is closed), so its presence proves the request is same-origin. -// Must match the BFF's CSRF_HEADER config (default: X-Requested-By). -export const CSRF_HEADER = getEnvOrDefault('APIP_AIW_CSRF_HEADER', 'X-Requested-By'); +// Fixed, not configurable: it is a contract between the BFF and this SPA, not a +// deployment concern. Must match the BFF's config.CSRFHeaderName constant. +export const CSRF_HEADER = 'X-Requested-By'; export const CSRF_VALUE = 'ai-workspace'; - -// JWT claim names for user display — configure to match your IDP's token structure. -// The defaults mirror the BFF's [oidc.claim_mappings] defaults, so both sides read the -// same claim when the key is left unset. -// Common alternatives: 'name', 'given_name', 'preferred_username' (Keycloak), 'upn' (Azure AD) -export const OIDC_USERNAME_CLAIM = getEnvOrDefault('APIP_AIW_OIDC_CLAIM_MAPPINGS_USERNAME_CLAIM_NAME', 'username'); -export const OIDC_EMAIL_CLAIM = getEnvOrDefault('APIP_AIW_OIDC_CLAIM_MAPPINGS_EMAIL_CLAIM_NAME', 'email'); - -// Auth mode: 'basic' (default) posts credentials to /api/portal/v0.9/auth/login; 'oidc' uses react-oidc-context. -export const AUTH_MODE = getEnvOrDefault('APIP_AIW_AUTH_MODE', 'basic') as 'oidc' | 'basic'; diff --git a/portals/ai-workspace/src/contexts/OIDCAppAuthProvider.tsx b/portals/ai-workspace/src/contexts/OIDCAppAuthProvider.tsx index 7e423eb46a..436c4f7d21 100644 --- a/portals/ai-workspace/src/contexts/OIDCAppAuthProvider.tsx +++ b/portals/ai-workspace/src/contexts/OIDCAppAuthProvider.tsx @@ -20,11 +20,11 @@ import React, { useCallback, useMemo } from 'react'; import { useAuth } from 'react-oidc-context'; import { AppAuthContext, type AppUser, type AppOrg } from './AppAuthContext'; import { - OIDC_USERNAME_CLAIM, - OIDC_EMAIL_CLAIM, - OIDC_ORG_ID_CLAIM, - OIDC_ORG_NAME_CLAIM, - OIDC_ORG_HANDLE_CLAIM, + USERNAME_CLAIM, + EMAIL_CLAIM, + ORG_ID_CLAIM, + ORG_NAME_CLAIM, + ORG_HANDLE_CLAIM, } from '../config.env'; import { checkPermission, isPlatformRole } from '../auth/permissions'; import type { PlatformRole } from '../auth/permissions'; @@ -77,16 +77,16 @@ export function OIDCAppAuthProvider({ children }: { children: React.ReactNode }) const claim = (key: string) => (idClaims[key] as string | undefined) || (atClaims[key] as string | undefined) || null; - const orgId = (atClaims[OIDC_ORG_ID_CLAIM] as string | undefined) || null; - const orgName = (atClaims[OIDC_ORG_NAME_CLAIM] as string | undefined) || null; - const orgHandle = (atClaims[OIDC_ORG_HANDLE_CLAIM] as string | undefined) || null; + const orgId = (atClaims[ORG_ID_CLAIM] as string | undefined) || null; + const orgName = (atClaims[ORG_NAME_CLAIM] as string | undefined) || null; + const orgHandle = (atClaims[ORG_HANDLE_CLAIM] as string | undefined) || null; const org: AppOrg | null = (orgId || orgHandle) ? { id: orgId ?? '', name: orgName ?? orgHandle ?? '', handle: orgHandle ?? '' } : null; return { - name: claim(OIDC_USERNAME_CLAIM), - email: claim(OIDC_EMAIL_CLAIM), + name: claim(USERNAME_CLAIM), + email: claim(EMAIL_CLAIM), role, scopes, org, diff --git a/portals/ai-workspace/src/utils/logger.ts b/portals/ai-workspace/src/utils/logger.ts index ed9c385b96..cd14d13200 100644 --- a/portals/ai-workspace/src/utils/logger.ts +++ b/portals/ai-workspace/src/utils/logger.ts @@ -19,7 +19,7 @@ import { DEBUG } from '../config.env'; /** - * Logger utility that respects the APIP_AIW_DEBUG environment variable. + * Logger utility that respects the APIP_AIW_LOGGING_BROWSER_DEBUG environment variable. * All console output is suppressed when DEBUG is disabled (default). * * Usage: diff --git a/portals/ai-workspace/vite.config.ts b/portals/ai-workspace/vite.config.ts index ea1b687325..e402d5777e 100644 --- a/portals/ai-workspace/vite.config.ts +++ b/portals/ai-workspace/vite.config.ts @@ -64,17 +64,16 @@ const readyLogPlugin: PluginOption = { // This mirrors the BFF's runtime allowlist (bff/internal/config/runtime_config.go // browserSafeKeys): the same APIP_AIW_ names work at build time and at runtime, but // only these — a blanket 'APIP_AIW_' prefix would also inline secrets that share the -// namespace (e.g. APIP_AIW_OIDC_CLIENT_SECRET) into the bundle if set at build time. +// namespace (e.g. APIP_AIW_AUTH_OIDC_CLIENT_SECRET) into the bundle if set at build time. const browserSafeEnvVars = [ - 'APIP_AIW_DOMAIN', + 'APIP_AIW_SERVER_DOMAIN', 'APIP_AIW_AUTH_MODE', 'APIP_AIW_DEFAULT_ORG_REGION', - 'APIP_AIW_CONTROLPLANE_HOST', - 'APIP_AIW_PLATFORM_GATEWAY_VERSIONS', - 'APIP_AIW_CSRF_HEADER', - 'APIP_AIW_DEBUG', - 'APIP_AIW_OIDC_SCOPE', - 'APIP_AIW_OIDC_CLAIM_MAPPINGS_', // all claim-name mappings, no secrets share this + 'APIP_AIW_GATEWAY_CONTROLPLANE_HOST', + 'APIP_AIW_GATEWAY_PLATFORM_GATEWAY_VERSIONS', + 'APIP_AIW_LOGGING_BROWSER_DEBUG', + 'APIP_AIW_AUTH_OIDC_SCOPE', + 'APIP_AIW_AUTH_CLAIM_MAPPINGS_', // all claim-name mappings, no secrets share this 'APIP_AIW_DEV_PORTAL_BASE_URL', 'APIP_AIW_API_POLICY_HUB', 'APIP_AIW_POLICY_HUB_WEB_URL', @@ -117,7 +116,7 @@ export default defineConfig({ // In dev, run the BFF locally (default https://localhost:8081) and route // all same-origin BFF traffic to it, mirroring the production topology. // `make bff-run` starts it against configs/config.toml, whose {{ env }} tokens read - // the APIP_AIW_* variables (PLATFORM_API_URL, LISTEN_ADDR, ...). + // the APIP_AIW_* variables (CONTROL_PLANE_URL, SERVER_PORT, ...). '/api': { target: process.env.BFF_DEV_TARGET || 'https://localhost:8081', changeOrigin: true, diff --git a/portals/developer-portal/.gitignore b/portals/developer-portal/.gitignore index 7e10a835c3..ddff699dad 100644 --- a/portals/developer-portal/.gitignore +++ b/portals/developer-portal/.gitignore @@ -97,10 +97,12 @@ resources/mock .env api-platform.env -configs/config-platform-api.toml # ./setup.sh output: bind-mounted TLS cert resources/certificates/ +# ./setup.sh output: bind-mounted RS256 JWT signing keypair +resources/keys/ + # Test reports it/reports/* diff --git a/portals/developer-portal/Makefile b/portals/developer-portal/Makefile index b15b30675f..a5fc039853 100644 --- a/portals/developer-portal/Makefile +++ b/portals/developer-portal/Makefile @@ -147,8 +147,14 @@ dist: clean-dist ## Build standalone developer portal distribution zip @cp -R samples/mcps $(DIST_DIR)/resources/samples/ @cp configs/config.toml $(DIST_DIR)/configs/config.toml @cp configs/config-template.toml $(DIST_DIR)/configs/config-template.toml - @cp configs/config-platform-api-template.toml $(DIST_DIR)/configs/config-platform-api-template.toml - @cp docker-compose.yaml $(DIST_DIR)/docker-compose.yaml + # The source docker-compose.yaml mounts ../../platform-api/config/config.toml + # directly (the monorepo's single source-of-truth Platform API config, no + # per-portal copy) — the standalone zip has no platform-api/ sibling, so bake + # a real copy into its own configs/ dir and repoint the compose file at it. + @cp ../../platform-api/config/config.toml $(DIST_DIR)/configs/config-platform-api.toml + @cp ../../platform-api/config/config-template.toml $(DIST_DIR)/configs/config-platform-api-template.toml + @sed 's#\.\./\.\./platform-api/config/config\.toml#./configs/config-platform-api.toml#' \ + docker-compose.yaml > $(DIST_DIR)/docker-compose.yaml @cp distribution/README.md $(DIST_DIR)/README.md @mkdir -p $(DIST_DIR)/scripts @cp scripts/setup.sh $(DIST_DIR)/scripts/setup.sh diff --git a/portals/developer-portal/README.md b/portals/developer-portal/README.md index b0b0550b96..64d49b0346 100644 --- a/portals/developer-portal/README.md +++ b/portals/developer-portal/README.md @@ -31,7 +31,7 @@ The fastest way to get the portal running — no local Node install required. Re docker compose up ``` -`./scripts/setup.sh` is a one-time step: it generates devportal's and the Platform API's encryption/JWT secrets, a self-signed TLS certificate, and an admin user into `api-platform.env` (git-ignored) and `configs/config-platform-api.toml` (also git-ignored — copy `configs/config-platform-api-template.toml` instead for a static, no-dependencies starting point). It prompts for an admin username/password interactively, or generates a random password if you press Enter; set `ADMIN_USERNAME`/`ADMIN_PASSWORD` env vars to skip the prompts (e.g. in CI). Safe to re-run — it only fills in what's missing and never overwrites an existing value; to build devportal from source instead of using the published image, run `docker compose up --build`. +`./scripts/setup.sh` is a one-time step: it generates devportal's and the Platform API's encryption/JWT secrets, a self-signed TLS certificate, and an admin user into `api-platform.env` (git-ignored). It prompts for an admin username/password interactively, or generates a random password if you press Enter; set `ADMIN_USERNAME`/`ADMIN_PASSWORD` env vars to skip the prompts (e.g. in CI). Safe to re-run — it only fills in what's missing and never overwrites an existing value; to build devportal from source instead of using the published image, run `docker compose up --build`. Then open **https://localhost:3000/default/views/default** and log in with the admin credentials `./scripts/setup.sh` printed. @@ -229,10 +229,10 @@ The full annotated list of settings is in [`configs/config-template.toml`](confi ### Local auth -For quick exploration without an IdP, the portal delegates credential validation to a Platform API sidecar. Users, bcrypt-hashed passwords, and `dp:*` scopes are defined in `configs/config-platform-api.toml` (copy from `configs/config-platform-api-template.toml`): +For quick exploration without an IdP, the portal delegates credential validation to a Platform API sidecar. `docker-compose.yaml` mounts the Platform API's own [`../../platform-api/config/config.toml`](../../platform-api/config/config.toml) directly — there is no per-portal copy. Users, bcrypt-hashed passwords, and `dp:*` scopes are defined there, under `[[platform_api.auth.file.users]]`: ```toml -[[auth.file_based.users]] +[[platform_api.auth.file.users]] username = "admin" password_hash = "$2y$10$..." # bcrypt hash — generate with: htpasswd -bnBC 12 "" | tr -d ':\n' scopes = "dp:org_manage dp:api_manage ..." @@ -243,10 +243,12 @@ The portal config (or `APIP_DP_PLATFORMAPI_*` env vars) must point to the Platfo ```toml [platform_api] base_url = "https://localhost:9243" # env: APIP_DP_PLATFORMAPI_BASEURL -jwt_secret = "" # same as the Platform API's APIP_CP_AUTH_JWT_SECRET_KEY — env: APIP_DP_PLATFORMAPI_JWTSECRET +jwt_private_key = "" # PEM RSA private key that signs portal-minted tokens; must match the Platform API's auth.jwt.public_key — env: APIP_DP_PLATFORMAPI_JWTPRIVATEKEY insecure = true # Platform API uses a self-signed cert ``` +Tokens are signed asymmetrically (RS256): the portal signs with the RSA private key above and the Platform API verifies against its `auth.jwt.public_key`. There is no shared HMAC secret — the two sides never exchange signing material. + For production, configure an OIDC identity provider per organization instead of local auth. ### Environment variable overrides diff --git a/portals/developer-portal/configs/config-platform-api-template.toml b/portals/developer-portal/configs/config-platform-api-template.toml deleted file mode 100644 index 9cf0bb1cdd..0000000000 --- a/portals/developer-portal/configs/config-platform-api-template.toml +++ /dev/null @@ -1,106 +0,0 @@ -# -------------------------------------------------------------------- -# Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). -# -# WSO2 LLC. licenses this file to you under the Apache License, -# Version 2.0 (the "License"); you may not use this file except -# in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -------------------------------------------------------------------- -# -# Platform API configuration for the Developer Portal. -# -# This file defines the local users who can log in to the Developer Portal -# via the username/password login form. Credentials are validated by the -# Platform API; the Developer Portal never sees the raw passwords. -# -# Admin credentials: -# ./setup.sh generates APIP_CP_ADMIN_USERNAME / APIP_CP_ADMIN_PASSWORD_HASH into -# api-platform.env (prompting for a username/password, or randomly generating -# one) and the {{ env "..." }} tokens below resolve them. Setting up manually -# instead: generate a bcrypt hash with -# htpasswd -bnBC 12 "" | tr -d ':\n' and put both values in -# `keys.env` file — never hardcode raw values here or as literals in -# docker-compose.yaml. -# -# Secrets: -# APIP_CP_AUTH_JWT_SECRET_KEY (signs login JWTs; the Developer Portal verifies them with -# the same value via APIP_DP_PLATFORMAPI_JWTSECRET) and APIP_CP_ENCRYPTION_KEY (encrypts -# data at rest) are both REQUIRED 32-byte keys and are never auto-generated — the -# Platform API fails to start if either is missing or malformed. Generate each -# with: openssl rand -hex 32. Put the values in `keys.env` file and -# let the {{ env "..." }} tokens below resolve them — never hardcode raw values -# here or as literals in docker-compose.yaml. Files are preferred in production -# ({{ file "..." }}). -# -------------------------------------------------------------------- - -# --------------------------------------------------------------------------- -# Server -# --------------------------------------------------------------------------- -log_level = "INFO" # DEBUG | INFO | WARN | ERROR - -# --------------------------------------------------------------------------- -# Encryption -# --------------------------------------------------------------------------- -# 32-byte key (64 hex chars or base64) for all at-rest encryption. REQUIRED. -# Resolved from the APIP_CP_ENCRYPTION_KEY env var in `keys.env` file. Files preferred: -# encryption_key = '{{ file "/secrets/platform-api/encryption_key" }}' -encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' - -# --------------------------------------------------------------------------- -# HTTPS -# --------------------------------------------------------------------------- -[https] -enabled = true -port = "9243" -# Directory containing cert.pem and key.pem — must match the volume mount in -# docker-compose.yaml. There is no self-signed fallback. -cert_dir = "/etc/platform-api/tls" - -# --------------------------------------------------------------------------- -# Database -# --------------------------------------------------------------------------- -[database] -driver = "sqlite3" -path = "/app/data/platform-api-devportal.db" - -# --------------------------------------------------------------------------- -# Authentication -# --------------------------------------------------------------------------- -[auth.jwt] -enabled = true -issuer = "platform-api" -# 32-byte key that signs login JWTs, shared with the Developer Portal -# (APIP_DP_PLATFORMAPI_JWTSECRET must match). Resolved from APIP_CP_AUTH_JWT_SECRET_KEY -# from `keys.env` file; files preferred: secret_key = '{{ file "/secrets/platform-api/jwt_secret" }}' -secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' -# Skip signature verification. MUST stay false — the server refuses to start -# when signature validation is disabled. -skip_validation = false - -[auth.idp] -enabled = false - -[auth.file_based] -enabled = true - -[auth.file_based.organization] -id = "default" # Required: organization handle (URL-safe slug) -display_name = "Default" -region = "us" - -# Admin user — username/password resolved from APIP_CP_ADMIN_USERNAME / -# APIP_CP_ADMIN_PASSWORD_HASH (see "Admin credentials" note above). -# Scopes use the dp:* namespace understood by the Developer Portal REST API. -# Add dp:*_manage scopes to grant full access to every resource area. -[[auth.file_based.users]] -username = '{{ env "APIP_CP_ADMIN_USERNAME" }}' -password_hash = '{{ env "APIP_CP_ADMIN_PASSWORD_HASH" }}' -scopes = "ap:organization:manage ap:gateway:manage ap:gateway_custom_policy:manage ap:rest_api:manage ap:llm_provider:manage ap:llm_proxy:manage ap:mcp_proxy:manage ap:webbroker_api:manage ap:websub_api:manage ap:application:manage ap:subscription:manage ap:subscription_plan:manage ap:project:manage ap:llm_template:manage ap:devportal:manage ap:git:read ap:api_key:read dp:org_read dp:org_write dp:org_manage dp:org_delete dp:org_content_read dp:org_content_write dp:org_content_manage dp:org_content_delete dp:api_read dp:api_write dp:api_manage dp:api_delete dp:api_content_read dp:api_content_write dp:api_content_manage dp:api_content_delete dp:mcp_create dp:mcp_read dp:mcp_update dp:mcp_delete dp:mcp_manage dp:mcp_content_create dp:mcp_content_read dp:mcp_content_update dp:mcp_content_delete dp:mcp_content_manage dp:mcp_key_create dp:mcp_key_read dp:mcp_key_update dp:mcp_key_revoke dp:mcp_key_manage dp:api_key_read dp:api_key_write dp:api_key_manage dp:api_key_revoke dp:api_flow_read dp:api_flow_write dp:api_flow_manage dp:api_flow_delete dp:api_workflow_read dp:api_workflow_create dp:api_workflow_update dp:api_workflow_delete dp:api_workflow_manage dp:app_read dp:app_write dp:app_manage dp:app_delete dp:app_key_write dp:app_key_manage dp:app_key_revoke dp:app_key_mapping_read dp:app_key_mapping_write dp:app_key_mapping_manage dp:subscription_read dp:subscription_write dp:subscription_manage dp:subscription_delete dp:sub_plan_read dp:sub_plan_write dp:sub_plan_manage dp:sub_plan_delete dp:idp_read dp:idp_write dp:idp_manage dp:idp_delete dp:view_read dp:view_write dp:view_manage dp:view_delete dp:km_read dp:km_write dp:km_manage dp:km_delete dp:label_read dp:label_write dp:label_manage dp:label_delete dp:provider_read dp:provider_write dp:provider_manage dp:provider_delete dp:event_read dp:delivery_manage dp:utility_write dp:utility_manage dp:webhook_subscriber_create dp:webhook_subscriber_read dp:webhook_subscriber_update dp:webhook_subscriber_delete dp:webhook_subscriber_manage dev" - -# --------------------------------------------------------------------------- -# DevPortal integration — disabled (devportal calls us, not the other way) -# --------------------------------------------------------------------------- -[default_devportal] -enabled = false diff --git a/portals/developer-portal/docker-compose.platform-api.yaml b/portals/developer-portal/docker-compose.platform-api.yaml index 2ddf5bb58b..6db5dc214a 100644 --- a/portals/developer-portal/docker-compose.platform-api.yaml +++ b/portals/developer-portal/docker-compose.platform-api.yaml @@ -19,8 +19,8 @@ # Platform API only — for local development with npm start # # Quick start: -# 1. Run ./scripts/setup.sh once — generates the required secrets into api-platform.env -# (also read directly by `npm start` below) and configs/config-platform-api.toml. +# 1. Run ./scripts/setup.sh once — generates the required secrets into +# api-platform.env (also read directly by `npm start` below). # 2. docker compose -f docker-compose.platform-api.yaml up -d # 3. npm start # 4. Open http://localhost:3000 @@ -31,15 +31,22 @@ services: image: ghcr.io/wso2/api-platform/platform-api:0.12.0 container_name: platform-api restart: unless-stopped - command: ["-config", "/etc/platform-api/config-platform-api.toml"] + # Mounts the single source-of-truth config shipped with the Platform API + # itself (../../platform-api/config/config.toml) rather than keeping a + # separate copy under this portal's configs/ — every portal's compose + # file mounts the same file. + command: ["-config", "/etc/platform-api/config.toml"] env_file: - path: api-platform.env required: true format: raw volumes: - - ./configs/config-platform-api.toml:/etc/platform-api/config-platform-api.toml:ro + - ../../platform-api/config/config.toml:/etc/platform-api/config.toml:ro - platform-api-data:/app/data - - ./resources/certificates:/etc/platform-api/tls:ro + # Certs land under /app/data/certs to match the binary's compiled + # default cert_file/key_file paths (./data/certs/{cert,key}.pem, + # resolved against WORKDIR /app) — no cert path override needed. + - ./resources/certificates:/app/data/certs:ro ports: - "9243:9243" healthcheck: diff --git a/portals/developer-portal/docker-compose.yaml b/portals/developer-portal/docker-compose.yaml index 2eccd6e47f..f329fd85a2 100644 --- a/portals/developer-portal/docker-compose.yaml +++ b/portals/developer-portal/docker-compose.yaml @@ -13,15 +13,27 @@ services: image: ghcr.io/wso2/api-platform/platform-api:0.12.0 container_name: platform-api restart: unless-stopped - command: ["-config", "/etc/platform-api/config-platform-api.toml"] + # Mounts the single source-of-truth config shipped with the Platform API + # itself (../../platform-api/config/config.toml) rather than keeping a + # separate copy under this portal's configs/ — every portal's compose + # file mounts the same file. + command: ["-config", "/etc/platform-api/config.toml"] env_file: - path: api-platform.env required: true format: raw volumes: - - ./configs/config-platform-api.toml:/etc/platform-api/config-platform-api.toml:ro + - ../../platform-api/config/config.toml:/etc/platform-api/config.toml:ro - platform-api-data:/app/data - - ./resources/certificates:/etc/platform-api/tls:ro + # Certs land under /app/data/certs to match the binary's compiled + # default cert_file/key_file paths (./data/certs/{cert,key}.pem, + # resolved against WORKDIR /app) — no cert path override needed. + - ./resources/certificates:/app/data/certs:ro + # RS256 JWT signing/verification keys, generated by scripts/setup.sh. + # Mounted under /etc/platform-api/keys so config.toml's + # {{ file "/etc/platform-api/keys/jwt_*.pem" }} references resolve (that + # prefix is on the Platform API's {{ file }} allowlist). + - ./resources/keys:/etc/platform-api/keys:ro ports: - "9243:9243" healthcheck: diff --git a/portals/developer-portal/it/Makefile b/portals/developer-portal/it/Makefile index 171cc89321..fd7010aa0d 100644 --- a/portals/developer-portal/it/Makefile +++ b/portals/developer-portal/it/Makefile @@ -36,6 +36,11 @@ CYPRESS_PLATFORM ?= $(shell [ "$$(uname -m)" = "arm64" ] && echo "linux/arm64" | # platform-api (docker-compose.test*.yaml) instead of the old certgen init # container + named volume. 644, not 600, so the container's non-root user can # read a file owned by the host user (same tradeoff as setup.sh's CERT_FILE_MODE). +# +# Also provisions the RS256 JWT signing keypair platform-api's auth.jwt config +# requires (config-platform-api-it.toml reads it via {{ file }}) — tokens are +# signed asymmetrically, there is no shared HMAC secret to pass via env anymore. +# Written into the same dir so one bind mount covers both TLS and JWT material. ensure-certs: @mkdir -p $(CERT_DIR) @if [ -f $(CERT_DIR)/cert.pem ] && [ -f $(CERT_DIR)/key.pem ]; then \ @@ -48,6 +53,16 @@ ensure-certs: -addext "subjectAltName=DNS:localhost,DNS:platform-api,IP:127.0.0.1" 2>/dev/null; \ chmod 644 $(CERT_DIR)/key.pem $(CERT_DIR)/cert.pem; \ fi + @if [ -f $(CERT_DIR)/jwt_private.pem ] && [ -f $(CERT_DIR)/jwt_public.pem ]; then \ + echo "Using existing $(CERT_DIR)/jwt_{private,public}.pem"; \ + else \ + echo "Generating platform-api RS256 JWT keypair at $(CERT_DIR) ..."; \ + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + -out $(CERT_DIR)/jwt_private.pem 2>/dev/null; \ + openssl rsa -in $(CERT_DIR)/jwt_private.pem -pubout \ + -out $(CERT_DIR)/jwt_public.pem 2>/dev/null; \ + chmod 644 $(CERT_DIR)/jwt_private.pem $(CERT_DIR)/jwt_public.pem; \ + fi # Tag the pre-built developer-portal image as :test so docker-compose.test.yaml can # reference a stable :test tag regardless of whether the local build is :VERSION. diff --git a/portals/developer-portal/it/configs/config-platform-api-it.toml b/portals/developer-portal/it/configs/config-platform-api-it.toml index 48c52818c6..86764acd6e 100644 --- a/portals/developer-portal/it/configs/config-platform-api-it.toml +++ b/portals/developer-portal/it/configs/config-platform-api-it.toml @@ -18,34 +18,42 @@ # Regenerate a password hash: htpasswd -bnBC 10 "" | tr -d ':\n' # -------------------------------------------------------------------- -log_level = "INFO" -port = "9243" +[platform_api] +[platform_api.logging] +level = '{{ env "APIP_CP_LOGGING_LEVEL" "info" }}' + +[platform_api.security] encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' -[database] -driver = "sqlite3" -path = "/app/data/platform-api-it.db" +[platform_api.server.https] +enabled = '{{ env "APIP_CP_SERVER_HTTPS_ENABLED" "true" }}' +port = '{{ env "APIP_CP_SERVER_HTTPS_PORT" "9243" }}' +cert_file = '{{ env "APIP_CP_SERVER_HTTPS_CERT_FILE" "/app/data/certs/cert.pem" }}' +key_file = '{{ env "APIP_CP_SERVER_HTTPS_KEY_FILE" "/app/data/certs/key.pem" }}' -[auth.jwt] -enabled = true -issuer = "platform-api-it" -secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' -skip_validation = false +[platform_api.database] +driver = '{{ env "APIP_CP_DATABASE_DRIVER" "sqlite3" }}' +path = '{{ env "APIP_CP_DATABASE_PATH" "/app/data/platform-api-it.db" }}' -[auth.idp] -enabled = false +[platform_api.auth] +mode = '{{ env "APIP_CP_AUTH_MODE" "file" }}' -[auth.file_based] -enabled = true +[platform_api.auth.jwt] +issuer = '{{ env "APIP_CP_AUTH_JWT_ISSUER" "platform-api-it" }}' +# RS256 keypair generated by `make ensure-certs` (it/Makefile) and bind-mounted +# read-only at /etc/platform-api/keys. The server reads these paths directly +# rather than inlining the PEM content into config. +public_key_file = '{{ env "APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE" "/etc/platform-api/keys/jwt_public.pem" }}' +private_key_file = '{{ env "APIP_CP_AUTH_JWT_PRIVATE_KEY_FILE" "/etc/platform-api/keys/jwt_private.pem" }}' -[auth.file_based.organization] -id = "default" -display_name = "Default" -region = "us" +[platform_api.auth.file.organization] +id = '{{ env "APIP_CP_AUTH_FILE_ORGANIZATION_ID" "default" }}' +display_name = '{{ env "APIP_CP_AUTH_FILE_ORGANIZATION_DISPLAY_NAME" "Default" }}' +region = '{{ env "APIP_CP_AUTH_FILE_ORGANIZATION_REGION" "us" }}' # Full access — organization management plus every resource area. -[[auth.file_based.users]] +[[platform_api.auth.file.users]] username = "admin" password_hash = "$2y$10$U2yKMwGamGwDoMu0hRPT7u8nCuP8z/qxHFOKV6dhIxkJN9NJ0eVQ." scopes = "dp:org_read dp:org_create dp:org_update dp:org_manage dp:org_delete dp:org_content_read dp:org_content_write dp:org_content_manage dp:org_content_delete dp:api_read dp:api_create dp:api_update dp:api_manage dp:api_delete dp:api_content_read dp:api_content_write dp:api_content_manage dp:api_content_delete dp:mcp_read dp:mcp_create dp:mcp_update dp:mcp_manage dp:mcp_delete dp:mcp_content_read dp:mcp_content_create dp:mcp_content_update dp:mcp_content_manage dp:mcp_content_delete dp:api_key_create dp:api_key_read dp:api_key_update dp:api_key_manage dp:api_key_revoke dp:mcp_key_create dp:mcp_key_read dp:mcp_key_update dp:mcp_key_manage dp:mcp_key_revoke dp:api_workflow_create dp:api_workflow_read dp:api_workflow_update dp:api_workflow_delete dp:api_workflow_manage dp:app_create dp:app_read dp:app_update dp:app_manage dp:app_delete dp:app_key_create dp:app_key_manage dp:app_key_revoke dp:app_key_mapping_read dp:app_key_mapping_write dp:app_key_mapping_manage dp:subscription_create dp:subscription_read dp:subscription_update dp:subscription_manage dp:subscription_delete dp:sub_plan_create dp:sub_plan_read dp:sub_plan_update dp:sub_plan_manage dp:sub_plan_delete dp:km_create dp:km_read dp:km_update dp:km_manage dp:km_delete dp:view_create dp:view_read dp:view_update dp:view_manage dp:view_delete dp:label_create dp:label_read dp:label_update dp:label_manage dp:label_delete dp:webhook_subscriber_create dp:webhook_subscriber_read dp:webhook_subscriber_update dp:webhook_subscriber_delete dp:webhook_subscriber_manage dp:event_read dp:delivery_manage" @@ -53,20 +61,14 @@ scopes = "dp:org_read dp:org_create dp:org_update dp:org_manage dp:org_de # Content/config manager — APIs, MCP servers, key managers, subscription # plans, views/labels, webhook subscribers, API workflows. No org management, # no application ownership (that's the developer's). -[[auth.file_based.users]] +[[platform_api.auth.file.users]] username = "publisher" password_hash = "$2y$10$BN9I5oPs34clNmhlO0CX0uDMKMnh9xkczGGmuLiInXSe/KOF5wqFW" scopes = "dp:org_read dp:api_read dp:api_create dp:api_update dp:api_manage dp:api_delete dp:api_content_read dp:api_content_write dp:api_content_manage dp:api_content_delete dp:mcp_read dp:mcp_create dp:mcp_update dp:mcp_manage dp:mcp_delete dp:mcp_content_read dp:mcp_content_create dp:mcp_content_update dp:mcp_content_manage dp:mcp_content_delete dp:api_key_create dp:api_key_read dp:api_key_update dp:api_key_manage dp:api_key_revoke dp:mcp_key_create dp:mcp_key_read dp:mcp_key_update dp:mcp_key_manage dp:mcp_key_revoke dp:api_workflow_create dp:api_workflow_read dp:api_workflow_update dp:api_workflow_delete dp:api_workflow_manage dp:sub_plan_create dp:sub_plan_read dp:sub_plan_update dp:sub_plan_manage dp:sub_plan_delete dp:km_create dp:km_read dp:km_update dp:km_manage dp:km_delete dp:view_create dp:view_read dp:view_update dp:view_manage dp:view_delete dp:label_create dp:label_read dp:label_update dp:label_manage dp:label_delete dp:webhook_subscriber_create dp:webhook_subscriber_read dp:webhook_subscriber_update dp:webhook_subscriber_delete dp:webhook_subscriber_manage dp:event_read" # Portal end-user — read APIs/MCP servers, own applications, subscriptions, # and API keys. No org, key-manager, view/label, or webhook-subscriber management. -[[auth.file_based.users]] +[[platform_api.auth.file.users]] username = "developer" password_hash = "$2y$10$jX3o2E5jF4i3EOgoyJ0k.uegbDYmsmFNDfIxnvcZgTNJifAPjgKKK" scopes = "dp:org_read dp:api_read dp:api_content_read dp:mcp_read dp:mcp_content_read dp:api_key_create dp:api_key_read dp:api_key_update dp:api_key_manage dp:api_key_revoke dp:app_create dp:app_read dp:app_update dp:app_manage dp:app_delete dp:app_key_create dp:app_key_manage dp:app_key_revoke dp:app_key_mapping_read dp:app_key_mapping_write dp:app_key_mapping_manage dp:subscription_create dp:subscription_read dp:subscription_update dp:subscription_manage dp:subscription_delete dp:sub_plan_read dp:view_read dp:label_read" - -[tls] -cert_dir = "/app/data/certs" - -[default_devportal] -enabled = false diff --git a/portals/developer-portal/it/docker-compose.test.postgres.yaml b/portals/developer-portal/it/docker-compose.test.postgres.yaml index 7bea200f4f..7be6258f7f 100644 --- a/portals/developer-portal/it/docker-compose.test.postgres.yaml +++ b/portals/developer-portal/it/docker-compose.test.postgres.yaml @@ -42,18 +42,25 @@ services: - it-devportal-network platform-api: - image: ghcr.io/wso2/api-platform/platform-api:0.12.0 + # Overridable so CI can point this at a freshly built image (make -C platform-api + # build) instead of the last release — this fixture's config-platform-api-it.toml + # tracks platform-api's current config schema, which a pinned old release may not + # understand. + image: ${PLATFORM_API_IMAGE:-ghcr.io/wso2/api-platform/platform-api:0.12.0} command: ["-config", "/etc/platform-api/config-platform-api.toml"] environment: APIP_CP_ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - APIP_CP_AUTH_JWT_SECRET_KEY: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" volumes: - ./configs/config-platform-api-it.toml:/etc/platform-api/config-platform-api.toml:ro - platform-api-data:/app/data - # TLS pair generated once, host-side, by `make ensure-certs` (mirrors - # ../scripts/setup.sh's approach for the production compose) — read-only, - # 644-mode so the container's non-root user can read a host-owned file. + # TLS pair + RS256 JWT keypair generated once, host-side, by `make + # ensure-certs` (mirrors ../scripts/setup.sh's approach for the production + # compose) — read-only, 644-mode so the container's non-root user can read + # a host-owned file. Same host dir, two mount points: TLS cert/key.pem + # under /app/data/certs, jwt_{public,private}.pem under + # /etc/platform-api/keys (the Platform API's {{ file }} allowlist). - ./.certs:/app/data/certs:ro + - ./.certs:/etc/platform-api/keys:ro healthcheck: test: ["CMD", "curl", "-fk", "https://localhost:9243/health"] interval: 5s diff --git a/portals/developer-portal/it/docker-compose.test.yaml b/portals/developer-portal/it/docker-compose.test.yaml index 0705c45657..5a47b44fa0 100644 --- a/portals/developer-portal/it/docker-compose.test.yaml +++ b/portals/developer-portal/it/docker-compose.test.yaml @@ -26,18 +26,25 @@ services: platform-api: - image: ghcr.io/wso2/api-platform/platform-api:0.12.0 + # Overridable so CI can point this at a freshly built image (make -C platform-api + # build) instead of the last release — this fixture's config-platform-api-it.toml + # tracks platform-api's current config schema, which a pinned old release may not + # understand. + image: ${PLATFORM_API_IMAGE:-ghcr.io/wso2/api-platform/platform-api:0.12.0} command: ["-config", "/etc/platform-api/config-platform-api.toml"] environment: APIP_CP_ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - APIP_CP_AUTH_JWT_SECRET_KEY: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" volumes: - ./configs/config-platform-api-it.toml:/etc/platform-api/config-platform-api.toml:ro - platform-api-data:/app/data - # TLS pair generated once, host-side, by `make ensure-certs` (mirrors - # ../scripts/setup.sh's approach for the production compose) — read-only, - # 644-mode so the container's non-root user can read a host-owned file. + # TLS pair + RS256 JWT keypair generated once, host-side, by `make + # ensure-certs` (mirrors ../scripts/setup.sh's approach for the production + # compose) — read-only, 644-mode so the container's non-root user can read + # a host-owned file. Same host dir, two mount points: TLS cert/key.pem + # under /app/data/certs, jwt_{public,private}.pem under + # /etc/platform-api/keys (the Platform API's {{ file }} allowlist). - ./.certs:/app/data/certs:ro + - ./.certs:/etc/platform-api/keys:ro healthcheck: test: ["CMD", "curl", "-fk", "https://localhost:9243/health"] interval: 5s diff --git a/portals/developer-portal/scripts/setup.sh b/portals/developer-portal/scripts/setup.sh index 9a3d27a962..e34aeee6a6 100755 --- a/portals/developer-portal/scripts/setup.sh +++ b/portals/developer-portal/scripts/setup.sh @@ -24,9 +24,10 @@ # - a self-signed TLS certificate for devportal # - devportal's own encryption/session keys (APIP_DP_SECURITY_*) # - the Platform API's at-rest encryption key (APIP_CP_ENCRYPTION_KEY) -# - a shared JWT signing key for the Platform API (APIP_CP_AUTH_JWT_SECRET_KEY, -# written a second time as APIP_DP_PLATFORMAPI_JWTSECRET since devportal's -# config.toml references it under its own name) +# - an RS256 JWT signing keypair for the Platform API, written as PEM files +# under resources/keys (jwt_private.pem / jwt_public.pem) and read by +# config.toml via {{ file }} — tokens are signed asymmetrically, so there is +# no shared HMAC secret to copy between services # - an admin username/password (prompted interactively — see below), bcrypt-hashed # into APIP_CP_ADMIN_USERNAME / APIP_CP_ADMIN_PASSWORD_HASH # @@ -70,7 +71,9 @@ cd "$ROOT_DIR" ENV_FILE="$ROOT_DIR/api-platform.env" DEVPORTAL_CERT_DIR="$ROOT_DIR/resources/certificates" -PLATFORM_API_CONFIG="$ROOT_DIR/configs/config-platform-api.toml" +# RS256 JWT keypair (PEM). Mounted into the platform-api container at +# /etc/platform-api/keys and read by config.toml via {{ file }}. +JWT_KEY_DIR="$ROOT_DIR/resources/keys" # Bind-mounted into a container running as a non-root UID: 644 (not 600) so the # container user can read a file owned by the host user. Local single-user @@ -128,84 +131,27 @@ set_env_var "APIP_DP_SECURITY_SESSIONSECRET" "$(openssl rand -hex 32)" log "Generating Platform API encryption key into api-platform.env ..." set_env_var "APIP_CP_ENCRYPTION_KEY" "$(openssl rand -hex 32)" -log "Generating shared Platform API JWT signing key into api-platform.env ..." -# Written under both names it needs to reach: APIP_CP_AUTH_JWT_SECRET_KEY for the -# platform-api container's own config-platform-api.toml reference, -# APIP_DP_PLATFORMAPI_JWTSECRET for the devportal container's config.toml -# reference — same value, two names, since each config.toml reads a variable -# only under its own exact name. -if grep -q "^APIP_CP_AUTH_JWT_SECRET_KEY=" "$ENV_FILE" 2>/dev/null; then - log " - APIP_CP_AUTH_JWT_SECRET_KEY already set in api-platform.env, leaving as-is" +log "Provisioning Platform API JWT signing keypair (RS256) ..." +# Tokens are signed asymmetrically now (RS256), not with a shared HMAC secret. +# The Platform API mints login tokens with the RSA private key and verifies every +# token with the matching public key. A PEM key is multi-line and does not survive +# an env file (one KEY=VALUE per line), so — like the TLS cert above — the keypair +# is written to files and read by config.toml via {{ file }}: +# config.toml -> public_key/private_key = '{{ file "/etc/platform-api/keys/jwt_*.pem" }}' +# resources/keys is mounted into the platform-api container at /etc/platform-api/keys +# (see docker-compose.yaml), which is on the Platform API's {{ file }} allowlist. +if [ -f "$JWT_KEY_DIR/jwt_private.pem" ] && [ -f "$JWT_KEY_DIR/jwt_public.pem" ]; then + log " - $JWT_KEY_DIR already has a JWT keypair, leaving as-is" else - printf 'APIP_CP_AUTH_JWT_SECRET_KEY=%s\n' "$(openssl rand -hex 32)" >> "$ENV_FILE" - log " - APIP_CP_AUTH_JWT_SECRET_KEY generated" -fi -JWT_SECRET_KEY="$(get_env_var APIP_CP_AUTH_JWT_SECRET_KEY)" -set_env_var "APIP_DP_PLATFORMAPI_JWTSECRET" "$JWT_SECRET_KEY" - -# Full-access scopes for the seeded admin user — ap:* (platform-admin) plus every -# dp:*_manage scope so it can manage every Developer Portal resource area. A plain -# literal in config-platform-api.toml (never templated), since it carries no secret. -ADMIN_SCOPES="ap:organization:manage ap:gateway:manage ap:gateway_custom_policy:manage ap:rest_api:manage ap:llm_provider:manage ap:llm_proxy:manage ap:mcp_proxy:manage ap:webbroker_api:manage ap:websub_api:manage ap:application:manage ap:subscription:manage ap:subscription_plan:manage ap:project:manage ap:llm_template:manage ap:devportal:manage ap:git:read ap:api_key:read dp:org_read dp:org_write dp:org_manage dp:org_delete dp:org_content_read dp:org_content_write dp:org_content_manage dp:org_content_delete dp:api_read dp:api_write dp:api_manage dp:api_delete dp:api_content_read dp:api_content_write dp:api_content_manage dp:api_content_delete dp:mcp_create dp:mcp_read dp:mcp_update dp:mcp_delete dp:mcp_manage dp:mcp_content_create dp:mcp_content_read dp:mcp_content_update dp:mcp_content_delete dp:mcp_content_manage dp:mcp_key_create dp:mcp_key_read dp:mcp_key_update dp:mcp_key_revoke dp:mcp_key_manage dp:api_key_read dp:api_key_write dp:api_key_manage dp:api_key_revoke dp:api_flow_read dp:api_flow_write dp:api_flow_manage dp:api_flow_delete dp:api_workflow_read dp:api_workflow_create dp:api_workflow_update dp:api_workflow_delete dp:api_workflow_manage dp:app_read dp:app_write dp:app_manage dp:app_delete dp:app_key_write dp:app_key_manage dp:app_key_revoke dp:app_key_mapping_read dp:app_key_mapping_write dp:app_key_mapping_manage dp:subscription_read dp:subscription_write dp:subscription_manage dp:subscription_delete dp:sub_plan_read dp:sub_plan_write dp:sub_plan_manage dp:sub_plan_delete dp:idp_read dp:idp_write dp:idp_manage dp:idp_delete dp:view_read dp:view_write dp:view_manage dp:view_delete dp:km_read dp:km_write dp:km_manage dp:km_delete dp:label_read dp:label_write dp:label_manage dp:label_delete dp:provider_read dp:provider_write dp:provider_manage dp:provider_delete dp:event_read dp:delivery_manage dp:utility_write dp:utility_manage dp:webhook_subscriber_create dp:webhook_subscriber_read dp:webhook_subscriber_update dp:webhook_subscriber_delete dp:webhook_subscriber_manage dev" - -log "Provisioning configs/config-platform-api.toml ..." -if [ -f "$PLATFORM_API_CONFIG" ]; then - log " - $PLATFORM_API_CONFIG already exists, leaving as-is" -else - mkdir -p "$ROOT_DIR/configs" - # Wired to api-platform.env via {{ env "..." }} tokens (never hardcode a - # secret here) — this is the file docker-compose.yaml bind-mounts into the - # platform-api container. It's gitignored: for a static, no-dependencies - # starting point instead (e.g. running platform-api directly, without - # ./setup.sh), copy configs/config-platform-api-template.toml instead. - # Unquoted heredoc: $ADMIN_SCOPES is interpolated by bash below, while every - # {{ env "..." }} token is left untouched for platform-api's own config - # loader to resolve at container startup (no literal "$" appears in this - # file otherwise, so nothing else gets touched by the expansion). - cat > "$PLATFORM_API_CONFIG" </dev/null + openssl rsa -in "$JWT_KEY_DIR/jwt_private.pem" -pubout \ + -out "$JWT_KEY_DIR/jwt_public.pem" 2>/dev/null + chmod "$CERT_FILE_MODE" "$JWT_KEY_DIR/jwt_private.pem" "$JWT_KEY_DIR/jwt_public.pem" + log " - RS256 JWT keypair generated at $JWT_KEY_DIR" fi log "Provisioning Platform API admin credentials ..." @@ -237,7 +183,7 @@ else # `format: raw`, which passes file content through byte-for-byte with no # ${VAR} interpolation, so a literal bcrypt hash ("$2y$12$...") survives # into the container as-is. Escaping "$" as "$$" here would corrupt it. - # Read by config-platform-api.toml's [[auth.file_based.users]] entry — + # Read by config-platform-api.toml's [[platform_api.auth.file.users]] entry — # scopes lives there as a plain literal, not in this env file. set_env_var "APIP_CP_ADMIN_USERNAME" "$ADMIN_USERNAME" set_env_var "APIP_CP_ADMIN_PASSWORD_HASH" "$ADMIN_HASH" diff --git a/portals/developer-portal/src/middlewares/authMiddleware.js b/portals/developer-portal/src/middlewares/authMiddleware.js index 68da0dcb18..2fae232368 100644 --- a/portals/developer-portal/src/middlewares/authMiddleware.js +++ b/portals/developer-portal/src/middlewares/authMiddleware.js @@ -161,10 +161,21 @@ async function verifyBearerToken(token, req) { const idp = resolveOrgIdp(); if (!idp || !idp.clientId) { // Local auth mode: verify the Platform API JWT with the shared secret. - // Fail closed if no secret is configured — never accept an unverified token. const jwtSecret = config.platformApi?.jwtSecret; - if (!jwtSecret) return { valid: false, scopes: '' }; - const claims = await verifyPlatformJwtClaims(token, jwtSecret); + if (jwtSecret) { + const claims = await verifyPlatformJwtClaims(token, jwtSecret); + if (!claims) return { valid: false, scopes: '' }; + return { valid: true, scopes: claims.scopes?.join(' ') ?? '' }; + } + // Platform API now signs its admin tokens with RS256 (asymmetric), so there + // is no shared HMAC secret to verify against. When platformApi.insecure is + // explicitly enabled, decode the payload without verifying its signature, + // trusting the direct HTTPS connection to Platform API instead (mirrors the + // session-based local-auth branch above, which already does this via + // decodePlatformJwtClaims). Fail closed otherwise — never accept an + // unverified token by default. + if (!config.platformApi?.insecure) return { valid: false, scopes: '' }; + const claims = decodePlatformJwtClaims(token); if (!claims) return { valid: false, scopes: '' }; return { valid: true, scopes: claims.scopes?.join(' ') ?? '' }; } diff --git a/tests/integration-e2e/devportal-config.toml b/tests/integration-e2e/devportal-config.toml index c09acbe82d..04ebf0349a 100644 --- a/tests/integration-e2e/devportal-config.toml +++ b/tests/integration-e2e/devportal-config.toml @@ -40,10 +40,13 @@ default_name = '{{ env "APIP_DP_ORGANIZATION_DEFAULTNAME" "default" }}' auto_create_subscription_plans = false [platform_api] -# The devportal validates the platform-api admin JWT locally; jwt_secret MUST -# equal the stack's APIP_CP_AUTH_JWT_SECRET_KEY so the admin token is accepted. +# platform-api signs admin JWTs with RS256 now — there is no shared HMAC secret +# to set here. jwt_secret left empty (APIP_DP_PLATFORMAPI_JWTSECRET is unset in +# docker-compose.yaml) makes the devportal decode the token payload without +# verifying its signature, trusting the direct HTTPS connection (insecure=true) +# instead — see extractPlatformJwtClaims in src/utils/platformJwt.js. base_url = '{{ env "APIP_DP_PLATFORMAPI_BASEURL" "https://platform-api:9243" }}' -jwt_secret = '{{ env "APIP_DP_PLATFORMAPI_JWTSECRET" }}' +jwt_secret = '{{ env "APIP_DP_PLATFORMAPI_JWTSECRET" "" }}' insecure = true [security] diff --git a/tests/integration-e2e/docker-compose.sqlite.yaml b/tests/integration-e2e/docker-compose.sqlite.yaml index b00ec5bf10..9b65c37f5d 100644 --- a/tests/integration-e2e/docker-compose.sqlite.yaml +++ b/tests/integration-e2e/docker-compose.sqlite.yaml @@ -23,6 +23,29 @@ services: - platform-api-certs:/certs networks: [e2e] + # One-shot init container: generates the RS256 keypair platform-api's + # auth.jwt config requires (platform-api-config.toml reads it via {{ file }}). + # Tokens are signed asymmetrically — there is no shared HMAC secret anymore. + platform-api-jwtkeygen: + image: alpine/openssl + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + [ -f /keys/jwt_private.pem ] && [ -f /keys/jwt_public.pem ] && exit 0 + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + -out /keys/jwt_private.pem 2>/dev/null + openssl rsa -in /keys/jwt_private.pem -pubout \ + -out /keys/jwt_public.pem 2>/dev/null + # jwtkeygen runs as root; platform-api runs as uid 10001, so the private + # key must be owned by that uid, not made world-readable. + chown 10001 /keys/jwt_private.pem + chmod 0600 /keys/jwt_private.pem + chmod 0644 /keys/jwt_public.pem + volumes: + - platform-api-jwt-keys:/keys + networks: [e2e] + platform-api: image: ${PLATFORM_API_IMAGE:-platform-api:it-e2e} command: ["-config", "/etc/platform-api/config.toml"] @@ -38,15 +61,17 @@ services: - APIP_CP_AUTH_FILE_BASED_ORGANIZATION_UUID=99089a17-72e0-4dd8-a2f4-c8dfbb085295 - APIP_CP_AUTH_FILE_BASED_ORGANIZATION_DISPLAY_NAME=Default - APIP_CP_AUTH_FILE_BASED_ORGANIZATION_REGION=us - - APIP_CP_AUTH_JWT_SECRET_KEY=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 volumes: - ./platform-api-config.toml:/etc/platform-api/config.toml:ro - platform-api-certs:/app/data/certs + - platform-api-jwt-keys:/etc/platform-api/keys:ro ports: - "${PA_HOST_PORT:-9243}:9243" depends_on: platform-api-certgen: condition: service_completed_successfully + platform-api-jwtkeygen: + condition: service_completed_successfully networks: [e2e] gateway-controller: @@ -103,3 +128,4 @@ networks: volumes: platform-api-certs: + platform-api-jwt-keys: diff --git a/tests/integration-e2e/docker-compose.sqlserver.yaml b/tests/integration-e2e/docker-compose.sqlserver.yaml index 2c83be0d9b..e395c576b6 100644 --- a/tests/integration-e2e/docker-compose.sqlserver.yaml +++ b/tests/integration-e2e/docker-compose.sqlserver.yaml @@ -62,6 +62,29 @@ services: - platform-api-certs:/certs networks: [e2e] + # One-shot init container: generates the RS256 keypair platform-api's + # auth.jwt config requires (platform-api-config.toml reads it via {{ file }}). + # Tokens are signed asymmetrically — there is no shared HMAC secret anymore. + platform-api-jwtkeygen: + image: alpine/openssl + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + [ -f /keys/jwt_private.pem ] && [ -f /keys/jwt_public.pem ] && exit 0 + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + -out /keys/jwt_private.pem 2>/dev/null + openssl rsa -in /keys/jwt_private.pem -pubout \ + -out /keys/jwt_public.pem 2>/dev/null + # jwtkeygen runs as root; platform-api runs as uid 10001, so the private + # key must be owned by that uid, not made world-readable. + chown 10001 /keys/jwt_private.pem + chmod 0600 /keys/jwt_private.pem + chmod 0644 /keys/jwt_public.pem + volumes: + - platform-api-jwt-keys:/keys + networks: [e2e] + platform-api: image: ${PLATFORM_API_IMAGE:-platform-api:it-e2e} command: ["-config", "/etc/platform-api/config.toml"] @@ -81,10 +104,10 @@ services: - APIP_CP_AUTH_FILE_BASED_ORGANIZATION_UUID=99089a17-72e0-4dd8-a2f4-c8dfbb085295 - APIP_CP_AUTH_FILE_BASED_ORGANIZATION_DISPLAY_NAME=Default - APIP_CP_AUTH_FILE_BASED_ORGANIZATION_REGION=us - - APIP_CP_AUTH_JWT_SECRET_KEY=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 volumes: - ./platform-api-config.toml:/etc/platform-api/config.toml:ro - platform-api-certs:/app/data/certs + - platform-api-jwt-keys:/etc/platform-api/keys:ro ports: - "${PA_HOST_PORT:-9243}:9243" depends_on: @@ -92,6 +115,8 @@ services: condition: service_completed_successfully platform-api-certgen: condition: service_completed_successfully + platform-api-jwtkeygen: + condition: service_completed_successfully networks: [e2e] gateway-controller: @@ -150,3 +175,4 @@ networks: volumes: platform-api-certs: + platform-api-jwt-keys: diff --git a/tests/integration-e2e/docker-compose.yaml b/tests/integration-e2e/docker-compose.yaml index d80997242c..24ba7f98da 100644 --- a/tests/integration-e2e/docker-compose.yaml +++ b/tests/integration-e2e/docker-compose.yaml @@ -63,6 +63,29 @@ services: - platform-api-certs:/certs networks: [e2e] + # One-shot init container: generates the RS256 keypair platform-api's + # auth.jwt config requires (platform-api-config.toml reads it via {{ file }}). + # Tokens are signed asymmetrically — there is no shared HMAC secret anymore. + platform-api-jwtkeygen: + image: alpine/openssl + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + [ -f /keys/jwt_private.pem ] && [ -f /keys/jwt_public.pem ] && exit 0 + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + -out /keys/jwt_private.pem 2>/dev/null + openssl rsa -in /keys/jwt_private.pem -pubout \ + -out /keys/jwt_public.pem 2>/dev/null + # jwtkeygen runs as root; platform-api runs as uid 10001, so the private + # key must be owned by that uid, not made world-readable. + chown 10001 /keys/jwt_private.pem + chmod 0600 /keys/jwt_private.pem + chmod 0644 /keys/jwt_public.pem + volumes: + - platform-api-jwt-keys:/keys + networks: [e2e] + platform-api: image: ${PLATFORM_API_IMAGE:-platform-api:it-e2e} command: ["-config", "/etc/platform-api/config.toml"] @@ -82,7 +105,6 @@ services: - APIP_CP_AUTH_FILE_BASED_ORGANIZATION_UUID=99089a17-72e0-4dd8-a2f4-c8dfbb085295 - APIP_CP_AUTH_FILE_BASED_ORGANIZATION_DISPLAY_NAME=Default - APIP_CP_AUTH_FILE_BASED_ORGANIZATION_REGION=us - - APIP_CP_AUTH_JWT_SECRET_KEY=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 # The @devportal stack injects the admin user (with dp:* scopes) here so the # issued JWT is authorized on the developer portal. Empty (the default) means # platform-api uses its built-in admin (ap:* scopes only). @@ -104,6 +126,7 @@ services: # fallback for manual `docker compose up` after a suite run has generated it. - ${PA_WEBHOOK_KEY:-./.webhook-key.it.pem}:/etc/platform-api/devportal-webhook.pem:ro - platform-api-certs:/app/data/certs + - platform-api-jwt-keys:/etc/platform-api/keys:ro ports: - "${PA_HOST_PORT:-9243}:9243" depends_on: @@ -111,6 +134,8 @@ services: condition: service_healthy platform-api-certgen: condition: service_completed_successfully + platform-api-jwtkeygen: + condition: service_completed_successfully networks: [e2e] gateway-controller: @@ -259,10 +284,14 @@ services: - APIP_DP_DEMO_ENABLED=false - APIP_DP_ORGANIZATION_AUTOCREATESUBSCRIPTIONPLANS=false - APIP_DP_LOGGING_CONSOLEONLY=true - # Validate platform-api-issued JWTs locally: this MUST equal the stack's - # APIP_CP_AUTH_JWT_SECRET_KEY so the admin token is accepted by the devportal. + # platform-api now signs admin tokens with RS256 (asymmetric), so there is + # no shared HMAC secret to hand the devportal for local verification. + # APIP_DP_PLATFORMAPI_JWTSECRET is left unset: the devportal decodes the + # token payload without verifying its signature, trusting the direct + # HTTPS connection to platform-api instead (see extractPlatformJwtClaims + # in src/utils/platformJwt.js) — the same trust boundary + # APIP_DP_PLATFORMAPI_INSECURE already relies on for this test stack. - APIP_DP_PLATFORMAPI_BASEURL=https://platform-api:9243 - - APIP_DP_PLATFORMAPI_JWTSECRET=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 - APIP_DP_PLATFORMAPI_INSECURE=true # The webhook delivery worker POSTs over raw https with the default agent, # and platform-api serves a self-signed cert here. APIP_DP_PLATFORMAPI_INSECURE @@ -286,3 +315,4 @@ networks: volumes: platform-api-certs: + platform-api-jwt-keys: diff --git a/tests/integration-e2e/platform-api-config.toml b/tests/integration-e2e/platform-api-config.toml index 04187bd5d7..0e88027610 100644 --- a/tests/integration-e2e/platform-api-config.toml +++ b/tests/integration-e2e/platform-api-config.toml @@ -2,9 +2,15 @@ # Database settings come from environment variables (see docker-compose.yaml); # this file supplies file-based auth so the scenario can log in (admin/admin). +[platform_api] + +[platform_api.logging] +level = '{{ env "APIP_CP_LOGGING_LEVEL" "info" }}' + +[platform_api.security] encryption_key = '{{ env "APIP_CP_ENCRYPTION_KEY" }}' -[database] +[platform_api.database] driver = '{{ env "APIP_CP_DATABASE_DRIVER" "sqlite3" }}' path = '{{ env "APIP_CP_DATABASE_PATH" "/app/data/platform.db" }}' host = '{{ env "APIP_CP_DATABASE_HOST" "" }}' @@ -14,29 +20,30 @@ user = '{{ env "APIP_CP_DATABASE_USER" "" }}' password = '{{ env "APIP_CP_DATABASE_PASSWORD" "" }}' ssl_mode = '{{ env "APIP_CP_DATABASE_SSL_MODE" "disable" }}' -[auth.jwt] -enabled = true -issuer = "platform-api" -secret_key = '{{ env "APIP_CP_AUTH_JWT_SECRET_KEY" }}' -skip_validation = false +[platform_api.auth] +mode = '{{ env "APIP_CP_AUTH_MODE" "file" }}' -[auth.file_based] -enabled = true +[platform_api.auth.jwt] +issuer = '{{ env "APIP_CP_AUTH_JWT_ISSUER" "platform-api" }}' +# RS256 keypair generated by the platform-api-jwtkeygen init container and +# bind-mounted read-only at /etc/platform-api/keys. The server reads these +# paths directly rather than inlining the PEM content into config. +public_key_file = '{{ env "APIP_CP_AUTH_JWT_PUBLIC_KEY_FILE" "/etc/platform-api/keys/jwt_public.pem" }}' +private_key_file = '{{ env "APIP_CP_AUTH_JWT_PRIVATE_KEY_FILE" "/etc/platform-api/keys/jwt_private.pem" }}' -[auth.file_based.organization] -id = "default" -display_name = "Default" -region = "us" -uuid = "99089a17-72e0-4dd8-a2f4-c8dfbb085295" +[platform_api.auth.file.organization] +id = '{{ env "APIP_CP_AUTH_FILE_ORGANIZATION_ID" "default" }}' +display_name = '{{ env "APIP_CP_AUTH_FILE_ORGANIZATION_DISPLAY_NAME" "Default" }}' +region = '{{ env "APIP_CP_AUTH_FILE_ORGANIZATION_REGION" "us" }}' +uuid = '{{ env "APIP_CP_AUTH_FILE_ORGANIZATION_UUID" "99089a17-72e0-4dd8-a2f4-c8dfbb085295" }}' # Default login: admin / admin (bcrypt hash of "admin", reused from the shipped sample config). -[[auth.file_based.users]] +[[platform_api.auth.file.users]] username = "admin" password_hash = "$2y$10$U2yKMwGamGwDoMu0hRPT7u8nCuP8z/qxHFOKV6dhIxkJN9NJ0eVQ." scopes = "ap:organization:manage ap:gateway:manage ap:gateway_custom_policy:manage ap:rest_api:manage ap:llm_provider:manage ap:llm_proxy:manage ap:mcp_proxy:manage ap:webbroker_api:manage ap:websub_api:manage ap:application:manage ap:subscription:manage ap:subscription_plan:manage ap:project:manage ap:llm_template:manage ap:devportal:manage ap:api_key:read ap:secret:manage dp:org_manage dp:api_manage dp:sub_plan_manage dp:app_manage dp:subscription_manage dp:api_key_manage dp:webhook_subscriber_manage" -[webhook] +[platform_api.webhook] enabled = '{{ env "APIP_CP_WEBHOOK_ENABLED" "false" }}' secret = '{{ env "APIP_CP_WEBHOOK_SECRET" "" }}' private_key_path = '{{ env "APIP_CP_WEBHOOK_PRIVATE_KEY_PATH" "" }}' -gateway_type = '{{ env "APIP_CP_WEBHOOK_GATEWAY_TYPE" "wso2/api-platform" }}' diff --git a/tests/integration-e2e/steps_devportal_test.go b/tests/integration-e2e/steps_devportal_test.go index 0ce034926e..549a96b63b 100644 --- a/tests/integration-e2e/steps_devportal_test.go +++ b/tests/integration-e2e/steps_devportal_test.go @@ -50,9 +50,12 @@ import ( // - Delivery is fire-once on a ~2s poll, and the plaintext key/token the portal // returns to the user are exactly what the gateway validates. // -// The devportal accepts the platform-api admin JWT directly (it verifies it with -// the shared APIP_DP_PLATFORMAPI_JWTSECRET and takes the org from the token's -// org_handle claim), so suite.token is reused for every call here. +// The devportal accepts the platform-api admin JWT directly (platform-api signs +// it with RS256, so with no shared HMAC secret configured the devportal decodes +// the payload without verifying the signature, trusting the direct HTTPS +// connection instead — see APIP_DP_PLATFORMAPI_INSECURE in docker-compose.yaml — +// and takes the org from the token's org_handle claim), so suite.token is reused +// for every call here. // webhookReceiverURL is the platform-api webhook receiver at its container-internal // host (the devportal reaches platform-api by service name on the compose network,