Skip to main content

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/. The app/ dir holds only spa-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 taskFE daysBE daysQA daysTotal
Task 1 — centralized_session flag on AppConfig0.50.51.0
Task 2 — useCentralizedSession composable (SDK lifecycle + msli + origin guard)2.00.52.5
Task 3 — Event → action map (4 events) inside the composable2.01.03.0
Task 4 — mekariSession.client.ts gated plugin wiring1.00.51.5
Task 5 — RUM observability for SDK init + events0.50.51.0
Task 6 — Docs: auth-sso / login flow spokes0.50.5
Task 7 — Current-company sync (BE current_company) [blocked]0.52.00.53.0
Task 8 — CSP frame-src at nginx ingress (deploy) [partially blocked]0.50.51.0
Grand total7.52.04.013.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/sdk Session package 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_account fallback semantics could collapse part of the server_down/msli logic 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 BE current_company ownership 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_session flag 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

ActionFileWhat changes
extendcommon/store/AppConfigStore.tsAdd centralized_session?: boolean; to the AppConfig interface (line ~2, alongside seamless_auth_first?)
extendcommon/store/__tests__/AppConfigStore.spec.tsAssert 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 a common/store/AppConfigStore.spec.ts (co-located, not under __tests__/). Both layouts exist in common/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

  1. Explore — Open common/store/AppConfigStore.ts and read the AppConfig interface (line 2) and getAppConfig() fetch (~line 76). Note how seamless_auth_first?: boolean is declared — mirror that exact style.
  2. Write failing test (red) — In common/store/__tests__/AppConfigStore.spec.ts, add a case that mocks /client_configs/config returning { centralized_session: true } and asserts appConfig.value?.centralized_session === true; add a second case with the field absent asserting it is falsy. Run pnpm test common/store/__tests__/AppConfigStore.spec.ts and confirm red.
  3. Implement — Add centralized_session?: boolean; to the AppConfig interface.
  4. Go green — Re-run the spec until all pass.
  5. Quality gatepnpm type-check && pnpm lint.

Acceptance criteria

  • AppConfig exposes centralized_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-check passes.

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

DisciplineDays
Frontend0.5
Backend
QA0.5
Total1.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 local msli heartbeat — 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

ActionFileWhat changes
createcommon/composables/useCentralizedSession.tsNew composable: start(ssoId), SDK construction, origin-verified message handler, msli read/write helpers, double-init guard
createcommon/composables/__tests__/useCentralizedSession.spec.tsvi.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

  1. Explore — Open common/composables/useClient.ts and common/composables/useJimo.ts to see the house composable shape: store access via storeToRefs(useAuthStore()) and imports with the ~/common/store/... alias. Open common/composables/useEventBus.ts for the bus API you'll use in Task 3.
  2. Write failing tests (red) — Create common/composables/__tests__/useCentralizedSession.spec.ts. vi.mock('@mekari/sdk', () => ({ Session: vi.fn() })). Assert: (a) start('abc') calls Session with { current_user: 'abc' }; (b) a message event with a mismatched origin does NOT invoke any handler; (c) a logged_in message writes a numeric msli to localStorage. Run pnpm test common/composables/__tests__/useCentralizedSession.spec.ts → red.
  3. Scaffold — Create common/composables/useCentralizedSession.ts exporting useCentralizedSession() returning { start }. Import Session from @mekari/sdk (stub for now).
  4. Wire state — Destructure { user, isAuthenticated } via storeToRefs(useAuthStore()) (~/common/store/AuthStore). Add readMsli() / writeMsli() helpers over localStorage.
  5. Implement behavior — In start(ssoId): guard against an existing instance, construct the SDK, attach a message/SDK-event listener that first checks event.origin === useRuntimeConfig().public.<accountOrigin> (env-resolved) and returns early on mismatch; on logged_in write msli.
  6. Go green — Re-run the spec until green.
  7. Quality gatepnpm 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 on logged_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

DisciplineDays
Frontend2.0
Backend
QA0.5
Total2.5

Assumptions: new composable, but reuses useAuthStore/storeToRefs and the env-resolved account origin from useRuntimeConfig; SDK is mocked at module level, so dev proceeds without the real package; throttled session.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/sdk real 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_down either soft-grants a grace window (fresh msli) 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_outnavigateTo('/logout'); server_down/logout unless msli is fresh (< 2h) and grace is enabled; switch_userresetAuthStore() → redirect to SSO auth?return_to=/sso-callback, then on return emit the "your account has changed" toast.

