Skip to content

feat(v2): Connectors page + nav rail entry - #1290

Merged
lilyshen0722 merged 3 commits into
mainfrom
feat/v2-connectors-page
Aug 26, 2026
Merged

feat(v2): Connectors page + nav rail entry#1290
lilyshen0722 merged 3 commits into
mainfrom
feat/v2-connectors-page

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Adds the missing shell surface for channel bridges — the thing the Rewire demo shows but the UI couldn't reach.

  • New /v2/connectors route + nav rail item (plug icon, between Community and Settings)
  • Lists the user's integrations with pod, status chip, and chat title
  • Pending telegram connectors surface the one-time /commonly-enable <code> instruction
  • Connected telegram connectors get a Live relay toggle — PATCHes config.liveRelay and stamps linkedUserId to the toggler (bridge attribution identity)
  • Create flow: pick a pod → new Telegram connector (POST auto-mints the code); public/community pods excluded from the picker
  • Tokens-only styling, 1px borders, no shadows, sentence case — per the design system

4 RTL tests: list render + enable code, PATCH shape on toggle, public-pod exclusion, create POST shape.

🤖 Generated with Claude Code

https://claude.ai/code/session_013pc6nGXRS8mHvrwcXMSRDK

Pure wiring over existing routes: GET /api/integrations/user/all,
POST /api/integrations (telegram auto-mints connectCode), PATCH /:id
(config.liveRelay persists since the schema declared it in #1282).
Live relay toggle sets linkedUserId to the toggler; public/community
pods are excluded from the bridge target picker (open-relay guard).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pc6nGXRS8mHvrwcXMSRDK
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

UX / design-system pass at 7161ec28 — tokens, borders and casing are clean. Every --v2-* var used resolves in v2.css, cards are 1px --v2-border on --v2-surface with no shadow, status pill uses the same soft-fill pattern as the rest of v2, labels are sentence case. Two blockers for using this as the demo backdrop, two nits.

Blockers

  1. Copy contradicts the 1:1 rule. connectors.enableHint says "In your Telegram group, add the Commonly bot" and connectors.footnote says "code to send in the group". fix(telegram): only relay inbound as the linked user from a private chat #1289 refuses groups/supergroups (relay gated on chatType === 'private'), and the stage script pins a private chat. Suggested: "Open a private chat with the Commonly bot (@handle) and send:" / "You get a one-time code to send to the bot. More platforms are on the way."
  2. No locale entries. 16 new connectors.* keys, zero additions to frontend/src/i18n/locales/{en,zh-CN}.json — a zh-CN user gets an all-English page via defaultValue fallback. That is exactly the class TASK-055 just closed (fix(v2): zh-CN never takes negative letter-spacing (TASK-055 class 1) #1253fix(v2): rail tooltip, community redirect, drawer aria-label and Lead badge through i18n (TASK-055 class 3) #1255). Add both files in this PR.

Nits
3. .v2-connectors__select re-implements .v2-byo__input (v2.css:5942) with slightly different padding (8/10 vs 10/12). Reuse v2-byo__input on the <select> — the BYO pod picker is the same control and should look identical.
4. Rail: this adds a 5th primary icon. #1274 (TASK-068) replaces the Community slot with Activity, so post-merge the rail is Pods / Agents / Activity / Connectors — fine, and the rail stays a column at every breakpoint (--v2-rail-w grid column holds at ≤760px), so no overflow. Just expect a mechanical conflict on NAV_ITEMS for whichever lands second.

#fff on the accent button and font-weight: 650 both match existing v2 usage — not flagging.

@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Change request at 94ce975a. The page is the right surface and the shape is close, but this is the enablement path for the Telegram bridge, and it hands the bridge's identity field to the client.

Blocking — linkedUserId is attacker-chosen, not caller-derived

The toggle sends the identity in the request body:

await api.patch(`/api/integrations/${c._id}`, {
  config: {
    liveRelay: next,
    // the toggler owns the bridge identity.
    ...(next ? { linkedUserId: currentUser?._id } : {}),
  },
});

The comment states the intent correctly and the server does not enforce it. PATCH /api/integrations/:id (routes/integrations.ts:388) merges the body wholesale — const nextConfig = config ? { ...currentConfig, ...config } : currentConfig — with no key allowlist. getMissingRequiredFields / validateManifestIfComplete check only that required fields are non-empty; neither rejects a key, and nothing compares linkedUserId to req.user.id.

Measured against the existing route harness, caller user-1, a plain non-admin who created the pod:

PATCH /api/integrations/integration-1
  { config: { liveRelay: true, linkedUserId: 'VICTIM-USER-ID', zzUnknown: 'x' } }

→ 200
→ findByIdAndUpdate config: {
    chatId: '42', chatType: 'private',
    liveRelay: true,
    linkedUserId: 'VICTIM-USER-ID',      // caller was user-1
    zzUnknown: 'x'
  }

canDeleteIntegration admits instance admins, the pod's creator, and the integration's creator — and any user can create a pod. So any authenticated user can link their own Telegram chat, name someone else as the bridge identity, and have everything they type in that chat written into the pod as that person, with deliverMessageToAgents waking agents under requestUser: { username: victim.username }. The forged author is the kernel's author field, not a display string.

#1289's private-chat gate does not close this, and this is precisely the residual I flagged there: chatType === 'private' proves the sender is the chat's counterpart, never that the counterpart is linkedUserId. #1289 closes "a group member speaks as the linked user"; this opens "the linked user is whoever I say."

Fix belongs on the server, not in the page: derive linkedUserId from req.user.id when liveRelay flips true and ignore any client-supplied value, or reject a config.linkedUserId that is not the caller. Either way the page can stop sending it.

Blocking — merge order

This is the enablement path. Nothing writes config.liveRelay today, which is the only reason the bridge's attribution defects are currently unreachable. If this lands before #1289, the group-impersonation vector is live in the interval. Land #1289 first, or gate this behind it.

Worth a look while you are here

{ ...currentConfig, ...config } accepts any config key from any caller who passes canDeleteIntegrationbotToken, secretToken, accessToken, globalAgentAccess, agentAccessEnabled. That is pre-existing on main and not this PR's doing, but this PR is the first UI that PATCHes config, so it is the first thing making the surface routine. Worth an issue against the route rather than a fix here.

Not verified

I could not run the four RTL tests — this workspace has no frontend node_modules, so the test file is read, not executed, and I am not asserting the create/exclusion cases pass. Design is @ux-lead's gate, not mine; I have not looked at the CSS or the rail placement. And I did not check whether /api/integrations/user/all scopes to the caller — if it does not, the list itself is a second exposure, and it is worth confirming before merge.

… copy, locales

Per sprint-review on #1290: PATCH /api/integrations/:id now rejects any
client-supplied config.linkedUserId and stamps the authenticated caller
when liveRelay flips on; the page stops sending it. Per ux-lead: copy
says private chat (matching #1289's gate), connectors.* locale entries
added for en + zh-CN, and the pod select reuses .v2-byo__input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pc6nGXRS8mHvrwcXMSRDK
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Both blockers addressed at the new head:

  • linkedUserId is now server-derived: PATCH rejects any client-supplied config.linkedUserId (400, no write) and stamps req.user.id when liveRelay flips on. Three route tests incl. the VICTIM-USER-ID probe. The page stops sending it.
  • Merge order: fix(telegram): only relay inbound as the linked user from a private chat #1289 is marked ready (with the $set co-location test you asked for) and merges first; this PR waits behind it.
  • ux-lead's pass: copy now says private chat, connectors.* locale entries added for en + zh-CN, select reuses .v2-byo__input.
  • /api/integrations/user/all is scoped — Integration.find({ createdBy: req.user?.id, isActive: true }) — so the list is the caller's own rows only.
  • The wholesale config-merge is filed as its own issue rather than fixed here.

@lilyshen0722
lilyshen0722 merged commit e35d89e into main Aug 26, 2026
10 checks passed
@lilyshen0722
lilyshen0722 deleted the feat/v2-connectors-page branch August 26, 2026 22:06
samxu01 pushed a commit that referenced this pull request Aug 26, 2026
 gated the inbound half

Finding 1's amendment said "shouldEscalate plus liveRelay defaulting to
false are the whole bound", and the closing section restated it. That was
written while liveRelay had no named writer anywhere in the product, so
the real bound was "nobody can turn it on" — a fact the sentence does not
carry and a reader cannot recover.

Both halves have since moved, in opposite directions:

- #1290 (e35d89e) ships the Connectors page. V2ConnectorsPage.tsx:117
  PATCHes {liveRelay} and integrations.ts:406 stamps linkedUserId from the
  authenticated caller when it flips on. Mode 4 is now reachable by an
  ordinary user path.
- #1289 (f9b97d8) narrows the inbound half to 1:1 chats —
  telegramBridgeService.ts:213 refuses any chatType that is not 'private',
  because every inbound message is authored as the linked user.

Amended both sites rather than the first, since the claim is restated in
the closing section where a reader arrives at D1. Also widened the
amendment's own caveat: it now names #1289 and #1290 alongside #1282
rather than claiming to cover #1282 and nothing else.

D1's naming decision is unaffected. This changes what the inventory says
exists, not what it should be called.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lilyshen0722 added a commit that referenced this pull request Aug 27, 2026
…user/all (#1299)

* fix(integrations): drop unregistered platformIntegration populate in user/all

The platformIntegration virtual resolves to the TelegramIntegration
model, which isn't registered at boot, so any call to GET
/api/integrations/user/all 500s (MissingSchemaError). The Connectors
page (#1290) is the first UI to call it. The field is unused by every
caller; drop the populate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pc6nGXRS8mHvrwcXMSRDK

* fix(integrations): rate-limit the user/all listing (CodeQL js/missing-rate-limiting)

Same token-hash/IP keying as routes/messages.ts read limiter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pc6nGXRS8mHvrwcXMSRDK

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant