Skip to content

feat(connectors): push a user-scoped invalidation so a visible tab re-reads (TASK-135) - #1751

Open
lilyshen0722 wants to merge 5 commits into
mainfrom
feat/task-135-user-scoped-invalidation
Open

lilyshen0722 wants to merge 5 commits into
mainfrom
feat/task-135-user-scoped-invalidation

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Closes the limit TASK-131 named rather than hid, and filed as TASK-135.

The gap this closes

#1728 (TASK-131) closed both staleness sources a viewer could see — no clock in rendering, one read at mount for the source. It explicitly left one case open: an already-visible tab receives nothing. Client A mints or revokes a grant, or changes an integration; client B sits on the Connectors page with the tab visible and keeps rendering what it read at mount. The only remedy was hiding and showing the tab, and the new 60-second clock made the failure harder to notice — the ages keep moving, so the page looks live while the row is stale.

The change

A user-scoped invalidation, connectors_updated, emitted into user_{userId}:

  • backend/services/connectorEventService.ts (new) — bindSocketIO / emitConnectorsChanged / emitConnectorsChangedFor / joinUserRoom, mirroring the shape taskEventService already established for the board.
  • backend/server.ts — joins each socket to its own user room at connection, from the JWT the socket already presented; binds the service alongside the other two.
  • backend/routes/grants.ts — create, attenuate, revoke.
  • backend/routes/integrations.ts — create, github-app, patch, delete, connect, disconnect, connect-code.
  • Both Connectors components subscribe and re-read, and re-read again on socket connect.

Two decisions worth the reader's time

Why user_{userId} and not the pod_{podId} fan-out the board uses. The two reads behind this page are user-scoped (/api/integrations/user/all is { createdBy: viewer }) and the page is the viewer's own inventory, so a pod-room event would reach members who are not entitled to the rows. The other way to get pod scoping — subscribing the page to every listed pod's room — goes through joinPod, which also emits presence: the Connectors page would mark the viewer present in every pod it lists. That is a visible social side effect on a different feature, so it is not something to fold in here.

Why the join lives in the service. server.ts is not importable in a test, and a missing join fails silently: every emit still succeeds, into an empty room. So the guard is a function (joinUserRoom) with the room name in one place, unit-tested directly.

Attenuation is agent-initiated, so the socket it invalidates is the connection owner's, not the caller's — an agent has no Connectors page. That costs one lookup on an agent-only path and it cannot fail the mint that already happened.

Deliberately not emitted

  • POST /ingest (no user session; writes the message buffer, not the list)
  • /:id/send (message delivery; emitting here would refetch every open tab per outbound message for no change)
  • /:id/ingest-tokens POST/DELETE (token management, not a row the Connectors page renders)

Each of these is a "the page did not change" judgement, not a mechanism — worth a look at review.

What it does NOT cover, stated rather than implied

A grant minted by a different granter in a shared pod still does not push to this viewer. That is the pod-scoped case, and it needs the presence-free pod subscription described above. It is out of scope here and named on TASK-135.

Verification

Backend: 79/79 across connectorEventService.test.js (new, 14), grants.mint.test.js (9), grants.read.test.js (9), integrations.linkedUserId.test.js (48), plus 40/40 across the eight existing integration/server suites as a regression check. Frontend: 59/59 across V2ConnectorsPage.test.tsx and V2ConnectorTools.test.tsx.

The four new frontend tests fail without the subscription, and each is mutation-proven alone:

mutation result
connectors_updated subscription removed (page) 1 red
connectors_updated subscription removed (tools) 1 red
reconnect subscription removed (page) 1 red
join guard removed — anonymous sockets join too 1 red
no-op-without-userId removed — emits user_undefined 2 red
grant-create emit removed 1 red
author emit removed (pod-creator case) 1 red

eslint: 0 errors on every touched .ts/.tsx file; the .js test files report the pre-existing import/no-unresolved corpus, at or below their HEAD counts. tsc: no errors in any touched file.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gate at 1acf5f68479fba69e8e490a5598eb0c54359fe2d: passes, nothing blocking. One unpinned line worth an assertion.

The room is safe to trust. joinUserRoom reads socket.userId, which server.ts:485 sets from jwt.verify(token, JWT_SECRET) in the io.use handshake — so the id is server-derived from a verified token and a client cannot name another user's room. user_ has no other use in the backend, so there is no room-name collision. The payload is { userId, reason } and carries no grant or connector content, so even a mis-joined room would leak nothing but the fact that something moved.

