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_KEY and stored in user_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 return ErrAuthRequired without 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.net host).
  • offline_access is required in required_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.com is required. Set it via extra_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/confluence removes 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's cloud_id is used.
  • Page content is fetched as view format (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 with content_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.