Skip to content

MM-68853: Confluence Cloud install path via OAuth 2.0 (3LO) + Forge bridge POC#228

Merged
nang2049 merged 14 commits into
masterfrom
MM-68853-forge-cloud-migration
Jun 12, 2026
Merged

MM-68853: Confluence Cloud install path via OAuth 2.0 (3LO) + Forge bridge POC#228
nang2049 merged 14 commits into
masterfrom
MM-68853-forge-cloud-migration

Conversation

@nang2049

@nang2049 nang2049 commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Atlassian removed the "Install a private app" path for new Confluence Cloud sites on March 31, 2026 and new customers can no longer connect Mattermost to Confluence Cloud at all and existing installs keep working until Atlassian's Q4 2026 end of support.

This POC replaces the dead Connect descriptor path with OAuth 2.0 (3LO) for user auth and a small Forge app for event
delivery. Legacy Connect installs are untouched and the webhook receiver at /cloud/{event}
stays so events from pre-cutoff customers keep flowing until Q4 2026.

  • New IsCloud config flag, set by the wizard, that branches /confluence connect
    between Atlassian's 3LO endpoints and the existing Server OAuth path.
  • New Cloud setup wizard (/confluence install cloud) that walks admins through registering a 3LO app at developer.atlassian.com/console, pasting client ID and installing the Forge bridge.
  • CompleteOAuth2 split into completeServerOAuth2 / completeCloudOAuth2.
  • Plugin-side Forge poller: 30s ticker, HMAC signed POSTs to Forge.
  • Two new System Console fields: ForgeSharedSecret (auto-generated) and ForgeDrainURL (admin pastes the URL from the Forge install).
  • One deploy by us serves every customer.
  • Clean install consent screen.
  • Private install link from the Atlassian developer console. We forge deploy once into the Mattermost developer account and put the link in the wizard.
  • Removed server/atlassian_connect.go and assets/templates/atlassian-connect.json
  • installCloudHelp const and GetAtlassianConnectURLPath() helper.

Ticket Link

https://mattermost.atlassian.net/browse/MM-68853

Change Impact: 🔴 High

Reasoning: The PR introduces Cloud OAuth2 (3LO) onboarding, a Mattermost-managed Forge bridge with HMAC-signed poll/drain mechanics, new configuration/secret handling (IsCloud, ForgeSharedSecret, ForgeDrainURL), and removes the legacy Connect descriptor—changes spanning authentication, event ingestion, background processing, and installation lifecycles.

Regression Risk: Very High — it touches authentication/token exchange, cross-service HMAC-signed communication, scheduled poller lifecycle, configuration auto-generation, event serialization/dispatch, and public plugin endpoints; defects could disrupt installs, event delivery, or plugin startup.

** QA Recommendation:** Do not skip manual QA. Perform comprehensive end-to-end tests for Cloud and Server OAuth flows, Forge install/register/drain (signature verification, conflict/timeout handling, replay/ack behavior), poller scheduling/stop on activation/deactivation, secrets auto-generation and persistence, and regression tests for existing Server/DC webhook and mention flows. Validate the Forge bridge in staging with representative Confluence Cloud instances before production rollout.

Generated by CodeRabbitAI

@nang2049
nang2049 requested a review from a team as a code owner May 20, 2026 14:57
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

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
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds a Forge-based Confluence event bridge, Cloud 3LO onboarding and Forge registration, Forge webtrigger handlers (enqueue/drain/register), a clustered poller that drains signed events and posts acks, Forge event serialization, mention extraction and DM dispatch, plugin config/settings and lifecycle wiring, CI/manifest validation, and removal of the legacy Atlassian Connect descriptor.

Changes

Confluence Cloud Forge Bridge GA Implementation