Backend suites (connectorEventService + grants.mint + grants.read + integrations.linkedUserId) 81/81; frontend V2ConnectorTools + V2ConnectorsPage 59/59. Each mutation changes one line (git diff --numstat 1+1-):

  • joinUserRoom without its !socket.userId guard (every unauthenticated socket joins user_undefined): 1 red
  • userRoom collapsed to one shared room — the cross-user case: 9 red
  • emitConnectorsChangedFor narrowed to req.userId alone: 3 red
  • emitConnectorsChanged without its !userId guard: 2 red
  • notifyConnectorsChanged dropping the author emit: 1 red
  • page listener / page reconnect / tools listener / tools reconnect removed: 1 red each

The one gap. Rewiring the attenuate path to emitConnectorsChanged(req.userId, …) instead of connectionOwnerId(connection) leaves 81/81 green. That line is the one place where the caller is an agent and the owner is someone else, and your own comment says why it must be the owner — an agent has no Connectors page. Unpinned, a later edit "simplifying" it to the caller would silently restore the stale-row bug on the mint path. One assertion that the attenuate emit targets the connection owner and not the caller would close it.

A method note, since it nearly cost me the result: my first mutation pass had a wrong backup path, so the restore silently failed and mutations 2–5 stacked on each other (the diff= counts climbed and the restore run came back red). I redid the whole set with git checkout -- as the restore. The numbers above are from the clean run.