Implementation Plan

ActionFileWhat changes
extendcommon/composables/useCentralizedSession.tsAdd the event→action switch; emit notification:show for the toast; org/company refetch calls
extendcommon/composables/__tests__/useCentralizedSession.spec.tsOne case per event with navigateTo, resetAuthStore, and the stores mocked

Implementation steps

  1. 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 (runSsoCallback at :541, performSsoSignIn at :271 — the re-auth path), common/store/CompanyStore.ts:28 (getCompanyDetail), common/store/OrganizationStore.ts:173 (getDetail), and plugins/eventBus.ts:7 (the notification:show payload shape { message, type }).
  2. 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; assert server_down with a fresh msli does NOT navigate to /logout (soft grace), and with a stale/absent msli DOES. Run the spec → red.
  3. Implement behavior — Add the switch. logged_in: await getDetail() + await getCompanyDetail(). logged_out: navigateTo('/logout'). server_down: if Date.now() - readMsli() < TWO_HOURS && graceEnabled then schedule a re-check else navigateTo('/logout'). switch_user: resetAuthStore() then redirect to the SSO auth URL with return_to=/sso-callback; on the post-callback path, refetch stores and emit notification:show with the toast copy.
  4. Go green — Re-run until green.
  5. Quality gatepnpm type-check && pnpm lint.

Acceptance criteria

  • logged_outnavigateTo('/logout').
  • server_down/logout, except when msli is fresh (< 2h) and grace is enabled → soft-allow + re-check.
  • switch_userresetAuthStore() + SSO re-auth redirect (return_to=/sso-callback).
  • logged_inOrganizationStore.getDetail() + CompanyStore.getCompanyDetail() both called.
  • After a switch_user round-trip, a notification:show toast ("your account has changed") is emitted.
  • (pending Q1) the server_down/msli grace assumes the _mekari_account check 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

DisciplineDays
Frontend2.0
Backend
QA1.0
Total3.0