Layer / File(s) Summary
Forge Bridge Package & Configs
forge/package.json, forge/tsconfig.json, forge/tsconfig.scripts.json, forge/scripts/tsconfig.json, forge/README.md, .github/workflows/forge-ci.yml, .gitignore, forge/scripts/validate-manifest.ts
Adds Forge package metadata, TypeScript configurations, README, manifest validation script, CI workflow, and ignore rules.
Forge Manifest
forge/manifest.yml
Declares Confluence lifecycle triggers, webtriggers (drain, register), function mappings, runtime (nodejs22.x), concrete app id, and permissions.
Forge Runtime Handlers
forge/src/index.ts
Implements enqueue, drain (HMAC verification, cursor/limit/ack), register (one-shot secret init), onInstalled, enrichment helpers, and JSON response utilities.
Plugin Config & Settings
server/config/main.go, plugin.json, server/plugin.go
Adds IsCloud, ForgeSharedSecret, ForgeDrainURL to config and settings; integrates secret auto-generation and plugin lifecycle (forge poller start/stop).
Cloud OAuth2 & Client
server/instance_cloud.go, server/client_cloud.go
Implements Atlassian 3LO endpoints and helper functions: OAuth2 config/auth URL, accessible-resources fetch, token-backed Cloud client, and GetSelf().
Cloud Setup Wizard & Command Wiring
server/flow.go, server/command.go
Adds cloud-setup and cloud-completion flows, edition-selection step, OAuth credential capture, Forge bridge registration step (validate/register/persist drain URL), and command to start the wizard plus mention settings commands.
OAuth Completion & Connection
server/user.go
Routes CompleteOAuth2 to cloud/server-specific completion; Cloud flow exchanges code, matches cloud resource, obtains user, and connects the account.
Forge Poller & Mapping
server/forge_event_mapping.go, server/forge_poller.go, server/plugin.go
Maps Forge event names to internal types, implements ForgePoller that drains signed batches, dispatches events, posts ack, alerts on errors, and integrates with plugin activation lifecycle.
Forge Event Serializer & Notifications
server/serializer/confluence_forge.go
Adds Forge event JSON wire types, tolerant ID unmarshalling, URL/page/space extraction helpers, and notification post generation for page and comment events.
Mention Extraction & Dispatch
server/service/adf.go, server/service/storage_xhtml.go, server/service/mentions.go, server/service/mention_settings.go, server/service/*_test.go
Adds ADF and storage mention extractors, per-user mention notification toggles, DM dispatch to recipients, and tests covering extraction and dispatch behavior.
Server Webhook & Client Changes
server/confluence_server.go, server/client_server.go, server/client.go, server/store/store.go
Server webhook now triggers asynchronous mention-DM dispatch (admin/non-admin), server client extracts mentions from storage, store preserves admin sentinel reverse mapping, and controller/util paths remove Atlassian Connect routing.
Tests & Cleanup
server/plugin_test.go, server/service/notification_test.go, server/controller.go, server/util/util.go, assets/templates/atlassian-connect.json
Expands secret auto-generation tests, relaxes log expectations in mocks, removes legacy Atlassian Connect route/helper and template asset.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • avasconcelos114

Poem

🐰 I bounced a secret through the cloud,

I queued the events, then signed them proud,
OAuth doors and webhooks bright,
Drains that hum in moonlit night,
A tiny bridge — the rabbit’s loud.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing Confluence Cloud install path via OAuth 2.0 (3LO) and Forge bridge POC.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MM-68853-forge-cloud-migration

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (1)
.gitignore (1)

91-91: ⚡ Quick win

Do not ignore forge/package-lock.json for this deployable Node project.

Ignoring the lockfile makes Forge CI/deploy dependency resolution non-deterministic. Keep the lockfile versioned and remove this ignore rule.

Suggested diff
 # Forge bridge
 forge/dist/
 forge/scripts-dist/
 forge/node_modules/
 forge/.forge/
-forge/package-lock.json
🤖 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 @.gitignore at line 91, The .gitignore currently excludes the project's Forge
lockfile causing non-deterministic installs; remove the ignore entry for
"forge/package-lock.json" so the lockfile is tracked. Edit the .gitignore and
delete the line "forge/package-lock.json" (or comment it out) to ensure
package-lock.json under the forge directory is committed and versioned, then
commit the change so CI/deploy uses the deterministic lockfile.
🤖 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 @.github/workflows/forge-ci.yml:
- Around line 23-29: The workflow uses mutable action tags and leaves checkout
credentials persisted; update the checkout and setup-node steps to pin each
action to a full commit SHA (replace actions/checkout@v4 and
actions/setup-node@v4 with their respective commit SHAs) and add
persist-credentials: false to the actions/checkout step so credentials are not
kept in the runner; locate the checkout and setup-node usages in the workflow
and make these changes for supply-chain and token-leak hardening.

In `@forge/README.md`:
- Line 67: Update the README sentence to use the correct product name casing by
changing "Github" (or any lowercase variant) to "GitHub" in the line mentioning
`server/` and `webapp/` builds and CI (`.github/workflows/forge-ci.yml`) so the
product name is properly capitalized.
- Around line 103-104: The README line referencing the Cloud OAuth file is
stale: locate the actual Cloud 3LO OAuth implementation (search for the Cloud
OAuth handler/flow code referenced near "Plugin-side Cloud 3LO OAuth" and the
implementation in the repository) and update the README so it points to the
correct source file instead of the outdated server/cloud_oauth_flow.go
reference; ensure the line mentions the confirmed file that contains the Cloud
OAuth flow (and keep the mention of server/instance_cloud.go only if that file
is still accurate).

In `@forge/src/index.ts`:
- Around line 41-46: Validate and clamp the incoming body.limit before calling
storage.query().limit: parse it to a number (e.g., Number(body.limit) or
parseInt), guard against NaN and non-integers with Math.floor, then clamp the
result to the range [1, MAX_DRAIN_BATCH] (default to MAX_DRAIN_BATCH when
missing/invalid). Replace the current const limit = Math.min(...) with a
validatedLimit variable and pass validatedLimit into
storage.query().limit(limit) so .limit always receives a positive integer
between 1 and MAX_DRAIN_BATCH; reference symbols: body.limit, MAX_DRAIN_BATCH,
QUEUE_PREFIX, storage.query().limit(...).
- Around line 37-39: The code currently deletes every key in body.ack via
storage.delete without validating ownership; change it to only delete keys that
belong to the target queue by validating each ack key before deletion. For each
key in body.ack, call a validation (e.g., storage.get(k) and check returned
metadata.owner === queueId or implement an isQueueKey(k, queueId) helper that
checks the queue-specific prefix/owner), filter the ack list to only those keys
that pass validation, then await Promise.all(filtered.map(k =>
storage.delete(k))); keep references to body.ack and storage.delete and add
storage.get or isQueueKey/queueId validation as needed.

In `@server/flow.go`:
- Around line 741-752: The config isn't being marked as Cloud before saving the
Cloud OAuth credentials, so ensure you set the configuration's IsCloud flag to
true on the cfg returned by fm.getConfiguration() (e.g., cfg.IsCloud = true)
prior to calling cfg.Sanitize(), cfg.ToMap(), and
fm.client.Configuration.SavePluginConfig; update the block that sets
cfg.ConfluenceOAuthClientID/Secret to also set cfg.IsCloud = true so the
subsequent saved config routes to the Cloud oauth handlers (affecting
fm.getConfiguration(), cfg.Sanitize, cfg.ToMap, and SavePluginConfig usage).

In `@server/forge_poller.go`:
- Around line 137-148: The loop is acking Forge events even when delivery may
have failed because dispatch currently returns before
SendConfluenceNotifications completes; change dispatch so it only returns nil
after SendConfluenceNotifications has finished successfully (or return an
error/result/future that this loop waits on), then only append evt.Key to
ackKeys when dispatch returns no error; update the same logic at the other
occurrence (lines around 194-195) so ackBody/json.Marshal and fp.post are only
called for successfully delivered events and errors from
SendConfluenceNotifications are propagated back and logged via
fp.plugin.client.Log.Warn.

In `@server/instance_cloud.go`:
- Around line 118-126: The refreshed Cloud token is being stored under cloudID
but should be persisted under the configured instance key (instance URL) so
later lookups by instanceURL see the updated token; update the call site in this
block to pass both the configured instance URL and the resolved cloudID to
refreshAndStoreToken (or adjust refreshAndStoreToken's signature) and ensure the
implementation writes the refreshed token into the KV record keyed by the
instance URL (the same key used by connectUser), while leaving the rest of the
function (e.g. httpClient creation and
newCloudClient(fmt.Sprintf(cloudAPIBaseFmt, cloudID), httpClient)) unchanged.
- Around line 43-51: The OAuth scopes list is missing the read:confluence-user
scope required by client.GetSelf(); update the scopes slice in
completeCloudOAuth2 to include "read:confluence-user" (so the GET
/wiki/rest/api/user/current will be allowed) and also update the
confluenceCloudScopes constant in server/flow.go to include the same
"read:confluence-user" entry so both places mirror each other; touch the scopes
defined where completeCloudOAuth2 builds the scopes and the
confluenceCloudScopes constant to ensure they match.

In `@server/user.go`:
- Around line 271-273: The current fallback that returns resources[0].ID when no
resource URL matched the configured ConfluenceURL silently masks a config
mismatch; instead, change the behavior in the function handling the resources
slice (the block that currently checks if len(resources) == 1 and returns
resources[0].ID) to fail fast: do not return the sole resource when URL matching
failed—return an explicit error that includes the configured ConfluenceURL and
the list of returned resource URLs (or at least the single resource URL) so
callers can detect and surface the configuration mismatch. Ensure the error
replaces the current silent return path and preserves the original
matched-return behavior when a proper URL match exists.

---

Nitpick comments:
In @.gitignore:
- Line 91: The .gitignore currently excludes the project's Forge lockfile
causing non-deterministic installs; remove the ignore entry for
"forge/package-lock.json" so the lockfile is tracked. Edit the .gitignore and
delete the line "forge/package-lock.json" (or comment it out) to ensure
package-lock.json under the forge directory is committed and versioned, then
commit the change so CI/deploy uses the deterministic lockfile.
🪄 Autofix (Beta)

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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a1ae89d3-4c20-433c-904e-ece254192d7a

📥 Commits

Reviewing files that changed from the base of the PR and between d95079e and 73eabaa.

📒 Files selected for processing (24)
  • .github/workflows/forge-ci.yml
  • .gitignore
  • assets/templates/atlassian-connect.json
  • forge/README.md
  • forge/manifest.yml
  • forge/package.json
  • forge/scripts/tsconfig.json
  • forge/scripts/validate-manifest.ts
  • forge/src/index.ts
  • forge/tsconfig.json
  • forge/tsconfig.scripts.json
  • plugin.json
  • server/atlassian_connect.go
  • server/client_cloud.go
  • server/command.go
  • server/config/main.go
  • server/controller.go
  • server/flow.go
  • server/forge_event_mapping.go
  • server/forge_poller.go
  • server/instance_cloud.go
  • server/plugin.go
  • server/user.go
  • server/util/util.go
💤 Files with no reviewable changes (4)
  • assets/templates/atlassian-connect.json
  • server/util/util.go
  • server/atlassian_connect.go
  • server/controller.go

Comment thread .github/workflows/forge-ci.yml
Comment thread forge/README.md
Comment thread forge/README.md Outdated
Comment thread forge/src/index.ts
Comment thread forge/src/index.ts Outdated
Comment thread server/flow.go
Comment thread server/forge_poller.go
Comment thread server/instance_cloud.go
Comment thread server/instance_cloud.go Outdated
Comment thread server/user.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
server/user.go (1)

350-358: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate StoreConnection calls will double-write unnecessarily.

Lines 350-353 and 355-358 are identical—both store the connection under (instanceID, mattermostUserID). This appears to be a copy-paste error. The second call should be removed.

🐛 Proposed fix to remove duplicate call
 	if err = store.StoreConnection(instanceID, mattermostUserID, connection); err != nil {
 		p.client.Log.Error("Error storing connection", "InstanceID", instanceID, "UserID", mattermostUserID, "error", err.Error())
 		return err
 	}
 
-	if err = store.StoreConnection(instanceID, mattermostUserID, connection); err != nil {
-		p.client.Log.Error("Error storing connection", "InstanceID", instanceID, "UserID", mattermostUserID, "error", err.Error())
-		return err
-	}
-
 	if err = store.StoreConnection(instanceID, AdminMattermostUserID, connection); err != nil {
🤖 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 `@server/user.go` around lines 350 - 358, The code duplicates a call to
store.StoreConnection with identical parameters and identical error handling,
causing a double-write; edit the function where these lines appear to remove the
second/duplicate store.StoreConnection(...) block (the repeated block that also
calls p.client.Log.Error on failure) so only a single call to
store.StoreConnection(instanceID, mattermostUserID, connection) remains with its
error check and p.client.Log.Error logging.
♻️ Duplicate comments (1)
forge/src/index.ts (1)

118-122: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Ensure clampLimit never returns 0 for fractional positive input.

For raw = 0.5, Math.floor makes the limit 0. That breaks the intended positive clamp behavior.

Suggested fix
 const clampLimit = (raw: unknown): number => {
     const n = typeof raw === 'number' ? raw : Number(raw);
     if (!Number.isFinite(n) || n <= 0) return MAX_DRAIN_BATCH;
-    return Math.min(Math.floor(n), MAX_DRAIN_BATCH);
+    return Math.max(1, Math.min(Math.floor(n), MAX_DRAIN_BATCH));
 };
🤖 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 `@forge/src/index.ts` around lines 118 - 122, clampLimit currently floors
positive fractional inputs which makes 0.5 become 0; update the final return to
ensure a minimum of 1 for any positive finite input. In the clampLimit function,
replace return Math.min(Math.floor(n), MAX_DRAIN_BATCH) with return
Math.min(Math.max(Math.floor(n), 1), MAX_DRAIN_BATCH) (keep the existing early
check using MAX_DRAIN_BATCH for non-finite or non-positive n).
🧹 Nitpick comments (3)
server/flow.go (3)

609-632: 💤 Low value

Cloud instance URL step also reuses stepConfluenceURL name.

Similar to the welcome step, stepCloudInstanceURL() returns a step with name stepConfluenceURL (line 610), which is the same as the generic instance URL step. This means both flows have steps with the same identifier but different behavior (different dialog handlers).

🤖 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 `@server/flow.go` around lines 609 - 632, The stepCloudInstanceURL function is
creating a step with the duplicate identifier stepConfluenceURL which collides
with the generic instance URL step; rename the step identifier to a unique name
(e.g., stepCloudConfluenceURL) in the flow.NewStep call inside
stepCloudInstanceURL and update any places that reference that step name to use
the new identifier, and ensure the dialog submit handler
(submitCloudConfluenceURL) remains wired to the renamed step so the
cloud-specific dialog and handler are correctly isolated from the generic step.

593-607: 💤 Low value

Cloud welcome step reuses stepWelcome name, may conflict with generic welcome step.

stepCloudWelcome() returns a step with name stepWelcome (line 602), which is the same constant used by stepWelcome() (line 246). While different flows can have steps with the same name, this could cause confusion or unexpected behavior if both steps are ever used in the same flow context or during debugging.

Consider using a distinct step name like stepCloudWelcome for the cloud-specific welcome step.

🤖 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 `@server/flow.go` around lines 593 - 607, The cloud-specific welcome step
currently reuses the generic step name constant stepWelcome in
stepCloudWelcome(), which can cause confusion or conflicts; change the step name
passed to flow.NewStep from stepWelcome to a distinct constant (e.g.,
stepCloudWelcome) and create that new constant, then update any
cloud-flow-specific references/handlers that expect the old name (e.g., in
stepCloudWelcome, any transition targets or lookups) so the cloud welcome is
uniquely identified from the generic stepWelcome.

761-781: 💤 Low value

Forge shared secret displayed in plaintext in the wizard instructions.

The wizard renders the ForgeSharedSecret directly in the step text (line 773) and passes it through state (lines 202, 757). While the secret is only visible to the admin running the wizard, be aware that:

  1. The secret will appear in the Mattermost channel/DM message history
  2. It could be logged or captured in screenshots

This may be acceptable for a setup flow, but consider noting in the instructions that the admin should treat this as sensitive.

🤖 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 `@server/flow.go` around lines 761 - 781, The stepCloudForgeBridge step
currently injects ForgeSharedSecret into the public step text (and passes it via
flow state), which exposes the secret in message history/screenshots; remove the
plaintext {{.ForgeSharedSecret}} from the step text in stepCloudForgeBridge and
instead either (a) show a masked placeholder and add a clear sensitive-handling
note telling the admin where to copy the secret from (e.g., plugin config/secure
storage), or (b) provide an explicit "Reveal secret" action that fetches the
secret directly from secure storage and displays it only in an ephemeral modal;
also stop including the secret in the generic flow/state payloads (references:
ForgeSharedSecret, stepCloudForgeBridge, and the places where state is
set/passed around) so the secret isn't stored or transmitted in step
text/history.
🤖 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.

Outside diff comments:
In `@server/user.go`:
- Around line 350-358: The code duplicates a call to store.StoreConnection with
identical parameters and identical error handling, causing a double-write; edit
the function where these lines appear to remove the second/duplicate
store.StoreConnection(...) block (the repeated block that also calls
p.client.Log.Error on failure) so only a single call to
store.StoreConnection(instanceID, mattermostUserID, connection) remains with its
error check and p.client.Log.Error logging.

---

Duplicate comments:
In `@forge/src/index.ts`:
- Around line 118-122: clampLimit currently floors positive fractional inputs
which makes 0.5 become 0; update the final return to ensure a minimum of 1 for
any positive finite input. In the clampLimit function, replace return
Math.min(Math.floor(n), MAX_DRAIN_BATCH) with return
Math.min(Math.max(Math.floor(n), 1), MAX_DRAIN_BATCH) (keep the existing early
check using MAX_DRAIN_BATCH for non-finite or non-positive n).

---

Nitpick comments:
In `@server/flow.go`:
- Around line 609-632: The stepCloudInstanceURL function is creating a step with
the duplicate identifier stepConfluenceURL which collides with the generic
instance URL step; rename the step identifier to a unique name (e.g.,
stepCloudConfluenceURL) in the flow.NewStep call inside stepCloudInstanceURL and
update any places that reference that step name to use the new identifier, and
ensure the dialog submit handler (submitCloudConfluenceURL) remains wired to the
renamed step so the cloud-specific dialog and handler are correctly isolated
from the generic step.
- Around line 593-607: The cloud-specific welcome step currently reuses the
generic step name constant stepWelcome in stepCloudWelcome(), which can cause
confusion or conflicts; change the step name passed to flow.NewStep from
stepWelcome to a distinct constant (e.g., stepCloudWelcome) and create that new
constant, then update any cloud-flow-specific references/handlers that expect
the old name (e.g., in stepCloudWelcome, any transition targets or lookups) so
the cloud welcome is uniquely identified from the generic stepWelcome.
- Around line 761-781: The stepCloudForgeBridge step currently injects
ForgeSharedSecret into the public step text (and passes it via flow state),
which exposes the secret in message history/screenshots; remove the plaintext
{{.ForgeSharedSecret}} from the step text in stepCloudForgeBridge and instead
either (a) show a masked placeholder and add a clear sensitive-handling note
telling the admin where to copy the secret from (e.g., plugin config/secure
storage), or (b) provide an explicit "Reveal secret" action that fetches the
secret directly from secure storage and displays it only in an ephemeral modal;
also stop including the secret in the generic flow/state payloads (references:
ForgeSharedSecret, stepCloudForgeBridge, and the places where state is
set/passed around) so the secret isn't stored or transmitted in step
text/history.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d1252f1c-4fb4-4832-a3d4-679384f8ea30

📥 Commits

Reviewing files that changed from the base of the PR and between 73eabaa and 73d08ee.

📒 Files selected for processing (6)
  • forge/README.md
  • forge/src/index.ts
  • plugin.json
  • server/flow.go
  • server/instance_cloud.go
  • server/user.go

@avasconcelos114

Copy link
Copy Markdown
Member

@nang2049 I've been looking through this and getting acquainted with Forge a little bit more, I think you got the right direction for the architecture with us directly maintaining the Forge bridge app and customers just use the private distribution link to set it up with their instances 👍

I guess now we should have a test app deployed and we can set it up against multiple Jira/MM instance pairs to confirm all is working well before moving onto doing more specific code-level reviews (and likely security reviews)

@nang2049

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (3)
forge/src/index.ts (1)

68-70: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate payload.secret type before one-shot registration.

Non-string values can pass the current check and be stored as the secret, which can permanently break signed drain requests after registration.

Suggested fix
-    if (!payload.secret || payload.secret.length < 32) {
+    if (typeof payload.secret !== 'string' || payload.secret.length < 32) {
         return jsonResponse(400, { error: 'secret must be at least 32 characters' });
     }
🤖 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 `@forge/src/index.ts` around lines 68 - 70, The handler currently only checks
payload.secret.length but doesn't verify payload.secret is a string, allowing
non-string values to bypass validation; update the validation in the
registration logic (the code around payload.secret check in forge/src/index.ts)
to first ensure typeof payload.secret === 'string' (and not null/undefined) and
only then verify length >= 32, returning a 400 JSON response if the type check
fails or the length is insufficient so non-string secrets cannot be stored and
break signed drain requests.
server/flow.go (2)

658-688: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-Cloud hosts before setting IsCloud.

This path only normalizes the URL, so any HTTPS host is accepted and persisted with IsCloud = true. A self-hosted or mistyped URL will send /confluence connect down the 3LO path and fail later instead of being rejected here.

Minimal fix
 	normalized, err := service.NormalizeConfluenceURL(confluenceURL)
 	if err != nil {
 		return "", nil, map[string]string{"confluence_url": err.Error()}, nil
 	}
+	parsed, err := url.Parse(normalized)
+	if err != nil || !strings.HasSuffix(parsed.Hostname(), ".atlassian.net") {
+		return "", nil, map[string]string{
+			"confluence_url": "Use your Confluence Cloud site URL (`*.atlassian.net/wiki`).",
+		}, nil
+	}
 	if normalized == strings.TrimSuffix(fm.MMSiteURL, "/") {
 		return "", nil, map[string]string{"confluence_url": "This is the Mattermost site URL. Please use the Confluence Cloud URL."}, nil
 	}
 	confluenceURL = normalized

63-78: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the server wizard server-only.

setupFlow is now only started from /confluence install server in server/command.go, but it still branches into stepEditionQuestion and the Cloud-only steps. If an admin clicks Cloud here, the flow skips stepCloudInstanceURL and reuses the URL already saved by submitConfluenceURL, which leaves the config on the Cloud path against the wrong site.

Minimal fix
 	setupFlow.WithSteps(
 		fm.stepWelcome(),
 		fm.stepInstanceURL(),
-		fm.stepEditionQuestion(),
-		fm.stepCloudOAuthConfigure(),
-		fm.stepCloudForgeBridge(),
 		fm.stepServerVersionQuestion(),
 		fm.stepCSversionGreaterthan9(),
 		fm.stepCSversionLessthan9(),
 		fm.stepOAuthInput(),
@@
-	return stepEditionQuestion, flow.State{
+	return stepServerVersionQuestion, flow.State{
 		keyConfluenceURL: cfg.GetConfluenceBaseURL(),
 	}, nil, nil

Also applies to: 410-412

🤖 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 `@server/flow.go` around lines 63 - 78, The flow currently includes
Cloud-specific steps (fm.stepEditionQuestion, fm.stepCloudOAuthConfigure,
fm.stepCloudForgeBridge, etc.) even though setupFlow is only invoked for the
server installer, causing Cloud selection to reuse the wrong URL saved by
submitConfluenceURL; update the setupFlow sequence to be server-only by removing
or replacing fm.stepEditionQuestion with a server-only edition step (or remove
Cloud branch entirely) and remove the Cloud-specific steps
(fm.stepCloudOAuthConfigure, fm.stepCloudForgeBridge, any stepCloudInstanceURL
references) so only server-related steps (e.g., fm.stepInstanceURL,
fm.stepServerVersionQuestion, fm.stepCSversionGreaterthan9,
fm.stepCSversionLessthan9, fm.stepOAuthInput, fm.stepOAuthConnect,
fm.stepAnnouncementQuestion, fm.stepAnnouncementConfirmation, fm.stepDone,
fm.stepCancel) remain; ensure submitConfluenceURL is not reused by Cloud steps
by keeping Cloud flow in a separate flow invoked only from Cloud commands.
♻️ Duplicate comments (1)
server/forge_poller.go (1)

176-189: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Deliver notifications synchronously before acking the Forge key.

dispatch returns immediately because SendConfluenceNotifications runs in a goroutine, but drainOnce still acknowledges the key right after dispatch returns. That makes this path drop events from Forge before delivery has actually finished.

Possible minimal fix
func (fp *ForgePoller) dispatch(evt forgeDrainEvent) error {
    forgeEvent, err := serializer.ForgeEventFromJSON(bytes.NewReader(evt.Value.Event))
    if err != nil {
        return errors.Wrap(err, "deserialize forge event")
    }

    internal, ok := forgeToInternalEvent[forgeEvent.EventType]
    if !ok {
        return fmt.Errorf("unmapped forge event type %q", forgeEvent.EventType)
    }

    forgeEvent.BaseURL = config.GetConfig().GetConfluenceBaseURL()

-   go service.SendConfluenceNotifications(forgeEvent, internal)
+   service.SendConfluenceNotifications(forgeEvent, internal)
    return nil
}
for _, evt := range resp.Events {
    if err := fp.dispatch(evt); err != nil {
        fp.plugin.client.Log.Warn("forge drain: dropping event", "key", evt.Key, "error", err.Error())
+       continue
    }
    ackKeys = append(ackKeys, evt.Key)
}
🤖 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 `@server/forge_poller.go` around lines 176 - 189, The dispatch path currently
launches notifications asynchronously (using "go
service.SendConfluenceNotifications") so drainOnce can ack the Forge key before
delivery completes; change this to perform notifications synchronously by
removing the goroutine and calling
service.SendConfluenceNotifications(forgeEvent, internal) directly, and ensure
SendConfluenceNotifications returns an error (or add a blocking wrapper) so you
can check its result and return that error instead of nil — this guarantees
delivery completes (or fails) before returning from the dispatch path that
acknowledges the key (references: forgeEvent, internal,
service.SendConfluenceNotifications).
🧹 Nitpick comments (1)
server/plugin_test.go (1)

91-198: ⚡ Quick win

Assert the persisted secret values, not just that a save happened.

These cases would still pass if OnConfigurationChange saved the wrong field or wrote back a non-32-char value. Please capture the SavePluginConfig payload and verify the regenerated fields are 32 chars while untouched fields stay unchanged; adding one non-empty wrong-length input would also cover the actual len(...) != 32 branch.

Example of a stronger assertion pattern
+ var saved map[string]interface{}
- mockAPI.On("SavePluginConfig", mock.AnythingOfType("map[string]interface {}")).Return(nil)
+ mockAPI.On("SavePluginConfig", mock.AnythingOfType("map[string]interface {}")).Run(func(args mock.Arguments) {
+     saved = args.Get(0).(map[string]interface{})
+ }).Return(nil)

  err := p.OnConfigurationChange()
  require.NoError(t, err)
+ require.NotNil(t, saved)
+ assert.Len(t, saved["encryptionkey"], 32)
+ assert.Equal(t, valid, saved["secret"])
+ assert.Equal(t, valid, saved["forgesharedsecret"])
🤖 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 `@server/plugin_test.go` around lines 91 - 198, Update the tests in
TestOnConfigurationChange_AutoGenerateSecrets to assert the actual persisted
config values instead of only checking SavePluginConfig was called: in each
subtest, use the plugintest API's mock SavePluginConfig handler to capture the
map[string]interface{} payload passed to SavePluginConfig (when
OnConfigurationChange calls SavePluginConfig), then assert the regenerated
fields (EncryptionKey, Secret, ForgeSharedSecret) are exactly 32 chars long and
that any pre-existing valid fields remain unchanged; for the "regenerates all
three" case assert all three are 32 chars, and for the "does not overwrite
existing valid secrets" case assert SavePluginConfig is not called or, if
called, that the fields equal the original values. Ensure you still load the
config via LoadPluginConfiguration and reference OnConfigurationChange,
LoadPluginConfiguration, SavePluginConfig and the config.Configuration struct
when locating code to change.
🤖 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 `@server/flow.go`:
- Around line 833-851: The current validation only checks scheme/host so secrets
and polling targets can be sent to arbitrary HTTPS endpoints; update validation
around isHTTPSURL, the register flow that calls postForgeRegister, and any code
that persists cfg.ForgeDrainURL (and used by forgePollInterval poller) to
enforce a strict hostname allowlist (e.g., only the official webtrigger host(s)
like webtrigger.atlassian.app), reject unknown hosts/ports, and disallow
redirects: when validating registerURL and drainURL resolve and follow any
redirects (or perform a HEAD/GET without sending secrets) and ensure the final
URL host exactly matches an allowed host; if it does not, return an error in
errorList and do not call postForgeRegister or persist cfg.ForgeDrainURL. Ensure
the same checks are applied to the code paths around postForgeRegister,
cfg.ForgeSharedSecret, and the poller in forge_poller.go so secrets and HMACs
are only sent to approved hosts.

In `@server/serializer/confluence_forge.go`:
- Around line 46-58: ForgeContent.ID is defined as string but Confluence Forge
events can emit content.id/container.id as JSON numbers, causing unmarshalling
failures; update the ForgeContent type to accept both string and numeric JSON
for ID by replacing the ID field with a tolerant type (e.g., json.Number or a
custom type) or implement a custom UnmarshalJSON for ForgeContent (or a
dedicated ID type) that accepts either a quoted string or a number and sets the
ID string field accordingly so both content.id and nested content.container.id
deserialize correctly; ensure the change preserves the exported symbol
ForgeContent and its use sites.

---

Outside diff comments:
In `@forge/src/index.ts`:
- Around line 68-70: The handler currently only checks payload.secret.length but
doesn't verify payload.secret is a string, allowing non-string values to bypass
validation; update the validation in the registration logic (the code around
payload.secret check in forge/src/index.ts) to first ensure typeof
payload.secret === 'string' (and not null/undefined) and only then verify length
>= 32, returning a 400 JSON response if the type check fails or the length is
insufficient so non-string secrets cannot be stored and break signed drain
requests.

In `@server/flow.go`:
- Around line 63-78: The flow currently includes Cloud-specific steps
(fm.stepEditionQuestion, fm.stepCloudOAuthConfigure, fm.stepCloudForgeBridge,
etc.) even though setupFlow is only invoked for the server installer, causing
Cloud selection to reuse the wrong URL saved by submitConfluenceURL; update the
setupFlow sequence to be server-only by removing or replacing
fm.stepEditionQuestion with a server-only edition step (or remove Cloud branch
entirely) and remove the Cloud-specific steps (fm.stepCloudOAuthConfigure,
fm.stepCloudForgeBridge, any stepCloudInstanceURL references) so only
server-related steps (e.g., fm.stepInstanceURL, fm.stepServerVersionQuestion,
fm.stepCSversionGreaterthan9, fm.stepCSversionLessthan9, fm.stepOAuthInput,
fm.stepOAuthConnect, fm.stepAnnouncementQuestion,
fm.stepAnnouncementConfirmation, fm.stepDone, fm.stepCancel) remain; ensure
submitConfluenceURL is not reused by Cloud steps by keeping Cloud flow in a
separate flow invoked only from Cloud commands.

---

Duplicate comments:
In `@server/forge_poller.go`:
- Around line 176-189: The dispatch path currently launches notifications
asynchronously (using "go service.SendConfluenceNotifications") so drainOnce can
ack the Forge key before delivery completes; change this to perform
notifications synchronously by removing the goroutine and calling
service.SendConfluenceNotifications(forgeEvent, internal) directly, and ensure
SendConfluenceNotifications returns an error (or add a blocking wrapper) so you
can check its result and return that error instead of nil — this guarantees
delivery completes (or fails) before returning from the dispatch path that
acknowledges the key (references: forgeEvent, internal,
service.SendConfluenceNotifications).

---

Nitpick comments:
In `@server/plugin_test.go`:
- Around line 91-198: Update the tests in
TestOnConfigurationChange_AutoGenerateSecrets to assert the actual persisted
config values instead of only checking SavePluginConfig was called: in each
subtest, use the plugintest API's mock SavePluginConfig handler to capture the
map[string]interface{} payload passed to SavePluginConfig (when
OnConfigurationChange calls SavePluginConfig), then assert the regenerated
fields (EncryptionKey, Secret, ForgeSharedSecret) are exactly 32 chars long and
that any pre-existing valid fields remain unchanged; for the "regenerates all
three" case assert all three are 32 chars, and for the "does not overwrite
existing valid secrets" case assert SavePluginConfig is not called or, if
called, that the fields equal the original values. Ensure you still load the
config via LoadPluginConfiguration and reference OnConfigurationChange,
LoadPluginConfiguration, SavePluginConfig and the config.Configuration struct
when locating code to change.
🪄 Autofix (Beta)

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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9ea6f152-5767-41fb-896f-d794653e61cc

📥 Commits

Reviewing files that changed from the base of the PR and between 73d08ee and 44156f2.

📒 Files selected for processing (9)
  • forge/manifest.yml
  • forge/src/index.ts
  • server/command.go
  • server/flow.go
  • server/forge_poller.go
  • server/plugin.go
  • server/plugin_test.go
  • server/serializer/confluence_forge.go
  • server/user.go
💤 Files with no reviewable changes (1)
  • server/user.go

Comment thread server/flow.go Outdated
Comment thread server/serializer/confluence_forge.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
server/flow.go (1)

63-78: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't skip the Cloud URL step in the shared setup flow.

setupFlow first saves the generic submitConfluenceURL result with IsCloud = false, then the Cloud button jumps straight to stepCloudOAuthConfigure. That bypasses submitCloudConfluenceURL, so the Cloud-only URL normalization/persistence never runs and a self-hosted URL can reach the 3LO/Forge path.

Also applies to: 374-412, 691-698

🤖 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 `@server/flow.go` around lines 63 - 78, The shared setup flow skips the Cloud
URL normalization because the Cloud button jumps directly to
fm.stepCloudOAuthConfigure after submitting the generic submitConfluenceURL
(IsCloud = false), so ensure the cloud path runs the cloud-specific URL handler:
modify the flow so the cloud branch invokes the cloud URL step/handler (e.g.,
call submitCloudConfluenceURL or insert a cloud-specific step such as
fm.stepCloudInstanceURL / fm.stepInstanceURL-cloud) before
fm.stepCloudOAuthConfigure; update setupFlow and any other locations flagged
(the other blocks around lines noted) so that submitCloudConfluenceURL is
executed for Cloud users prior to fm.stepCloudOAuthConfigure to guarantee
Cloud-only normalization/persistence runs.
♻️ Duplicate comments (2)
server/forge_poller.go (2)

117-129: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Revalidate ForgeDrainURL before polling.

The wizard now validates pasted URLs, but the poller still trusts whatever is stored in config. Since ForgeDrainURL is also admin-configurable, this path will keep sending HMAC-signed requests to any host present there unless you enforce the same Forge-webtrigger check here before fp.post.

Also applies to: 155-163

🤖 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 `@server/forge_poller.go` around lines 117 - 129, Revalidate the admin-provided
ForgeDrainURL inside drainOnce before calling fp.post: check that
cfg.ForgeDrainURL passes the same forge/webtrigger validation used by the wizard
(call the shared validation function the wizard uses, e.g.
ValidateForgeURL/IsValidForgeWebtrigger) and return nil or an error if it fails;
do this check in drainOnce (and the similar block at lines 155-163) so
HMAC-signed requests are only sent to validated hosts even when the URL is
stored in config, and only proceed to fp.post when the URL is accepted.

137-150: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Ack Forge events only after notification delivery finishes.

dispatch still returns nil as soon as it spawns SendConfluenceNotifications, and this loop still appends every key to ackKeys. Any downstream send failure or shutdown race turns into a permanent drop instead of a retry.

Also applies to: 181-195

🤖 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 `@server/forge_poller.go` around lines 137 - 150, The current loop acknowledges
all Forge events immediately because fp.dispatch returns nil after spawning
SendConfluenceNotifications; change the logic so that you only append an event's
key to ackKeys after its notification delivery completes successfully. Modify
fp.dispatch (or provide a new blocking variant) so it returns an error only
after SendConfluenceNotifications finishes (or return a result/channel you wait
on), then in the loop over resp.Events wait for that completion and only ack
(append evt.Key) on success; log and skip ack on failures so the post call
(fp.post with forgeDrainRequest) only includes keys for confirmed deliveries.
Apply the same fix to the duplicate ack loop referenced at lines 181-195.
🤖 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.

Outside diff comments:
In `@server/flow.go`:
- Around line 63-78: The shared setup flow skips the Cloud URL normalization
because the Cloud button jumps directly to fm.stepCloudOAuthConfigure after
submitting the generic submitConfluenceURL (IsCloud = false), so ensure the
cloud path runs the cloud-specific URL handler: modify the flow so the cloud
branch invokes the cloud URL step/handler (e.g., call submitCloudConfluenceURL
or insert a cloud-specific step such as fm.stepCloudInstanceURL /
fm.stepInstanceURL-cloud) before fm.stepCloudOAuthConfigure; update setupFlow
and any other locations flagged (the other blocks around lines noted) so that
submitCloudConfluenceURL is executed for Cloud users prior to
fm.stepCloudOAuthConfigure to guarantee Cloud-only normalization/persistence
runs.

---

Duplicate comments:
In `@server/forge_poller.go`:
- Around line 117-129: Revalidate the admin-provided ForgeDrainURL inside
drainOnce before calling fp.post: check that cfg.ForgeDrainURL passes the same
forge/webtrigger validation used by the wizard (call the shared validation
function the wizard uses, e.g. ValidateForgeURL/IsValidForgeWebtrigger) and
return nil or an error if it fails; do this check in drainOnce (and the similar
block at lines 155-163) so HMAC-signed requests are only sent to validated hosts
even when the URL is stored in config, and only proceed to fp.post when the URL
is accepted.
- Around line 137-150: The current loop acknowledges all Forge events
immediately because fp.dispatch returns nil after spawning
SendConfluenceNotifications; change the logic so that you only append an event's
key to ackKeys after its notification delivery completes successfully. Modify
fp.dispatch (or provide a new blocking variant) so it returns an error only
after SendConfluenceNotifications finishes (or return a result/channel you wait
on), then in the loop over resp.Events wait for that completion and only ack
(append evt.Key) on success; log and skip ack on failures so the post call
(fp.post with forgeDrainRequest) only includes keys for confirmed deliveries.
Apply the same fix to the duplicate ack loop referenced at lines 181-195.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6c08f3ad-e5da-4c18-a46e-28be106521b3

📥 Commits

Reviewing files that changed from the base of the PR and between 44156f2 and 9058133.

📒 Files selected for processing (3)
  • server/flow.go
  • server/forge_poller.go
  • server/serializer/confluence_forge.go

@nang2049

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@avasconcelos114 avasconcelos114 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tremendous work! I had a few general questions about how we want to prepare for production rollout plus some smaller comments but things are looking great!

Comment thread forge/scripts/validate-manifest.ts Outdated
};
};

const PLACEHOLDER_APP_ID = 'REPLACE_WITH_FORGE_APP_ID';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wondering if we want this to be and env var that we inject via the repository variables since it's mainly something to run in CI

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was from previous implementation so i am dropping it as the check was dead code. forge create always writes a real ID. CI substitution of the prod ID into manifest.yml belongs in the deploy workflow

Comment thread forge/README.md Outdated

We `forge deploy` this app once into the Mattermost Atlassian developer
account, then share a private install link. Customers do NOT run
`forge deploy` themselves.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I imagine we'll be adding a workflow with secret env vars that can potentially handle the forge deployments to a specific version for the production deployments? I'm wondering whether we want to have something made in the delivery platform repo that can be shared between Confluence and Jira but we can figure this out as a follow-up

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

thats also a good point, we need to keep in mind before merging and when we have the Forge app. Question is who might own this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I could look into that (likely with some help from someone in SRE), we might need to discuss the current game plan for how we'll deploy and maintain this in the next team meeting beforehand

Comment thread server/flow.go Outdated

const confluenceCloudScopes = "offline_access, read:confluence-user, read:confluence-content.summary, read:confluence-content.all, read:confluence-space.summary, write:confluence-content"

var forgeInstallURL = "https://developer.atlassian.com/console/install/mattermost-confluence-bridge"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wondering if this is a value we want to have configurable via the system console, just to both simplify testing and to make possible for customers to deploy and connect to their own forge bridge apps if they so choose (but mainly the testing bit :D)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes :D turned it into a required System Console setting. If no URL is configured since today we don't have a prod Forge app deployed yet the wizard shows a warning. Once we have the prod app live sysadmin pastes the install URL once!

Comment thread server/flow.go Outdated
":wave: Welcome — let's connect Mattermost to a Confluence **Cloud** site.\n\n"+
"This sets up two things:\n"+
"1. **OAuth 2.0** so Mattermost can act on Confluence as your users.\n"+
"2. A small **Forge bridge app** that delivers Confluence events into Mattermost (the GA replacement for the Connect descriptor Atlassian removed on March 31, 2026).\n\n"+

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess we'll want a task to eventually removed the text in parenthesis once it's no longer relevant (or just remove it altogether now and document this change in other places)

Comment thread server/forge_poller.go
Comment thread server/flow.go Outdated
@nang2049
nang2049 requested a review from avasconcelos114 May 26, 2026 15:13

@avasconcelos114 avasconcelos114 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code changes themselves look good! I think we can start QAing and getting some eyes from security on this while we also define how to go about production deployments

@nang2049

nang2049 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nang2049

nang2049 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nang2049

nang2049 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

* MM-67455: DM connected users when @-mentioned in Confluence

* PR feedback

* Linter

* coderabbitai feedback

* README.md

---------

Co-authored-by: Nevyana Angelova <nevyangelova@192.168.100.47>
@nang2049

nang2049 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
server/service/mention_settings.go (1)

15-21: 💤 Low value

Consider logging KV errors for observability.

The fail-open behavior (defaulting to enabled on error) is reasonable for mention notifications, but silently ignoring appErr on line 17 could mask KV infrastructure issues during troubleshooting.

📊 Suggested observability enhancement
 func IsMentionNotificationEnabled(mattermostUserID string) bool {
 	data, appErr := config.Mattermost.KVGet(mentionNotifKey(mattermostUserID))
-	if appErr != nil || len(data) == 0 {
+	if appErr != nil {
+		config.Mattermost.LogDebug("mention settings: KV get failed, defaulting enabled", "user_id", mattermostUserID, "error", appErr.Error())
+		return true
+	}
+	if len(data) == 0 {
 		return true
 	}
 	return string(data) != "0"
 }
🤖 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 `@server/service/mention_settings.go` around lines 15 - 21, In
IsMentionNotificationEnabled, keep the current fail-open behavior but surface KV
errors by logging appErr when
config.Mattermost.KVGet(mentionNotifKey(mattermostUserID)) returns an error; add
a log call that includes the error, the mattermostUserID and the key
(mentionNotifKey(...)) for context (use the project's standard logger used
elsewhere) and then continue to return true on error.
🤖 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 `@forge/src/index.ts`:
- Around line 153-155: The force re-registration path currently skips any proof
of ownership (the check around storage.get(REGISTERED_KEY) and payload.force in
the register handler), allowing anyone with the URL to overwrite the secret;
change the handler so that when payload.force is true you validate a proof
derived from the existing stored secret (e.g., verify an HMAC or signature
included in the request using the current secret fetched via
storage.get(REGISTERED_KEY)) before accepting the rotation, and reject the
request with 403 if the HMAC/signature verification fails; apply the same
verification to the other forced-overwrite branch referenced near lines 166-170
so all forced resets require the existing-secret proof.
- Around line 36-38: The enqueue logic (around storage.set in function enqueue)
never bounds queued items; before calling storage.set(key, …) add queue-size
control: define a MAX_QUEUE_LENGTH constant and query existing queued items
(e.g., storage.list or storage.get with the same key prefix) to count entries,
and if the count >= MAX_QUEUE_LENGTH either evict the oldest entries (sort by
enqueuedAt and delete via storage.delete) to make room or return an
error/backpressure instead of writing; ensure you use the same key prefix and
the stored enqueuedAt timestamp to identify oldest items, update logging to
reflect evictions or rejection, and keep storage.set only after space has been
guaranteed.

In `@server/confluence_server.go`:
- Around line 423-434: The code is constructing Cloud-style page URLs in the
Page/Comment branches (variables pageURL, eventData.Page, eventData.Comment)
instead of using the Server/DC-provided webui link in the payload; change each
branch that sets pageURL to first use the payload's _links.webui (e.g.,
eventData.Page._links.webui or eventData.Comment._links.webui) when present and
only fall back to the fmt.Sprintf("/spaces/%s/pages/%s", ...) logic if the webui
link is missing, and keep the existing calls to
client.MentionAccountIDsInPage/MentionAccountIDsInComment unchanged.

In `@server/forge_poller.go`:
- Around line 223-234: The debug logs currently emit raw Atlassian account IDs
(accountIDs and evt.ActorAccountID()) which is PII; update the log.Debug calls
in the mention handling (the one referencing internalEvent, evt.Content.ID,
accountIDs and the later one referencing evt.ActorAccountID()) to omit the
actual ID slices/strings and only log non-PII values such as mention_count
(len(accountIDs)) and other content metadata (e.g., internalEvent,
evt.Content.ID, pageURL, kind, instanceID); remove or replace any direct
references to accountIDs and evt.ActorAccountID() in the log arguments while
preserving the rest of the diagnostic fields.

In `@server/store/store.go`:
- Around line 166-170: The delete path currently removes the reverse AccountID
-> mattermostUserID mapping even for the admin sentinel, undoing the store
guard; in DeleteConnectionFromKVStore add the same sentinel check (compare
mattermostUserID to AdminMattermostUserID) and skip deleting the reverse mapping
when it's the admin sentinel so the real user's reverse mapping is preserved;
locate the reverse-mapping cleanup logic in DeleteConnectionFromKVStore and wrap
it with the same conditional used where mappings are stored.

---

Nitpick comments:
In `@server/service/mention_settings.go`:
- Around line 15-21: In IsMentionNotificationEnabled, keep the current fail-open
behavior but surface KV errors by logging appErr when
config.Mattermost.KVGet(mentionNotifKey(mattermostUserID)) returns an error; add
a log call that includes the error, the mattermostUserID and the key
(mentionNotifKey(...)) for context (use the project's standard logger used
elsewhere) and then continue to return true on error.
🪄 Autofix (Beta)

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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: daa55d30-65ff-4aa4-b5c5-a359dfe5ef81

📥 Commits

Reviewing files that changed from the base of the PR and between e8194d5 and 836a6dc.

📒 Files selected for processing (21)
  • forge/README.md
  • forge/manifest.yml
  • forge/src/index.ts
  • plugin.json
  • server/client.go
  • server/client_cloud.go
  • server/client_server.go
  • server/command.go
  • server/confluence_server.go
  • server/flow.go
  • server/forge_poller.go
  • server/serializer/confluence_forge.go
  • server/service/adf.go
  • server/service/adf_test.go
  • server/service/mention_settings.go
  • server/service/mentions.go
  • server/service/mentions_test.go
  • server/service/notification_test.go
  • server/service/storage_xhtml.go
  • server/service/storage_xhtml_test.go
  • server/store/store.go
✅ Files skipped from review due to trivial changes (1)
  • forge/README.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • plugin.json
  • server/client_cloud.go
  • forge/manifest.yml
  • server/serializer/confluence_forge.go
  • server/command.go
  • server/flow.go

Comment thread forge/src/index.ts
Comment thread forge/src/index.ts Outdated
Comment thread server/confluence_server.go Outdated
Comment thread server/forge_poller.go Outdated
Comment thread server/store/store.go
@nang2049

nang2049 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@forge/src/index.ts`:
- Around line 153-154: The current unconditional 409 when
storage.get(REGISTERED_KEY) is present prevents idempotent re-registration and
hides secret drift; in the handler around storage.get(REGISTERED_KEY) replace
the unconditional jsonResponse(409, ...) with logic that reads the stored
mm.drainSecret and compares it to the posted secret: if they match, return 200
(idempotent success), if they differ return a non-409 error (e.g., 400/422 with
a clear message that mm.registered must be cleared) so the setup flow halts;
update the check that uses REGISTERED_KEY and use the posted secret vs stored
mm.drainSecret comparison to decide the response.
🪄 Autofix (Beta)

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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f6b2d02a-58ad-425b-900e-3fa107b44777

📥 Commits

Reviewing files that changed from the base of the PR and between 836a6dc and dd8e8c9.

📒 Files selected for processing (6)
  • forge/README.md
  • forge/src/index.ts
  • server/confluence_server.go
  • server/forge_poller.go
  • server/serializer/confluence_forge.go
  • server/store/store.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • server/forge_poller.go
  • server/serializer/confluence_forge.go
  • server/confluence_server.go
  • forge/README.md

Comment thread forge/src/index.ts Outdated
@nang2049

nang2049 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nang2049

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ogi-m

ogi-m commented Jun 11, 2026

Copy link
Copy Markdown

Tested upgrade scenario and fresh install, both working as expected.

@nang2049

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nang2049

Copy link
Copy Markdown
Contributor Author

@coderabbitai approve

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@nang2049
nang2049 merged commit 0d4a33a into master Jun 12, 2026
9 checks passed
@nang2049
nang2049 deleted the MM-68853-forge-cloud-migration branch June 12, 2026 10:48
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.

3 participants