samxu01 pushed a commit that referenced this pull request Sep 18, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
samxu01 pushed a commit that referenced this pull request Sep 18, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from 56763a2 to 157a628 Compare September 18, 2026 12:53

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-gate at 157a628eac (rebased onto ac544d5c): passes; the gap is pinned.

  • src patch-id over backend/routes, backend/services, backend/server.ts, frontend/src/v2/components is 8d6fd3f2, unchanged from the 1acf5f68 gate. The source really is test-only on top. (Several non-test files differ between the two heads — cli/*, agent-config.ts, V2ConnectorTools.tsx — but all of them sit below the new merge-base: that is main moving under the rebase, not this PR, which is why the PR-relative patch-id is the right measure and is identical.)
  • Backend suites 82/82 (was 81), restored 82/82.
  • emitConnectorsChanged(connectionOwnerId(connection), …)req.userId: 1 red. Green at the previous head, so the new assertion is what closed it. That was the whole finding.

Nothing further from me on #1751.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-gate at 157a628eac (rebased onto ac544d5c): passes; the gap is pinned.

  • src patch-id over backend/routes, backend/services, backend/server.ts, frontend/src/v2/components is 8d6fd3f2, unchanged from the 1acf5f68 gate. The source really is test-only on top. (Several non-test files differ between the two heads — cli/*, agent-config.ts, V2ConnectorTools.tsx — but all of them sit below the new merge-base: that is main moving under the rebase, not this PR, which is why the PR-relative patch-id is the right measure and is identical.)
  • Backend suites 82/82 (was 81), restored 82/82.
  • emitConnectorsChanged(connectionOwnerId(connection), …)req.userId: 1 red. Green at the previous head, so the new assertion is what closed it. That was the whole finding.

Nothing further from me on #1751.

@lilyshen0722

Copy link
Copy Markdown
Contributor Author

New head f512367. CI failed on the previous head and the failure was mine, in an instructive way.

routeRateLimitGuard.test.js failed its second assertion — "the baseline only lists registrations that still violate the rule". Capping POST /:id/connect and POST /:id/disconnect in 0c388f2 made those two routes compliant, which left their rows in routeRateLimitGuard.baseline.json over-reporting. That list is a burn-down in both directions by design: a new violation fails the first test and must be fixed in the route, and a baseline row that becomes compliant fails the second until it is deleted. Both routes were correctly capped, so the fix is the deletion, not the route.

Removed the two rows. Guard 16/16, connector + grants suites 98/98 combined.

Worth flagging as a process note rather than a footnote: the guard runs in the full backend suite, and I had verified this change with the focused integrations and grants suites, which do not include it — so the local signal was green while CI's required check went red on both heads. The guard's own header says it exists exactly for this ("this test finds the next one before the push, in npm test"), and it was written in September after CodeQL found three un-limited routes after a push. It did its job; I just hadn't run it. Running routeRateLimitGuard.test.js alongside any change that touches a route registration or a limiter is now the habit.

The remaining un-limited write routes in this file (/:id/send, POST/DELETE /:id/ingest-tokens, DELETE /:id, plus the GET rows) are still in the baseline and untouched by this PR — those are the follow-up if the Connectors lane wants the surface capped.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-gate at f512367af74973e4733593a03c10df0476291bd4: passes, nothing blocking.

The delta since my 157a628e gate is exactly two files — integrations.ts (the limiter on the two routes) and routeRateLimitGuard.baseline.json (the two rows retired). Everything gated before is untouched.

The CodeQL fix is real, not a suppression, and the guard proves it both ways. Suites: guard + connectorEvent + integrations.linkedUserId + grants.mint + grants.read = 98/98; guard alone 16/16.

  • uncap POST /:id/connect: 1 red — uncap POST /:id/disconnect: 1 red
  • re-add a retired baseline row for a now-capped route: 2 red. The baseline cannot be padded to silence a route; a second test asserts it only lists registrations that still violate. That is why retiring those two rows was mandatory here rather than optional.
  • empty the baseline entirely: 1 red, 230 known violations surface — the positive control that the guard reads the baseline at all.

Placement is right, and I checked the thing that would have made it wrong. Both routes mount the limiter ahead of auth, which is what the guard's own rule requires. That ordering is only safe because integrationsRateLimitKey is IP-keyed (req.ip ? ipKeyGenerator(req.ip) : 'anon'); had the bucket been keyed on req.user.id, running before auth would have collapsed every unauthenticated request onto one key and turned the cap into a denial-of-service lever. It is IP-keyed, so pre-auth placement caps unauthenticated floods as intended.

One consequence to know, not a defect: these two routes now share the 30/min write bucket with the five integration writes beside them, per IP. That is the module's stated design, and an OAuth connect/disconnect flow is nowhere near it.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ f512367 — v2 Connectors + Tools gated from the diff, no fixture: the frontend delta is hook wiring only (connectors_updated + connectload()), no copy, no class, no CSS touched (git diff ac544d5c..f512367a --name-only has no .css / i18n file). Re-read does not flash: load() never sets loading back to true, so the visible list is replaced in place. Ran V2ConnectorTools.test.tsx + V2ConnectorsPage.test.tsx at this head: 62/62. Design call (sprint-impl 69620): user room is the right scope — the page is the viewer's own inventory, and a pod-room subscription via joinPod would mark the viewer present in every listed pod as a side effect of watching a list. Src patch-id over routes/services/server/components: 763c6688. Note: main is now ba4c20c (#1752), so this head is BEHIND; a rebase-only head with the same patch-id gets a quiet re-pin.

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from f512367 to b219e1d Compare September 19, 2026 11:08

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ b219e1d — re-pin after the rebase onto ba4c20c: src patch-id 763c6688 over backend/routes + backend/services + server.ts + frontend/src/v2/components is identical to my f512367 gate, same 13 files, +538/−11. Carried from the diff; nothing re-measured. Note: main has since moved again (bb889c3), so this head is BEHIND once more — same patch-id on the next rebase gets the same quiet re-pin.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carry re-gate at b219e1db: carries. Patch-id 1ef6914b identical to the f512367a gate, measured against current main (bb889c37); same 13 files, same 4 commits. Rebase only, so every finding from that gate stands — including the rate-limit guard discriminating in both directions (uncap either route 1 red; re-add a retired baseline row 2 red).

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from b219e1d to 43455b3 Compare September 19, 2026 11:18

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carry re-gate at 43455b38: carries. Patch-id 1ef6914b identical to the f512367a gate, measured against current main (c381031e, which #1748 just moved); same 13 files, same 4 commits. Rebase only, so every finding stands — the user-room authorization (socket.userId from the verified handshake), the attenuate-emit assertion, and the rate-limit guard discriminating both ways.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 43455b3 — re-pin after the rebase onto c381031 (#1748 merged): src patch-id 763c6688 over backend/routes + backend/services + server.ts + frontend/src/v2/components is identical to my f512367 and b219e1d gates, same 13 files, +538/−11, 4 commits, merge-base = current main. Carried from the diff; nothing re-measured.

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from 43455b3 to cbf4d32 Compare September 19, 2026 11:30

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ cbf4d32 — re-pin after Lily's rebase onto 373ac26; patch-id 763c6688 over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components identical to f512367 / b219e1d / 43455b3, same 13 files (+538/−11) and 4 commits. Carried from the f512367 gate, nothing re-measured.

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from cbf4d32 to 0933a35 Compare September 19, 2026 12:07

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 0933a35 — re-pin after Lily's rebase onto 66a7827; patch-id 763c6688 over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components identical to f512367 / b219e1d / 43455b3 / cbf4d32, same 13 files (+538/−11) and 4 commits. Carried from the f512367 gate, nothing re-measured.

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from 0933a35 to f1fabb8 Compare September 19, 2026 12:22

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ f1fabb8 — re-pin after Lily's rebase onto 260a2fd (#1743 merge); patch-id 763c6688 over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components identical to f512367 / b219e1d / 43455b3 / cbf4d32 / 0933a35, same 13 files (+538/−11) and 4 commits. Carried from the f512367 gate, nothing re-measured.

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from f1fabb8 to 112e9c0 Compare September 19, 2026 12:35

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 112e9c0 — re-pin after sprint-impl's rebase onto e77b1b4 (#1767 merge); patch-id 763c6688 over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components identical to f512367 / b219e1d / 43455b3 / cbf4d32 / 0933a35 / f1fabb8, same 13 files (+538/−11) and 4 commits. Carried from the f512367 gate, nothing re-measured.

@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from e053f8a to 6a8ced3 Compare September 19, 2026 15:32
samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from 6a8ced3 to ce89846 Compare September 19, 2026 15:32

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ ce89846 — re-pin after sprint-impl's rebase onto e938afc (cli + review-checklist docs merges); patch-id 393ef85e over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components identical to 13a4d09 / e053f8a, range-diff shows all 4 commits '=', same 13 files (+538/−11), Lily-authored. Carried from the 13a4d09 stamp, nothing re-measured.

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from ce89846 to a37149a Compare September 19, 2026 17:33

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ a37149a — re-pin after the rebase onto 79306fd; patch-id 393ef85e over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components identical to 13a4d09 / e053f8a / ce89846, range-diff shows all 4 commits '=', interdiff differs only in one index line and one hunk offset in V2ConnectorTools.tsx (context moved by what landed on main), no +/− lines. Same 13 files (+538/−11), Lily-authored. Carried from the 13a4d09 stamp, nothing re-measured.

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from a37149a to 5b95f8a Compare September 19, 2026 19:55

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 5b95f8a — re-pin after the rebase onto bd80188; patch-id 393ef85e over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components identical to a37149a / 13a4d09, range-diff shows all 4 commits '='. Carried from the 13a4d09 stamp, nothing re-measured.

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from 5b95f8a to 7ea5af0 Compare September 19, 2026 20:24

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 7ea5af0 — re-pin after the rebase onto 6e56dfe; patch-id 393ef85e over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components identical to 5b95f8a / 13a4d09, range-diff shows all 4 commits '='. Carried from the 13a4d09 stamp, nothing re-measured.

samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from 7ea5af0 to 7337dd6 Compare September 19, 2026 20:43

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 7337dd6 — rebase onto 8bfca7b; src patch-id 393ef85e unchanged over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components, all 4 commits '=' in range-diff vs 7ea5af0. Gate from f512367 / 13a4d09 carries. Test-dir edits (granted 1h ago → granted 1h) are outside this scope and will not void the stamp.

samxu01 pushed a commit that referenced this pull request Sep 20, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from 7337dd6 to 07fab24 Compare September 20, 2026 00:26
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Post-merge fix — head 07fab24e, rebased onto main ba3306f5 (the #1782 squash merge).

Why this PR has 5 commits now. #1782 changed the rendered grant age from granted 1h ago to pod · granted 1h (Direction A rule 3: tools.grantedAge + the time.age.* keys, which is why the ago suffix disappears). Main's own assertions in V2ConnectorTools.test.tsx were updated inside #1782; the two assertions this branch adds in its TASK-135 tests were not, so they were the only ones left asserting the old string. This is the semantic collision sprint-review measured before the merge (2 on my side vs 3 on theirs) — the pair merged textually clean and each was green alone.

Evidence, all measured on the merged tree at this head:

  • Old string, mutation run: reverting both assertions to 'granted 1h ago'exactly 2 failed, 19 passed; restored afterwards.
  • Fixed: V2ConnectorTools + V2ConnectorsPage2 suites, 64/64 passing.
  • Rebase was clean (4/4, no conflicts). The range-diff marks commit 1 as ! rather than =, which is context onlyfeat(v2): Connectors and Tools rows in Direction A — marks, one worded act, mono kicker #1782 inserted an ActGlyph import on the line beside mine. Settled with the +/- line diff instead of the hash: all 549 patch lines from the pre-merge head are present unchanged, and the only delta is the 3 lines of this fix.

Test-only edit under frontend/src/v2/__tests__/, so no stamp is voided and no pixel moves. behind_by=0 server-side.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 07fab24 — rebase onto ba3306f (#1782 in base). src patch-id moved 393ef85e → 16254cfc from context lines only: range-diff vs 7337dd6 shows commits 2–4 '=' and commit 1 differing solely in the ActGlyph/MarkGlyph import context in V2ConnectorTools.tsx + V2ConnectorsPage.tsx, zero +/− content lines over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components. New 5th commit (test-only, granted 1h agogranted 1h) is outside this scope. Gate from f512367 / 13a4d09 carries.

samxu01 pushed a commit that referenced this pull request Sep 20, 2026
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from 07fab24 to 78228fb Compare September 20, 2026 01:10

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 78228fb — rebase onto 2635b85; src patch-id 16254cfc unchanged over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components (5 commits, test-only 5th commit out of scope). Re-stamp only, no visual delta.

…-reads

TASK-131 closed the staleness a viewer could see and left one case open, named
on TASK-135: a tab that is already VISIBLE still renders whatever it read at
mount when another client of the same user mints or revokes a grant, or
changes an integration. Hiding and showing the tab was the only remedy, and the
new 60s clock made it worse by keeping the ages moving — the page looks live
while the row is stale.

This adds the push. connectorEventService emits 'connectors_updated' into
'user_{userId}', the room a socket joins once at connection from the token it
already presented. Writes emit from their own sites: grant create / attenuate /
revoke in routes/grants.ts, and the connector lifecycle writes in
routes/integrations.ts (create, github-app, patch, delete, connect,
disconnect, connect-code). Both Connectors components subscribe and re-read,
and re-read again on socket reconnect — an event fired while the socket was
down is not replayed.

Why the user room and not the pod_{podId} fan-out the board uses: the page is
the viewer's own inventory, so a pod-room event would reach members who cannot
read those rows. Subscribing the page to each listed pod's room instead goes
through joinPod, which also emits presence, and would mark the viewer present
in every pod the page lists.

The join is a function in the service rather than two lines in server.ts so the
room name has one spelling and the guard is unit-testable; server.ts is not
importable in a test, and a missing join is silent — every emit still succeeds
and reaches an empty room.
…ner, not the caller

The one path where the caller and the invalidated user differ: attenuation is
agent-initiated, so the socket that must re-read belongs to the connection
owner — an agent has no Connectors page. Rewiring the emit to the caller left
every test green (sprint-review on #1751), which is the same silent-stale-row
bug this PR exists to remove, restored by an edit that looks like a
simplification.

Ran alone: the mutation is 1 red / 10 pass, restored 11/11.
…d connector bucket

CodeQL flagged two new high-severity js/missing-rate-limiting alerts in the
files this PR changes, at POST /:id/connect and POST /:id/disconnect. The gap
is pre-existing — neither route ever carried a limiter — but the invalidation
emit put both in the diff, and the analyzer is right on the merits: the five
write routes beside them share one bucket (integrationRateLimit.ts says so in
its own comment), so these two were the pair that could be burst while every
other connector write was capped.

Adding writeIntegrationsRateLimit is the fix the module was built for, not a
suppression. The remaining un-limited write routes (/:id/send,
/:id/ingest-tokens, DELETE /:id, DELETE /:id/ingest-tokens/:tokenId) are
untouched by this PR and left for a follow-up rather than widened into it.

Integrations suites 75/75 across 7 files; eslint 0; tsc clean.

(cherry picked from commit 91c51c6)
…imit baseline

The rate-limit guard burns down in both directions: a route that becomes
compliant fails the second test until its baseline row is removed, so a fix
cannot leave the file over-reporting. Capping POST /:id/connect and
POST /:id/disconnect in the previous commit did exactly that, and CI caught
it at 0c388f2 — the guard runs in the full backend suite, which the focused
suites I ran locally do not cover.

That guard is itself the September response to CodeQL finding three
un-limited routes after a push; this is the first row of its integrations.ts
block to be retired rather than reported.

Guard 16/16; connector + grants suites 98/98 combined.
#1782)

#1782 landed on main as ba3306f and rule 3 renders the grant age as
`pod · granted 1h` — the `ago` suffix is gone (main's own assertions at
this file were updated in the same PR). These two are the TASK-135 tests
added by this branch, so they were the only ones left asserting the old
string; they fail on the merged tree without this. Mutation-checked:
reverting both to 'granted 1h ago' reds exactly these two.
@samxu01
samxu01 force-pushed the feat/task-135-user-scoped-invalidation branch from 78228fb to 2e18b25 Compare September 20, 2026 01:47

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: PASS @ 2e18b25 — rebase onto f064d57 (the Deploy Dev cut). src patch-id 16254cfc unchanged over backend/routes + backend/services + backend/server.ts + frontend/src/v2/components; all 5 commits carried; re-stamp only, nothing re-measured.

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