Connects Gadgets to Atlassian Cloud Confluence
(*.atlassian.net) using OAuth 2.0 (3LO). It exposes spaces, pages, blog posts, comments, labels,
and attachments through a capability-based Session API, with page/blog bodies converted to and from
Markdown. Runs as its own Cloudflare Worker and is auto-discovered by the backend from its
GATEKEEPER_CONFLUENCE binding.
Only Atlassian Cloud is supported; there is no Server / Data Center adapter.
Three connectable resource granularities:
| Granularity | URL pattern | Session type |
|---|---|---|
| Site — search and open any space/page/blog post the account can access | https://*.atlassian.net/wiki |
ConfluenceSite |
| Space — list/search/create pages and blog posts within one space | https://*.atlassian.net/wiki/spaces/:spaceKey |
ConfluenceSpace |
| Page or blog post — read/edit the body, manage labels/comments/attachments, and (pages only) read/create child pages | https://*.atlassian.net/wiki/spaces/:spaceKey/pages/:pageId/* |
ConfluenceContent |
Pages and blog posts share the ConfluenceContent session type (distinguished by
ContentMetadata.type); child-page methods are page-only.
A single account-level grant can span multiple sites. getGatekeeperClassFor() resolves a URL's
host to one of the connection's accessible sites (its cloud ID) — the one chokepoint enforcing site
access.
See src/types.d.ts for the full, agent-facing Session API.
-
Go to the Atlassian Developer Console and create an OAuth 2.0 integration.
-
Add the Confluence API to the app and enable both scope families below (see
CONFLUENCE_SCOPESinconfluence-api.ts). The console has separate Granular scopes and Classic scopes tabs; this gatekeeper needs scopes from each because it uses the v2 API for most operations and falls back to v1 for a few (search, label writes, attachment upload, restore).Granular scopes (authorize the v2 API — v2 doesn't accept classic scopes):
read:space:confluenceread:page:confluence,write:page:confluence,delete:page:confluenceread:blogpost:confluence,write:blogpost:confluence,delete:blogpost:confluenceread:comment:confluence,write:comment:confluence,delete:comment:confluenceread:label:confluence,write:label:confluenceread:attachment:confluence,write:attachment:confluence,delete:attachment:confluenceread:user:confluence
Classic scopes (authorize the v1 fallbacks — v1 doesn't accept granular scopes):
search:confluence(CQL search)read:confluence-content.all,read:confluence-content.summary(search results / content reads)read:confluence-space.summarywrite:confluence-content(label add/remove, restore-from-trash)write:confluence-file(attachment upload)readonly:content.attachment:confluence(attachment download)read:confluence-user
Account-level:
read:me(authorizing user's identity)offline_access(required for refresh tokens)
-
Under Authorization, set the callback URL to
<BASE_URL>/oauth:http://localhost:8787/gatekeeper/confluence/oauthfor local dev<PUBLIC_BASE_URL>/gatekeeper/confluence/oauthotherwise
-
Copy the app's Client ID and Secret.
API version: this gatekeeper targets the Confluence v2 REST API (
/wiki/api/v2/...), which Atlassian now requires (the v1 content/space endpoints have been removed) — and which is why the granular scopes are needed. A few capabilities with no v2 equivalent (CQL search, label add/remove, attachment upload, restore-from-trash) fall back to v1 endpoints, which need the classic scopes, and degrade with a clear error if Atlassian has removed them too. The app therefore must enable scopes from both the Granular and Classic tabs.
For local development, set these in the repo-root environment (e.g. .dev.vars) so the dev server
injects them into this Worker:
CONFLUENCE_CLIENT_ID=your-client-id
CONFLUENCE_CLIENT_SECRET=your-client-secret
run-dev-server.js maps CONFLUENCE_CLIENT_ID / CONFLUENCE_CLIENT_SECRET into the Worker's
CLIENT_ID / CLIENT_SECRET vars. For production, set CLIENT_ID and CLIENT_SECRET as secrets
on the deployed Worker and set BASE_URL to the public gatekeeper URL.
providesAuth is false: Confluence is a data connector, not a sign-in method. Wiring it up as a
"Sign in with Atlassian" provider (using the verified /me email) is a possible fast follow.
- The user starts a connection;
connectAccount()creates aUserAccountDurable Object and returns a URL of the form<BASE_URL>/<doId>/<nonce>. The flow uses a two-phase nonce (initiation → OAuth). - Visiting that URL redirects to
auth.atlassian.comto authorize the requested scopes. - Atlassian redirects back to
<BASE_URL>/oauth; the gatekeeper exchanges the code for access + rotating refresh tokens, lists the account's accessible Confluence sites (/oauth/token/accessible-resources), reads the authorizing user's identity (/me), and stores them. - Each connected resource routes API calls through
https://api.atlassian.com/ex/confluence/<cloudId>/wiki/.... Access tokens are short-lived and refreshed automatically (proactively before expiry and again on a 401); each refresh persists the new rotating refresh token.
A connection is created either by pasting a Confluence URL into the chat, or from the Connections UI.
Page/blog bodies are exchanged as Markdown (best-effort conversion to/from Confluence "storage
format" XHTML; see confluence-markdown.ts). Highlights:
ConfluenceSite:listSpaces,getSpace,getContent,search(text or raw CQL),getCurrentUserConfluenceSpace:listPages,listBlogPosts,getContent,search,createPage,createBlogPostConfluenceContent:getMetadata/getContent,setContent/appendContent/setTitle,listChildPages/createChildPage, labels, comments, attachments (downloadAttachmentis capped at 16 KB),trash/restore
Endpoint split: v2 for spaces, pages, blog posts, child pages, and reading comments/labels/ attachments; v1 (with graceful degradation) for CQL search, label add/remove, attachment upload, and restore-from-trash.
Every read calls ApprovalQueue.authorizeObservation(); every side-effecting operation
(create/edit page or blog post, comments, labels, attachment upload, trash/restore) is recorded as
a pending action and submitted via submitAction() — nothing reaches Confluence until the Overseer
calls applyAction(). Reads simulate pending (unapproved) writes so the agent can keep working
before approval (provisional IDs for created content, body/title/label/comment overlays, trashed
state). rejectAction() / revertAction() discard or undo. Content responses are cached in DO
storage with a short TTL and invalidated on write. This logic lives in confluence-actions.ts.
The Connections UI shows space / page-or-blog / site pickers supplied by
startResourceConfigurator(). Pasting a resource URL — or an agent's
requestConnection(resourceUrl) — also works and pre-fills the picker. For multi-site Atlassian
accounts the space/page pickers search across all accessible sites (each option's value carries
the correct site), so pasting a URL works for any connected site too.
- Body conversion between Confluence storage format and Markdown is best-effort; unsupported macros degrade to a labeled placeholder.
author/lastUpdatedByexpose only anaccountId(the v2 content API returns IDs, not user objects).- CQL search, label add/remove, attachment upload, and restore-from-trash rely on v1 endpoints; if Atlassian removes them, those operations surface a clear "not available" error.
pnpm --filter @gadgets/confluence-gatekeeper build— build the configurator UIs (build:configurator) and type-check (tsc).pnpm --filter @gadgets/confluence-gatekeeper test— run unit tests (URL/CQL parsing, Markdown conversion, v2 converters, action simulation).- The Worker is run via the root
pnpm dev-server, not directly.