Skip to content

feat(settings): the Integrations surface — verify, disconnect, and per-provider config (G33) #47

Description

@Adron

Summary

The web /integrations page manages every connected provider in one place — link, verify, disconnect, and per-provider configuration. The macOS LinkedAccountsView can only link. Once an account is connected there is no way to check it, reconfigure it, or remove it from the Mac.

This issue supersedes G26 (identity unlink + verify) and absorbs the personal-scope half of G25's LinkedIn work.

What the web offers, per provider (/help/settings, /help/account)

Provider Link Verify Disconnect Extra
GitHub ✅ ✅ ✅ Org access + "Update orgs"; default repo; Reconnect for GitHub Issues (scope upgrade)
Bluesky ✅ ✅ ✅ —
LinkedIn ✅ ✅ ✅ Posting-target editor; company-page sync
Mastodon ✅ ✅ ✅ Multiple instances, each independently verifiable and disconnectable
X (Twitter) ✅ ✅ ✅ Cross-posting only

Plus an Integration reconnect reminders preference: "When it's on (the default), InterlinedList reminds you when one of your cross-posting connections (such as LinkedIn) is expiring or needs reconnecting, so your cross-posts don't quietly stop."

And the GitHub-backed-lists prerequisite: "If you already linked GitHub for sign-in only, click Reconnect for GitHub Issues in the Connected Accounts section to grant the additional scope."

What macOS ships today

  • LinkedAccountsView — link only.
  • IdentityProvider.linkableProviders — X/Twitter is absent entirely, even though the test account has a linked X identity and cross-posting to X is a shipped feature.
  • UserDTO.githubDefaultRepo (UserDTO.swift:96) is decoded and never written.
  • Kit lacks DELETE /api/user/identities, POST /api/user/identities/verify, GET /api/auth/github/status, GET /api/github/orgs, PUT /api/linkedin/posting-targets, and POST /api/linkedin/sync-pages.

Live probes (2026-09-07, Bearer, all read-only)

GET /api/auth/github/status → 200
{"configured":true,"clientId":"Ov23li9eXYK1i6psJW6G",
 "manageOrgAccessUrl":"https://github.com/settings/connections/applications/Ov23li9eXYK1i6psJW6G"}

GET /api/github/orgs        → 200  []        ← bare array, not wrapped

manageOrgAccessUrl is exactly the "Update orgs" affordance — open it in the browser rather than trying to render GitHub's org-access UI natively.

Note the wire shape: /api/github/orgs returns a bare array, while most single-resource routes on this API answer {message?, <resource>}. Decode accordingly and add a contract test — this is the kind of drift that has bitten this client before.

Division of labor

Kit

  • DELETE /api/user/identities (unlink)
  • POST /api/user/identities/verify
  • GET /api/auth/github/status — the one provider-status route Kit lacks (bluesky/mastodon/linkedin/twitter are already there)
  • GET /api/github/orgs (bare-array DTO)
  • PUT /api/linkedin/posting-targets
  • POST /api/linkedin/sync-pages
  • Add githubDefaultRepo to UpdateUserRequest
  • Contract tests for each new shape against the .env test account

Domain

  • Extend the identity model to carry per-provider capability (verifiable / disconnectable / multi-instance) so the UI is data-driven rather than a switch statement per provider.
  • Mastodon must be modelled as a collection of instances, not a single identity — this is the one that will not retrofit cleanly if it is modelled as a scalar first.
  • Add X/Twitter to linkableProviders.
  • LinkedIn posting-target read/write, closing the "needs a verified per-target wire shape" note left on G11a.

App — a new Integrations pane

  • One row per provider, with Link / Verify / Disconnect as the row's actions and its status inline.
  • Disconnect is destructive and breaks cross-posting — confirm before firing, and say what stops working.
  • GitHub: default-repo field, an "Update orgs" button that opens manageOrgAccessUrl in the browser, and the Reconnect for GitHub Issues scope-upgrade path (reuses the existing ASWebAuthenticationSession flow).
  • Mastodon: a list of instances with per-instance actions.
  • LinkedIn: posting-target selector + company-page sync.
  • "Integration reconnect reminders" toggle — confirm which stored field backs it before wiring.

Tests

  • happy — link, verify, and disconnect each provider
  • invalid — disconnecting a provider that is not linked
  • upstream-failure — verify returns non-200 → row shows a failed state, other rows unaffected
  • boundary — zero linked providers; several Mastodon instances; disconnecting the last provider used for sign-in

