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, currentUser/interval)1.50.52.0
Task 3 — Event → action map (3 real statuses) inside the composable1.00.51.5
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, shared Chat backend) [blocked]0.52.00.53.0
Task 8 — CSP frame-src at nginx ingress (deploy) [partially blocked]0.50.51.0
Grand total6.02.03.511.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 in hub-fe.task-breakdown.md, which sets BE 0) because Hub Chat v2 and Hub (Chat v1) are two frontends of the same hub-core/hub-service backend — 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 one organization_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-sdk v0.3.0 (latest review): Q2 (SDK availability) and Q1 (cross-domain _mekari_account fallback) 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 owns msli/session evaluation entirely inside the iframe, so hub-chat never reads _mekari_account or msli. This removed the origin-guard and msli-grace work from Task 2/3 and the switch_user handling entirely (there is no such event — an account switch is delivered as logged_out), which is why Tasks 2–3 shrank from the original estimate. The remaining mover: Q4 [important] — Task 7's BE current_company ownership is unresolved, which is why it is the one fully blocked task; Q3 (refresh throttle) is moot (use the constructor's interval option).


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/AppConfigStore.spec.tsAssert 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

  1. Explore — Open common/store/AppConfigStore.ts and read the AppConfig interface (line 2) and getAppConfig() fetch (:81, endpoint /api/core/v1/client_configs/config). Note how seamless_auth_first?: boolean is declared — mirror that exact style.
  2. Write failing test (red) — In common/store/AppConfigStore.spec.ts, add a case that mocks /api/core/v1/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/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/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_id and a periodic interval, and subscribes to the SDK's single "event" — without yet acting on specific statuses (that is Task 3). There is no msli or event.origin handling to build in hub-chat: the SDK owns msli internally, and event.origin validation is not implementable by a consumer (the SDK owns the message listener and checks only event.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

ActionFileWhat changes
createcommon/composables/useCentralizedSession.tsNew composable: start(ssoId), SDK construction (currentUser, interval), single "event" subscription, stop()session.destroy()
createcommon/composables/__tests__/useCentralizedSession.spec.tsvi.mock('mekari-account-web-sdk'); assert construction args, single-subscription shape, teardown

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 (kept available for Task 3, though no toast is emitted today).
  2. 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') calls Session with { currentUser: 'abc', interval: <ms> }; (b) subscription happens via exactly one on("event", fn) call (assert NOT called with any other event name, e.g. "logged_in"); (c) stop() calls session.destroy(). Run pnpm test common/composables/__tests__/useCentralizedSession.spec.ts → red.
  3. Scaffold — Create common/composables/useCentralizedSession.ts exporting useCentralizedSession() returning { start, stop }. Import Session from mekari-account-web-sdk.
  4. Wire state — Destructure { user, isAuthenticated } via storeToRefs(useAuthStore()) (~/common/store/AuthStore).
  5. Implement behavior — In start(ssoId): guard against an existing instance (the SDK itself is also a singleton, so this is belt-and-braces), construct new Session({ currentUser: ssoId, interval: 5 * 60 * 1000 }) (PRD constraint 6.9), and call session.on("event", handleEvent) where handleEvent is a no-op placeholder wired to the real switch in Task 3. stop() calls session.destroy().
  6. Go green — Re-run the spec until green.
  7. Quality gatepnpm type-check && pnpm lint.

Acceptance criteria

  • SDK constructed with currentUser === user.sso_id and an interval option (≥1000ms).
  • Subscription is exactly one session.on("event", handler) call — no per-status subscriptions.
  • stop()/teardown calls session.destroy() (not just off()).
  • No msli or event.origin logic 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

DisciplineDays
Frontend1.5
Backend
QA0.5
Total2.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 and msli read/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_down takes 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_outnavigateTo('/logout') (this also covers account switches); server_down → fail-open (log/alert only, no navigation, no store mutation).

Implementation Plan

ActionFileWhat changes
extendcommon/composables/useCentralizedSession.tsImplement the data.status switch inside the existing "event" handler; org/company refetch calls on logged_in
extendcommon/composables/__tests__/useCentralizedSession.spec.tsOne case per status with navigateTo 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+), common/store/CompanyStore.ts:33 (companies endpoint ${IAGServiceUrl}/launchpad/v1/companies — current-company refetch), and common/store/OrganizationStore.ts:176 (getDetail).
  2. Write failing tests (red) — Add to the spec: mock navigateTo (Nuxt auto-import), useOrganizationStore().getDetail, and useCompanyStore().getCompanyDetail. Assert: data.status === "logged_out"navigateTo('/logout') called; data.status === "server_down"navigateTo is NOT called and no store action fires (fail-open); data.status === "logged_in" → both refetches called. Run the spec → red.
  3. 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).
  4. Go green — Re-run until green.
  5. Quality gatepnpm type-check && pnpm lint.

Acceptance criteria

  • logged_outnavigateTo('/logout') (covers plain logout AND account switch — the SDK cannot distinguish them).
  • server_down → fail-open: no navigateTo, no store mutation, no destructive action (PRD 6.10).
  • logged_inOrganizationStore.getDetail() + CompanyStore.getCompanyDetail() both called.
  • No msli read/write and no event.origin check 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

DisciplineDays
Frontend1.0
Backend
QA0.5
Total1.5

Assumptions: reuses pages/logout.vue rather than re-implementing cookie clearing; org/company refetch is the chosen current-company sync (the BE current_company piece is Task 7, decoupled from the SDK per R10). Reduced from the original estimate: the switch_user reset/re-auth/toast path and the msli-grace branch for server_down are 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_session flag is on, the user is authenticated, and an sso_id is 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

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 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 start entrypoint).

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_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 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

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:153 (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 status, 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 or 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

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 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

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 (including the case where it follows an account switch — the SDK never distinguishes a switch from a fresh logged_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

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.
  • (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 sm.mekari.com to load on chat.qontak.com (CSP frame-src/child-src, plus frame-ancestors on 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

ActionFileWhat changes
extenddeploy/ (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/ 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/child-src to include the sm.mekari.com origin (not script-src — nothing is script-loaded from there).
  3. Verify — After deploy to a test env: curl -I https://<env>/ | grep -i content-security-policy shows sm.mekari.com allowed.

Acceptance criteria

  • CSP header present on chat.qontak.com responses, allowing the sm.mekari.com iframe origin via frame-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.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 with the real currentUser/interval SDK 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-sdk v0.3.0 is a confirmed git dependency with a pinned API (Q2 resolved) — Task 2's vi.mock now 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 owns msli/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, not account.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 / 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. 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 questionQ5 [important] — confirm whether centralized session applies to super_admin sessions (today they bypass billing/MQTT, middleware/sso-callback.ts:342).
event.origin upstream infosec findingopen question, tracked not builtQ6 [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 namingopen questionQ7 [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.