Task Breakdown — Centralized Web Session (Hub FE Integration with mekari-account-web-sdk)
Source RFC:
centralized-web-session.md· Mode: Vertical (1 task = 1 chunk/story end-to-end: UI + API + tests) · Scope: full picture (blocked tasks shown inline) · Target repo:local/hub(verified at/Users/mekari/Documents/hub)
Reconnaissance notes (verified against the repo):
- Test command:
npm run test(jest --coverage); single-pattern run:npm run test -- --testPathPattern="<name>". - Lint:
npm run lint· Build:npm run build(nuxt build). - Test files predominantly live in a
__test__/(singular) subfolder beside the source, named*.spec.js— e.g.assets/mixins/metric/__test__/mixpanelMixin.spec.js(37 such directories repo-wide). A pluralized__tests__/convention also exists in some domains (mixins/utils/layouts/store) — both patterns are present in the repo; singular dominates and is used here, but__tests__is not simply "wrong". (The RFC's §4.C wrote__tests__/plural — this breakdown uses the dominant singular__test__/convention.) - Import alias is
@/(e.g.import { EventBus } from '@/plugins/event-bus'). - Nuxt plugins are registered in the
plugins:array atnuxt.config.js:84. mekari-account-web-sdkis not inpackage.json(only@mekari/pixel@^1.1.14) — it is added as a git dependency per ADR-2, not a registry package, so there is no publish/access blocker (OQ-3 resolved).assets/mixins/session/does not exist yet (sibling domain dirs do:contact/,mqtt/,metric/, …) — new dir is consistent with convention.- All RFC-cited files and line anchors confirmed (
InitComponent.vue:610emitsuser-sign-out;SwitchAccount.vue:299/302$on/$off;:376doSignOut;:409SSOsign_out;hubAuthScheme.js:36logout;store/organization.js:30feature_flag, getters at:41;middleware/login.js:22auth-code redirect).
Effort Summary
| Task | Story / Chunk | Status | FE days | BE days | QA days | Total |
|---|---|---|---|---|---|---|
| 1 — Toggle plumbing + SDK boot plugin + mixin scaffold | Chunk 1–2 (toggle, load SDK) | ✅ Actionable (OQ-3 resolved — git dependency, no publish/registry blocker) | 2 | — | 0.5 | 2.5 |
2 — Wire mixin into boot + logged_out | Chunk 3 | ✅ Actionable | 1 | — | 0.5 | 1.5 |
4 — server_down — fail-open, no action | Chunk 5 | ✅ Actionable | 0.5 | — | 0.5 | 1 |
| 5 — Observability (RUM + Mixpanel) | Chunk 7 | ✅ Actionable | 0.5 | — | 0 | 0.5 |
6 — logged_in company sync | Chunk 6 / story "logged_in + company sync" | 🚫 Blocked (OQ-1, OQ-2) | 1 | 0 | 0.5 | 1.5 |
| Grand total (recomputed, latest review + BE grounding) | 5 | 0 | 2 | 7 |
BE grounding (latest review, verified against
hub-core+hub-service, the Chat backend). Task 6's BE was set to 0 here to remove a double-count: Hub (Chat v1) and Hub Chat v2 are two frontends of the samehub-core/hub-servicebackend, so the current-company sync endpoint is built once and its BE effort is carried inhub-chat-v2-fe.task-breakdown.mdTask 7 (2.0 BE). Hub's FE still consumes it (FE 1.0 retained). Grounded facts: the SSO company plumbing already exists (hub-core/app/apps/mekari_sso/services/get_owned_companies.rb,get_company.rb, and login-time reconciliationinteractors/oauths/user_get_token.rb:127-138); Chat has no per-session "current company" (a user is hard-bound 1:1 to oneorganization_id), so the shared BE work is "wire a new sync onto existing SSO services + the single-org model," not greenfield SSO integration. (Logout-chain flagsseamless_auth_token/revoke_sso_token_on_logoutandInteractors::Oauths::Revokeatrevoke.rb:19-26are confirmed present, unchanged by this pass.)
Task 3 removed (was:
switch_userre-auth, ~2 FE + 0.5 QA d). The real SDK (mekari-account-web-sdkv0.3.0) has noswitch_userstatus — an SSO account switch surfaces indistinguishably aslogged_outand is handled by Task 2's existing sign-out path (R5). The task-number gap at 3 is intentional, not an error — it preserves traceability to the RFC's chunk numbers (hub-fe.md§4.C also keeps the gap at Chunk 4).Grand total revised from 12.5 d to 9 d (latest review reconciliation): the deleted
switch_usertask (-2 FE/-0.5 QA), the simplifiedserver_downno-op (Task 4: -1 FE/0 QA vs. the originalmsli-heuristic estimate), and the removedmsli-write helper work in Task 2 (-0.5 FE) account for the reduction.
Confidence: medium-high. Tasks 1, 2, 4, 5 are well-grounded (every auth primitive being reused already exists and is verified in-repo), and the SDK contract itself is now fully resolved against the real
mekari-account-web-sdkv0.3.0 source (three statuses, single-arg callback, noswitch_user) — the single biggest historical rework risk (thelogout/logged_outcontradiction, OQ-4) is gone. Task 6 (company sync) is fully blocked on an unbuilt Hub BE endpoint (OQ-1), and its BE side is estimated from the ADR-3users/me/current_companycontract, not a confirmed spec. The toggle is also inert until OQ-2 ships thecentralized_sessionorg-payload flag, but that does not block coding/testing behind a mocked flag.
Task 1: [FE] Toggle plumbing + SDK boot plugin + centralized-session mixin scaffold (Chunk 1–2 · stories "Load SDK with user_sso_id", "Gate behind centralized_session toggle")
A user on a company with the
centralized_sessionflag ON has themekari-account-web-sdkSessionconstructed once per authed shell with theirsso_id(ascurrentUser); with the flag OFF, no SDK and no iframe — today's behaviour byte-for-byte.
Status: ✅ Actionable. mekari-account-web-sdk is a git dependency, not a
registry package — OQ-3 is resolved, so this task is not blocked waiting on a
publish/registry grant. The canonical event contract is also resolved
(OQ-4): three statuses, single-arg callback (wired in Task 2).
Design reference: n/a — no visible UI (SDK injects a hidden iframe;
§1.5 "SDK injects iframe; no visible UI").
What to build
A centralizedSession mixin in a new assets/mixins/session/ folder that,
guarded by organization.feature_flag.centralized_session and the presence
of $auth.user.sso_id, dynamically imports mekari-account-web-sdk and
constructs new Session({ currentUser: sso_id }), subscribing to the single
session.on('event', handleSessionEvent) channel and tearing the session
down via session.destroy() (not .off() alone — the SDK is a singleton
and destroy() is what resets it and removes the internal window listener,
R11) in beforeDestroy. A thin Nuxt plugin handles registration; the dynamic
import() lives inside the toggle guard (ADR-2) so the bundle stays lazy.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | plugins/centralized-session.js | Nuxt plugin stub mirroring plugins/hotjar.js's registration pattern. The SDK is a bundled git dependency (ADR-2) — no CDN/env plumbing to manage. |
| create | assets/mixins/session/centralizedSession.js | Mixin: isCentralizedSessionEnabled computed (reads org getter), initSession() (toggle + sso_id guard → dynamic import('mekari-account-web-sdk') → new Session({ currentUser }) → session.on('event', handler)), beforeDestroy calls this._session?.destroy(). Handlers stubbed in Task 2. |
| create | assets/mixins/session/__test__/centralizedSession.spec.js | Tests: toggle ON + sso_id present ⇒ Session constructed once with currentUser = sso_id; toggle OFF ⇒ Session never constructed (mock asserts 0 calls); sso_id absent ⇒ not constructed. |
| modify | nuxt.config.js (plugins array :84) | Register '@/plugins/centralized-session.js'. |
| modify | package.json | Add mekari-account-web-sdk as a git dependency pinned to a version tag (ADR-2). No mock required at merge time since there is no registry wait, but tests still use jest.mock('mekari-account-web-sdk') for isolation. |
| read | store/organization.js:30,41 | Confirm feature_flag getter exposes the map; centralized_session resolves to undefined→falsy (safe OFF) until OQ-2. No code change unless the getter isn't generic. |
File path rule: all paths above are repo-verified except
mekari-account-web-sdkitself, which is a new dependency to be added per ADR-2.
Implementation steps
- Explore — Open
plugins/hotjar.js(3-line third-party boot pattern) andassets/mixins/metric/mixpanelMixin.js+ its test atassets/mixins/metric/__test__/mixpanelMixin.spec.jsto copy the mixin +__test__/spec layout and@/alias style. Openstore/organization.js:41to see the getter shape. - Write failing tests (red) — Create
assets/mixins/session/__test__/centralizedSession.spec.js.jest.mock('mekari-account-web-sdk', () => ({ Session: jest.fn() })). Assert: flag-ON +sso_id⇒Sessioncalled once with{ currentUser: <sso_id> }; flag-OFF ⇒Sessionnot called; nosso_id⇒ not called. Runnpm run test -- --testPathPattern="centralizedSession"and confirm red. - Scaffold plugin — Create
plugins/centralized-session.jsmodelled onplugins/hotjar.js; register'@/plugins/centralized-session.js'in thenuxt.config.js:84plugins array. - Scaffold mixin — Create
assets/mixins/session/centralizedSession.jswithcomputed.isCentralizedSessionEnabledreading the orgfeature_flaggetter, aninitSession()method, and an emptyhandleSessionEvent(data)(single argument — handlers added in Task 2), plusbeforeDestroyteardown callingthis._session?.destroy(). - Wire state — In
initSession(), guard onisCentralizedSessionEnabled && this.$auth?.user?.sso_id; inside the guardconst { Session } = await import('mekari-account-web-sdk');this._session = new Session({ currentUser: this.$auth.user.sso_id });this._session.on('event', this.handleSessionEvent). - Go green —
npm run test -- --testPathPattern="centralizedSession"until all pass. - Quality gate —
npm run lint && npm run build. Addmekari-account-web-sdktopackage.jsonas part of this task (git dependency, ADR-2) so the build resolves the real import — there is no registry wait to gate on.
Acceptance criteria
- Toggle ON +
sso_idpresent ⇒Sessionconstructed exactly once withcurrentUser = sso_id. - Toggle OFF ⇒
Sessionnever constructed, no iframe (mock asserts 0 calls). -
sso_idabsent ($auth not ready) ⇒ SDK init skipped (Branch & Skip Catalog §2.9). -
beforeDestroycallssession.destroy()(not.off()alone) — resets the SDK singleton and tears down the iframe +windowlistener (R11, AGENTS.md leak rule). -
mekari-account-web-sdkadded topackage.jsonas a git dependency; dynamic import resolves at build.
Test strategy
Jest unit test on the mixin in isolation. Mock mekari-account-web-sdk so Session is a jest.fn(); mount the mixin with a fake $auth and a stubbed Vuex getter for feature_flag. Key assertion: expect(Session).toHaveBeenCalledWith({ currentUser: 'sso-123' }) under flag-ON, and expect(Session).not.toHaveBeenCalled() under flag-OFF.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 2 |
| Backend | — |
| QA | 0.5 |
| Total | 2.5 |
Assumptions: reuses the existing org
feature_flaggetter (no store change); SDK mocked in tests for isolation even though there's no registry blocker;@mekari/pixelis already a dependency (no new tooling). Newassets/mixins/session/dir follows existing sibling-dir convention.
Run to verify
npm run test -- --testPathPattern="centralizedSession" && npm run lint
Depends on
- [External:
centralized_sessionorg-payload flag — OQ-2 (pending; toggle inert until shipped, but does not block dev/test behind a mocked flag)]
Task 2: [FE] Wire mixin into boot + handle logged_out (Chunk 3 · story "Handle logged_out")
When SSO reports the user is logged out — including an SSO account switch, which the SDK cannot distinguish from a plain logout and never exposes the incoming user's ssoId for (R5) — Hub signs the user out automatically.
logged_inis captured by the same dispatcher but takes no action here; it is wired to company sync in Task 6.
Status: ✅ Actionable — the sign-out path maps onto a primitive that
already exists and is verified (EventBus.$emit('user-sign-out') →
doSignOut()).
Design reference: n/a — no visible UI (behavioural; sign-out reuses the existing doSignOut redirect).
What to build
Use the centralizedSession mixin inside InitComponent.vue (after
$auth.loggedIn), and implement the single-arg session.on('event', handleSessionEvent) dispatcher with a logged_out branch: emit
EventBus.$emit('user-sign-out') (the relay SwitchAccount.vue:299 already
turns into doSignOut()). The dispatcher also has a logged_in case that is
intentionally a no-op placeholder until Task 6 wires company sync.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| modify | components/layouts/main/InitComponent.vue | Import + register the centralizedSession mixin; call initSession() after $auth.loggedIn is confirmed (alongside existing boot side-effects ~:1138 where sso_id is already read). |
| extend | assets/mixins/session/centralizedSession.js | Implement handleSessionEvent(data): switch (data.status) { case 'logged_out': EventBus.$emit('user-sign-out'); break; case 'logged_in': /* no-op here — see Task 6 */ break; }. Import EventBus from @/plugins/event-bus. |
| extend | assets/mixins/session/__test__/centralizedSession.spec.js | Test: { status: 'logged_out' } ⇒ EventBus.$emit called once with 'user-sign-out'; { status: 'logged_in' } ⇒ no EventBus.$emit call (no-op). |
All paths repo-verified. No
utils/general.jschanges and nomsliread/write anywhere — the SDK ownsmsliinternally and Hub must not touch it (R7).
Implementation steps
- Explore — Open
components/layouts/main/InitComponent.vue:609-610(the existingEventBus.$emit('user-sign-out')relay) andSwitchAccount.vue:299-302,376to confirm the relay→doSignOutchain you're reusing. - Write failing tests (red) — Extend
centralizedSession.spec.js: dispatch a fake{ status: 'logged_out' }⇒ assertEventBus.$emitcalled with'user-sign-out'; dispatch{ status: 'logged_in' }⇒ assert no side effect. Runnpm run test -- --testPathPattern="centralizedSession"→ red. - Wire state — In the mixin import
{ EventBus }from@/plugins/event-bus. - Implement behavior — Fill
handleSessionEvent'slogged_outandlogged_inbranches (the latter a documented no-op); register the mixin inInitComponent.vueand invokeinitSession()after$auth.loggedIn. - Go green —
npm run test -- --testPathPattern="centralizedSession"until green. - Quality gate —
npm run lint && npm run build.
Acceptance criteria
-
logged_outevent ⇒EventBus.$emit('user-sign-out')invoked exactly once. -
logged_inevent ⇒ handled by the dispatcher with no side effect (nolocalStorage/msliaccess — R7). - Mixin is active only inside the authed shell (after
$auth.loggedIn). - No direct
doSignOut()re-implementation — only the EventBus relay is used (ADR-5).
Test strategy
Jest. Mock the SDK's event emitter to push { status } payloads into handleSessionEvent; spy on EventBus.$emit. Key assertion: expect(EventBus.$emit).toHaveBeenCalledWith('user-sign-out') for logged_out, and expect(EventBus.$emit).not.toHaveBeenCalled() for logged_in.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1 |
| Backend | — |
| QA | 0.5 |
| Total | 1.5 |
Assumptions: reuses the verified
user-sign-outEventBus relay anddoSignOut()(no new sign-out code). Effort reduced from the original 2-day estimate: themsli-write helper work (writeMsli/readMsli/removeMsliinutils/general.js) is removed entirely — the SDK ownsmsliinternally and a consumer write would collide with its own fallback (R7).
Run to verify
npm run test -- --testPathPattern="centralizedSession" && npm run lint
Depends on
- [Task 1] (mixin + boot plugin scaffold must exist)
Task 4: [FE] Handle server_down — fail-open, no action (Chunk 5 · story "Handle server_down")
When the Session Manager is unreachable, Hub does not sign the user out, change company, or take any other visible action. The SDK already exhausts its own internal
msli-based grace period (localStorage key"msli", 2h expiry, owned and read/written by the SDK itself) before ever emittingserver_down, and PRD constraint 6.10 requires fail-open behaviour on this path. This replaces the original design's consumer-sidemsliheuristic, which would have required Hub to read/write an SDK-internal key — a correctness risk, not a refinement (R7).
Status: ✅ Actionable.
Design reference: n/a — no visible UI (silent no-op).
What to build
A server_down branch in handleSessionEvent (ADR-6) that takes no action —
no EventBus.$emit, no localStorage access, no token-validity check.
Optional observability hook is wired in Task 5.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | assets/mixins/session/centralizedSession.js | case 'server_down': explicit no-op (fail-open, ADR-6), with a code comment referencing R7/ADR-6 so it isn't "helpfully" reintroduced later. |
| extend | assets/mixins/session/__test__/centralizedSession.spec.js | Test: server_down event ⇒ EventBus.$emit (and any sign-out path) is not invoked. |
All paths repo-verified. No
utils/general.jshelper (isMsliFreshor similar) is built — there is no consumer-sidemsli/token-validity logic to implement (R7).
Implementation steps
- Explore — Re-read ADR-6 (
hub-fe.md§2.3) for the fail-open rationale and the state machine (§2.7) self-loop onserver_down. - Write failing tests (red) — Extend the spec with
{ status: 'server_down' }⇒ assertEventBus.$emitwas not called. Runnpm run test -- --testPathPattern="centralizedSession"→ red. - Implement behavior — Add the
server_downcase as an explicit, commented no-op. - Go green —
npm run test -- --testPathPattern="centralizedSession"until green. - Quality gate —
npm run lint && npm run build.
Acceptance criteria
-
server_down⇒ noEventBus.$emit('user-sign-out'), no company-state change, nolocalStorageaccess. - No consumer-side
mslior token-validity logic exists anywhere in the mixin.
Test strategy
Jest. Dispatch { status: 'server_down' } into handleSessionEvent; assert no side-effecting spy (EventBus.$emit) was called.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | 0.5 |
| Total | 1 |
Assumptions: fail-open is a pure no-op branch — no new persistence, no predicate to name (unlike the original design's
msli/token-validity check). Effort reduced from the original 2-day estimate accordingly.
Run to verify
npm run test -- --testPathPattern="centralizedSession" && npm run lint
Depends on
- [Task 1] (mixin scaffold)
Task 5: [FE] Observability — RUM + Mixpanel per SDK event (Chunk 7)
Engineers and the on-call team can see, per piloted company, that centralized-session events are firing (a
centralized_session.logged_inRUM action appears) and can alert on aserver_downspike as a proxy for an A&L outage.
Status: ✅ Actionable — Datadog RUM (plugins/datadog-rum.ts) and the Mixpanel v2 mixin (assets/mixins/metric/mixpanelMixin.js) both exist and are verified.
Design reference: n/a — internal telemetry, no UI.
What to build
Emit one RUM custom action centralized_session.<status> per handled SDK
event, and log forced sign-outs (logged_out) to Mixpanel for funnel analysis
(§3.3). There is no switch_user status to track separately (R5).
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | assets/mixins/session/centralizedSession.js | In handleSessionEvent, after each branch, emit datadogRum.addAction('centralized_session.' + data.status, {...}); for the logged_out (sign-out) path also call the Mixpanel mixin's track method. |
| read | plugins/datadog-rum.ts | Confirm the RUM client accessor (global vs injected) to call addAction. |
| read | assets/mixins/metric/mixpanelMixin.js | Reuse the existing track method signature. |
| extend | assets/mixins/session/__test__/centralizedSession.spec.js | Test: each event ⇒ RUM addAction called with centralized_session.<status>; logged_out ⇒ Mixpanel track called. |
All paths repo-verified.
Implementation steps
- Explore — Open
plugins/datadog-rum.tsfor theaddActionaccessor andassets/mixins/metric/mixpanelMixin.js(+ its__test__/mixpanelMixin.spec.js) for the track signature and how it's mocked in tests. - Write failing tests (red) — Extend the spec: spy on the RUM
addActionand the Mixpanel track; dispatch each event; assert the action name string. Runnpm run test -- --testPathPattern="centralizedSession"→ red. - Implement behavior — Add the telemetry calls inside each
handleSessionEventbranch. - Go green —
npm run test -- --testPathPattern="centralizedSession"until green. - Quality gate —
npm run lint && npm run build.
Acceptance criteria
- Each handled event emits a RUM action named
centralized_session.<status>(verifiable via mocked tracker). - Forced sign-outs (
logged_out) are tracked in Mixpanel. - No PII / token values are included in any telemetry payload (§3.2 A02).
Test strategy
Jest with the RUM client and the Mixpanel mixin method both mocked. Key assertion: expect(addAction).toHaveBeenCalledWith('centralized_session.logged_in', expect.any(Object)).
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | 0 |
| Total | 0.5 |
Assumptions: pure instrumentation reusing existing RUM + Mixpanel clients; no user-facing behaviour ⇒ QA 0 (covered by unit tests).
Run to verify
npm run test -- --testPathPattern="centralizedSession" && npm run lint
Depends on
- [Task 2], [Task 4] (the event branches to instrument)
Task 6: [FE+BE] logged_in company sync (Chunk 6 · story "Handle logged_in + company sync")
When SSO confirms login, Hub fetches and sets the user's current company so the user never sees the wrong company after an SSO-side account switch — fixing the second concrete bug in the source RFC. This is a Hub BE/SSO dependency, decoupled from the SDK (R10): the SDK exposes no company data and does not "drive" this sync beyond firing the
logged_inevent Hub already listens for.
Status: 🚫 Blocked — OQ-1 [critical]: Hub BE has no current-company
endpoint (repo grep current_company|currentCompany → 0 hits, verified). The
auth-code BE contract (users/me/current_company per ADR-3) must be built
and confirmed by Hub BE before the FE wiring can be written against anything
real. Also gated by OQ-2 (centralized_session org flag) before the path
executes. To unblock: Hub BE delivers and documents the GET current_company proxy endpoint and its response shape (OQ-1), and the
org-payload centralized_session flag (OQ-2).
Design reference: n/a — no visible UI (company is set in state; surfaced through existing company UI).
What to build
BE (blocked): a Hub BE proxy endpoint GET current_company that calls SSO
GET /v1.1/users/me/current_company (auth-code variant) and returns the
company. FE (blocked on the BE contract): register the endpoint key in
common/constants/endpoint.js, add a store action that calls it, and dispatch
that action from the logged_in branch of handleSessionEvent. Because an
SSO account switch now surfaces as logged_out → full sign-out → normal
re-login (R5), the subsequent logged_in event naturally re-triggers this
same sync — there is no separate "post-switch" case to implement.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create (BE) | Hub BE service — current-company proxy handler | [unverified — Hub BE repo, not present in local/hub FE repo] Proxy to SSO GET /v1.1/users/me/current_company; contract pending OQ-1. |
| modify | common/constants/endpoint.js | Register the user.currentCompany key under the appropriate version block (file shape verified: vN.user.<key> map). |
| extend | store/organization.js (or a store/users action) | Add a fetchCurrentCompany action that calls the new endpoint key and commits the company. |
| extend | assets/mixins/session/centralizedSession.js | In the logged_in branch, await this.$store.dispatch('organization/fetchCurrentCompany'). |
| extend | assets/mixins/session/__test__/centralizedSession.spec.js | Test (FE): on logged_in, the sync action is dispatched once (action itself mocked). |
endpoint.jsandstore/organization.jspaths repo-verified. The BE handler path is[unverified — Hub BE repo]— not part of thelocal/hubFE checkout; contract itself is[pending OQ-1].
Implementation steps
(Do not start FE wiring until OQ-1 returns the confirmed endpoint contract and OQ-2 ships the flag.)
- Explore — Open
common/constants/endpoint.js(versionedusermap) for the key registration pattern, andstore/organization.jsfor the action/mutation style. - (BE, blocked) Build + document the
current_companyproxy per the OQ-1 contract; confirm method, path, and response shape. - Write failing tests (red) — Extend the spec:
logged_in⇒$store.dispatchcalled with the sync action (action mocked). Runnpm run test -- --testPathPattern="centralizedSession"→ red. - Implement (FE) — Register the endpoint key; add
fetchCurrentCompany; dispatch it from thelogged_inbranch (replacing the Task 2 no-op placeholder). - Go green —
npm run test -- --testPathPattern="centralizedSession"until green. - Quality gate —
npm run lint && npm run build.
Acceptance criteria
- (pending OQ-1) Hub BE
GET current_companyendpoint exists with a confirmed contract. - On
logged_in, the company-sync action is dispatched exactly once. - After an SSO account switch (which now surfaces as
logged_out→ full sign-out → new login), the nextlogged_innaturally re-syncs the company — no special-case handling needed (R5). - (pending OQ-2) the path only runs when
centralized_sessionis ON.
Test strategy
Jest (FE side only, once unblocked): mock the store action; assert $store.dispatch('organization/fetchCurrentCompany') is called on logged_in. The BE endpoint gets its own service-side test once the contract lands.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1 |
| Backend | 0 |
| QA | 0.5 |
| Total | 1.5 |
Assumptions: BE = 0 here to avoid a double-count — Hub (Chat v1) and Hub Chat v2 are two frontends of the same
hub-core/hub-servicebackend, so the current-company BE endpoint is built once and its 2.0 BE is carried inhub-chat-v2-fe.task-breakdown.mdTask 7. Hub's FE still consumes it (FE 1.0 retained). The company-sync spec itself is still blocked on the real contract (OQ-1); FE side reuses the existing endpoint-registry + store-action patterns.
Run to verify
npm run test -- --testPathPattern="centralizedSession" && npm run lint
Depends on
- [Task 1], [Task 2] (mixin + event dispatch +
logged_inplaceholder branch) - [External: Hub BE
current_companyendpoint contract — OQ-1 (blocking)] - [External:
centralized_sessionorg-payload flag — OQ-2 (path inert until shipped)]
Ordering rationale
- Critical path runs through Task 1. The mixin + boot plugin are the
spine everything else hangs off; the SDK dependency
(
mekari-account-web-sdk) is a straightforward git install (ADR-2, OQ-3 resolved), so nothing blocks starting this task immediately. Tasks 2, 4, 5 are pure extensions ofhandleSessionEventon the same two files, so they merge naturally into one developer's flow (vertical merging rule applied — every task touchescentralizedSession.js+ its single spec). - Task 2 → Task 4 are ordered by primitive reuse:
logged_out(Task 2) establishes the sign-out relay and the dispatcher'sswitchskeleton;server_down(Task 4) is an independent no-op branch added to the same dispatcher. - Task 5 (observability) comes last among the actionable set — it instruments branches that must already exist, so it can't precede Tasks 2/4, but it carries no behavioural risk and can ship in the same PR series.
- Task 6 (company sync) is fully blocked and parked at the end — it needs a Hub BE endpoint that does not exist (OQ-1) and an org flag that does not exist (OQ-2). It is the one task that genuinely cannot start; the other four are executable today behind the (currently OFF) toggle, exactly as the RFC's §5 "Known limitation" states.
- Task 3 (
switch_userre-auth) has been removed entirely — the real SDK (mekari-account-web-sdkv0.3.0) cannot emit aswitch_userstatus. An SSO account switch surfaces aslogged_out, which Task 2 already handles via the existing sign-out flow; the subsequent re-login naturally triggers a freshlogged_inevent, which Task 6 (once unblocked) re-syncs company for — no dedicated re-auth/toast code is needed (R5). The numbering below intentionally keeps the gap at 3 rather than renumbering, to preserve traceability to the RFC's chunk numbers. - Push externally on two fronts in parallel with Task 1: Hub BE to build
the
current_companyproxy (OQ-1) and add thecentralized_sessionorg flag (OQ-2). These are now the only remaining blockers — the previous SDK-side blockers (OQ-3 package publish, OQ-4 event contract) are resolved.
Skipped stories
(Full-scope mode: every 🚫 Blocked task listed with its unblocking condition.)
| Story / Task | Reason / unblock condition |
|---|---|
Task 3 — switch_user re-auth (removed entirely) | Not a story to unblock — it never existed. The real SDK (mekari-account-web-sdk v0.3.0) has no switch_user status; an account switch surfaces as logged_out and is handled by Task 2. Removed per R5 (latest review reconciliation); ~2 FE + 0.5 QA days dropped from the original estimate. |
Task 6 — logged_in company sync (story "Handle logged_in + company sync", Chunk 6) | 🚫 Blocked on OQ-1 [critical] — Hub BE has no current_company endpoint (repo grep: 0 hits); needs the confirmed auth-code users/me/current_company proxy contract. Also gated by OQ-2 (the centralized_session org flag) before the path executes. Decoupled from the SDK (R10) — the SDK exposes no company data; this is purely a Hub BE/SSO dependency. |
| (story "Wire logout to also hit SSO sign_out") | Excluded — already implemented (SwitchAccount.vue:409 redirects to ${SSO_ACCOUNT_URL}/sign_out); RFC marks it n/a — already implemented, reused by Task 2's sign-out relay, no new work. |
(Out of scope §1.3) mekari-account-web-sdk package, Session Manager (Golang), dedicated Redis, SSO Kong sm.mekari.com/* | Owned by Account & Launchpad — upstream dependencies, not built in this RFC. |
| (Out of scope §1.3) Auto-revoke of access/refresh tokens on inactivity; multiple-sessions-per-account UX | Explicitly out of scope in the source RFC; no Hub FE change required. |