Acceptance criteria

  • Every provider the web can manage can be linked, verified, and disconnected from macOS.
  • A GitHub account linked for sign-in only can be upgraded to Issues scope without leaving the app.
  • Full E2E gate green.

Notes

work-consolidation.md tracks this as G33 (supersedes G26, overlaps G25). Size L — the largest single item in the account cluster. Consider splitting the Mastodon multi-instance work into its own PR if the diff gets unwieldy.


Implementation plan (added 2026-09-15) — split, as this issue anticipated

This issue is sized L and says "consider splitting the Mastodon multi-instance work into its own PR if the diff gets unwieldy." The probe changed which half is urgent: Mastodon is not a modelling preference, it is a live defect, so it leads rather than being split off.

What the probe found (2026-09-15)

GET /api/user/identities on the test account:

{"identities":[
  {"provider":"bluesky","providerUsername":"interlinedlist.bsky.social", …},
  {"provider":"mastodon:techhub.social","providerUsername":"interlinedlist_crew@techhub.social", …},
  {"provider":"twitter","providerUsername":"interlinedlist", …},
  {"provider":"github","providerUsername":"InterlinedListMessenger", …}]}

Three defects, all live:

  1. Mastodon arrives instance-qualified. IdentityProvider.init(wireToken:) matched "mastodon" exactly, so "mastodon:techhub.social" fell to .other — every Mastodon identity rendered as an unknown provider, and the instance host was discarded entirely. The doc comment claimed the host was "carried on the LinkedIdentity"; there was no such field.
  2. X was absent from the provider enum, as this issue noted — and the consequence is sharper than "missing from a list": a linked X identity decoded as .other("twitter"), so the one provider the account could already cross-post to was the one it could not recognise.
  3. GET /api/user/identities accepts Bearer, contrary to its .session annotation. Not a hard failure — the kit has a lazy cookie-login transport — but a redundant credentialed round-trip on every load, and the same class of error as the send-verification-email correction in PR feat(gating): land the capability gate and reconcile it with G22-G25 #83, in the opposite direction.

Shipped in this PR

Kit — DELETE /api/user/identities, POST /api/user/identities/verify, GET /api/auth/github/status, GET /api/github/orgs (bare array, as recorded); identities() re-annotated .bearer.

Domain — .twitter added; mastodon:host split in init(wireToken:) with instanceHost(fromWireToken:); LinkedIdentity.instance plus providerWireToken, which is what unlink and verify must send; per-provider capability flags (supportsMultipleInstances, requiresInstanceHost, isVerifiable, isDisconnectable) so the pane is data-driven rather than a switch statement per provider, which is what this issue asked for; displayName moved into the domain next to them; GitHubConnection; unlinkIdentity / verifyIdentity / githubConnectionStatus.

App — the Integrations pane: one row per connection with Verify and Disconnect, a confirmation that names what stops working per provider rather than a generic "are you sure", per-row status and per-row errors, Mastodon as one row per instance, and GitHub's "Update organization access…" opening manageOrgAccessUrl in the browser.

Deliberately deferred, and why

Deferred Reason
LinkedIn posting-target editing (PUT /api/linkedin/posting-targets) The read returns {"targets":[],"orgScopeMissing":false} on this account — no populated payload has been seen. Building a target editor against an empty array is how the G21 and G25 silent-decode defects happened. It is also the supply side #79 needs, so it belongs with that work. Note orgScopeMissing is an undocumented field this issue did not mention.
githubDefaultRepo write, Reconnect for GitHub Issues GET /api/github/repos returns [] — the test account has no accessible repositories, exactly as #51 records. A repo picker cannot be exercised end to end, and the scope-upgrade path shares the OAuth flow with #51.
Integration reconnect reminders toggle The issue says "confirm which stored field backs it". It is not on GET /api/user, not on /api/user/notification-preferences (which returns an event catalogue with push/inApp channels and no reconnect key). The backing field is unidentified, so nothing is wired to a guess.
Linking itself Stays in the Linked accounts pane, which owns the ASWebAuthenticationSession and the Mastodon instance prompt. Two copies of an OAuth handshake is worse than two panes.

Each of these is blocked on evidence rather than effort.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestparityWeb-parity gap with the InterlinedList web app

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions