Task Breakdown — Centralized Web Session (Hub Chat v2 FE Integration)
Source RFC:
centralized-web-session.md· Slicing mode: Vertical (1 task = 1 story end-to-end) · Blocked tasks: included (full-picture mode). Target repo:hub-chat-v2(Nuxt 4, legacy srcDir layout — feature folders live at repo root:common/,plugins/,pages/,middleware/,layouts/. Theapp/dir holds onlyspa-loading-template.html, NOT the source tree). All paths below verified against the checked-out repo on 2026-06-29 unless flagged[unverified — check repo].
Effort Summary
| Story task | FE days | BE days | QA days | Total |
|---|---|---|---|---|
Task 1 — centralized_session flag on AppConfig | 0.5 | — | 0.5 | 1.0 |
Task 2 — useCentralizedSession composable (SDK lifecycle, currentUser/interval) | 1.5 | — | 0.5 | 2.0 |
| Task 3 — Event → action map (3 real statuses) inside the composable | 1.0 | — | 0.5 | 1.5 |
Task 4 — mekariSession.client.ts gated plugin wiring | 1.0 | — | 0.5 | 1.5 |
| Task 5 — RUM observability for SDK init + events | 0.5 | — | 0.5 | 1.0 |
| Task 6 — Docs: auth-sso / login flow spokes | 0.5 | — | — | 0.5 |
| Task 7 — Current-company sync (BE current_company, shared Chat backend) [blocked] | 0.5 | 2.0 | 0.5 | 3.0 |
Task 8 — CSP frame-src at nginx ingress (deploy) [partially blocked] | 0.5 | — | 0.5 | 1.0 |
| Grand total | 6.0 | 2.0 | 3.5 | 11.5 |
BE grounding (latest review, verified against
hub-core+hub-service). Task 7's 2.0 BE is the single Chat-backend current-company build, and it is counted here (not inhub-fe.task-breakdown.md, which sets BE 0) because Hub Chat v2 and Hub (Chat v1) are two frontends of the samehub-core/hub-servicebackend — the endpoint is built once and consumed by both. Grounded: SSO company services already exist (get_owned_companies.rb,get_company.rb) plus login-time reconciliation (user_get_token.rb:127-138); Chat has no per-session "current company" (user is 1:1-bound to oneorganization_id), so the 2.0 covers wiring a new sync onto existing SSO services + the single-org model, not greenfield SSO integration.
Confidence: high. Resolved against the real SDK
mekari-account-web-sdkv0.3.0 (latest review): Q2 (SDK availability) and Q1 (cross-domain_mekari_accountfallback) are both RESOLVED — the package is a confirmed git dependency with a pinned API (constructor({ currentUser, interval?, ... }),on("event", cb),off(),destroy()), and the SDK ownsmsli/session evaluation entirely inside the iframe, so hub-chat never reads_mekari_accountormsli. This removed the origin-guard andmsli-grace work from Task 2/3 and theswitch_userhandling entirely (there is no such event — an account switch is delivered aslogged_out), which is why Tasks 2–3 shrank from the original estimate. The remaining mover: Q4[important]— Task 7's BEcurrent_companyownership is unresolved, which is why it is the one fully blocked task; Q3 (refresh throttle) is moot (use the constructor'sintervaloption).
Task 1: [FE] Add centralized_session flag to AppConfig (Gate behind toggle)
A user's centralized-session behavior is shipped dark — nothing changes until the BE turns the
centralized_sessionflag on for their account/deploy.
Status: ✅ Actionable
Design reference: n/a — no UI surface (config/type-only change).
What to build
Add an optional centralized_session?: boolean field to the AppConfig interface so the FE can read the BE-owned feature flag from /client_configs/config, and prove (in tests) that an absent flag is treated as off.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | common/store/AppConfigStore.ts | Add centralized_session?: boolean; to the AppConfig interface (line ~2, alongside seamless_auth_first?) |
| extend | common/store/AppConfigStore.spec.ts | Assert the field is readable from a fetched config and that undefined reads as falsy/off |
Correction: the RFC's §4.C cites
common/store/__tests__/AppConfigStore.spec.ts— that path does not exist. The real, verified spec is co-located:common/store/AppConfigStore.spec.ts. There is only one spec file, not both; add the new assertions there.
Implementation steps
- Explore — Open
common/store/AppConfigStore.tsand read theAppConfiginterface (line 2) andgetAppConfig()fetch (:81, endpoint/api/core/v1/client_configs/config). Note howseamless_auth_first?: booleanis declared — mirror that exact style. - Write failing test (red) — In
common/store/AppConfigStore.spec.ts, add a case that mocks/api/core/v1/client_configs/configreturning{ centralized_session: true }and assertsappConfig.value?.centralized_session === true; add a second case with the field absent asserting it is falsy. Runpnpm test common/store/AppConfigStore.spec.tsand confirm red. - Implement — Add
centralized_session?: boolean;to theAppConfiginterface. - Go green — Re-run the spec until all pass.
- Quality gate —
pnpm type-check && pnpm lint.
Acceptance criteria
-
AppConfigexposescentralized_session?: boolean. - Test asserts the field is readable when present in the fetched config.
- Test asserts an absent field is treated as off (falsy).
-
pnpm type-checkpasses.
Test strategy
Unit test mocks the /client_configs/config fetch (vitest, the suite's existing fetch-mock pattern in AppConfigStore.spec.ts) and asserts the new field reads through; one negative case asserts the off-by-default behavior.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | 0.5 |
| Total | 1.0 |
Assumptions: pure type + read; no new fetch, no store-action change; reuses the existing AppConfigStore fetch-mock pattern.
Run to verify
pnpm test common/store/AppConfigStore.spec.ts && pnpm type-check && pnpm lint
Depends on
- None (do this first — it gates every other task).
Task 2: [FE] useCentralizedSession composable — SDK lifecycle (currentUser/interval)
When the toggle is on and the user is authenticated, hub-chat starts the centralized-session SDK with the user's
sso_idand a periodicinterval, and subscribes to the SDK's single"event"— without yet acting on specific statuses (that is Task 3). There is nomsliorevent.originhandling to build in hub-chat: the SDK ownsmsliinternally, andevent.originvalidation is not implementable by a consumer (the SDK owns themessagelistener and checks onlyevent.data.source) — that gap is tracked as an upstream infosec finding against Account & Launchpad, not a hub-chat task (§3.3 / §5 Q6).
Status: ✅ Actionable — the real SDK is a confirmed, pinned dependency (Q2 resolved: mekari-account-web-sdk v0.3.0, git install, ESM). Build the composable against vi.mock('mekari-account-web-sdk') now; swap in the real import in the same PR (Chunk/Task 4 wires it up, no separate "drop-in" step needed).
Design reference: n/a — no UI surface (there is no toast; account-switch handling is an ordinary sign-out, see Task 3).
What to build
A new composable useCentralizedSession.ts that owns the SDK instance lifecycle: a start(ssoId) that constructs new Session({ currentUser: ssoId, interval: FIVE_MINUTES }), subscribes via the SDK's single event name (session.on("event", (data) => { ... })), and exposes stop()/teardown via session.destroy() (singleton reset — off() alone is insufficient). There is no msli and no event.origin guard to build here: the SDK owns msli internally, and event.origin validation is not implementable by a consumer (§3.3 / §5 Q6 — upstream SDK gap, not a hub-chat task).
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | common/composables/useCentralizedSession.ts | New composable: start(ssoId), SDK construction (currentUser, interval), single "event" subscription, stop() → session.destroy() |
| create | common/composables/__tests__/useCentralizedSession.spec.ts | vi.mock('mekari-account-web-sdk'); assert construction args, single-subscription shape, teardown |
Implementation steps
- Explore — Open
common/composables/useClient.tsandcommon/composables/useJimo.tsto see the house composable shape: store access viastoreToRefs(useAuthStore())and imports with the~/common/store/...alias. Opencommon/composables/useEventBus.tsfor the bus API (kept available for Task 3, though no toast is emitted today). - Write failing tests (red) — Create
common/composables/__tests__/useCentralizedSession.spec.ts.vi.mock('mekari-account-web-sdk', () => ({ Session: vi.fn(() => ({ on: vi.fn(), off: vi.fn(), destroy: vi.fn() })) })). Assert: (a)start('abc')callsSessionwith{ currentUser: 'abc', interval: <ms> }; (b) subscription happens via exactly oneon("event", fn)call (assert NOT called with any other event name, e.g."logged_in"); (c)stop()callssession.destroy(). Runpnpm test common/composables/__tests__/useCentralizedSession.spec.ts→ red. - Scaffold — Create
common/composables/useCentralizedSession.tsexportinguseCentralizedSession()returning{ start, stop }. ImportSessionfrommekari-account-web-sdk. - Wire state — Destructure
{ user, isAuthenticated }viastoreToRefs(useAuthStore())(~/common/store/AuthStore). - Implement behavior — In
start(ssoId): guard against an existing instance (the SDK itself is also a singleton, so this is belt-and-braces), constructnew Session({ currentUser: ssoId, interval: 5 * 60 * 1000 })(PRD constraint 6.9), and callsession.on("event", handleEvent)wherehandleEventis a no-op placeholder wired to the real switch in Task 3.stop()callssession.destroy(). - Go green — Re-run the spec until green.
- Quality gate —
pnpm type-check && pnpm lint.
Acceptance criteria
- SDK constructed with
currentUser === user.sso_idand anintervaloption (≥1000ms). - Subscription is exactly one
session.on("event", handler)call — no per-status subscriptions. -
stop()/teardown callssession.destroy()(not justoff()). - No
msliorevent.originlogic exists in this composable (verified by its absence from the spec — those concerns are SDK-internal / an upstream gap, §2.2/§3.3).
Test strategy
vi.mock('mekari-account-web-sdk') to capture constructor args and subscription calls. Key assertions: constructor receives { currentUser: <sso_id>, interval: <ms> }; on is called exactly once with the literal string "event"; destroy() fires on teardown.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | — |
| QA | 0.5 |
| Total | 2.0 |
Assumptions: new composable, but reuses
useAuthStore/storeToRefs; SDK is mocked at module level in tests, matching the real, now-pinned API. Reduced from the original estimate: the origin-guard andmsliread/write work is removed entirely (unimplementable / SDK-internal, R7/R9), which was roughly half of the original scope.
Run to verify
pnpm test common/composables/__tests__/useCentralizedSession.spec.ts && pnpm type-check && pnpm lint
Depends on
- [Task 1] (reads
appConfig.centralized_session, though the gate itself is applied in the plugin).
Task 3: [FE] Event → action map for the 3 real SDK statuses
When the SSO session changes, hub-chat reacts correctly: a logout anywhere (including an account switch, which the SDK cannot distinguish from a logout) forces sign-out; a recovered session resumes; and a
server_downtakes no destructive action at all (fail-open, PRD constraint 6.10).
Status: ✅ Actionable — fully buildable and testable now with mocked navigation/stores. There is no switch_user status on the real SDK (Q1/R5 resolved: an account switch is delivered as logged_out, and the SDK never exposes the new ssoId) and no msli/_mekari_account grace branch to build (Q1/R7 resolved: msli is owned entirely by the SDK; hub-chat must not read or write it). server_down is a simple fail-open — no branching logic at all.
Design reference: n/a — no UI surface. There is no "user has changed" toast; account switching is not distinguishable from a logout and follows the ordinary forced sign-out path.
What to build
Extend useCentralizedSession.ts's session.on("event", (data) => { switch (data.status) { ... } }) handler with the real action map: logged_in → refetch org + company; logged_out → navigateTo('/logout') (this also covers account switches); server_down → fail-open (log/alert only, no navigation, no store mutation).
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | common/composables/useCentralizedSession.ts | Implement the data.status switch inside the existing "event" handler; org/company refetch calls on logged_in |
| extend | common/composables/__tests__/useCentralizedSession.spec.ts | One case per status with navigateTo and the stores mocked |
Implementation steps
- Explore — Read
pages/logout.vue(forced sign-out target — it clears auth and redirects to${SSO.url}/sign_out, verified ~line 183+),common/store/CompanyStore.ts:33(companiesendpoint${IAGServiceUrl}/launchpad/v1/companies— current-company refetch), andcommon/store/OrganizationStore.ts:176(getDetail). - Write failing tests (red) — Add to the spec: mock
navigateTo(Nuxt auto-import),useOrganizationStore().getDetail, anduseCompanyStore().getCompanyDetail. Assert:data.status === "logged_out"→navigateTo('/logout')called;data.status === "server_down"→navigateTois NOT called and no store action fires (fail-open);data.status === "logged_in"→ both refetches called. Run the spec → red. - Implement behavior — Add the switch inside the existing handler.
logged_in:await getDetail()+await getCompanyDetail().logged_out:navigateTo('/logout').server_down: no-op beyond an optional log/RUM call (Task 5) — explicitly take no destructive action (PRD 6.10). - Go green — Re-run until green.
- Quality gate —
pnpm type-check && pnpm lint.
Acceptance criteria
-
logged_out→navigateTo('/logout')(covers plain logout AND account switch — the SDK cannot distinguish them). -
server_down→ fail-open: nonavigateTo, no store mutation, no destructive action (PRD 6.10). -
logged_in→OrganizationStore.getDetail()+CompanyStore.getCompanyDetail()both called. - No
msliread/write and noevent.origincheck exist anywhere in this task's code (verified by absence from the spec).
Test strategy
Vitest with navigateTo and the org/company store actions mocked. Each it() dispatches one data.status through the handler and asserts the single mapped spy fired (or, for server_down, that nothing fired).
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1.0 |
| Backend | — |
| QA | 0.5 |
| Total | 1.5 |
Assumptions: reuses
pages/logout.vuerather than re-implementing cookie clearing; org/company refetch is the chosen current-company sync (the BEcurrent_companypiece is Task 7, decoupled from the SDK per R10). Reduced from the original estimate: theswitch_userreset/re-auth/toast path and themsli-grace branch forserver_downare removed entirely (R5/R7) — those were roughly half of the original scope and two of the four original branches.
Run to verify
pnpm test common/composables/__tests__/useCentralizedSession.spec.ts && pnpm type-check && pnpm lint
Depends on
- [Task 2] (extends the same composable + spec).
Task 4: [FE] mekariSession.client.ts — gated, client-only plugin wiring
The centralized-session SDK actually boots in a running hub-chat session — but only when the
centralized_sessionflag is on, the user is authenticated, and ansso_idis present; otherwise it never starts.
Status: ✅ Actionable — the gating logic and the watch wiring are fully buildable and testable now (the composable's start is mocked in the plugin spec); the SDK dependency itself is resolved (Q2), so this wiring is also the real, final integration, not a temporary mock-only stand-in.
Design reference: n/a — boot plugin, no UI.
What to build
A new client-only Nuxt plugin that watches [appConfig.centralized_session, user.sso_id, isAuthenticated] and calls useCentralizedSession().start(ssoId) exactly once when all three conditions hold; never starts otherwise.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | plugins/mekariSession.client.ts | defineNuxtPlugin: watch the gate triple, call start(ssoId) when enabled && authed && ssoId |
| create | plugins/__tests__/mekariSession.client.spec.ts | New __tests__/ dir under plugins/; assert start is/ isn't called per gate state |
plugins/__tests__/does not exist yet — creating it follows the repo's co-located__tests__/Name.spec.tsconvention seen incommon/composables/__tests__/.[verify the vitest include glob picks up plugins/__tests__/ — composables/store __tests__/ are picked up; plugin tests are new here]
Implementation steps
- Explore — Open
plugins/datadog.client.tsfor the*.client.tsplugin precedent (client-only by filename) andapp.vue:203for the boot-wiring pattern. Confirm thedefineNuxtPlugin+useRuntimeConfig/store-access idiom. - Write failing tests (red) — Create
plugins/__tests__/mekariSession.client.spec.ts. MockuseCentralizedSessionsostartis a spy. Drive the plugin with: (a) flag on + authed + sso_id →startcalled once with the sso_id; (b) flag off → never called; (c) authed but empty sso_id → never called; (d) not authenticated → never called. Run → red. - Scaffold — Create
plugins/mekariSession.client.tswithdefineNuxtPlugin(() => { ... }). - Wire state —
const appConfig = useAppConfigStore();const { user, isAuthenticated } = storeToRefs(useAuthStore());const { start } = useCentralizedSession();. - Implement behavior —
watch([() => appConfig.appConfig?.centralized_session, () => user.value.sso_id, isAuthenticated], ([enabled, ssoId, authed]) => { if (enabled && authed && ssoId) start(ssoId); }, { immediate: true });with a started-once guard. - Go green — Re-run until green.
- Quality gate —
pnpm type-check && pnpm lint.
Acceptance criteria
- SDK started only when
centralized_session === trueANDisAuthenticatedANDsso_idnon-empty. - Never started when the flag is off, the user is unauthenticated, or
sso_idis empty. - Plugin is client-only (filename
.client.ts) — does not run during SSR/generate. - No double-start across re-renders.
Test strategy
Vitest with useCentralizedSession mocked (spy on start) and the auth/appconfig stores stubbed via reactive refs. Four cases drive the gate matrix; the assertion is the call-count and argument of start.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1.0 |
| Backend | — |
| QA | 0.5 |
| Total | 1.5 |
Assumptions: mirrors the
plugins/datadog.client.tsplugin shape; the composable is mocked in the unit spec (gating logic only), but the real SDK boot is unblocked (Q2 resolved) and validated in the post-deploy verification recipe (§4.D).
Run to verify
pnpm test plugins/__tests__/mekariSession.client.spec.ts && pnpm type-check && pnpm lint
Depends on
- [Task 1] (the flag), [Task 2] (the
startentrypoint).
Task 5: [FE] Observability — RUM events for SDK init + each session event
Operators can see in Datadog RUM whether centralized session is live (init), what session statuses fire (
logged_in/logged_out/server_downrates), and when a forced logout originated from session reconciliation — with no raw token orsso_idin the payload.
Status: ✅ Actionable (event names are proposed pending Q7 [nice-to-have] naming convention — use the proposed centralized_session.* names and adjust if the team rules otherwise).
Design reference: n/a — telemetry, no UI.
What to build
Add Datadog RUM custom-action calls inside useCentralizedSession.ts: centralized_session.init on SDK construction, centralized_session.event with { event_type } per status, and centralized_session.forced_logout on a session-driven sign-out. Payloads carry event_type + page URL only — never the sso_id.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | common/composables/useCentralizedSession.ts | Add datadogRum.addAction('centralized_session.<x>', { event_type }) calls at init / per event / on forced logout |
| extend | common/composables/__tests__/useCentralizedSession.spec.ts | Assert RUM called with the right action name + that payload omits any token/sso_id |
@datadog/browser-rum@^5.23.0is already a dependency (verified inpackage.json);datadogRumis imported inapp.vue:19andplugins/datadog.client.ts:1. No existing customaddActioncall exists to copy verbatim — confirms RFC Q7.[verify datadogRum.addAction is the correct RUM API in v5 — repo currently only uses datadogRum.setUser/init]
Implementation steps
- Explore — Open
plugins/datadog.client.ts(RUM init) andapp.vue:153(datadogRum.setUser) to confirm the import and that RUM is initialized before the session plugin runs. - Write failing tests (red) — In the composable spec,
vi.mock('@datadog/browser-rum', () => ({ datadogRum: { addAction: vi.fn() } })). AssertaddActionis called with'centralized_session.init'onstart,'centralized_session.event'+{ event_type }per status, and that no call payload contains the sso_id string. Run → red. - Implement behavior — Add the
addActioncalls at the three points; build the payload fromevent_typeandwindow.location.pathnameonly. - Go green — Re-run until green.
- Quality gate —
pnpm type-check && pnpm lint(note:console.logis a prod lint error per the RFC — use RUM only).
Acceptance criteria
-
centralized_session.initRUM action emitted on SDK construction. -
centralized_session.eventemitted per event with{ event_type }. -
centralized_session.forced_logoutemitted on a session-driven/logout. - No payload contains
sso_idor raw token (OWASP A09).
Test strategy
Mock @datadog/browser-rum. Assert action names and that a stringified payload never includes the test sso_id value. One negative assertion guards the no-token rule.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | 0.5 |
| Total | 1.0 |
Assumptions: RUM already initialized (
plugins/datadog.client.ts); onlyaddActioncalls are added. Final action names pending Q7 but do not block.
Run to verify
pnpm test common/composables/__tests__/useCentralizedSession.spec.ts && pnpm type-check && pnpm lint
Depends on
- [Task 2], [Task 3] (instruments their code paths).
Task 6: [FE] Docs — auth-sso / login flow spokes
A future maintainer reading the architecture docs sees the centralized-session SDK reconciliation flow documented alongside the existing login/auth-sso flows.
Status: ✅ Actionable.
Design reference: n/a — documentation.
What to build
Add the SDK session-reconciliation flow (init gate, the 3 real statuses — logged_in/logged_out/server_down — and the fact that an account switch surfaces as logged_out) to the login and/or auth-sso architecture spokes, with a diagram, and set their status: ready.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | docs/architecture/flows/login/README.md | Add the SDK session-reconciliation section + sequence diagram; bump status |
| extend | docs/architecture/flows/cross-cutting/auth-sso/README.md | Same, from the auth/SSO angle |
[unverified — check repo]: did not open these two README files in reconnaissance; thedocs/dir exists at repo root. Confirm both paths and their currentstatusfrontmatter before editing.
Implementation steps
- Explore — Open both READMEs; match their existing heading depth, frontmatter, and mermaid style.
- Write — Add a "Centralized Web Session (SDK reconciliation)" section to each, reusing the §2.3 sequence diagrams from the RFC.
- Quality gate —
pnpm lint(prettier check covers markdown).
Acceptance criteria
- Login and auth-sso spokes describe the SDK reconciliation flow with a diagram.
- Spoke
statusset toready(or the repo's equivalent).
Test strategy
No unit tests — docs only; the prettier lint:prettier check must pass.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | — |
| Total | 0.5 |
Assumptions: docs-only, no behavior; QA = 0 (no user-facing behavior).
Run to verify
pnpm lint
Depends on
- [Task 3] (document the finalized event mappings).
Task 7: [BE] Current-company sync — SSO current_company (cross-product)
After a
logged_in(including the case where it follows an account switch — the SDK never distinguishes a switch from a freshlogged_out→re-login cycle), the user lands on the correct current company — the one selected in SSO — rather than a stale company from the previous product session. This is a BE/SSO dependency, decoupled from the SDK itself: the SDK exposes no company data (R10).
Status: 🚫 Blocked — Q4 [important]. The FE half (re-fetch OrganizationStore.getDetail() + CompanyStore.getCompanyDetail()) is already done inside Task 3. The blocking question is the BE ownership: does hub-core set current company server-side after re-auth (so the FE refetch suffices, and this BE task is not needed), or must something call SSO api.mekari.com/v1.1/users/{sso_id}/current_company? No current_company endpoint or client exists in this repo (grep confirmed). Unblock condition: A&L + hub-core confirm where current-company is resolved; if the FE refetch is sufficient, this task is dropped entirely (effort → 0).
Design reference: n/a — BE.
What to build
(Only if Q4 resolves to "hub-core/SSO must expose or set current_company".) The server-side work — owned by Account & Launchpad / hub-core, NOT this FE repo — to ensure the current company is set server-side after SSO re-auth so hub-chat's existing org/company refetch resolves the right company.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| — | [hub-core / Account & Launchpad service — repo not in scope here] [unverified — out of this repo] | Set/expose current_company after re-auth |
This is explicitly not hub-chat-v2 FE code (§2.6-A7 decision (b)). No file in this repo changes for the BE portion — the FE portion is fully covered by Task 3.
Implementation steps
- Resolve Q4 with hub-core + A&L: confirm whether current-company is server-resolved after
/sso-callback. - If server-resolved already → close this task as covered by Task 3's refetch (no BE work).
- If not → A&L/hub-core implement the current_company set/expose in their service (outside this repo).
Acceptance criteria
- Org/company stores re-resolve to the SSO-selected company after
logged_in. - (pending Q4) ownership of current-company resolution confirmed (FE refetch sufficient vs BE work needed).
Test strategy
Owned by the BE service team if work is needed; the FE assertion is already in Task 3 (refetch is called).
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | 2.0 |
| QA | 0.5 |
| Total | 3.0 |
Assumptions: estimate assumes Q4 lands on "BE work needed" (endpoint or post-callback set in hub-core). If the FE refetch is sufficient, this whole task drops to 0 — FE 0.5 already lives in Task 3. The 2.0 BE is a rough placeholder for an SSO/hub-core endpoint with auth + cross-product current-company logic; not estimable precisely without the contract.
Run to verify
# BE: owned by hub-core/A&L. FE side already verified in Task 3's spec.
Depends on
- [External: Q4
[important]— current-company ownership]; [Task 3] for the FE refetch already in place.
Task 8: [FE/Infra] CSP frame-src for the mekari iframe at nginx ingress
The browser permits the invisible SDK iframe to
sm.mekari.comto load onchat.qontak.com(CSPframe-src/child-src, plusframe-ancestorson the SM side), and infosec-required headers are present in production.
Status: ⚠️ Partially blocked — the CSP/nginx change is buildable now, but ships only after infosec sign-off (Q8 [important], mandatory infosec approver still TBD) and after A&L adds chat.qontak.com to the SDK-side frame-ancestors whitelist (their config repo). This repo's SPA is static (no runtime server → CSP lives at nginx ingress, per memory sso-cross-domain-cookies).
Design reference: n/a — deploy/infra.
What to build
An nginx ingress CSP rule allowing the sm.mekari.com iframe origin via frame-src/child-src (NOT script-src — the SDK is bundled, not script-loaded), deployed with chat.qontak.com, verified by curl -I showing the content-security-policy header.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | deploy/ (nginx ingress config) [unverified — check repo: deploy/ and deploy-alicloud/ both exist; confirm which holds the prod nginx CSP] | Add sm.mekari.com to frame-src/child-src |
The repo has both
deploy/anddeploy-alicloud/dirs at root — did not open them in reconnaissance.[unverified — locate the actual nginx/CSP manifest before editing]
Implementation steps
- Explore — Locate the prod nginx/ingress config under
deploy/(anddeploy-alicloud/); find any existingcontent-security-policyheader. - Implement — Add/extend
frame-src/child-srcto include thesm.mekari.comorigin (notscript-src— nothing is script-loaded from there). - Verify — After deploy to a test env:
curl -I https://<env>/ | grep -i content-security-policyshowssm.mekari.comallowed.
Acceptance criteria
- CSP header present on
chat.qontak.comresponses, allowing thesm.mekari.comiframe origin viaframe-src/child-src(§4.D step 3). - (pending Q8) infosec approver signs off on the CSP / iframe / postMessage origin posture.
- (pending A&L)
chat.qontak.comadded to the SDK-sideframe-ancestorswhitelist.
Test strategy
No unit test (infra). Verified by curl -I against a deployed env per §4.D.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | 0.5 |
| Total | 1.0 |
Assumptions: a single nginx CSP directive edit; the FE 0.5 covers locating + editing the deploy manifest. Blocked on infosec (Q8) and A&L's whitelist, not on code.
Run to verify
curl -sI https://<env>/ | grep -i content-security-policy
Depends on
- [External: Q8
[important]infosec sign-off]; [External: A&Lframe-ancestorswhitelist forchat.qontak.com].
Ordering rationale
- Task 1 (flag) is the keystone — do it first. It is a half-day type change that gates everything else and lets the whole feature ship dark; nothing should start before the flag exists.
- Critical path is 1 → 2 → 3 → 4 (flag → composable scaffold with the real
currentUser/intervalSDK options → event mapping (3 real statuses) → gated plugin). This is exactly the RFC's §4.C chunk order and, since the real SDK contract is now confirmed (Q2 resolved), it is a straight shot to end-to-end behavior — no separate "mock now, swap later" step needed. Tasks 5 (RUM) and 6 (docs) hang off it and can be done in parallel by a second hand once Task 3 lands. - The whole FE critical path is unblocked.
mekari-account-web-sdkv0.3.0 is a confirmed git dependency with a pinned API (Q2 resolved) — Task 2'svi.mocknow targets the real shape, and Task 4's live boot needs no further external confirmation. - Push externally on Q4 and Q5 in parallel with FE dev. Q4
[important]decides whether Task 7 (BE current-company) exists at all — if hub-core already resolves current-company server-side, Task 7 drops to zero and the FE refetch in Task 3 is the complete answer. Q5 gates only the super_admin scope of Task 4's gating logic. (Q1 — cross-domain_mekari_account— is resolved/moot: the SDK ownsmsli/session evaluation entirely inside the iframe, so there is no consumer-side grace branch to build in Task 3.) - Task 8 (CSP) is deploy-time and gated by infosec (Q8) + A&L's whitelist — start drafting the nginx rule (targeting
sm.mekari.com, notaccount.mekari.com) early so it is ready, but it cannot merge-to-prod-enabled until those externals clear.
Skipped stories
(Full-scope mode: every 🚫 Blocked or externally-gated item, with its unblock condition. Partially-blocked tasks remain in the main list above with their actionable portion.)
| Story / Task | Status | Unblock condition |
|---|---|---|
Task 7 — Current-company sync (BE current_company) | 🚫 Blocked | Q4 [important] — A&L/hub-core confirm whether current-company is server-resolved after re-auth (FE refetch in Task 3 may already suffice → task drops to 0) or a BE endpoint is needed. No current_company endpoint exists in this repo. Decoupled from the SDK (R10) — the SDK exposes no company data. |
| Task 8 — CSP at nginx ingress | ⚠️ Partial (in list) | Q8 [important] infosec approver sign-off + A&L adds chat.qontak.com to the SDK frame-ancestors whitelist. Host is sm.mekari.com, not account.mekari.com. |
| Super_admin scope (Task 4 gate) | open question | Q5 [important] — confirm whether centralized session applies to super_admin sessions (today they bypass billing/MQTT, middleware/sso-callback.ts:342). |
event.origin upstream infosec finding | open question, tracked not built | Q6 [important] — the real SDK checks only event.data.source, never validates event.origin, and owns its own message listener; hub-chat cannot implement an origin guard. This is a finding for Account & Launchpad, not a hub-chat task — track under Task 8's infosec sign-off. |
| RUM action naming | open question | Q7 [nice-to-have] — confirm the <domain>.<action> Datadog convention (Task 5 uses proposed names). |
Resolved (no longer skipped/gated): Q1 (cross-domain _mekari_account fallback — moot, SDK-internal), Q2 (SDK availability — mekari-account-web-sdk v0.3.0 confirmed), Q3 (session.refresh() throttle — moot, use the constructor's interval option). The former switch_user handling and "toast UX copy" (Q6, old numbering) rows are removed entirely: there is no switch_user status on the real SDK and no dedicated toast surface to copy for.
Note on §2.4 "Wire product logout to SSO sign_out": not a task — already implemented (
pages/logout.vue,TheSwitchAccount.vue:299), both verified present. The execution plan must keep them green, not rewrite them.