You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(settings): the Integrations surface — verify, disconnect, and per-provider config (G33) #47
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)
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.
"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.
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.
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.
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.
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.
Summary
The web
/integrationspage manages every connected provider in one place — link, verify, disconnect, and per-provider configuration. The macOSLinkedAccountsViewcan 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)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.DELETE /api/user/identities,POST /api/user/identities/verify,GET /api/auth/github/status,GET /api/github/orgs,PUT /api/linkedin/posting-targets, andPOST /api/linkedin/sync-pages.Live probes (2026-09-07, Bearer, all read-only)
manageOrgAccessUrlis 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/orgsreturns 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/verifyGET /api/auth/github/status— the one provider-status route Kit lacks (bluesky/mastodon/linkedin/twitterare already there)GET /api/github/orgs(bare-array DTO)PUT /api/linkedin/posting-targetsPOST /api/linkedin/sync-pagesgithubDefaultRepotoUpdateUserRequest.envtest accountDomain
linkableProviders.App — a new Integrations pane
manageOrgAccessUrlin the browser, and the Reconnect for GitHub Issues scope-upgrade path (reuses the existingASWebAuthenticationSessionflow).Tests
Acceptance criteria
Notes
work-consolidation.mdtracks 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/identitieson 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:
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 theLinkedIdentity"; there was no such field..other("twitter"), so the one provider the account could already cross-post to was the one it could not recognise.GET /api/user/identitiesaccepts Bearer, contrary to its.sessionannotation. 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 thesend-verification-emailcorrection 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 —
.twitteradded;mastodon:hostsplit ininit(wireToken:)withinstanceHost(fromWireToken:);LinkedIdentity.instanceplusproviderWireToken, 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;displayNamemoved 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
manageOrgAccessUrlin the browser.Deliberately deferred, and why
PUT /api/linkedin/posting-targets){"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. NoteorgScopeMissingis an undocumented field this issue did not mention.githubDefaultRepowrite, Reconnect for GitHub IssuesGET /api/github/reposreturns[]— 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.GET /api/user, not on/api/user/notification-preferences(which returns an event catalogue withpush/inAppchannels and no reconnect key). The backing field is unidentified, so nothing is wired to a guess.ASWebAuthenticationSessionand 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.