RFC: Centralized Web Session — Hub FE Integration with mekari-account-web-sdk
Document Conventions (do not remove)
Governance follows the Qontak RFC Template (metadata table + sections 1–6 + Comment logs). This RFC is agent-execution-ready: §1 references, §2 Repo Reading Guide, mermaid diagrams, and §4 Agent Execution Plan + Verification & Rollback Recipe must be complete before §7 flips to yes.
Scope of THIS RFC = Hub frontend integration only (the
local/hubrepo). The cross-product SDK (mekari-account-web-sdk), the Golang Session Manager service, the dedicated Redis, and the SSO/Kong changes are owned by Account & Launchpad and are treated here as upstream dependencies — not in scope to build.
Metadata
| Field | Value | Notes |
|---|---|---|
| Status | RFC | One of IDEA / RFC / ABANDON / AGREED |
| Type / Sub-type | frontend / enhancement | Hub consumes a new shared SDK |
| Title | Centralized Web Session — Hub FE Integration | — |
| Owner | Hub (Qontak) | Consuming squad |
| Authors | Syafrizal Muhammad | — |
| Reviewers | Hub Tech Lead · Account & Launchpad | A&L owns SDK + Session Manager |
| Approver(s) | Hub EM · Infosec [REQUIRED] | Infosec mandatory (iframe / postMessage / CSP) |
| Submitted | 2026-06-27 | ISO-8601 |
| Last updated | 2026-07-02 | ISO-8601 |
| Target release | 2026-Q3 | After Launchpad pilot (rollout step 4) |
| Related docs | Source RFC (Confluence) | See frontmatter |
| Discussion | [REQUIRED] | Slack thread |
Sections at a Glance
| § | Section | Hub-FE hints |
|---|---|---|
| 1 | Overview | Problem, scope, PRD coverage, design references, decision index |
| 2 | Technical Design | Repo Reading Guide, SDK wiring, event handling, sequences, ER/state |
| 3 | High-Availability & Security | iframe/postMessage/CSP/referrer, observability |
| 4 | Backwards Compatibility & Rollout | feature toggle, agent execution plan, verify/rollback |
| 5 | Concerns / Questions / Limitations | Open questions with severity |
| 6 | Comment logs | Review trail |
| 7 | Ready for agent execution | Gate marker |
1. Overview
1.1 Problem
Hub maintains its own session (bearer token in qontak._token.hub cookie, see
schemes/hubAuthScheme.js:152) independently of the SSO session. When a user
logs out on SSO, or switches account on SSO, or is idle on SSO for >2h, Hub does
not notice — it stays logged in to the stale account/company. The source RFC
documents two concrete bugs (stale session after SSO logout; wrong company shown
after account switch). Hub already redirects to SSO for login
(middleware/login.js:22) and to ${SSO_ACCOUNT_URL}/sign_out on logout
(components/layouts/main/SwitchAccount.vue:409), but there is no continuous
session-status check while the user sits inside Hub.
This RFC integrates the new mekari-account-web-sdk SDK into Hub so Hub reacts
to SSO session state (logged_in / logged_out / server_down) on every page.
An SSO account switch is delivered by the SDK as logged_out — the SDK cannot
distinguish a switch from a plain logout and does not expose the incoming
user's ssoId to consumers (see R5 / §2.6) — so it is handled by the same
sign-out path, not a dedicated event.
1.2 Success Criteria (Hub-scoped slice of source RFC)
- Hub constructs the
mekari-account-web-sdkSessionon authenticated pages withcurrentUser: this.$auth.user.sso_id(available atcomponents/layouts/main/InitComponent.vue:1138). - Hub signs out when the SDK reports
logged_out(no SSO session, or >2h idle). - An SSO account switch is delivered as
logged_out(indistinguishable from a plain logout — R5); Hub runs the same sign-out flow. Any company re-sync after the user re-authenticates is a separate BE/SSO concern, not driven by the SDK (§2.10). - Behaviour is gated by a
centralized_sessiontoggle; OFF = today's behaviour, byte-for-byte. - On
server_down, Hub takes no action (fail-open, per PRD constraint 6.10) — the SDK owns its own internalmslifallback, and Hub must not read/write that key or otherwise duplicate the SDK's retry logic.
1.3 Out of Scope
- Building the
mekari-account-web-sdkpackage, Session Manager (Golang), dedicated Redis, SSO Kong pathsm.mekari.com/*— owned by Account & Launchpad. - Auto-revoke of access/refresh tokens on inactivity (explicitly out of scope in source RFC).
- Multiple-sessions-per-account UX (server-side; rolls out in parallel, no Hub FE change required for the base integration).
- Hub backend changes for
current_companysync — see §5 OQ-1 (cross-squad, unverified; blocks the company-sync half of this RFC). This dependency is independent of the SDK (§2.10, R10).
1.4 Detail 1.A — PRD Section Coverage
| Source RFC section | Covered in | Note |
|---|---|---|
| 1. Overview (web + FE-app bugs) | §1.1 | Hub is a web-session product |
| Success Criteria | §1.2 | Hub-scoped subset |
| Out of Scope | §1.3 | inherited |
| Dependencies (SDK, Session Mgr, Redis) | §1.6, §2.0 | upstream, READ-ONLY for Hub |
| 2. Technical Design — Proposal | §2 | Hub uses SDK on all authed pages |
| SDK usage / contract / events | §2.4, §2.6 | three statuses handled (logged_in/logged_out/server_down); no switch_user (R5) |
Local Storage msli | §2.5, §2.6 | SDK-owned internally; Hub does not read/write it (R7) |
Cookies _mekari_account | §2.0, §3 | set by SSO/Rails, consumed SM-service-side; not observable by the SDK's client code or by Hub |
| FE Product Integration Flows | §2.6 sequences | Hub = OAuth2 auth-code flow |
| Web Session Flow (client_credentials) | n/a — Hub uses auth-code flow | see ADR-3 |
| OAuth2 Authorization Code Flow | §2.6 | matches existing sso-callback |
| User Logout / Switch Account | §2.6, §2.7 | maps to doSignOut (an SSO account switch surfaces as logged_out, indistinguishable — R5) |
| Database Model (no change) | §2.8 | no DB; FE-only |
| 3. HA & Security | §3 | Hub-side CSP/referrer + observability |
| 4. Rollout Plan | §4 | Hub = "Next Chapter" consumer (step 5) |
| 5. Open Questions | §5 | extended with Hub-specific OQs |
| FE Implementation Scope (per repo) | §4.C | Hub execution plan |
1.5 Detail 1.A — UI / Consumer Surface Coverage
| Surface | Role(s) | Backing | Note |
|---|---|---|---|
Every authenticated Hub page (under layouts/hub.vue) | all | SDK loaded once via boot component | SDK injects iframe; no visible UI |
| Sign-out redirect | all | existing doSignOut() | reused |
| No new page / route | — | — | FE behavioural change only |
1.5b Detail 1.A — Role Coverage
| Role | Behaviour | Note |
|---|---|---|
| super_admin | same event handling | no role-specific branch in SDK flow |
| admin | same | — |
| supervisor | same; existing online-status side-effects on sign-out (SwitchAccount.vue:383) preserved | — |
| agent | same; same online-status side-effects | — |
Authorization is not endpoint-gated by this RFC (no new authz surface) — see
§3 Role × Endpoint Authorization (n/a).
1.6 Detail 1.B — Decisions Closed (index → §2 ADR blocks)
| # | Decision | Chosen | ADR |
|---|---|---|---|
| 1 | Where to load the SDK | Boot component (InitComponent.vue) under layouts/hub.vue | ADR-1 |
| 2 | How the SDK is installed | Bundled git dependency (mekari-account-web-sdk) — no CDN alternative | ADR-2 |
| 3 | Which session-sync flow Hub follows | OAuth2 Authorization Code flow | ADR-3 |
| 4 | Feature-flag source for centralized_session | store/organization.js feature_flag map | ADR-4 |
| 5 | Map SDK events onto existing auth primitives | Reuse $auth.logout / doSignOut / middleware/login redirect | ADR-5 |
| 6 | server_down behaviour | Fail-open (no action taken) | ADR-6 |
1.7 Detail 1.C — Per-Story Change Map
| Story | Layer scope | Changes | Acceptance criteria | RFC anchors |
|---|---|---|---|---|
Load SDK with user_sso_id on authed pages | FE-only | plugins/centralized-session.js (new), components/inbox/.../MekariSession.vue or mixin (new), wire in InitComponent.vue | Unit test: SDK constructed with currentUser from $auth.user.sso_id; toggle OFF → not constructed | §2.4, §4.C-2 |
Gate behind centralized_session toggle | FE-only | read organization.feature_flag.centralized_session | Test: OFF → zero SDK calls (no iframe) | §2.3 ADR-4, §4.C-1 |
Handle logged_out | Runtime / behavior | call existing doSignOut() | Test: event → doSignOut invoked once | §2.6, §4.C-3 |
Handle server_down | Runtime / behavior | fail-open — no action taken (ADR-6) | Test: event → no doSignOut / no state change (assert no-op) | §2.6, §4.C-5 |
Handle logged_in + company sync | FE + BE | call Hub BE current-company sync endpoint (new, OQ-1; decoupled from the SDK — R10) | Test (FE): on logged_in, sync action dispatched | §2.4, §5 OQ-1 |
| Wire logout to also hit SSO sign_out | FE-only | already done (SwitchAccount.vue:409) | reuse — no change | n/a — already implemented |
2. Technical Design
2.0 Repo Reading Guide (read before writing)
Repo Map (slice this RFC touches)
flowchart LR
subgraph hub["local/hub (FE — write here)"]
layout["layouts/hub.vue\n(authed shell, read)"]
init["components/layouts/main/InitComponent.vue\n(boot, modify)"]
sess["plugins/centralized-session.js\n(new)"]
mixin["assets/mixins/session/centralizedSession.js\n(new)"]
sdk["mekari-account-web-sdk Session\n(bundled git dependency, imported)"]
auth["schemes/hubAuthScheme.js\n(read — logout/refresh)"]
sw["components/layouts/main/SwitchAccount.vue\n(read — doSignOut, EventBus)"]
mwlogin["middleware/login.js\n(read — auth-code redirect)"]
org["store/organization.js\n(read — feature_flag)"]
ep["common/constants/endpoint.js\n(modify — current_company key)"]
end
subgraph sm_ext["sm.mekari.com (Account & Launchpad — READ-ONLY)"]
sm["Session Manager /current\n(iframe + postMessage)"]
end
layout --> init
init --> mixin
mixin --> sess
sess --> sdk
mixin --> sdk
sdk -. iframe + postMessage .-> sm
mixin --> auth
mixin --> sw
mixin --> mwlogin
mixin --> org
mixin --> ep
Existing Code Anchors
| File | What to learn |
|---|---|
layouts/hub.vue:1 | Authenticated shell; renders InitComponent; gated by $auth.loggedIn |
components/layouts/main/InitComponent.vue | Boot component; has this.$auth.user.sso_id (:1138); cross-tab logout relay (:602-611) emits EventBus user-sign-out |
components/layouts/main/SwitchAccount.vue:376 | doSignOut() — full sign-out (MQTT, FCM, moengage, $auth.logout, redirect to SSO sign_out) |
components/layouts/main/SwitchAccount.vue:299 | EventBus.$on('user-sign-out', this.doSignOut) + $off at :302 — the relay hook to reuse |
schemes/hubAuthScheme.js:36 | logout() — revokes via endpoint + clears all token cookies (removeChatTokenCookie etc.) |
middleware/login.js:22 | SSO authorization-code redirect (/auth/?client_id=...&redirect_uri=.../sso-callback) — the re-auth pattern |
middleware/sso-callback.js:130 | doSSOValidation() — auth-code callback handler; already uses window.opener.postMessage (:24) |
store/organization.js:30 | feature_flag: {} state + getter (:41); flags come from org payload |
store/preferences.js:91 | alt flag source: getFeatureFlag action (server settings) returning res.data[param] |
plugins/hotjar.js:1 | Pattern for a third-party SDK boot plugin registered in nuxt.config.js:96 |
common/constants/endpoint.js | Canonical API URL registry (per AGENTS.md) — register current_company key here |
Patterns to Follow
| Concern | Reference file | Note |
|---|---|---|
| Third-party SDK boot | plugins/hotjar.js:1-3 | init in a Nuxt plugin; register in nuxt.config.js:84 plugins array |
| Cross-tab / decoupled events | components/layouts/main/InitComponent.vue:610, SwitchAccount.vue:299-302 | EventBus.$on/$off with beforeDestroy cleanup (AGENTS.md leak rule) |
| Feature flag (org payload) | store/organization.js:30,41 | v-if / computed getter; never v-show |
| Sign-out | SwitchAccount.vue:376-414 | reuse doSignOut(); do not re-implement |
| Re-auth (auth-code) | middleware/login.js:19-24 | redirect to SSO /auth with redirect_uri=.../sso-callback |
| Token revoke / reset | schemes/hubAuthScheme.js:36-48, :24-34 | $auth.logout(payload) then reset() |
| Mixin scoping | AGENTS.md "Mixins" row | assets/mixins/<domain>/; remove listeners in beforeDestroy |
Detail 2.0 — Source Verification (anti-hallucination)
| Claim | Evidence |
|---|---|
user_sso_id available as $auth.user.sso_id | components/layouts/main/InitComponent.vue:1138 const ssoId = this.$auth.user.sso_id; also middleware/utils.js:10 $auth.user.sso_id |
| Hub uses OAuth2 authorization-code flow with SSO | middleware/sso-callback.js:15 builds data.auth = route.query.code; middleware/login.js:22 redirect with response_type=code |
| Sign-out already redirects to SSO sign_out | components/layouts/main/SwitchAccount.vue:407-410 window.location.replace(\${SSO_ACCOUNT_URL}/sign_out...`)` |
| EventBus relay exists for sign-out | SwitchAccount.vue:299 $on('user-sign-out', this.doSignOut); InitComponent.vue:610 $emit('user-sign-out') |
| Custom auth scheme owns logout/refresh | schemes/hubAuthScheme.js:23 class HubAuthScheme extends LocalScheme; logout :36, refresh :210 |
| Org feature_flag map exists | store/organization.js:30 feature_flag: {}, :1665 state.feature_flag = payload |
| postMessage already used in callback | middleware/sso-callback.js:24 window.opener.postMessage({ type: 'SSO_LOGIN_SUCCESS' }, '*') |
| Third-party SDK boot pattern | plugins/hotjar.js:1-3; registered nuxt.config.js:96 |
| Test/build commands | package.json:21 "test": "jest --coverage", :9 "build": "nuxt build", :19 "lint" |
mekari-account-web-sdk not yet a dependency | package.json:34-60 deps list contains @mekari/pixel@^1.1.14 only, no mekari-account-web-sdk → SDK add is new (bundled git dependency, ADR-2) |
No existing current_company usage in Hub FE | repo grep `current_company |
| SSO env vars exist | nuxt.config.js:250 env block; SSO_ACCOUNT_URL/SSO_URL/SSO_UNIFIED_CLIENT_ID used across middleware/*, SwitchAccount.vue |
| SDK real contract (constructor/options/events/statuses) | verified against mekari-account-web-sdk v0.3.0 src/session.ts, src/index.ts, README.md (latest review reconciliation) — see §2.4 |
2.1 Current State
Each page under layouts/hub.vue boots InitComponent.vue (MQTT, FCM, identity).
Session is purely local: bearer token cookie + $auth state. SSO is contacted
only at login (middleware/login.js) and logout (SwitchAccount.vue). No
continuous SSO session check exists.
2.2 Proposal
Add a thin, toggle-gated centralized-session mixin booted from
InitComponent.vue. When organization.feature_flag.centralized_session is on,
it instantiates the mekari-account-web-sdk Session with
currentUser: $auth.user.sso_id, subscribes to the single session.on('event', (data) => ...) channel, and maps data.status onto Hub's existing auth
primitive: logged_out → the existing sign-out relay (EventBus.$emit ('user-sign-out') → doSignOut()). The SDK itself injects a hidden iframe
pointed at https://sm.mekari.com/current and relays state via postMessage
handled entirely inside the SDK (the SDK checks only event.data.source, not
event.origin — a security finding, see §3.2/OQ-9); Hub only consumes the
resulting session.on('event', ...) callback, never postMessage directly.
An SSO account switch is delivered by the SDK as logged_out — the SDK cannot
distinguish a switch from a logout and never exposes the other user's ssoId —
so Hub runs its normal sign-out flow in that case too; any post-re-login
company re-sync is a separate BE/SSO concern (§2.10), not SDK-driven.
Hub uses the OAuth2 Authorization Code variant (ADR-3) because that is
already how Hub authenticates; this is unrelated to the SDK, which exposes no
login/auth-code API of its own (its public surface is exactly constructor,
destroy(), on(), off()).
2.3 Technical Decisions (ADR format)
ADR-1 — Where to load the SDK
- Context: SDK must run on "all pages" of the authenticated app; Hub already has a single boot component per authed shell.
- Options: (a)
InitComponent.vueboot component underlayouts/hub.vue; (b) a Nuxt global middleware; (c) per-page. - Decision: (a). The SDK lifecycle lives in a mixin used by
InitComponent.vue. - Rationale:
InitComponentalready centralises boot side-effects (MQTT/FCM/ identity) and already reads$auth.user.sso_id(:1138). One mount point = one iframe without per-route churn; this also matches the SDK's singleton behaviour — a 2ndnew Session()call returns the 1st instance and ignores new options — so a single, well-defined mount point avoids accidental re-construction (R11). - Consequences: SDK runs only inside authed shell (correct — unauthed pages
have no
sso_id). Login/SSO-callback pages keep current behaviour. On full teardown (e.g. sign-out), the mixin must callsession.destroy()— not.off()alone — sincedestroy()is what resets the singleton and tears down the iframe + internalwindowlistener (R11). - Reversibility: High — remove the mixin import + toggle.
ADR-2 — How the SDK is installed
- Context: The SDK,
mekari-account-web-sdk(v0.3.0), is distributed as a private git dependency:npm install git+https://<user>:<pass>@bitbucket.org/mid-kelola-indonesia/mekari-account-web-sdk#<version>, imported asimport { Session } from 'mekari-account-web-sdk'. There is no CDN, no<script src>build, and no npm-registry package — this is not a choice between alternatives, it is the SDK's only distribution mechanism. - Decision: Add
mekari-account-web-sdktopackage.jsonas a git dependency pinned to a version tag; import it (statically, or via dynamicimport()inside the toggle guard to keep the bundle lazy). - Rationale: Bundled at build time — versioned by git ref, tree-shakeable,
mockable in Jest (
jest.mock('mekari-account-web-sdk')). No CSPscript-srcallow-listing is needed for the SDK itself (see §3.2). - Consequences: Adds
mekari-account-web-sdktopackage.jsondependencies. Previously tracked as OQ-3 ("SDK not published to a registry"); resolved — it exists as a git dependency, not an npm-registry package, so no registry access is needed (R12). - Reversibility: High — remove the dependency + mixin import.
ADR-3 — Which session-sync flow Hub follows
- Context: Source RFC defines a Web-Session (client_credentials, BE→SSO) flow and an OAuth2 Authorization-Code flow.
- Options: (a) Web-session/client_credentials; (b) OAuth2 auth-code.
- Decision: (b) OAuth2 Authorization Code.
- Rationale: Hub already authenticates via auth-code (
middleware/sso-callback.js:15consumesroute.query.code;middleware/login.js:22requestsresponse_type=code). Reusing it means thecurrent_companyfetch (a separate BE/SSO concern, §2.10 — not SDK-driven, R10) follows theusers/me/current_company(auth-code) contract, notusers/{id}/current_company. - Consequences:
logged_outmust revoke tokens then sign out (auth-code semantics), which$auth.logoutalready does.server_downdoes not trigger this — it is fail-open, no destructive action (ADR-6). - Reversibility: Medium — switching to client_credentials would require a Hub BE proxy endpoint instead.
ADR-4 — Feature-flag source
- Context: Two flag mechanisms exist: org payload
feature_flag(store/organization.js:30) and server-settingspreferences/getFeatureFlag(store/preferences.js:91). - Options: (a) org payload map; (b) preferences server-setting.
- Decision: (a)
organization.feature_flag.centralized_session. - Rationale: AGENTS.md mandates org-payload flags as the standard; read synchronously via getter; supports per-company rollout (matches source RFC "enable per company/application").
- Consequences: Backend must add
centralized_sessionto the org payload (OQ-2). Until then the toggle readsundefined→ treated as OFF (safe default). - Reversibility: High.
ADR-5 — Reuse existing auth primitives for event handling
- Context: Each handled SDK status maps onto an action Hub already performs.
- Options: (a) reuse
doSignOut()/$auth.logout/middleware/loginredirect; (b) build new session-control code. - Decision: (a) reuse.
- Rationale:
doSignOut()already tears down MQTT/FCM/moengage and redirects to SSO sign_out; duplicating it risks leaks.EventBus 'user-sign-out'is the existing decoupled trigger (SwitchAccount.vue:299). - Consequences: Mixin emits
EventBus.$emit('user-sign-out')onlogged_outrather than calling sign-out directly, matching the cross-tab relay pattern. - Reversibility: High.
ADR-6 — server_down behaviour
- Context: The SDK emits
server_downwhen no iframe message arrives withincheckTimeout(constructor option, default 2000ms) and its own internalmslifallback (localStorage key"msli", 2h expiry, owned and read/written by the SDK itself) is also stale. By the time a consumer receivesserver_down, the SDK has already exhausted its own grace period. PRD constraint 6.10 mandates fail-open behaviour for this path. - Options: (a) immediate sign-out; (b) a consumer-side
msliheuristic then sign-out; (c) fail-open — take no action. - Decision: (c) fail-open. Hub takes no destructive action on
server_down(no sign-out, no company change). - Rationale:
msliis owned internally by the SDK — a consumer read/write of the same key would collide with and corrupt the SDK's own fallback logic (R7). Since the SDK already applies its own 2h-gracemslicheck before ever surfacingserver_down, a second consumer-side heuristic is redundant and unsafe. Fail-open also directly satisfies PRD constraint 6.10. - Consequences: Hub does not read or write
msli, does not checkqontak._token.hubvalidity in this branch, and does not sign the user out onserver_down. Optionally log/alert for observability (§3.3). - Reversibility: High.
Minimum coverage checklist: Storage → n/a — FE-only, no DB (§2.8);
msli is SDK-owned internally, Hub does not read/write it (R7); cookies are
read-only where relevant. Sync vs async → SDK events are async, handled via a
single session.on('event', ...) listener (ADR-5). Caching → periodic
re-validation is via the SDK's own interval constructor option (ms, ≥1000);
Hub configures/caches nothing itself — the source RFC's 5s/6k-RPS/50ms figures
belong to A&L's /current service, not the SDK (ADR-1, ADR-6). Third-party →
bundled git dependency, no CSP script-src needed (ADR-2, §3). Consistency →
eventual (iframe reload on mount + interval). Multi-tenancy → currentUser
(ssoId) comparison happens internally in the SDK; a different SSO user
surfaces to Hub only as logged_out, never as a distinct event (R5). Reuse vs
new → ADR-5 (reuse auth primitives); only one new endpoint key (OQ-1,
decoupled from the SDK — R10).
2.4 APIs
Outbound (Hub → others)
| Method | URL (key) | Status | Owner | Note |
|---|---|---|---|---|
| iframe GET | sm.mekari.com/current | new (A&L) | Account & Launchpad | invoked by SDK, not Hub code directly |
| GET | Hub BE current-company sync → endpoint.*.user.currentCompany | new-with-justification | Hub BE (OQ-1) | no existing key (repo grep: 0 hits); needed to set company on logged_in; a separate BE/SSO concern, not SDK-driven (R10) |
| GET (existing) | SSO /auth/?response_type=code...&redirect_uri=.../sso-callback | reused | SSO | re-auth entry point (middleware/login.js:22) — used for normal login, not triggered by any SDK event |
| GET (existing) | ${SSO_ACCOUNT_URL}/sign_out | reused | SSO | logout (SwitchAccount.vue:409) |
new-with-justification (current-company): reuse is impossible because Hub has
no current-company endpoint today (verified by repo grep). The exact BE contract
(/me/current_company per ADR-3, auth-code variant) must be confirmed by Hub BE
— OQ-1.
Inbound (others → Hub)
| Channel | From | Payload | Note |
|---|---|---|---|
postMessage (window) | Session Manager iframe (sm.mekari.com/current) | session state | consumed inside the SDK; the SDK validates only event.data.source === 'mekari-account-web-sdk', NOT event.origin (security finding — §3.2/OQ-9). Surfaced to Hub as the statuses below; Hub does not parse postMessage directly |
| SDK event | mekari-account-web-sdk Session | { status, sessionId? } (single-arg callback, no error param) | see §2.6 |
SDK contract consumed by Hub
// assets/mixins/session/centralizedSession.js (new)
import { Session } from 'mekari-account-web-sdk'
const session = new Session({ currentUser: this.$auth.user.sso_id })
session.on('event', (data) => {
// data.status is EXACTLY one of: logged_in | logged_out | server_down
// (verified: mekari-account-web-sdk v0.3.0, session.ts — resolved OQ-4).
// There is no `switch_user` status and no second `error` argument. A
// different SSO user (account switch) surfaces indistinguishably as
// `logged_out` — the SDK never exposes the other user's ssoId (R5).
})
// Periodic re-validation, if desired, is configured via the constructor's
// `interval` option (ms, >= 1000; PRD constraint 6.9 recommends 5*60*1000).
// There is no activity-driven refresh method on the SDK's public API
// (constructor, destroy(), on(), off() only) — OQ-7 is moot (R6).
2.5 Local Storage & Cookies (Hub-side)
| Key | Type | Owner | Hub use |
|---|---|---|---|
msli | localStorage (timestamp, 2h expiry) | Owned internally by the SDK | Hub must NOT read or write this key — set on logged_in / cleared on logged_out by the SDK itself; a consumer write would collide with and corrupt the SDK's own server_down fallback (R7) |
_mekari_account | cookie | SSO/Rails, consumed SM-service-side | Not observable by the SDK's client-side code or by Hub JS — it travels with the browser's cookie jar to sm.mekari.com and is validated server-side by Session Manager, not read/parsed by SDK or Hub code |
qontak._token.hub | cookie | Hub (hubAuthScheme.js:152) | existing Hub session token; unaffected by SDK events (no server_down validity check needed — ADR-6 is fail-open) |
2.6 Sequence Diagrams
Happy path — page load, logged_in + company sync
sequenceDiagram
autonumber
participant U as User
participant Hub as Hub FE (InitComponent + mixin)
participant SDK as mekari-account-web-sdk
participant IF as SM iframe (sm.mekari.com/current)
participant SM as Session Manager (A&L)
participant R as Redis (session)
participant BE as Hub BE
participant SSO as SSO API
U->>Hub: open authed page
Hub->>Hub: toggle centralized_session ON?
Hub->>SDK: new Session({currentUser: sso_id})
SDK->>IF: inject hidden iframe
IF->>SM: GET /current
SM->>R: validate + update last_request_at (<2h)
R-->>SM: session ok (ssoId)
SM-->>IF: render page w/ ssoId
IF-->>SDK: postMessage(session state)
SDK-->>Hub: event {status: logged_in}
Hub->>BE: GET current_company (OQ-1 — separate BE/SSO concern, R10)
BE->>SSO: GET /v1.1/users/me/current_company
SSO-->>BE: company
BE-->>Hub: company → set → allow interaction
Failure path — server_down (Session Manager unreachable, fail-open)
sequenceDiagram
autonumber
participant Hub as Hub FE (mixin)
participant SDK as mekari-account-web-sdk
participant SM as Session Manager
SDK->>SM: GET /current (iframe reload)
Note over SDK,SM: no reply within checkTimeout (default 2000ms) triggers the SDK's OWN internal msli fallback
SDK-->>Hub: event {status: server_down}
Hub->>Hub: fail-open — no action taken (no sign-out, no company change — ADR-6 / PRD 6.10)
logged_out (includes SSO account switch)
sequenceDiagram
autonumber
participant Hub as Hub FE (mixin)
participant SDK as mekari-account-web-sdk
SDK-->>Hub: event {status: logged_out}
Hub->>Hub: EventBus.$emit('user-sign-out') → doSignOut() → SSO /sign_out
Note: an SSO account switch is also delivered as logged_out — the SDK
cannot distinguish it from a plain logout and never exposes the other user's
ssoId — so the same handling applies (R5). There is no dedicated switch event
or handler.
2.7 State Surface Contract & State Machine
stateDiagram-v2
[*] --> Unknown
Unknown --> LoggedIn: event logged_in
Unknown --> LoggedOut: event logged_out
LoggedIn --> LoggedOut: event logged_out (incl. SSO account switch — indistinguishable, R5)
LoggedIn --> LoggedIn: event server_down (fail-open — no action, ADR-6)
LoggedOut --> [*]: doSignOut → SSO /sign_out
| State | Visibility | Transitions | Note |
|---|---|---|---|
| LoggedIn | normal app | → LoggedOut; self-loop on server_down (no-op) | fail-open per ADR-6; SDK owns msli internally |
| LoggedOut | redirect to SSO | terminal | doSignOut(); also reached when SSO reports a different user (account switch), since the SDK cannot distinguish the two (R5) |
2.8 Database Model
n/a — FE-only, no schema change (matches source RFC "Database Model: No
database changes"). No erDiagram.
2.9 Branch & Skip Catalog
| Branch | Owner | Behaviour |
|---|---|---|
| toggle OFF | Hub FE | SDK never constructed; today's behaviour verbatim |
sso_id absent ($auth not ready) | Hub FE | skip SDK init until $auth.user.sso_id present |
server_down (any) | Hub FE | fail-open — no action taken (ADR-6); the SDK owns its own msli-based grace period internally |
| sign-out / mixin teardown | Hub FE | call session.destroy() (not .off() alone) — resets the singleton and tears down the iframe + window listener (R11) |
| multiple-sessions-per-account | A&L (server) | n/a — no Hub FE branch |
2.10 Responsibility Boundary (cross-squad)
| Step | Owner |
|---|---|
SDK (bundled git dependency), iframe (sm.mekari.com/current), Redis, SSO Kong path | Account & Launchpad |
current_company SSO contract + Hub BE proxy (decoupled from the SDK — R10, not driven by any SDK event) | Hub BE (OQ-1) |
org payload centralized_session flag | Hub BE (OQ-2) |
| SDK wiring, event handling, toggle, observability | Hub FE (this RFC) |
3. High-Availability & Security
3.1 Performance (Hub-side)
Hub adds one SDK init + one iframe load per authed shell mount. The 6k RPS /
50ms latency targets in the source RFC apply to A&L's /current service, not
the SDK itself. Hub requirement: SDK init must not block first paint — load
lazily after $auth.loggedIn (ADR-1) and never gate route rendering on it
(toggle-guarded).
3.2 Security Implications (Hub-side)
| Check | Rule / action |
|---|---|
| iframe origin (A05) | SDK iframe targets sm.mekari.com only (SDK's default sessionUrl); add CSP frame-src/child-src allow-list entry for sm.mekari.com |
| postMessage (A03/A07) | Open upstream security finding: the SDK's window message listener does NOT validate event.origin — it checks only event.data.source === 'mekari-account-web-sdk'. Hub cannot mitigate this; the SDK owns the listener internally. Tracked as an explicit open infosec item for Account & Launchpad (OWASP A08/A07) — see OQ-9. Hub must NOT add its own wildcard postMessage listener for session data (Hub already uses postMessage('*') at sso-callback.js:24 for a different purpose — keep separate) |
| token handling (A02) | no token logged; $auth.logout clears cookies (hubAuthScheme.js:43-46) |
| referrer/origin | source RFC option 2 (referrer/origin whitelist) is A&L-side; Hub's prod domain must be added to A&L's sm.mekari.com frame-ancestors/referrer-origin whitelist before rollout (OQ-5) |
CSP/whitelist update mechanism is A&L-owned (source RFC §3); Hub's contribution is supplying its production domain(s) for the allow-list.
3.3 Observability (Hub-side)
- Datadog RUM already initialised (
plugins/datadog-rum.ts,nuxt.config.js:92). Emit a RUM custom action per SDK event (centralized_session.<status>). - Mixpanel v2 (
assets/mixins/metric/mixpanelMixin.js, AGENTS.md) — log forced sign-outs (logged_out) for funnel analysis. - Alert: spike in
centralized_session.server_down(proxy for A&L outage; fail-open means no user-facing action is taken, so this is purely a health signal).
4. Backwards Compatibility & Rollout Plan
4.A Compatibility
Toggle OFF ⇒ no SDK, no iframe, no behavioural change — fully backwards
compatible. Hub is rollout step 5 ("The Next Chapter") in the source RFC,
after the Launchpad pilot (step 4) proves the SDK. Hub's prod domain must be
added to the SM (sm.mekari.com) CSP frame-ancestors/referrer-origin
whitelist before enabling.
4.B Pre-merge Verification (from package.json)
npm run lint # package.json:19
npm run test # package.json:21 (jest --coverage)
npm run build # package.json:9 (nuxt build) — must exit 0
4.C Agent Execution Plan (ordered chunks)
Chunk 1 — Toggle plumbing
- Files:
store/organization.js(confirmfeature_flaggetter exposescentralized_session; no code change if generic), new computed in mixin. - Acceptance: unit test — getter returns
falsewhen flag absent.
Chunk 2 — SDK boot plugin + mixin (TDD first)
- Files (new):
plugins/centralized-session.js,assets/mixins/session/centralizedSession.js,assets/mixins/session/__test__/centralizedSession.spec.js. - Files (modify):
package.json(addmekari-account-web-sdkas a git dependency, ADR-2),nuxt.config.js:84(register plugin). - Commands:
npm run test -- --testPathPattern="centralizedSession". - Acceptance: toggle ON +
sso_idpresent ⇒Sessionconstructed withcurrentUser = sso_id; toggle OFF ⇒Sessionnever constructed (mock asserts 0 calls).
Chunk 3 — Wire into boot + logged_out/logged_in
- Files (modify):
components/layouts/main/InitComponent.vue(use mixin after$auth.loggedIn). - Acceptance:
logged_out⇒EventBus.$emit('user-sign-out')called once;logged_in⇒ handled (nomsliwrite — SDK-owned, R7).
Chunk 5 — server_down fallback (fail-open)
- Files: mixin handler only — no consumer-side
mslihelper (SDK ownsmsliinternally, R7). - Acceptance:
server_down⇒ mixin takes no action — assertEventBus.$emit/doSignOutare NOT called.
Chunk 6 — company sync (BLOCKED on OQ-1)
- Files:
common/constants/endpoint.js(register key),store/organization.jsorstore/usersaction for current_company. - Acceptance:
logged_in⇒ sync action dispatched; sets company. Do not start until OQ-1 resolved.
Chunk 7 — observability
- Files: mixin (RUM/Mixpanel calls).
- Acceptance: each event emits a RUM action; verifiable via mocked tracker.
Order rationale: toggle (1) → SDK/mixin (2) → boot+core events (3) → fallback (5) → company sync (6, gated) → telemetry (7).
4.D Verification & Rollback Recipe
Post-deploy signals
- Datadog RUM action
centralized_session.logged_inappears for piloted company. centralized_session.server_downrate ≈ 0 under normal A&L health.- No regression in Hub login funnel (Mixpanel v2).
Rollback (numbered, agent-executable)
- Set org
feature_flag.centralized_session = falsefor affected company (instant, no deploy) → SDK stops constructing. - If code-level: revert the InitComponent wiring PR (Chunk 3) — toggle already makes this safe; revert removes the mixin import entirely.
- Confirm
centralized_session.*RUM actions drop to zero. - Ask A&L to remove Hub's domain from the
sm.mekari.comwhitelist if fully reverting.
Rollback respects order: disable flag (1) before reverting code (2); no other Hub layer depends on this mixin.
5. Concerns, Questions, or Known Limitations
| # | Severity | Question / limitation | Owner |
|---|---|---|---|
| OQ-1 | [critical] | Hub BE has no current-company endpoint (repo grep: 0 hits). Need confirmed BE contract for users/me/current_company proxy (auth-code variant per ADR-3). Blocks Chunk 6. Independent of the SDK (R10). | Hub BE |
| OQ-2 | [critical] | centralized_session flag is not yet in Hub's org payload (store/organization.js). Without it the toggle is permanently OFF. | Hub BE |
| OQ-3 | [resolved] | @mekari/sdk not published/accessiblemekari-account-web-sdk v0.3.0, distributed as a git dependency (npm install git+https://...#<version>) — no CDN, no npm-registry access needed (ADR-2). Resolved by real-SDK verification, latest review. | A&L |
| OQ-4 | [resolved] | logout vs logged_out)Status enum is EXACTLY logged_in | logged_out | server_down; single-arg callback, no error param, no switch_user (verified: mekari-account-web-sdk v0.3.0 session.ts, latest review). | — |
| OQ-5 | [important] | Hub production domain(s) must be added to SM (sm.mekari.com) CSP frame-ancestors / referrer-origin whitelist before enable. | A&L + Hub |
| OQ-6 | [important] | CSP frame-src/child-src for sm.mekari.com — confirm Hub's current CSP source (header vs meta) and where to edit. | Hub FE / Infra |
| OQ-7 | [moot] | destroy(), on(), off() only). Periodic re-validation uses the constructor's interval option (ms, ≥1000; PRD 6.9 suggests 5*60*1000) — resolved (R6). | — |
| OQ-8 | [important] | Infosec approver name + Slack discussion thread unfilled in metadata. | Hub |
| OQ-9 | [important] | New upstream infosec finding: the SDK's window message listener does not validate event.origin (checks only event.data.source). Hub cannot mitigate this client-side. Needs remediation/acceptance from Account & Launchpad (OWASP A08/A07). | A&L / Infosec |
Known limitation: until OQ-1/OQ-2 close, only Chunks 1, 2, 3, 5, 7 (session
detect + sign-out + server_down fail-open, toggle-gated, no company sync)
are agent-executable; Chunk 6 remains blocked.
6. Comment logs
| Date | Author | Note |
|---|---|---|
| 2026-06-27 | Syafrizal Muhammad | Initial Hub-FE integration draft derived from A&L source RFC |
| 2026-07-02 | latest review reconciliation | Reconciled against real SDK mekari-account-web-sdk v0.3.0 (latest review): corrected package/import, currentUser, single-arg on("event"), removed switch_user/session.refresh()/consumer-msli, fixed iframe host to sm.mekari.com, recorded the event.origin security finding, reclassified resolved OQs. |
7. Ready for agent execution
Ready for agent execution: no
Blocking gates (must resolve before flipping to yes):
- OQ-1
[critical]— Hub BE current-company endpoint contract unverified (blocks company sync / Chunk 6). - OQ-2
[critical]—centralized_sessionorg-payload flag does not exist yet (toggle inert).
Non-blocking but needed for full completion: OQ-5/OQ-6 (CSP/whitelist), OQ-9
(upstream event.origin infosec finding, tracked with A&L).
Chunks 1, 2, 3, 5, and 7 are executable today against the live repo behind the (currently OFF) toggle; Chunk 6 is gated on OQ-1/OQ-2.