Skip to content

Add Secret Management UI Implementation - #3129

Open
npamudika wants to merge 8 commits into
wso2:mainfrom
npamudika:secret-management
Open

Add Secret Management UI Implementation#3129
npamudika wants to merge 8 commits into
wso2:mainfrom
npamudika:secret-management

Conversation

@npamudika

@npamudika npamudika commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Purpose

Platform API already supports encrypting upstream credentials for LLM providers, LLM proxies, MCP proxies, and REST APIs via {{ secret "handle" }} placeholders (added in #2149, but two gaps made that feature incomplete in practice:

  • No UI to manage secrets directly. The only way to create, rotate, or delete a secret was a raw API/CLI call — there was no screen in AI Workspace for it, "no dedicated Secrets screen in any portal").
  • Rotation/deletion wasn't live. A gateway only picked up a rotated or deleted secret on its next reconnect-triggered poll sync. An already-connected gateway kept serving the old credential — e.g. rotating a leaked API key didn't actually stop it being used until that gateway happened to reconnect or restart.
  • While building and testing the fix for (2), a related latent bug surfaced: BuildAPIDeploymentYAML silently dropped upstream.main/sandbox.auth from the REST API deployment YAML, so secret-backed REST API upstream credentials never reached the gateway at all — it just quietly forwarded requests with no auth header.
  • Hard delete instead of soft-delete. SecretRepository.FindRefsAndSoftDelete → FindRefsAndDelete: same transactional lock-then-check pattern, but the final step is now DELETE FROM secrets instead of UPDATE ... SET status='DEPRECATED'. This alone fixes your second point too — cleanupRotatedSecret (which already ran this same check-then-delete after a rotation) now actually removes the old secret's row when nothing else references it, instead of leaving it around forever as DEPRECATED.
  • Orphan cleanup on resource deletion. LLMProviderRepo.Delete, LLMProxyRepo.Delete, and MCPProxyRepo.Delete now capture every secret handle the resource referenced before removing it, and return that list. Each corresponding service's Delete method then calls a new best-effort SecretService.CleanupOrphanedSecrets, which re-checks each handle and permanently deletes any that are no longer referenced by anything else — silently skipping ones still in use.

Resolves:

#2149

Goals

  • Ship a dedicated Secrets UI in AI Workspace (list/search, create, view, rotate, delete) under Organization Settings, scope-gated on ap:secret:read.
  • Push secret.updated / secret.deprecated events from platform-api to every connected gateway-controller over the existing control-plane WebSocket, so rotation/deletion takes effect in about a second instead of waiting for the next reconnect.
  • Fix the REST API deployment YAML gap so secret-backed REST API upstreams actually work.
  • Close a set of correctness, UX, and hardening gaps found via a dedicated live-testing pass against a real running stack (docker platform-api + ai-workspace + gateway-controller + gateway-runtime): a rotate-form field-name mismatch that silently dropped display-name edits, missing server-side handle validation (a handle containing / was creatable but then permanently unreachable via the API), a broadcast-ordering bug, an unhelpful validation error message, and frontend accessibility/robustness gaps.

Approach