Assumptions: reuses pages/logout.vue and the /sso-callback middleware rather than re-implementing cookie clearing; toast reuses the existing notification:show bus key; org/company refetch is the chosen current-company sync (the BE current_company piece 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 the server_down/_mekari_account semantics; 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_session flag is on, the user is authenticated, and an sso_id is 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

ActionFileWhat changes
createplugins/mekariSession.client.tsdefineNuxtPlugin: watch the gate triple, call start(ssoId) when enabled && authed && ssoId
createplugins/__tests__/mekariSession.client.spec.tsNew __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.ts convention seen in common/composables/__tests__/. [verify the vitest include glob picks up plugins/__tests__/ — composables/store __tests__/ are picked up; plugin tests are new here]

Implementation steps

  1. Explore — Open plugins/datadog.client.ts for the *.client.ts plugin precedent (client-only by filename) and app.vue:203 for the boot-wiring pattern. Confirm the defineNuxtPlugin + useRuntimeConfig/store-access idiom.
  2. Write failing tests (red) — Create plugins/__tests__/mekariSession.client.spec.ts. Mock useCentralizedSession so start is a spy. Drive the plugin with: (a) flag on + authed + sso_id → start called 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.
  3. Scaffold — Create plugins/mekariSession.client.ts with defineNuxtPlugin(() => { ... }).
  4. Wire stateconst appConfig = useAppConfigStore(); const { user, isAuthenticated } = storeToRefs(useAuthStore()); const { start } = useCentralizedSession();.
  5. Implement behaviorwatch([() => 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.
  6. Go green — Re-run until green.
  7. Quality gatepnpm type-check && pnpm lint.

Acceptance criteria

  • SDK started only when centralized_session === true AND isAuthenticated AND sso_id non-empty.
  • Never started when the flag is off, the user is unauthenticated, or sso_id is 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

DisciplineDays
Frontend1.0
Backend
QA0.5
Total1.5

Assumptions: mirrors the plugins/datadog.client.ts plugin 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 start entrypoint).
  • [External: @mekari/sdk real 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_down rates), and when a forced logout originated from session reconciliation — with no raw token or sso_id in 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

ActionFileWhat changes
extendcommon/composables/useCentralizedSession.tsAdd datadogRum.addAction('centralized_session.<x>', { event_type }) calls at init / per event / on forced logout
extendcommon/composables/__tests__/useCentralizedSession.spec.tsAssert RUM called with the right action name + that payload omits any token/sso_id

@datadog/browser-rum@^5.23.0 is already a dependency (verified in package.json); datadogRum is imported in app.vue:19 and plugins/datadog.client.ts:1. No existing custom addAction call 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

  1. Explore — Open plugins/datadog.client.ts (RUM init) and app.vue:152 (datadogRum.setUser) to confirm the import and that RUM is initialized before the session plugin runs.
  2. Write failing tests (red) — In the composable spec, vi.mock('@datadog/browser-rum', () => ({ datadogRum: { addAction: vi.fn() } })). Assert addAction is called with 'centralized_session.init' on start, 'centralized_session.event' + { event_type } per event, and that no call payload contains the sso_id string. Run → red.
  3. Implement behavior — Add the addAction calls at the three points; build the payload from event_type and window.location.pathname only.
  4. Go green — Re-run until green.
  5. Quality gatepnpm type-check && pnpm lint (note: console.log is a prod lint error per the RFC — use RUM only).

Acceptance criteria

  • centralized_session.init RUM action emitted on SDK construction.
  • centralized_session.event emitted per event with { event_type }.
  • centralized_session.forced_logout emitted 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

DisciplineDays
Frontend0.5
Backend
QA0.5
Total1.0

Assumptions: RUM already initialized (plugins/datadog.client.ts); only addAction calls 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

ActionFileWhat changes
extenddocs/architecture/flows/login/README.mdAdd the SDK session-reconciliation section + sequence diagram; bump status
extenddocs/architecture/flows/cross-cutting/auth-sso/README.mdSame, from the auth/SSO angle

[unverified — check repo]: did not open these two README files in reconnaissance; the docs/ dir exists at repo root. Confirm both paths and their current status frontmatter before editing.

Implementation steps

  1. Explore — Open both READMEs; match their existing heading depth, frontmatter, and mermaid style.
  2. Write — Add a "Centralized Web Session (SDK reconciliation)" section to each, reusing the §2.3 sequence diagrams from the RFC.
  3. Quality gatepnpm lint (prettier check covers markdown).

Acceptance criteria

  • Login and auth-sso spokes describe the SDK reconciliation flow with a diagram.
  • Spoke status set to ready (or the repo's equivalent).

Test strategy

No unit tests — docs only; the prettier lint:prettier check must pass.

Effort estimate

DisciplineDays
Frontend0.5
Backend
QA
Total0.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

ActionFileWhat 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

  1. Resolve Q4 with hub-core + A&L: confirm whether current-company is server-resolved after /sso-callback.
  2. If server-resolved already → close this task as covered by Task 3's refetch (no BE work).
  3. 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

DisciplineDays
Frontend0.5
Backend2.0
QA0.5
Total3.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.com to load on chat.qontak.com (CSP frame-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

ActionFileWhat changes
extenddeploy/ (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/ and deploy-alicloud/ dirs at root — did not open them in reconnaissance. [unverified — locate the actual nginx/CSP manifest before editing]

Implementation steps

  1. Explore — Locate the prod nginx/ingress config under deploy/ (and deploy-alicloud/); find any existing content-security-policy header.
  2. Implement — Add/extend frame-src to include the env-resolved account.mekari.com origin.
  3. Verify — After deploy to a test env: curl -I https://<env>/ | grep -i content-security-policy shows the mekari origin allowed.

Acceptance criteria

  • CSP header present on chat.qontak.com responses, 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.com added to the SDK-side frame-ancestors whitelist.

Test strategy

No unit test (infra). Verified by curl -I against a deployed env per §4.D.

Effort estimate

DisciplineDays
Frontend0.5
Backend
QA0.5
Total1.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&L frame-ancestors whitelist for chat.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 the server_down/msli grace branch inside Task 3 (build the msli-only version now, confirm the SDK owns the _mekari_account check). 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 / TaskStatusUnblock condition
Task 7 — Current-company sync (BE current_company)🚫 BlockedQ4 [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() throttleopen questionQ3 [important] — agreed refresh cadence with A&L (parent RFC marks it TBD). Implemented as a tunable constant in Task 2.
Toast UX copyopen questionQ6 [nice-to-have] — UX provides the "your account has changed" string (Task 3 emits the toast; copy is a string swap).
RUM action namingopen questionQ7 [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.