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 + msli + origin guard) | 2.0 | — | 0.5 | 2.5 |
| Task 3 — Event → action map (4 events) inside the composable | 2.0 | — | 1.0 | 3.0 |
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) [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 | 7.5 | 2.0 | 4.0 | 13.5 |
Confidence: medium. The flag-gating, composable scaffold, origin-guard and event-mapping work is well-specified and anchored to real, verified files (Tasks 1–6 are actionable now using a mocked SDK). The biggest movers: (1) Q2
[critical]— the@mekari/sdkSessionpackage is not in the repo and its name/registry/version/module-shape are unconfirmed, so the real wiring in Task 4 and the live origin/event behavior cannot be finalized until it lands (everything is mockable in the meantime, so dev is not blocked, only final integration); (2) Q1[critical]— cross-domain_mekari_accountfallback semantics could collapse part of theserver_down/mslilogic in Task 3 if hub-chat is expected to read the mekari cookie (structurally impossible from the qontak origin); (3) Q4[important]— Task 7's BEcurrent_companyownership is unresolved, which is why it is the one fully blocked task.
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/__tests__/AppConfigStore.spec.ts | Assert the field is readable from a fetched config and that undefined reads as falsy/off |
Note: the RFC's §4.C cites
common/store/__tests__/AppConfigStore.spec.ts. The repo also has acommon/store/AppConfigStore.spec.ts(co-located, not under__tests__/). Both layouts exist incommon/store/; follow the__tests__/location for the new assertions to match the RFC, but confirm which file is actually run — grep showed both present.[verify which spec the suite picks up — both AppConfigStore.spec.ts and __tests__/ variant exist]
Implementation steps
- Explore — Open
common/store/AppConfigStore.tsand read theAppConfiginterface (line 2) andgetAppConfig()fetch (~line 76). Note howseamless_auth_first?: booleanis declared — mirror that exact style. - Write failing test (red) — In
common/store/__tests__/AppConfigStore.spec.ts, add a case that mocks/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/__tests__/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/__tests__/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, origin guard, msli
When the toggle is on and the user is authenticated, hub-chat starts the centralized-session SDK with the user's
sso_id, ignores any postMessage from an untrusted origin, and stamps a localmsliheartbeat — without yet acting on specific events (that is Task 3).
Status: ⚠️ Partially blocked — the real @mekari/sdk Session package is not in the repo (Q2 [critical]: name/registry/version/CJS-or-ESM entry unconfirmed). Actionable now: build the composable against a vi.mock'd Session, with the origin guard, msli write, and a start(ssoId) entrypoint. The real import is dropped in once Q2 resolves (note in Task 4).
Design reference: n/a — no UI surface (the toast emission lives in Task 3).
What to build
A new composable useCentralizedSession.ts that owns the SDK instance lifecycle: a start(ssoId) that constructs new Session({ current_user: ssoId }), a postMessage handler that verifies event.origin === <env-resolved account.mekari.com> before trusting any message, and a msli localStorage write (msli = Date.now()) on logged_in. Guards against double-init.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | common/composables/useCentralizedSession.ts | New composable: start(ssoId), SDK construction, origin-verified message handler, msli read/write helpers, double-init guard |
| create | common/composables/__tests__/useCentralizedSession.spec.ts | vi.mock('@mekari/sdk'); assert construction args, origin rejection, msli write |
SDK import path is a stub until Q2 resolves —
import { Session } from "@mekari/sdk"[unverified — package not in repo; grep '@mekari/sdk' empty]. Everything else verified.
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 you'll use in Task 3. - Write failing tests (red) — Create
common/composables/__tests__/useCentralizedSession.spec.ts.vi.mock('@mekari/sdk', () => ({ Session: vi.fn() })). Assert: (a)start('abc')callsSessionwith{ current_user: 'abc' }; (b) amessageevent with a mismatchedorigindoes NOT invoke any handler; (c) alogged_inmessage writes a numericmslito localStorage. Runpnpm test common/composables/__tests__/useCentralizedSession.spec.ts→ red. - Scaffold — Create
common/composables/useCentralizedSession.tsexportinguseCentralizedSession()returning{ start }. ImportSessionfrom@mekari/sdk(stub for now). - Wire state — Destructure
{ user, isAuthenticated }viastoreToRefs(useAuthStore())(~/common/store/AuthStore). AddreadMsli()/writeMsli()helpers overlocalStorage. - Implement behavior — In
start(ssoId): guard against an existing instance, construct the SDK, attach amessage/SDK-event listener that first checksevent.origin === useRuntimeConfig().public.<accountOrigin>(env-resolved) and returns early on mismatch; onlogged_inwritemsli. - Go green — Re-run the spec until green.
- Quality gate —
pnpm type-check && pnpm lint.
Acceptance criteria
- SDK constructed with
current_user === user.sso_id. - A postMessage with a mismatched origin fires no action (origin guard, §3.3 / OWASP A07).
-
msli(localStorage timestamp on the qontak origin) is written onlogged_in. - Double
start()does not construct two SDK instances.
Test strategy
vi.mock('@mekari/sdk') to capture constructor args. Key assertions: constructor receives { current_user: <sso_id> }; a forged message event with a foreign origin leaves all spies un-called; localStorage.getItem('msli') is a fresh epoch ms after a logged_in.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 2.0 |
| Backend | — |
| QA | 0.5 |
| Total | 2.5 |
Assumptions: new composable, but reuses
useAuthStore/storeToRefsand the env-resolved account origin fromuseRuntimeConfig; SDK is mocked at module level, so dev proceeds without the real package; throttledsession.refresh()cadence is Q3[important](TBD) — implement a configurable interval and leave the value as a constant to tune.
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). - [External:
@mekari/sdkreal package — Q2[critical](pending)] — mocked until then.
Task 3: [FE] Event → action map for the 4 SDK events
When the SSO session changes, hub-chat reacts correctly: a logout anywhere forces sign-out, an account switch re-authenticates and toasts "your account has changed", a recovered session resumes, and a
server_downeither soft-grants a grace window (freshmsli) or forces sign-out.
Status: ⚠️ Partially blocked — the logged_in/logged_out/switch_user mappings are fully buildable and testable now with mocked navigation/stores. The server_down + msli grace branch is gated by Q1 [critical]: if hub-chat is (incorrectly) expected to read the cross-domain _mekari_account cookie, that branch is structurally impossible from the qontak origin and must collapse to "the _mekari_account check lives in the SDK/iframe; hub-chat only consumes events + its own msli". Build the msli-only grace now; confirm Q1 before relying on it.
Design reference: n/a — the "user has changed" toast reuses the existing Pixel toast via the notification:show bus event (verified at plugins/eventBus.ts:7); copy is UX-pending (Q6 [nice-to-have]).
What to build
Extend useCentralizedSession.ts with a switch mapping each SDK event to its action: logged_in → refetch org + company; logged_out → navigateTo('/logout'); server_down → /logout unless msli is fresh (< 2h) and grace is enabled; switch_user → resetAuthStore() → redirect to SSO auth?return_to=/sso-callback, then on return emit the "your account has changed" toast.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | common/composables/useCentralizedSession.ts | Add the event→action switch; emit notification:show for the toast; org/company refetch calls |
| extend | common/composables/__tests__/useCentralizedSession.spec.ts | One case per event with navigateTo, resetAuthStore, 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+),middleware/sso-callback.ts(runSsoCallbackat :541,performSsoSignInat :271 — the re-auth path),common/store/CompanyStore.ts:28(getCompanyDetail),common/store/OrganizationStore.ts:173(getDetail), andplugins/eventBus.ts:7(thenotification:showpayload shape{ message, type }). - Write failing tests (red) — Add to the spec: mock
navigateTo(Nuxt auto-import),useAuthStore().resetAuthStore,useOrganizationStore().getDetail,useCompanyStore().getCompanyDetail, and the event bus. Assert each event triggers exactly its mapped action; assertserver_downwith a freshmslidoes NOT navigate to/logout(soft grace), and with a stale/absentmsliDOES. Run the spec → red. - Implement behavior — Add the switch.
logged_in:await getDetail()+await getCompanyDetail().logged_out:navigateTo('/logout').server_down: ifDate.now() - readMsli() < TWO_HOURS && graceEnabledthen schedule a re-check elsenavigateTo('/logout').switch_user:resetAuthStore()then redirect to the SSO auth URL withreturn_to=/sso-callback; on the post-callback path, refetch stores and emitnotification:showwith the toast copy. - Go green — Re-run until green.
- Quality gate —
pnpm type-check && pnpm lint.
Acceptance criteria
-
logged_out→navigateTo('/logout'). -
server_down→/logout, except whenmsliis fresh (< 2h) and grace is enabled → soft-allow + re-check. -
switch_user→resetAuthStore()+ SSO re-auth redirect (return_to=/sso-callback). -
logged_in→OrganizationStore.getDetail()+CompanyStore.getCompanyDetail()both called. - After a
switch_userround-trip, anotification:showtoast ("your account has changed") is emitted. - (pending Q1) the
server_down/msligrace assumes the_mekari_accountcheck lives inside the SDK/iframe, not in hub-chat.
Test strategy
Vitest with navigateTo, the auth/org/company store actions, and the event bus all mocked. Each it() dispatches one event through the (origin-verified) handler and asserts the single mapped spy fired. The server_down cases assert the grace branch by seeding localStorage.msli fresh vs stale.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 2.0 |
| Backend | — |
| QA | 1.0 |
| Total | 3.0 |
Assumptions: reuses
pages/logout.vueand the/sso-callbackmiddleware rather than re-implementing cookie clearing; toast reuses the existingnotification:showbus key; org/company refetch is the chosen current-company sync (the BEcurrent_companypiece is Task 7). QA is at the higher 1.0 end because four behavioral branches + the grace edge cases are user-impacting auth flows.
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).
- [External: Q1
[critical]] — only theserver_down/_mekari_accountsemantics; the other three events are unblocked.
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: ⚠️ Partially blocked — the gating logic and the watch wiring are fully buildable and testable now (the composable's start is mocked in the plugin spec). The plugin only does real work once the SDK package lands (Q2 [critical]); the wiring itself does not need the package.
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, so this is gating logic only. Real SDK boot is validated post-Q2 in the verification recipe (§4.D), not in this unit.
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). - [External:
@mekari/sdkreal package — Q2[critical]] for the live boot only; wiring is mockable now.
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 events fire (
logged_in/switch_user/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 event, and centralized_session.forced_logout on a session-driven sign-out. Payloads carry event_type + page URL only — never the sso_id or _mekari_account.
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:152(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 event, 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_id, raw token, or_mekari_account(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 4 events, the switch_user re-auth round-trip) 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/switch_user, the user lands on the correct current company — the one selected in SSO — rather than a stale company from the previous product session.
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/switch_user. - (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
account.mekari.comto load onchat.qontak.com(CSPframe-src/frame-ancestors), 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 mekari iframe origin in frame-src (and any matching directive), 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 the mekari iframe origin to frame-src/CSP |
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-srcto include the env-resolvedaccount.mekari.comorigin. - Verify — After deploy to a test env:
curl -I https://<env>/ | grep -i content-security-policyshows the mekari origin allowed.
Acceptance criteria
- CSP header present on
chat.qontak.comresponses, allowing the mekari iframe origin (§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 + origin guard/
msli→ event mapping → gated plugin). This is exactly the RFC's §4.C chunk order and is the minimum to demo end-to-end behavior against a mocked SDK. 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 buildable today against a
vi.mock'd@mekari/sdk. Q2[critical](real SDK package) blocks only the live integration/boot, not unit development — so push hard on A&L for the package name/registry/version, but do not wait on it to start. - Push externally on Q1 and Q4 in parallel with FE dev. Q1
[critical]only affects theserver_down/msligrace branch inside Task 3 (build themsli-only version now, confirm the SDK owns the_mekari_accountcheck). 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. - Task 8 (CSP) is deploy-time and gated by infosec (Q8) + A&L's whitelist — start drafting the nginx rule 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. |
Task 3 — server_down / msli grace branch | ⚠️ Partial (in list) | Q1 [critical] — confirm the _mekari_account check happens inside the SDK/iframe (mekari origin); hub-chat cannot read it cross-domain. The other three events are unblocked. |
| Task 2 / Task 4 — live SDK boot | ⚠️ Partial (in list) | Q2 [critical] — @mekari/sdk Session package name/registry/version/module entry confirmed and installable. Unit work proceeds mocked. |
| 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. |
Secondary tuning — session.refresh() throttle | open question | Q3 [important] — agreed refresh cadence with A&L (parent RFC marks it TBD). Implemented as a tunable constant in Task 2. |
| Toast UX copy | open question | Q6 [nice-to-have] — UX provides the "your account has changed" string (Task 3 emits the toast; copy is a string swap). |
| RUM action naming | open question | Q7 [nice-to-have] — confirm the <domain>.<action> Datadog convention (Task 5 uses proposed names). |
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.