Backend (Go, platform-api):

  • SecretService — full CRUD, broadcastSecretEvent (best-effort, fans out to every gateway in the org via GatewayEventsService), server-side handle validation (constants.SecretHandlePattern / SecretHandleMaxLength, mirroring the frontend's slug rule), and ValidateSecretRefs now distinguishes a missing handle from a deprecated one in its error message.
  • GatewayEventsService.BroadcastSecretUpdatedEvent / BroadcastSecretDeprecatedEvent — new event types on the existing EventHub WebSocket channel used for all other control-plane push events.
    dto.UpstreamTarget gained an Auth field, and BuildAPIDeploymentYAML now copies it through — the deployment-YAML fix.

Backend (Go, gateway-controller):

  • client.go dispatch: handleSecretUpdatedEvent fetches the new plaintext on demand and re-encrypts it into local storage; handleSecretDeprecatedEvent evicts the local copy. Both fall back cleanly to the existing poll-based sync if the push is missed (disconnected gateway, delivery failure, etc.).

Frontend (AI Workspace):

  • Five new pages under portals/ai-workspace/src/pages/appShell/appShellPages/secret/: SecretsList, CreateSecret, SecretOverview, RotateSecret, DeleteSecretDialog — wired into App.tsx routing and the Settings nav.
  • Accessibility: list rows are keyboard-navigable (role="link", tabIndex, Enter/Space handling, aria-label).
  • Hardening: autoComplete="new-password" on credential inputs (avoids browser password-manager prompts), and a new useIsMounted hook guards state updates in Create/Rotate/Delete after the component has unmounted.

UIs

Screenshot 2026-08-05 at 11 14 57 Screenshot 2026-08-05 at 11 15 16 Screenshot 2026-08-05 at 11 15 35 Screenshot 2026-08-05 at 11 15 59 Screenshot 2026-08-05 at 11 16 12 Screenshot 2026-08-05 at 11 16 48

User Stories

  • As an AI Workspace admin, I can create, view, rotate, and delete secrets from the UI without needing API/CLI access.
  • As an admin rotating a leaked or expiring credential, the new value takes effect on every already-connected gateway within about a second, not just on that gateway's next reconnect.
  • As an admin trying to delete a secret still in use, I see exactly which resources reference it instead of a generic failure.
  • As a keyboard/assistive-technology user, I can navigate and operate the Secrets list without a mouse.

Documentation

https://github.com/wso2/api-platform/blob/main/docs/rest-apis/platform-api/secrets.md

Automation Tests

  • Unit tests (Go): 42 test functions across the secret-management surface
  • Integration/e2e tests (Cucumber/godog, tests/integration-e2e/):
  1. features/secret_lifecycle.feature (2 scenarios) — rotation and deletion pushing live to an already-connected gateway, verified against the gateway's own management API.
  2. features/rest_api_secret.feature, features/policy_secret.feature (1 each) — on-demand secret resolution at deploy time for REST APIs and policy params.
  3. Additionally hand-verified live against a real docker stack (platform-api + ai-workspace + gateway-controller + gateway-runtime) this session: full create/rotate/delete UI flow, XSS-payload rendering safety, delete-blocked-by-reference dialog, disconnected-gateway reconnect catch-up, and every fix above reproduced pre-fix and confirmed post-fix.

Related PRs

Refer the PRs mentioned in #2149

Test Environment

  • Go: 1.26.5 (per platform-api/go.mod)
  • Node: 20.20.2 (per portals/ai-workspace/.nvmrc)
  • OS: macOS 15.7.3 (Darwin 24.6.0, arm64)
  • Database: SQLite (local dev); the shared ValidateSecretRefs/handle-validation logic is DB-agnostic and covered against SQLite in tests — not separately re-verified against PostgreSQL/SQL Server in this pass.
  • Browser: Chromium-based embedded test browser (headless automation) against the built AI Workspace frontend bundle.
  • Containers: Docker Compose stack — platform-api, ai-workspace, gateway-controller, gateway-runtime, sample-backend, all built from this branch's source.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Naduni Pamudika seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds revision-aware secret lifecycle events, permanent orphan cleanup, gateway synchronization, secret usage APIs, portal secret management, upstream authentication serialization, and live lifecycle integration coverage.

Changes

Secret lifecycle backend

Layer / File(s) Summary
Secret contracts and validation
gateway/gateway-controller/pkg/controlplane/events.go, platform-api/internal/model/secret.go, platform-api/internal/constants/constants.go, platform-api/internal/service/secret_service.go
Adds lifecycle event payloads, revision fields, handle constraints, metadata-only updates, and separate missing/deprecated reference validation.
Permanent deletion and orphan cleanup
platform-api/internal/repository/*, platform-api/internal/service/{api,llm,mcp}.go, platform-api/internal/service/secret_service.go
Permanently deletes unreferenced secrets and removes orphaned secrets after API, provider, proxy, or artifact deletion.
Gateway event broadcasting and consumption
platform-api/internal/service/gateway_events.go, gateway/gateway-controller/pkg/controlplane/{client.go,sync_secrets.go}
Broadcasts secret lifecycle events. The gateway preserves revision precision, rejects stale events, applies updates, deletes local secrets, and evicts inactive secrets during synchronization.

Secret API and portal

Layer / File(s) Summary
Secret usage API and upstream authentication
platform-api/internal/handler/secret.go, platform-api/internal/dto/secret.go, platform-api/resources/openapi.yaml, platform-api/internal/dto/api.go, platform-api/internal/utils/api.go
Adds the secret usages endpoint, allows metadata-only updates, and preserves upstream authentication in deployment YAML.
Secret management portal
portals/ai-workspace/src/App.tsx, portals/ai-workspace/src/apis/secretApis.ts, portals/ai-workspace/src/pages/appShell/appShellPages/secret/*, portals/ai-workspace/src/pages/appShell/appShellPages/settings/Main.tsx, portals/ai-workspace/src/Components/common/SecretValueField.tsx
Adds scoped routes and pages for listing, creating, viewing, rotating, and permanently deleting secrets. Reuses SecretValueField for credential entry.

Integration coverage

Layer / File(s) Summary
Lifecycle integration coverage
tests/integration-e2e/*, portals/ai-workspace/cypress/e2e/001-providers/*
Adds live rotation and deletion scenarios, gateway polling helpers, bounded response handling, lifecycle documentation, and compact test handles.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: krishanx92, lasanthas, thushani-jayasekera

Sequence Diagram(s)

sequenceDiagram
  participant Portal
  participant PlatformAPI
  participant GatewayEventsService
  participant GatewayController
  participant SecretStore
  Portal->>PlatformAPI: Rotate or delete secret
  PlatformAPI->>GatewayEventsService: Broadcast lifecycle event
  GatewayEventsService->>GatewayController: Publish WebSocket event
  GatewayController->>PlatformAPI: Fetch rotated value
  GatewayController->>SecretStore: Upsert or delete local secret
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main user-facing change: adding secret management UI capabilities.
Description check ✅ Passed The description covers the main template sections with detailed scope, UI evidence, testing, documentation, and environment information, but omits Security checks and Samples.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch secret-management
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform-api/internal/model/secret.go`:
- Around line 72-90: Add a revision or generation field to SecretUpdatedEvent
and SecretDeprecatedEvent, then update gateway lifecycle handling to ignore
events older than the locally recorded revision while preserving existing
idempotency and delivery coverage. Ensure deletion followed by reactivation
cannot be undone by a stale event, and add a test covering that sequence.

In `@portals/ai-workspace/src/App.tsx`:
- Around line 591-622: Update portals/ai-workspace/src/App.tsx lines 591-622 to
guard the entire secrets route branch, including nested routes, with
SCOPES.SECRET_READ and replace the hard-coded settings index redirect with a
scope-aware destination. In
portals/ai-workspace/src/pages/appShell/appShellPages/settings/Main.tsx lines
79-84, authorize the matched settings route using its requiredScope rather than
visibleNavItems, preserving access only for users with the route’s required
scope.

In
`@portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretOverview.tsx`:
- Around line 59-76: Bind secret fetch results to the active organization and
handle across SecretOverview.tsx lines 59-76, RotateSecret.tsx lines 68-89, and
SecretsList.tsx lines 77-94. Update the request flows using request IDs or
cancellation, and reject responses that are stale after the organization or
handle changes; ensure SecretOverview cannot replace the current secret
metadata, RotateSecret cannot submit stale metadata, and SecretsList cannot
replace the current organization’s list.

In
`@portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretsList.tsx`:
- Around line 252-257: Update the row keyboard handler in SecretsList so
keyboard events originating from child controls such as the delete IconButton
are ignored, while Enter and Space pressed directly on the row still prevent
default behavior and navigate to the secret. Use the event target/currentTarget
relationship to distinguish the row from its descendants.

In `@tests/integration-e2e/ingress_helpers_test.go`:
- Around line 68-78: Introduce one shared delay helper that validates a positive
interval and a non-zero interval/2 before adding bounded random jitter, then use
it before every polling fetch. Update
tests/integration-e2e/ingress_helpers_test.go:68-78 in waitIngressWithHeaders,
tests/integration-e2e/secret_helpers_test.go:213-240 before each secret-value
fetch, and tests/integration-e2e/secret_helpers_test.go:246-270 before each
deletion-status fetch; replace the fixed two-second sleeps while preserving the
existing polling behavior.

In `@tests/integration-e2e/README.md`:
- Around line 255-257: Update the “Deletion” scenario in the README to document
the explicit deleteSecret operation: state that DELETE /secrets/:handle triggers
the original secret’s deprecation event, rather than attributing deprecation to
cleanupRotatedSecret. Preserve the existing description of redeployment and
gateway cache eviction.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 600a8b72-f6f9-4d74-b6e9-34bdeb670754

📥 Commits

Reviewing files that changed from the base of the PR and between 4bca0ae and d00f44d.

📒 Files selected for processing (31)
  • gateway/gateway-controller/pkg/controlplane/client.go
  • gateway/gateway-controller/pkg/controlplane/events.go
  • gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go
  • platform-api/internal/constants/constants.go
  • platform-api/internal/dto/api.go
  • platform-api/internal/model/secret.go
  • platform-api/internal/server/server.go
  • platform-api/internal/service/gateway_events.go
  • platform-api/internal/service/secret_service.go
  • platform-api/internal/service/secret_service_broadcast_test.go
  • platform-api/internal/service/secret_service_test.go
  • platform-api/internal/utils/api.go
  • platform-api/internal/utils/api_test.go
  • portals/ai-workspace/src/App.tsx
  • portals/ai-workspace/src/apis/secretApis.ts
  • portals/ai-workspace/src/clients/choreoApiClient.ts
  • portals/ai-workspace/src/hooks/useIsMounted.ts
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/CreateSecret.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/DeleteSecretDialog.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/RotateSecret.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretOverview.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretsList.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/settings/Main.tsx
  • tests/integration-e2e/README.md
  • tests/integration-e2e/features/secret_lifecycle.feature
  • tests/integration-e2e/features/secured-api-invocation.feature
  • tests/integration-e2e/ingress_helpers_test.go
  • tests/integration-e2e/secret_helpers_test.go
  • tests/integration-e2e/secret_lifecycle_steps_test.go
  • tests/integration-e2e/steps_secured_test.go
  • tests/integration-e2e/steps_test.go
💤 Files with no reviewable changes (2)
  • tests/integration-e2e/features/secured-api-invocation.feature
  • tests/integration-e2e/steps_secured_test.go

Comment thread platform-api/internal/model/secret.go
Comment thread portals/ai-workspace/src/App.tsx
Comment thread tests/integration-e2e/ingress_helpers_test.go Outdated
Comment thread tests/integration-e2e/README.md Outdated
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration-e2e/secret_helpers_test.go`:
- Around line 217-236: Add randomized positive pre-request jitter to both
polling loops around the visible HTTP fetches, using a non-zero half-interval
rather than a fixed two-second retry cadence. Apply the same jittered delay
before every request in the loops containing httpClient.Do, while preserving the
existing deadline and retry behavior.
- Line 162: Update the response-body reads in the relevant secret helper test
paths around the existing io.ReadAll calls to wrap resp.Body with io.LimitReader
before reading. Use the configured response-size limit when available, with a
safe default fallback, and apply the same bounded-reader pattern to all three
occurrences.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3402839c-59e4-48ab-9753-061df24b30f4

📥 Commits

Reviewing files that changed from the base of the PR and between 9f7d4bd and b6cbfc3.

📒 Files selected for processing (28)
  • gateway/gateway-controller/pkg/controlplane/client.go
  • gateway/gateway-controller/pkg/controlplane/events.go
  • gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go
  • platform-api/internal/constants/constants.go
  • platform-api/internal/dto/api.go
  • platform-api/internal/model/secret.go
  • platform-api/internal/server/server.go
  • platform-api/internal/service/gateway_events.go
  • platform-api/internal/service/secret_service.go
  • platform-api/internal/service/secret_service_broadcast_test.go
  • platform-api/internal/service/secret_service_test.go
  • platform-api/internal/utils/api.go
  • platform-api/internal/utils/api_test.go
  • portals/ai-workspace/src/App.tsx
  • portals/ai-workspace/src/apis/secretApis.ts
  • portals/ai-workspace/src/clients/choreoApiClient.ts
  • portals/ai-workspace/src/hooks/useIsMounted.ts
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/CreateSecret.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/DeleteSecretDialog.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/RotateSecret.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretOverview.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretsList.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/settings/Main.tsx
  • tests/integration-e2e/README.md
  • tests/integration-e2e/features/secret_lifecycle.feature
  • tests/integration-e2e/secret_helpers_test.go
  • tests/integration-e2e/secret_lifecycle_steps_test.go
  • tests/integration-e2e/steps_test.go
🚧 Files skipped from review as they are similar to previous changes (21)
  • portals/ai-workspace/src/hooks/useIsMounted.ts
  • portals/ai-workspace/src/apis/secretApis.ts
  • platform-api/internal/model/secret.go
  • tests/integration-e2e/features/secret_lifecycle.feature
  • platform-api/internal/server/server.go
  • gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go
  • platform-api/internal/service/secret_service_test.go
  • platform-api/internal/utils/api_test.go
  • platform-api/internal/dto/api.go
  • platform-api/internal/utils/api.go
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/CreateSecret.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/RotateSecret.tsx
  • gateway/gateway-controller/pkg/controlplane/events.go
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretOverview.tsx
  • tests/integration-e2e/secret_lifecycle_steps_test.go
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/DeleteSecretDialog.tsx
  • platform-api/internal/service/secret_service_broadcast_test.go
  • platform-api/internal/service/secret_service.go
  • portals/ai-workspace/src/App.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretsList.tsx
  • gateway/gateway-controller/pkg/controlplane/client.go

Comment thread tests/integration-e2e/secret_helpers_test.go Outdated
Comment thread tests/integration-e2e/secret_helpers_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-controller/pkg/controlplane/client.go`:
- Around line 3810-3827: Update handleMessage and the revision-bearing event
decode path to preserve revision values as full int64 numbers instead of default
float64 values before isStaleSecretEvent compares them. Ensure equal revisions
remain idempotent while adjacent epoch-nanosecond revisions remain
distinguishable, and add a regression test covering adjacent revision values and
reordered events.

In `@platform-api/internal/model/secret.go`:
- Around line 79-85: Replace the time-based revision described in the Secret
lifecycle event flow with a database-backed per-secret version. Add an atomic
data_version/revision increment to both SecretService.Update and
SecretService.Delete, ensure the version is read or returned only after the
corresponding row write commits, and use that committed value in emitted events
so concurrent update and soft-delete operations preserve commit order.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 28faceb6-1ae3-4e64-b38c-13f7f0827ddd

📥 Commits

Reviewing files that changed from the base of the PR and between b6cbfc3 and cf89180.

📒 Files selected for processing (13)
  • gateway/gateway-controller/pkg/controlplane/client.go
  • gateway/gateway-controller/pkg/controlplane/events.go
  • gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go
  • platform-api/internal/model/secret.go
  • platform-api/internal/service/secret_service.go
  • portals/ai-workspace/cypress/e2e/001-providers/002-provider-secret-management.cy.js
  • portals/ai-workspace/cypress/e2e/001-providers/003-llm-proxy-secret-management.cy.js
  • portals/ai-workspace/src/App.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/RotateSecret.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretOverview.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretsList.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/settings/Main.tsx
  • tests/integration-e2e/secret_helpers_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/RotateSecret.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretOverview.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretsList.tsx
  • gateway/gateway-controller/pkg/controlplane/events.go
  • platform-api/internal/service/secret_service.go

Comment thread gateway/gateway-controller/pkg/controlplane/client.go
Comment thread platform-api/internal/model/secret.go
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform-api/internal/repository/secret.go`:
- Around line 247-251: Update the SQL Server branch in FindRefsAndSoftDelete so
the secret lookup acquires the same blocking lock semantics as the PostgreSQL
path instead of only switching to SELECT TOP (1). Apply a SQL Server row-locking
equivalent on the secrets read, and make sure the later refsQuery and UPDATE
still run under that contract so concurrent artifact_secret_refs writes cannot
slip past the empty-reference check. If needed, align the reference write path
with the same locking behavior, and add a SQL Server concurrency test that
exercises secret deprecation against concurrent reference insertion or update.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f8c2da6-333e-4a66-9f64-b4f231e49064

📥 Commits

Reviewing files that changed from the base of the PR and between cf89180 and f2bfd50.

📒 Files selected for processing (2)
  • gateway/gateway-controller/pkg/controlplane/client.go
  • platform-api/internal/repository/secret.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • gateway/gateway-controller/pkg/controlplane/client.go

Comment thread platform-api/internal/repository/secret.go Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersOverview.tsx (1)

1213-1226: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve a selected secret reference during save.

SecretValueField submits {{ secret "name" }} for an existing secret. The MCP update path stores that placeholder as the new secret value when hasBackendConnectionChanges is true, then references the new secret. Use the existing handle when extractSecretHandle(authHeaderValue) returns a value, and skip createSecret.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersOverview.tsx`
around lines 1213 - 1226, Update the save logic for the MCP auth header
associated with SecretValueField to call extractSecretHandle(authHeaderValue)
first; when it returns an existing handle, reuse that handle and skip
createSecret, including when hasBackendConnectionChanges is true. Only create
and reference a new secret when no existing handle is present, preserving
selected {{ secret "name" }} references.
🧹 Nitpick comments (2)
gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go (2)

809-847: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider mirroring the precision test for secret.updated.

These tests prove handleMessage preserves exact int64 revisions for secret.deleted. secret.updated decodes through the same path and uses the same revision cache. A reordered secret.updated pair with adjacent UnixNano revisions would regress silently. Add one test that sends two raw-JSON secret.updated messages with revisions 100ns apart and asserts the stale one is rejected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go` around
lines 809 - 847, Add a precision regression test for secret.updated alongside
the existing secret.deleted tests, sending raw JSON update messages with
UnixNano revisions 100ns apart in newer-then-stale order. Use the existing
handleMessage, revision cache, and mock syncer symbols to assert the newer
update is applied, the reordered stale update is rejected, and the cached
revision remains the exact int64 value.

950-959: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Build the fixtures from utils.PlatformSecretMeta instead of a parallel struct.

platformSecretJSON duplicates the wire tags of PlatformSecretMeta by hand. If a production tag changes, for example uuid or name, these fixtures still encode the old names. Every secret then decodes with an empty Handle and an empty Status, so the eviction tests still pass, but for the wrong reason. Marshal the production type directly so a tag change breaks the fixture at compile time or fails loudly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go` around
lines 950 - 959, Remove the parallel platformSecretJSON fixture type and build
test server responses directly from utils.PlatformSecretMeta, preserving the
fixture values and optional Value behavior. Update all references that construct
or marshal platformSecretJSON so production JSON tags are used and tag changes
fail loudly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-controller/pkg/controlplane/events.go`:
- Around line 424-432: Update SecretService.Create to assign an explicit current
UTC UpdatedAt timestamp when creating a recycled secret handle, ensuring its
revision is strictly newer than the deleted predecessor. Preserve
isStaleSecretEvent’s strict less-than comparison so equal revisions remain
idempotent and older deletion events are ignored.

In `@gateway/gateway-controller/pkg/controlplane/sync_secrets.go`:
- Around line 189-220: Snapshot the keys in secretHashCache before
FetchPlatformSecrets begins, then pass that snapshot into evictSecretsNotIn and
only consider handles present in it for eviction. Update the sync flow and
evictSecretsNotIn signature accordingly, preserving the existing deletion and
logging behavior while preventing handles added during the fetch from being
removed.
- Around line 202-211: Update the stale-secret eviction loop around Delete to
generate a non-deterministic, per-attempt correlation ID instead of calling
GenerateDeterministicUUIDv7 with handle and time.Now(). Keep the existing
correlation_id logging and Delete behavior unchanged.

In `@portals/ai-workspace/src/Components/common/SecretValueField.tsx`:
- Around line 180-185: Restore keyboard access to the visibility control by
removing the tabIndex={-1} override from the IconButton in SecretValueField.
Preserve its existing show/hide behavior, aria-label, and styling while allowing
the button to participate in normal keyboard tab navigation.

In
`@portals/ai-workspace/src/pages/appShell/appShellPages/secret/RotateSecret.tsx`:
- Around line 102-106: Update the RotateSecret submission flow to validate that
the trimmed display name is non-empty, while preserving a trimmed empty
description as ''. In updateSecret, append metadata fields whenever their values
are !== undefined so an explicit empty description is included in the request
rather than omitted.

---

Outside diff comments:
In
`@portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersOverview.tsx`:
- Around line 1213-1226: Update the save logic for the MCP auth header
associated with SecretValueField to call extractSecretHandle(authHeaderValue)
first; when it returns an existing handle, reuse that handle and skip
createSecret, including when hasBackendConnectionChanges is true. Only create
and reference a new secret when no existing handle is present, preserving
selected {{ secret "name" }} references.

---

Nitpick comments:
In `@gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go`:
- Around line 809-847: Add a precision regression test for secret.updated
alongside the existing secret.deleted tests, sending raw JSON update messages
with UnixNano revisions 100ns apart in newer-then-stale order. Use the existing
handleMessage, revision cache, and mock syncer symbols to assert the newer
update is applied, the reordered stale update is rejected, and the cached
revision remains the exact int64 value.
- Around line 950-959: Remove the parallel platformSecretJSON fixture type and
build test server responses directly from utils.PlatformSecretMeta, preserving
the fixture values and optional Value behavior. Update all references that
construct or marshal platformSecretJSON so production JSON tags are used and tag
changes fail loudly.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f5a6aea5-f4d3-44e9-8eeb-2cc141108cf5

📥 Commits

Reviewing files that changed from the base of the PR and between f2bfd50 and fc1bd91.

📒 Files selected for processing (51)
  • gateway/gateway-controller/pkg/controlplane/client.go
  • gateway/gateway-controller/pkg/controlplane/events.go
  • gateway/gateway-controller/pkg/controlplane/sync_secrets.go
  • gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go
  • platform-api/internal/dto/secret.go
  • platform-api/internal/handler/secret.go
  • platform-api/internal/handler/secret_integration_test.go
  • platform-api/internal/model/secret.go
  • platform-api/internal/repository/api.go
  • platform-api/internal/repository/api_test.go
  • platform-api/internal/repository/artifact.go
  • platform-api/internal/repository/artifact_refs.go
  • platform-api/internal/repository/interfaces.go
  • platform-api/internal/repository/llm.go
  • platform-api/internal/repository/llm_test.go
  • platform-api/internal/repository/mcp.go
  • platform-api/internal/repository/secret.go
  • platform-api/internal/repository/secret_test.go
  • platform-api/internal/server/scope_route_coverage_test.go
  • platform-api/internal/service/api.go
  • platform-api/internal/service/api_secret_integration_test.go
  • platform-api/internal/service/api_test.go
  • platform-api/internal/service/gateway_events.go
  • platform-api/internal/service/llm.go
  • platform-api/internal/service/llm_test.go
  • platform-api/internal/service/mcp.go
  • platform-api/internal/service/mcp_secret_integration_test.go
  • platform-api/internal/service/mcp_secret_resolution_test.go
  • platform-api/internal/service/secret_service.go
  • platform-api/internal/service/secret_service_broadcast_test.go
  • platform-api/internal/service/secret_service_test.go
  • platform-api/resources/openapi.yaml
  • portals/ai-workspace/src/App.tsx
  • portals/ai-workspace/src/Components/common/SecretValueField.tsx
  • portals/ai-workspace/src/apis/secretApis.ts
  • portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersNew.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersOverview.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyNew.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyProviderTab.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/CreateSecret.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/DeleteSecretDialog.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/RotateSecret.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretOverview.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretsList.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/AddNewProvider/ProviderTemplateFormFields.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderConnectionTab.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderNew.tsx
  • portals/ai-workspace/src/pages/appShell/appShellPages/settings/Main.tsx
  • tests/integration-e2e/README.md
  • tests/integration-e2e/secret_helpers_test.go
  • tests/integration-e2e/secret_lifecycle_steps_test.go
💤 Files with no reviewable changes (1)
  • portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderNew.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • platform-api/internal/service/secret_service_broadcast_test.go
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/SecretsList.tsx
  • platform-api/internal/model/secret.go
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/CreateSecret.tsx
  • tests/integration-e2e/README.md
  • tests/integration-e2e/secret_helpers_test.go
  • platform-api/internal/service/secret_service.go
  • gateway/gateway-controller/pkg/controlplane/client.go
  • portals/ai-workspace/src/pages/appShell/appShellPages/secret/DeleteSecretDialog.tsx

Comment on lines +424 to +432
// SecretDeletedEventPayload represents the payload of a secret.deleted event,
// fired when a secret is permanently deleted.
type SecretDeletedEventPayload struct {
Handle string `json:"handle"`
// Revision — see SecretUpdatedEventPayload.Revision. Compared against the same
// cache so a late deletion cannot evict a secret that was recreated under the
// same handle after it.
Revision int64 `json:"revision"`
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline platform-api/internal/model/secret.go --items all
rg -n -C 5 'Revision|isStaleSecretEvent|secretRevisionCache|SecretDeletedEventPayload' \
  platform-api/internal/model/secret.go \
  platform-api/internal/service/secret_service.go \
  gateway/gateway-controller/pkg/controlplane/client.go \
  gateway/gateway-controller/pkg/controlplane/events.go

Repository: wso2/api-platform

Length of output: 19590


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '180,285p' platform-api/internal/service/secret_service.go
sed -n '384,100p' platform-api/internal/store/postgres/secret_repository.go
sed -n '70,220p' platform-api/internal/store/postgres/secret_repository.go

printf '\n--- create/update/delete helpers references ---\n'
rg -n -C 8 'CreatedAt|UpdatedAt|UnixNano|Find[ ]+\\(|Get[ ]+\\(|Create\\(|Update\\(' platform-api/internal/store platform-api/internal/service | sed -n '1,260p'

Repository: wso2/api-platform

Length of output: 3706


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate secret repositories ---'
git ls-files | rg 'secret.*repository|secret.*store|postgres.*secret|sql/.*secret|*.sql' | rg -i 'secret' | sed -n '1,120p'

printf '%s\n' '--- Update/GetByHandle/Create definitions'
rg -n -C 8 'func .*GetByHandle|func .*Create|func .*Update|type Secret\s+\{|CreatedAt\s+|UpdatedAt\s+' platform-api/internal platform-api/api | sed -n '1,360p'

printf '%s\n' '--- Create flow around SecretCreate'
rg -n -C 12 'func \(s \*SecretService\) Create|NewSecret|Created_at|CreatedAt|UpdatedAt' platform-api/internal/service/secret_service.go platform-api/internal/model platform-api/api | sed -n '1,280p'

Repository: wso2/api-platform

Length of output: 411


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate secret repository files ---'
git ls-files | rg -i 'secret.*(repository|store|reposit)|.*postgres|.*sql' | rg -i 'secret' | sed -n '1,160p'

printf '%s\n' '--- Create/GetByHandle/Update definitions ---'
rg -n -C 12 'func \(s \*SecretService\) Create|func .*GetByHandle|func .*Update|func .*FindRefsAndDelete|NewSecret|CreatedAt\s+|UpdatedAt\s+' platform-api/internal/service/secret_service.go platform-api/internal/model platform-api/internal/store platform-api/internal/dto | sed -n '1,380p'

printf '%s\n' '--- exact Update implementation ---'
sed -n '190,270p' platform-api/internal/service/secret_service.go

Repository: wso2/api-platform

Length of output: 36046


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '88,185p' platform-api/internal/service/secret_service.go
rg -n -C 12 'type Secret struct|CreatedAt|UpdatedAt|db:"created_at"|db:"updated_at"|Update\(|FindRefsAndDelete|GetByHandle' platform-api/internal platform-api/api --glob '*.go' --glob '*.sql' | sed -n '1,320p'

Repository: wso2/api-platform

Length of output: 31888


🌐 Web query:

GORM callback create UpdatedAt Before Create Now() documentation

💡 Result:

In GORM, you do not need to manually create a callback to set CreatedAt or UpdatedAt to the current time, as this is handled automatically by GORM’s built-in functionality [1]. Automatic Time Tracking GORM tracks creation and update times by convention if your model includes fields named CreatedAt and UpdatedAt [1]. GORM will automatically populate these fields with the current time during create and update operations [1]. You can control this behavior using GORM tags in your model struct [1]: - autoCreateTime: Automatically sets the field to the current time upon creation [1]. - autoUpdateTime: Automatically sets the field to the current time upon creation and update [1]. For example: type User struct { CreatedAt time.Time UpdatedAt time.Time // Automatically set/updated by GORM } Custom Callback Behavior If you need custom logic (such as using a specific time source or formatting), you can implement GORM hooks like BeforeCreate [2][3]. Hooks are methods defined on your model that GORM calls during the operation lifecycle [2]. func (u *User) BeforeCreate(tx *gorm.DB) (err error) { // Custom logic before creation u.UpdatedAt = time.Now return } If you must manipulate callbacks directly (e.g., to replace the default timestamping behavior), you can register custom callbacks or remove existing ones via the GORM callback API [4]. However, this is generally unnecessary for standard timestamp management [1]. GORM's built-in callbacks are executed in a specific order, which you can inspect or modify if required [4]. Top results: [2][1][4]

Citations:


Make recreated secret handles use strictly newer revisions.

SecretDeletedEventPayload depends on recreated handles being newer than deleted predecessors, but secret creation does not set UpdatedAt in SecretService.Create; a later reuse can receive an older/generated timestamp. Set an explicit UTC update timestamp for the recycled handle, and keep isStaleSecretEvent using strict less-than so equal revisions remain idempotent while older deletions are ignored.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-controller/pkg/controlplane/events.go` around lines 424 -
432, Update SecretService.Create to assign an explicit current UTC UpdatedAt
timestamp when creating a recycled secret handle, ensuring its revision is
strictly newer than the deleted predecessor. Preserve isStaleSecretEvent’s
strict less-than comparison so equal revisions remain idempotent and older
deletion events are ignored.

Comment on lines +189 to +220
func (c *Client) evictSecretsNotIn(activeHandles map[string]struct{}) int {
var stale []string
c.secretHashCache.Range(func(key, _ any) bool {
handle, ok := key.(string)
if !ok {
return true
}
if _, ok := activeHandles[handle]; !ok {
stale = append(stale, handle)
}
return true
})

for _, handle := range stale {
correlationID := utils.GenerateDeterministicUUIDv7(handle, time.Now())
if err := c.secretSyncer.Delete(handle, correlationID); err != nil {
c.logger.Warn("Failed to evict stale secret from local store",
slog.String("secret_handle", handle),
slog.String("correlation_id", correlationID),
slog.Any("error", err),
)
continue
}
c.secretHashCache.Delete(handle)
c.logger.Info("Evicted stale secret from local store during poll sync",
slog.String("secret_handle", handle),
slog.String("correlation_id", correlationID),
)
}

return len(stale)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Evict only handles that were cached before the poll started.

evictSecretsNotIn compares live secretHashCache state against activeHandles, which was derived from a response fetched earlier. secretHashCache is a shared sync.Map. syncSecretRefsFromYAML writes to it from artifact-deployment goroutines, and handleSecretUpdatedEvent writes to it from the WebSocket read loop. A handle added between the FetchPlatformSecrets call and this eviction is absent from activeHandles, so the poll deletes a secret that was just fetched for a live artifact. Resolution for that artifact then fails until the next poll restores it.

Snapshot the cache keys before the fetch and restrict eviction to that snapshot.

🛠️ Proposed fix
-	metas, err := c.apiUtilsService.FetchPlatformSecrets(nil, false)
+	preFetchHandles := c.cachedSecretHandles()
+	metas, err := c.apiUtilsService.FetchPlatformSecrets(nil, false)
 	if err != nil {
 		c.logger.Error("Failed to fetch platform secrets metadata", slog.Any("error", err))
 		return
 	}
-	evicted := c.evictSecretsNotIn(activeHandles)
+	evicted := c.evictSecretsNotIn(activeHandles, preFetchHandles)
-func (c *Client) evictSecretsNotIn(activeHandles map[string]struct{}) int {
+// cachedSecretHandles snapshots the handles currently in secretHashCache.
+func (c *Client) cachedSecretHandles() map[string]struct{} {
+	snapshot := make(map[string]struct{})
+	c.secretHashCache.Range(func(key, _ any) bool {
+		if handle, ok := key.(string); ok {
+			snapshot[handle] = struct{}{}
+		}
+		return true
+	})
+	return snapshot
+}
+
+// candidates limits eviction to handles cached before the poll response was
+// fetched, so a handle added concurrently is never mistaken for a stale one.
+func (c *Client) evictSecretsNotIn(activeHandles, candidates map[string]struct{}) int {
 	var stale []string
 	c.secretHashCache.Range(func(key, _ any) bool {
 		handle, ok := key.(string)
 		if !ok {
 			return true
 		}
+		if _, eligible := candidates[handle]; !eligible {
+			return true
+		}
 		if _, ok := activeHandles[handle]; !ok {
 			stale = append(stale, handle)
 		}
 		return true
 	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-controller/pkg/controlplane/sync_secrets.go` around lines 189
- 220, Snapshot the keys in secretHashCache before FetchPlatformSecrets begins,
then pass that snapshot into evictSecretsNotIn and only consider handles present
in it for eviction. Update the sync flow and evictSecretsNotIn signature
accordingly, preserving the existing deletion and logging behavior while
preventing handles added during the fetch from being removed.

Comment on lines +202 to +211
for _, handle := range stale {
correlationID := utils.GenerateDeterministicUUIDv7(handle, time.Now())
if err := c.secretSyncer.Delete(handle, correlationID); err != nil {
c.logger.Warn("Failed to evict stale secret from local store",
slog.String("secret_handle", handle),
slog.String("correlation_id", correlationID),
slog.Any("error", err),
)
continue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect GenerateDeterministicUUIDv7 and its existing call sites.
set -euo pipefail

ast-grep run --pattern 'func GenerateDeterministicUUIDv7($$$) { $$$ }' --lang go gateway/gateway-controller/pkg/utils
rg -nP --type=go -C 3 '\bGenerateDeterministicUUIDv7\s*\('

Repository: wso2/api-platform

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg 'sync_secrets\.go|utils\.go|uuid|correlation|GenerateDeterministicUUIDv7' || true

echo "== find function by text =="
rg -n --type=go -C 4 'GenerateDeterministicUUIDv7|DeterministicUUIDv7|deterministic.*UUID|UUIDv7|UUID.*v7' . || true

echo "== target file excerpt if present =="
if [ -f gateway/gateway-controller/pkg/controlplane/sync_secrets.go ]; then
  nl -ba gateway/gateway-controller/pkg/controlplane/sync_secrets.go | sed -n '170,225p'
fi

Repository: wso2/api-platform

Length of output: 31695


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== gateway utils GenerateDeterministicUUIDv7 =="
sed -n '80,115p' gateway/gateway-controller/pkg/utils/commonutils.go

echo "== platform-api GenerateDeterministicUUIDv7 =="
sed -n '526,550p' platform-api/internal/utils/common.go

echo "== relevant deterministic UUID test cases =="
sed -n '137,174p' gateway/gateway-controller/pkg/utils/api_deployment_test.go
sed -n '230,262p' gateway/gateway-controller/pkg/controlplane/sync_test.go

echo "== retry/poll context for secret eviction (if present) =="
rg -n --type=go -C 4 'stale|secretSyncer\.Delete|GenerateDeterministicUUIDv7\(handle' gateway/gateway-controller/pkg/controlplane/sync_secrets.go gateway/gateway-controller/pkg/controlplane || true

Repository: wso2/api-platform

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== sync polling/eviction call context =="
rg -n --type=go -C 4 'evictSecretsNotIn|Start.*poll|poll|Ticker|Ticker\(|time\.Ticker|ticker\.' gateway/gateway-controller/pkg/controlplane || true

echo "== deterministic UUID millisecond edge behavior probe =="
python3 - <<'PY'
import hashlib, struct, datetime
def uuid7(handle, t):
    ms = int(t.timestamp()*1000)
    b = bytearray(16)
    b[0:2] = struct.pack(">H", (ms >> 32) & 0xFFFF)
    b[2:6] = struct.pack(">I", ms & 0xFFFFFFFF)
    b[6:] = hashlib.sha256(handle.encode()).digest()[:10]
    b[6] |= 0x70
    b[8] |= 0x80
    return "{:08x}-{:04x}-{:04x}-{:02}{:02}-{:02}{:02}{:02}{:02}{:02}{:02}{:02}{:02}".format(
        struct.unpack(">I", bytes(b[:4]))[0],
        struct.unpack(">H", bytes(b[4:6]))[0],
        b[6] << 8 | b[7],
        b[8], b[9],
        struct.unpack(">H", bytes(b[10:12]))[0],
        struct.unpack(">H", bytes(b[12:14]))[0],
        struct.unpack(">H", bytes(b[14:16]))[0],
    )
base = datetime.datetime(2026, 1, 2, 3, 4, 5, 123456, tzinfo=datetime.timezone.utc)
for ns in [0, 500_000_000, 999_000_000, 999_999_999]:
    t = base + datetime.timedelta(nanoseconds=ns)
    print(base.isoformat(), "+", ns, "ns ->", uuid7("handle", t))
print("equal within same millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=999)))
print("different millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=1)))
PY

Repository: wso2/api-platform

Length of output: 18094


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deterministic UUID millisecond edge behavior probe =="
python3 - <<'PY'
import hashlib, struct, datetime
def uuid7(handle, t):
    ms = int(t.timestamp()*1000)
    b = bytearray(16)
    b[0:2] = struct.pack(">H", (ms >> 32) & 0xFFFF)
    b[2:6] = struct.pack(">I", ms & 0xFFFFFFFF)
    b[6:] = hashlib.sha256(handle.encode()).digest()[:10]
    b[6] |= 0x70
    b[8] |= 0x80
    return "{:08x}-{:04x}-{:04x}-{:02}{:02}-{:02}{:02}{:02}{:02}{:02}{:02}{:02}{:02}".format(
        struct.unpack(">I", bytes(b[:4]))[0],
        struct.unpack(">H", bytes(b[4:6]))[0],
        b[6] << 8 | b[7],
        b[8], b[9],
        struct.unpack(">H", bytes(b[10:12]))[0],
        struct.unpack(">H", bytes(b[12:14]))[0],
        struct.unpack(">H", bytes(b[14:16]))[0],
    )
base = datetime.datetime(2026, 1, 2, 3, 4, 5, 123456, tzinfo=datetime.timezone.utc)
for add_us in [0, 500000, 999000, 999999]:
    t = base + datetime.timedelta(microseconds=add_us)
    print(base.isoformat(), "+", add_us, "us ->", uuid7("handle", t))
print("equal within same millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=999)))
print("different millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=1)))
PY

echo "== retry/test around delete error keeps same correlation seed if same time still in same millisecond =="
sed -n '920,940p' gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go

Repository: wso2/api-platform

Length of output: 393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deterministic UUID millisecond edge behavior probe =="
python3 - <<'PY'
import hashlib, struct, datetime

def uuid7(handle, t):
    ms = int(t.timestamp() * 1000)
    b = bytearray(16)
    b[0:2] = struct.pack(">H", (ms >> 32) & 0xFFFF)
    b[2:6] = struct.pack(">I", ms & 0xFFFFFFFF)
    b[6:] = hashlib.sha256(handle.encode()).digest()[:10]
    b[6] |= 0x70
    b[8] |= 0x80
    return "{:08x}-{:04x}-{:04x}-{:02}{:02}-{:02}{:02}{:02}{:02}{:02}{:02}{:02}{:02}".format(
        struct.unpack(">I", bytes(b[:4]))[0],
        struct.unpack(">H", bytes(b[4:6]))[0],
        b[6] << 8 | b[7],
        b[8], b[9],
        struct.unpack(">H", bytes(b[10:12]))[0],
        struct.unpack(">H", bytes(b[12:14]))[0],
        struct.unpack(">H", bytes(b[14:16]))[0],
    )

base = datetime.datetime(2026, 1, 2, 3, 4, 5, 123456, tzinfo=datetime.timezone.utc)
for add_us in [0, 500000, 999000, 999999]:
    t = base + datetime.timedelta(microseconds=add_us)
    print(base.isoformat(), "+", add_us, "us ->", uuid7("handle", t))

print("equal within same millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=999)))
print("different millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=1)))
PY

echo "== retry behavior in sync_secrets_test =="
sed -n '920,940p' gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go

Repository: wso2/api-platform

Length of output: 393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import hashlib, struct, datetime

def uuid7(handle, t):
    ms = int(t.timestamp() * 1000)
    b = bytearray(16)
    b[0:2] = struct.pack(">H", (ms >> 32) & 0xFFFF)
    b[2:6] = struct.pack(">I", ms & 0xFFFFFFFF)
    b[6:] = hashlib.sha256(handle.encode()).digest()[:10]
    b[6] |= 0x70
    b[8] |= 0x80
    return ("{:08x}-{:04x}-{:04x}-{:02}{:02}-{:02}{:02}{:02}{:02}{:02}{:02}{:02}{:02}").format(
        struct.unpack(">I", bytes(b[:4]))[0],
        struct.unpack(">H", bytes(b[4:6]))[0],
        b[6] << 8 | b[7],
        b[8], b[9],
        struct.unpack(">H", bytes(b[10:12]))[0],
        struct.unpack(">H", bytes(b[12:14]))[0],
        struct.unpack(">H", bytes(b[14:16]))[0],
    )

base = datetime.datetime(2026, 1, 2, 3, 4, 5, 123456, tzinfo=datetime.timezone.utc)
for add_us in [0, 500000, 999000, 999999]:
    t = base + datetime.timedelta(microseconds=add_us)
    print(base.isoformat(), "+", add_us, "us ->", uuid7("handle", t))

print("equal within same millisecond:", uuid7("	handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=999)))
print("different millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=1)))
PY

Repository: wso2/api-platform

Length of output: 336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import hashlib, struct, datetime

def uuid7(handle, t):
    ms = int(t.timestamp() * 1000)
    b = bytearray(16)
    b[0:2] = struct.pack(">H", (ms >> 32) & 0xFFFF)
    b[2:6] = struct.pack(">I", ms & 0xFFFFFFFF)
    b[6:] = hashlib.sha256(handle.encode()).digest()[:10]
    b[6] |= 0x70
    b[8] |= 0x80
    parts = (
        struct.unpack(">I", bytes(b[:4]))[0],
        struct.unpack(">H", bytes(b[4:6]))[0],
        b[6] << 8 | b[7],
        b[8], b[9],
        struct.unpack(">H", bytes(b[10:12]))[0],
        struct.unpack(">H", bytes(b[12:14]))[0],
        struct.unpack(">H", bytes(b[14:16]))[0],
    )
    return "{:08x}-{:04x}-{:04x}-{:02}{:02}-{:02}{:02}{:02}{:02}{:02}{:02}{:02}{:02}".format(*parts)

base = datetime.datetime(2026, 1, 2, 3, 4, 5, 123456, tzinfo=datetime.timezone.utc)
for add_us in [0, 500000, 999000, 999999]:
    t = base + datetime.timedelta(microseconds=add_us)
    print(base.isoformat(), "+", add_us, "us ->", uuid7("handle", t))

print("equal within same millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=999)))
print("different millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=1)))
PY

echo "== retry behavior in sync_secrets_test =="
sed -n '920,940p' gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go

Repository: wso2/api-platform

Length of output: 336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import hashlib, struct, datetime

def uuid7(handle, t):
    ms = int(t.timestamp() * 1000)
    b = bytearray(16)
    b[0:2] = struct.pack(">H", (ms >> 32) & 0xFFFF)
    b[2:6] = struct.pack(">I", ms & 0xFFFFFFFF)
    b[6:] = hashlib.sha256(handle.encode()).digest()[:10]
    b[6] |= 0x70
    b[8] |= 0x80
    return uuid7_bytes(bytes(b))

def uuid7_bytes(b):
    parts = ["{:08x}".format(struct.unpack(">I", b[:4])[0]),
             "{:04x}".format(struct.unpack(">H", b[4:6])[0]),
             "{:04x}".format(b[6] << 8 | b[7]),
             "{:02x}{:02x}".format(b[8], b[9]),
             "{:02x}{:02x}".format(struct.unpack(">H", b[10:12])[0]),
             "{:02x}{:02x}".format(struct.unpack(">H", b[12:14])[0]),
             "{:02x}{:02x}".format(struct.unpack(">H", b[14:16])[0])]
    return "-".join(parts)

base = datetime.datetime(2026, 1, 2, 3, 4, 5, 123456, tzinfo=datetime.timezone.utc)
for add_us in [0, 500000, 999000, 999999]:
    t = base + datetime.timedelta(microseconds=add_us)
    print(base.isoformat(), "+", add_us, "us ->", uuid7("handle", t))

print("equal within same millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=999)))
print("different millisecond:", uuid7("handle", base) == uuid7("handle", base + datetime.timedelta(milliseconds=1)))
PY

echo "== retry behavior in sync_secrets_test =="
sed -n '920,940p' gateway/gateway-controller/pkg/controlplane/sync_secrets_test.go

Repository: wso2/api-platform

Length of output: 378


Use per-attempt entropy for stale-secret eviction correlation IDs.

GenerateDeterministicUUIDv7 truncates the UUIDv7 timestamp to milliseconds and derives the rest of the UUID from the handle hash. A retry within the same millisecond for the same secret handle can reuse the same correlation_id, so separate eviction attempts can appear merged in logs. Use a non-deterministic or per-attempt value for this retry path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-controller/pkg/controlplane/sync_secrets.go` around lines 202
- 211, Update the stale-secret eviction loop around Delete to generate a
non-deterministic, per-attempt correlation ID instead of calling
GenerateDeterministicUUIDv7 with handle and time.Now(). Keep the existing
correlation_id logging and Delete behavior unchanged.

Comment on lines +180 to +185
<IconButton
size="small"
onClick={() => setShowValue((prev) => !prev)}
aria-label={showValue ? 'Hide value' : 'Show value'}
tabIndex={-1}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore keyboard access to the visibility control.

Line 184 removes the IconButton from the tab order. Keyboard users cannot focus or activate the show/hide control.

Proposed fix
-                    tabIndex={-1}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<IconButton
size="small"
onClick={() => setShowValue((prev) => !prev)}
aria-label={showValue ? 'Hide value' : 'Show value'}
tabIndex={-1}
>
<IconButton
size="small"
onClick={() => setShowValue((prev) => !prev)}
aria-label={showValue ? 'Hide value' : 'Show value'}
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/ai-workspace/src/Components/common/SecretValueField.tsx` around lines
180 - 185, Restore keyboard access to the visibility control by removing the
tabIndex={-1} override from the IconButton in SecretValueField. Preserve its
existing show/hide behavior, aria-label, and styling while allowing the button
to participate in normal keyboard tab navigation.

Comment on lines +102 to +106
await updateSecret(handle, {
value: value.trim(),
name: displayName.trim() || undefined,
description: description.trim() || undefined,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve explicit metadata edits.

A blank description becomes undefined, and updateSecret then omits it from the request. Users cannot clear an existing description. A blank display name also becomes a silent no-op instead of a validation error.

Validate a non-empty display name. Preserve '' for description and update updateSecret to append fields when they are !== undefined.

Proposed fix
-        name: displayName.trim() || undefined,
-        description: description.trim() || undefined,
+        name: displayName.trim(),
+        description: description.trim(),
-  if (request.name) form.append('displayName', request.name);
-  if (request.description) form.append('description', request.description);
+  if (request.name !== undefined) form.append('displayName', request.name);
+  if (request.description !== undefined) form.append('description', request.description);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@portals/ai-workspace/src/pages/appShell/appShellPages/secret/RotateSecret.tsx`
around lines 102 - 106, Update the RotateSecret submission flow to validate that
the trimmed display name is non-empty, while preserving a trimmed empty
description as ''. In updateSecret, append metadata fields whenever their values
are !== undefined so an explicit empty description is included in the request
rather than omitted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants