Delegated Content Providers - ericfitz/tmi GitHub Wiki
Delegated Content Providers
What delegated content providers are
TMI's Timmy workflow reads external documents (Google Drive, Confluence, OneDrive, etc.) when indexing threat-model context. Two access models exist, depending on how the operator trusts TMI to reach the provider:
| Model | How credentials flow | Typical providers |
|---|---|---|
| Service content provider | Operator configures one bot/service account. Users explicitly share docs with it. | Google Drive, OneDrive (planned) |
| Delegated content provider | Each user links their own account once via OAuth. TMI stores an encrypted refresh token and makes API calls on the user's behalf. | Confluence Cloud, Google Workspace delegated |
Service providers are described on a separate wiki page. This page covers the delegated model and its infrastructure.
Operator prerequisites
Before enabling any delegated content provider, the operator must configure the token-encryption key:
| Variable | Purpose |
|---|---|
TMI_CONTENT_TOKEN_ENCRYPTION_KEY |
32-byte hex (64 chars) AES-256-GCM key. Separate from settings-encryption key. Required when any delegated content provider is enabled. |
If a delegated provider is enabled (TMI_CONTENT_OAUTH_PROVIDERS_*_ENABLED=true) and the key is missing, the server refuses to start.
Generate a random key: openssl rand -hex 32. Store securely (e.g., secrets manager). Rotating this key invalidates all existing user content tokens — users will need to re-link.
Per-provider configuration
Delegated providers are configured under content_oauth.providers.<id> in YAML, or via environment variables:
content_oauth:
callback_url: "https://tmi.example.com/oauth2/content_callback"
allowed_client_callbacks:
- "https://app.example.com/content-linked"
providers:
confluence:
enabled: true
client_id: "atlassian-client-id"
client_secret: "atlassian-client-secret"
auth_url: "https://auth.atlassian.com/authorize"
token_url: "https://auth.atlassian.com/oauth/token"
userinfo_url: "https://api.atlassian.com/me"
revocation_url: "" # Atlassian does not expose RFC 7009; leave empty
required_scopes:
- "read:confluence-content.all"
- "offline_access" # required: Atlassian only issues refresh tokens with this scope
extra_authorize_params:
audience: "api.atlassian.com" # required by Atlassian 3LO
prompt: "consent" # recommended: predictable consent screen
The Confluence content source is enabled separately under content_sources:
content_sources:
confluence:
enabled: true
Equivalent env var: TMI_CONTENT_SOURCE_CONFLUENCE_ENABLED=true. When enabled, the OAuth provider entry above must also be enabled, otherwise the server refuses to start.
Equivalent env vars:
| Variable | Purpose |
|---|---|
TMI_CONTENT_OAUTH_CALLBACK_URL |
TMI's callback URL. Register this with the provider. |
TMI_CONTENT_OAUTH_ALLOWED_CLIENT_CALLBACKS |
Comma-separated allow-list for the client_callback URLs that the UI supplies to POST /me/content_tokens/{id}/authorize. |
TMI_CONTENT_OAUTH_PROVIDERS_{ID}_ENABLED |
true to enable this provider. |
TMI_CONTENT_OAUTH_PROVIDERS_{ID}_CLIENT_ID |
OAuth client id registered with the provider. |
TMI_CONTENT_OAUTH_PROVIDERS_{ID}_CLIENT_SECRET |
OAuth client secret. |
TMI_CONTENT_OAUTH_PROVIDERS_{ID}_AUTH_URL |
Provider's authorization endpoint. |
TMI_CONTENT_OAUTH_PROVIDERS_{ID}_TOKEN_URL |
Provider's token endpoint. |
TMI_CONTENT_OAUTH_PROVIDERS_{ID}_USERINFO_URL |
Optional. Used to fetch a human-readable account label at link time. |
TMI_CONTENT_OAUTH_PROVIDERS_{ID}_REVOCATION_URL |
Optional (RFC 7009). If provided, TMI revokes at the provider on disconnect. |
TMI_CONTENT_OAUTH_PROVIDERS_{ID}_REQUIRED_SCOPES |
Space-separated scopes. |
extra_authorize_params is yaml-only and lets a provider append non-standard parameters to the authorize URL (e.g. Atlassian's audience=api.atlassian.com). Standard OAuth + PKCE parameters always win on key collision.
API endpoints
| Endpoint | Purpose |
|---|---|
GET /me/content_tokens |
List the caller's linked providers. No secrets. |
POST /me/content_tokens/{provider_id}/authorize |
Start the link flow. Body: {"client_callback": "https://..."}. Returns {authorization_url, expires_at}. |
DELETE /me/content_tokens/{provider_id} |
Unlink a provider. Revokes at the provider (if revocation URL is configured), then deletes the row. Idempotent. |
GET /oauth2/content_callback |
Public. Provider redirects here after user consent. Exchanges code for tokens, stores encrypted, 302s back to the client_callback with status=success or status=error. |
GET /admin/users/{user_id}/content_tokens |
Admin: list a target user's links. |
DELETE /admin/users/{user_id}/content_tokens/{provider_id} |
Admin: revoke + delete for a target user. |
When no delegated providers are configured, these endpoints return 503 Service Unavailable — this is the expected state for a default TMI deployment.
Token lifecycle
- Link: user goes through the authorize → callback flow once per provider. Access + refresh tokens are encrypted with
TMI_CONTENT_TOKEN_ENCRYPTION_KEYand stored inuser_content_tokens. - Fetch: delegated sources look up the user's token on each content fetch. If expired (with a 30-second skew), TMI refreshes lazily and serializes concurrent refreshes via
SELECT ... FOR UPDATE. - Refresh failure: 4xx from the provider's token endpoint flips the row to
status=failed_refresh. Subsequent fetches returnErrAuthRequiredwithout calling the provider; the user must re-link. - User deletion: TMI sweeps the user's content tokens and attempts revocation at each provider before the database cascade removes the rows. Revocation failures are logged but do not block deletion.
Provider-specific notes
Confluence Cloud
- URL pattern matched:
https://*.atlassian.net/wiki/spaces/{SPACE}/pages/{id}/.... Legacy/wiki/display/...and/wiki/x/...short links are not supported and will return a clear error rather than being followed. - Server / Data Center is not supported. Only Atlassian Cloud (the
atlassian.nethost). offline_accessis required inrequired_scopes. Atlassian only issues refresh tokens to clients that explicitly request this scope; without it, users will need to re-link after each access-token expiry. The server logs a warning at startup if the scope is missing.audience=api.atlassian.comis required. Set it viaextra_authorize_params(see config example above). Atlassian's authorize endpoint will reject the request without this parameter.- Revocation URL is empty. Atlassian's 3LO does not expose a public RFC 7009 endpoint.
DELETE /me/content_tokens/confluenceremoves the local row; users who want the OAuth grant fully revoked at Atlassian must do so via Atlassian's "Connected apps" management UI. - Multi-site Atlassian users are supported transparently. The page's URI host (e.g.
acme.atlassian.net) is matched against the user's accessible Atlassian resources at fetch time; the matching site'scloud_idis used. - Page content is fetched as
viewformat (rendered HTML) and processed by the standard HTML extractor. - Rate limiting (429): Atlassian rate limits are propagated as transient fetch errors. The background access poller retries on its normal cycle.
When to use what
- Building an automation that reads a few docs on behalf of a user → delegated provider.
- Running a bot/service that operates on docs the user has shared with it → service provider (see the separate wiki page).
- Integrating with an SSO identity provider for logging users into TMI itself → that's user-auth OAuth, a different subsystem under
oauth.providers.*. Do not conflate it withcontent_oauth.providers.*.
Troubleshooting
| Symptom | Likely cause |
|---|---|
Server refuses to start with a TMI_CONTENT_TOKEN_ENCRYPTION_KEY error |
Enabled a provider without setting the key. Generate one or disable the provider. |
GET /me/content_tokens returns 503 |
No delegated providers are configured. Expected on a fresh deployment. |
Callback redirects with error=client_callback_not_allowed |
The client_callback URL you supplied is not in content_oauth.allowed_client_callbacks. Add it (supports trailing-* wildcards). |
Callback redirects with error=invalid_state or renders "missing_state" |
The OAuth state TTL is 10 minutes. If the user took longer, they need to restart. Possible: the Redis instance was reset between authorize and callback. |
| User sees "account needs reconnecting" | Row has status=failed_refresh — provider revoked or invalidated the token. User reconnects via the link flow. |
Related
- Content provider architecture overview (service + delegated model separation): see the Content Providers wiki page.
- Specs for this subsystem (in the repo):
docs/superpowers/specs/2026-04-18-delegated-content-provider-infrastructure-design.md(infrastructure)docs/superpowers/specs/2026-04-25-confluence-delegated-provider-design.md(Confluence provider)
- Tracking issue: ericfitz/tmi#249.