RFC: Centralized Web Session — Launchpad SDK Integration
Document Conventions (do not remove)
Follows the Qontak RFC Template governance (metadata table + sections 1–6 + Comment logs) and is agent-execution-ready (§1 Design/PRD derivation, §2 Repo Reading Guide, mermaid diagrams, §4 Execution Plan + Verification/Rollback). YAML frontmatter and the metadata table must agree. ISO-8601 dates.
Scope note. This is the frontend RFC for the Launchpad pilot only (Rollout step 4 "Taking Off"). The Session Manager service (
sm.mekari.com/current), its Redis, and the gateway routing to it are upstream backend dependencies owned by SSO/Account — they are tracked here as external contracts, not built by this RFC. Themekari-account-web-sdkpackage itself is a bundled git dependency of Launchpad (no CDN, no<script>tag) — it ships inside this RFC's own build, not as a separate backend deliverable. CRM / Hub / Hub Chat v2 integration is Rollout step 5 and is out of scope for execution (mapped in Detail 1.C asdeferred).
Metadata
| Field | Value | Notes |
|---|---|---|
| Status | RFC | IDEA | RFC | ABANDON | AGREED |
| Type / Sub-type | frontend / new-feature | Launchpad SPA integration |
| Title | Centralized Web Session — Launchpad SDK Integration | |
| Owner | Account & Launchpad (AL) | Pilot product squad |
| Authors | Syafrizal Muhammad | |
| Reviewers | AL squad reviewer; SSO/Session Manager BE reviewer | [REQUIRED] names |
| Approver(s) | AL tech lead; infosec approver | infosec mandatory (SDK/iframe/CSP) |
| Submitted | 2026-06-27 | |
| Last updated | 2026-06-27 | |
| Target release | [REQUIRED] | |
| Related Documents | Centralized Web Session PRD | Confluence PT space |
| Discussion | [REQUIRED] |
Sections at a Glance
| § | Section | Frontend hints |
|---|---|---|
| 1 | Overview | Problem, success criteria, PRD-to-behavior derivation, decisions index, per-story change map |
| 2 | Technical Design | Repo Reading Guide, topology, SDK contract, event routing, state surface, sequence diagrams, state machine, branch catalog |
| 3 | High-Availability & Security | CSP/frame-ancestors, referrer/origin, token handling, OWASP |
| 4 | Backwards Compatibility & Rollout | Feature flag, execution plan, verification & rollback recipe |
| 5 | Concerns / Open Questions | Severity-tagged blockers |
| 6 | Comment logs | Review history |
| 7 | Ready for agent execution | Gate marker |
1. Overview
1.1 Problem
Mekari products keep independent web sessions and never observe SSO logout. A
user who logs out on SSO (or switches account on SSO) stays "logged in" on
Launchpad with stale company/account context. Concretely (PRD §1): after an SSO
logout the user remains active on the product, and after an SSO account switch
the product still shows the previous company. Launchpad today holds the SSO
tokens in cookies (app/common/composables/useAuthCookies.ts:5-30) and never
re-validates them against SSO between navigations — authenticated.global.ts
only refreshes an expired access token, it never asks "is this SSO session still
the one I think it is?".
This RFC integrates the mekari-account-web-sdk Session SDK (bundled git
dependency; see §2.4, R1/R2 of the latest review reconciliation) into Launchpad
so the SPA detects, on every page, whether the SSO session is logged_in,
logged_out, or server_down. The SDK has no switch_user status — an SSO
account switch is delivered as logged_out (§2.4) — and Launchpad reacts
consistently with its existing OAuth2 authorization-code auth.
1.2 Success Criteria (from PRD)
| # | Criterion | FE-observable assertion |
|---|---|---|
| SC1 | SDK reports session status across products | Launchpad reacts to all 3 SDK statuses — logged_in, logged_out, server_down (test-asserted in event-handler spec) |
| SC2 | Session expires after 2h idle | On logged_out event Launchpad runs sign-out flow (no manual reload needed) |
| SC3 | Session re-validated periodically | Achieved via the Session constructor's interval option (ms, ≥1000; recommended 5*60*1000 per PRD constraint 6.9) — the SDK reloads its hidden iframe on that fixed timer. There is no session.refresh() API (OQ-6 moot). |
| SC4 | 1 account → multiple sessions | No FE change required; Launchpad treats each browser independently (Detail 1.C n/a — BE-owned) |
1.3 Out of Scope
- Auto-revoking access/refresh tokens on idle logout (PRD out-of-scope).
- Building the Session Manager service or its Redis (backend; tracked as a
dependency). The
mekari-account-web-sdkpackage is likewise not built by this RFC — it is an existing git dependency this RFC bundles (§2.4), not a BE build. - CRM / Hub / Hub Chat v2 wiring (Rollout step 5 — deferred; mapped in 1.C).
- Multiple-sessions-per-account FE work (no FE change; BE/SSO concern).
1.4 Detail 1.A — PRD Section Coverage
| PRD section | Covered in | Status |
|---|---|---|
| 1. Overview (issues) | §1.1 | covered |
| Success Criteria | §1.2 | covered |
| Out of Scope | §1.3 | covered |
| Dependencies (SDK, Session Manager, Redis) | §2.2 topology, §5 OQ-2/OQ-4 | covered (SDK dependency resolved; Session Manager + company-sync remain external) |
| 2. Technical Design — Current/Proposal | §2.4, §2.5 | covered |
| How to use the SDK | §2.4 | covered |
Local Storage (msli) | §2.6 | covered (SDK-owned only — Launchpad does not touch it) |
Cookies (_mekari_account) | §2.6 | covered |
| SDK contract (events) | §2.4 | covered |
| FE Product Integration — SDK Flow | §2.7 seq | covered |
| FE — Web Session Flow | §2.7 | n/a — Launchpad is OAuth2 (see ADR-3) |
| FE — OAuth2 Authorization Code Flow | §2.7 seq | covered (Launchpad's model) |
| User Logout From Product | §2.7, §2.9 | covered |
| User Switch Account | §2.4 (delivered as logged_out) | covered — the SDK cannot distinguish an account switch from a logout and never exposes the incoming ssoId, so Launchpad runs the normal sign-out flow (R5) |
| Database Model (no change) | §2.8 | covered |
| 3. HA & Security | §3 | covered (FE-side: CSP, referrer) |
| 4. Rollout Plan | §4 | covered (step 4 only; 5 deferred) |
| 5. Open Questions | §5 | covered |
| FE Implementation Scope | §4.C execution plan | covered |
UI / Consumer Surface Coverage
| Surface | Trigger | Backing read | Coverage |
|---|---|---|---|
| Every authenticated Launchpad page | SDK loaded app-wide via plugin | SDK iframe → Session Manager | §2.7 SDK flow |
| Session-expired toast + redirect | logged_out / server_down | existing authenticated.global.ts:97-110 pattern | §2.9 |
Role Coverage
| Role | Behavior delta | Coverage |
|---|---|---|
| All authenticated Launchpad roles | Identical session lifecycle; no role-specific branch | §2 — n/a — session is role-agnostic |
Launchpad permission roles (
permission.global.ts) are orthogonal to session validity; this RFC adds no role × session matrix beyond the existing permission middleware.
1.5 Detail 1.B — Decisions Closed (index → §2 ADRs)
| # | Decision | Chosen | ADR |
|---|---|---|---|
| 1 | Feature-flag mechanism | New centralized_session key in configs/{development,production}.json → runtimeConfig.public | ADR-1 |
| 2 | Where SDK loads | New Nuxt plugin ~/plugins/centralized-session.ts, registered after auth.ts | ADR-2 |
| 3 | Which PRD flow applies | OAuth2 authorization-code flow (Launchpad confirmed response_type=code) | ADR-3 |
| 4 | currentUser source | launchpad.sso_id cookie / authProfile.sso_id | ADR-4 |
| 5 | current-company sync | Reuse /users/me (authStore.fetchAuthLaunchpad) unless OQ-4 forces new endpoint. The SDK exposes no company data at all — this stays a separate BE/SSO dependency, not SDK-driven (R10). | ADR-5 |
| 6 | Event→action routing | Central composable useCentralizedSession, single session.on("event", (data) => { switch(data.status) {...} }) subscription (the SDK's only valid event name is the literal "event" — per-status subscriptions are a silent no-op), reuses authStore.resetAuth + existing logout redirect | ADR-6 |
1.6 Detail 1.C — Per-Story Change Map
| Story | Layer scope | Changes | Acceptance criteria | RFC anchors | Notes |
|---|---|---|---|---|---|
| Detect SSO logout on Launchpad | FE-only | useCentralizedSession composable; plugin load | logged_out event → sign-out redirect; spec passes | §2.4, §4.C ch.3 | Also covers an SSO account switch — the SDK reports it as logged_out (R5); there is no separate "account switch" story. |
| Periodic session re-validation | FE-only | pass interval (recommended 5*60*1000ms) to the Session constructor | SDK re-checks on a fixed timer via hidden iframe reload; no manual/throttled refresh call exists | §2.4, §4.C ch.5 | Replaces the earlier session.refresh() design — the SDK has no refresh() API (R6; OQ-6 moot). |
server_down fail-open | FE-only | on server_down, take no destructive action (PRD constraint 6.10); the SDK owns msli internally and Launchpad must not read/write it | no forced sign-out; log + monitor only | §2.6, §4.C ch.5 | Removed the consumer-side msli predicate (R7) — by the time a consumer observes server_down, the SDK's own msli fallback has already lapsed. |
| Feature gate | Config | centralized_session flag in configs/{development,production}.json | flag off → zero behavior change (spec) | §4.A, §4.C ch.1 | |
| Multiple sessions / account | n/a — BE-owned | none | n/a | — | SC4 |
| CRM/Hub/HubChat integration | Cross-squad | per-repo SDK wiring | n/a | — | deferred — Rollout step 5 |
2. Technical Design
2.0 Repo Reading Guide (read before writing code)
Repo Map (slice this RFC touches)
flowchart LR
subgraph LP["FE: qontak-launchpad-fe (MAIN — write here)"]
cfg["configs/{development,production}.json (modified)"]
nuxt["nuxt.config.ts (modified)"]
plug["plugins/centralized-session.ts (new)"]
comp["composables/useCentralizedSession.ts (new)"]
cookies["composables/useAuthCookies.ts (read)"]
client["composables/useClient.ts (read)"]
store["store/authStore.ts (modified: resetAuth reuse)"]
mw["middleware/authenticated.global.ts (read/modified)"]
sso["features/sso-callback/composable/ssoCallback.ts (read)"]
sdk["mekari-account-web-sdk Session (bundled git dependency, no CDN)"]
plug --> comp
plug --> sdk
comp --> store
comp --> cookies
comp --> mw
end
subgraph EXT["External (BE dep — READ-ONLY contract)"]
sm["Session Manager + iframe (sm.mekari.com/current)"]
end
sdk -.iframe+postMessage.-> sm
Existing Code Anchors
| # | File:Line | What to learn |
|---|---|---|
| 1 | app/common/composables/useClient.ts:9-15,98-143 | HTTP wrapper + 401 singleton refresh; do not duplicate refresh logic |
| 2 | app/common/composables/useAuthCookies.ts:5-30 | Token cookie keys (global_sso_token, global_sso_refresh_token) + launchpad.sso_id (line 30) |
| 3 | app/common/store/authStore.ts:30,135-149 | useAuthStore, setProfileAuth sets LAUNCHPAD_SSO_ID, resetAuth clears state |
| 4 | app/middleware/authenticated.global.ts:43-120 | Existing refresh + session-expired toast + logout redirect to CHATPANEL_URL/logout |
| 5 | app/features/sso-callback/composable/ssoCallback.ts:3-15 | OAuth2 code-flow redirect URL shape (response_type=code&scope=sso:profile) |
| 6 | app/plugins/auth.ts + nuxt.config.ts:89-95 | Plugin registration + load order (auth runs last) |
| 7 | app/plugins/pixel.ts, app/plugins/mixpanel.ts | Pattern for initializing a 3rd-party SDK in a Nuxt plugin |
| 8 | nuxt.config.ts:131-149 | runtimeConfig.public + env spread (line 148) — where flags surface |
| 9 | configs/development.json (+ production.json) | Env config files; add centralized_session here. There is no configs/local.json in this repo — corrected from an earlier draft (ADR-1). |
| 10 | app/common/composables/useErrorHandler.spec.ts, useAuthCookies.spec.ts | Vitest + vi.mock pattern for composable specs |
Patterns to Follow
| Concern | Reference file | Pattern |
|---|---|---|
| State management | app/common/store/authStore.ts:30 | Pinia defineStore setup-style, ref() state, returned actions |
| 3rd-party SDK init | app/plugins/pixel.ts:1-10, app/plugins/mixpanel.ts | defineNuxtPlugin, init inside plugin, register in nuxt.config.ts:89-95 |
| HTTP / refresh | app/common/composables/useClient.ts:98-112 | singleton refreshPromise to dedupe refresh |
| Error normalization | app/common/composables/useErrorHandler.ts | route API errors through handleError |
| User-facing notice | app/middleware/authenticated.global.ts:97-101 | toast.notify from @mekari/pixel3 |
| Logout redirect | app/middleware/authenticated.global.ts:109,116-119 | window.location.replace(CHATPANEL_URL/logout) / navigateTo(... {external:true}) |
| Cookie access | app/common/composables/useAuthCookies.ts:9-30 | useCookie with prod COOKIE_DOMAIN else localhost |
| Test | app/common/composables/useErrorHandler.spec.ts | Vitest, vi.mock, co-located .spec.ts |
No in-repo pattern exists for iframe injection /
postMessagelistening — themekari-account-web-sdkencapsulates the iframe and its ownwindowmessage listener; Launchpad code never injects an iframe or listens formessagedirectly. Launchpad only consumes SDK events viasession.on("event", ...). (No fabricated pattern.)
Reading Order for the Agent
app/common/composables/useAuthCookies.tsapp/common/store/authStore.tsapp/common/composables/useClient.tsapp/middleware/authenticated.global.tsapp/features/sso-callback/composable/ssoCallback.tsapp/plugins/auth.tsthenapp/plugins/pixel.tsnuxt.config.ts(plugins + runtimeConfig)configs/development.jsonapp/common/composables/useErrorHandler.spec.ts
Source Verification
| Claim | Evidence |
|---|---|
| HTTP client + 401 refresh singleton | useClient.ts:43 $fetch, :51 if (errorStatus === 401), :98 performTokenRefresh, :100 if (refreshPromise) |
| Token cookie keys | useAuthCookies.ts:5 "global_sso_token", :6 "global_sso_refresh_token", :30 useCookie("launchpad.sso_id") |
| Auth store + sso_id write | authStore.ts:30 defineStore("auth-profile"...), :139 LAUNCHPAD_SSO_ID.value = payload?.sso_id ?? "", :142 resetAuth |
| Launchpad is OAuth2 code flow | ssoCallback.ts:6 ...response_type=code&scope=sso:profile&redirect_uri=... ; mirrored in authenticated.global.ts:80 |
| Plugin registration/order | nuxt.config.ts:89-95 plugins array, auth.ts last |
| 3rd-party SDK init pattern | plugins/pixel.ts:1-10 defineNuxtPlugin, nuxtApp.vueApp.use(PixelPlugin, ...) |
| Flags surface via env spread | nuxt.config.ts:148 ...CONFIGENVIRONMENT.env into runtimeConfig.public |
| Config env files exist | configs/development.json, production.json (listed; no local.json in this repo — corrected) |
| Logout redirect target | authenticated.global.ts:109 window.location.replace(\${config.public.CHATPANEL_URL}/logout`)` |
| Test command + framework | package.json:16 "test": "vitest --dom --pool=forks", :19 vue-tsc --noEmit, specs in app/common/composables/*.spec.ts |
mekari-account-web-sdk not yet added | package.json:63-77 deps — not present yet, but available as a real git dependency mekari-account-web-sdk v0.3.0 (resolved — OQ-1; add via npm install git+https://<user>:<pass>@bitbucket.org/mid-kelola-indonesia/mekari-account-web-sdk#<version>) |
| Session Manager / Redis NOT in FE repos | absent from all 4 repos (→ OQ-2) |
current_company not called in Launchpad | no match in repo; company from authStore.ts:88 data.value.data → authProfile.company_id (→ OQ-4) |
Cross-Service Responsibility Map
| Step | Action | Owner |
|---|---|---|
Maintain mekari-account-web-sdk (git dependency, no CDN) | SDK package source | SSO/Account (BE) |
Session Manager sm.mekari.com/current + Redis + timeoutable | iframe page, session store | SSO/Account (BE) |
Gateway routing to sm.mekari.com + MAG | gateway | Platform/SSO |
CSP frame-src/child-src whitelist update mechanism | config repo | SSO/Account (BE) + infosec |
| Consume SDK events, gate behind flag, wire sign-out | Launchpad FE (this RFC) | AL squad |
| CRM / Hub / Hub Chat v2 wiring | per-repo | respective squads (deferred) |
Existing Contracts check (endpoints)
| Contract | Tag | Evidence / justification |
|---|---|---|
GET {apiBaseUrl}/users/me (profile + company) | reused | authStore.ts:81 — primary company source |
POST {apiBaseUrl}/seamless/refresh_sso_token | reused | useClient.ts:120, authenticated.global.ts:47 |
SSO /auth/?...response_type=code | reused | ssoCallback.ts:6 |
{SSO}/sign_out?client_id=... / CHATPANEL_URL/logout | reused | existing logout redirect |
sm.mekari.com/current (iframe, via SDK) | new-with-justification | provided by SDK iframe; Launchpad never calls directly — only consumes events (BE dep, OQ-2) |
SSO /v1.1/users/me/current_company | new-with-justification | PRD-specified for OAuth2 flow; reuse /users/me preferred unless OQ-4 requires it |
2.1 Current
Launchpad (ssr: false SPA, nuxt.config.ts:77) holds SSO tokens in cookies and
only re-validates by refreshing an expired access token in
authenticated.global.ts. No mechanism observes SSO logout or account switch.
2.2 Infrastructure / Deployment Topology
flowchart TB
user["User Browser"]
subgraph LPnet["launchpad.qontak.com / launchpad.mekari.io (SPA, static)"]
spa["Launchpad SPA (bundles mekari-account-web-sdk Session, no CDN)"]
end
subgraph smnet["sm.mekari.com (via gateway/MAG)"]
gw["Gateway /current"]
smcur["Session Manager /current (iframe page, Golang)"]
redis[("Dedicated Session Redis<br/>cache.t3.small, RDB")]
end
api["api.mekari.io Kong → Launchpad BE /users/me, /seamless/refresh_sso_token"]
user --> spa
spa -.iframe.-> gw --> smcur --> redis
smcur -.postMessage.-> spa
spa -->|profile/company, token refresh| api
Service use cases & third-party connections
| Service | Use cases (FE-relevant) | FE calls | 3rd-party |
|---|---|---|---|
| Launchpad SPA | render app; consume SDK events; gate by flag | /users/me, /seamless/refresh_sso_token, SSO /auth, /sign_out | mekari-account-web-sdk (bundled; owns the iframe internally) |
| Session Manager (BE dep) | validate session in Redis, update last_request_at, render ssoId | n/a (iframe, SDK-mediated) | Redis |
2.3 Database Model
No database changes (PRD §2 "Database Model: No database changes"). FE does
not add or manage a localStorage key — msli is owned entirely by the SDK
(§2.6). n/a — no schema.
2.4 SDK Contract (consumed by Launchpad)
Ground truth verified against mekari-account-web-sdk v0.3.0
(src/session.ts, src/index.ts, README.md):
import { Session } from "mekari-account-web-sdk"
const session = new Session({
currentUser: "<user sso ID>", // ADR-4: launchpad.sso_id — camelCase, NOT current_user
interval: 5 * 60 * 1000, // ms, >=1000; periodic re-check via hidden iframe reload (PRD 6.9). No session.refresh() API exists.
})
session.on("event", (data) => {
// "event" is the ONLY valid event name; per-status names are a silent no-op.
// Callback is single-arg — there is no second "error" parameter.
switch (data.status) {
// route by data.status — see table below
}
})
// Teardown: session.destroy() (the SDK is a singleton — a 2nd `new Session()`
// call returns the 1st instance and ignores new options; re-init requires
// destroy() first — R11).
Event (data.status) | Meaning | Launchpad action (OAuth2 flow, ADR-3/ADR-6) |
|---|---|---|
logged_in | SDK's internal check found data.ssoId === currentUser | continue; ensure company synced via /users/me (ADR-5) |
logged_out | any other case — a plain logout or an SSO account switch. The SDK cannot distinguish the two and never exposes the incoming/other user's ssoId to the consumer. | sign-out flow (reuse authenticated.global.ts:109 redirect). Any post-re-login company re-sync is a separate BE/SSO concern (ADR-5/OQ-4), not SDK-driven (R5/R10). |
server_down | the SDK's internal checkTimeout window (default 2000ms) elapsed with no iframe response, and its internal msli fallback (SDK-owned, 2h expiry) also lapsed | fail-open (PRD constraint 6.10) — take no destructive action, do not force sign-out; log + monitor only (§3) |
There is no
switch_userstatus in the real SDK. An earlier draft of this RFC modeled a dedicatedswitch_userevent, handler, sequence diagram, and state — all removed per the latest review SDK reconciliation (R5): the SDK'sStatustype is exactlylogged_in \| logged_out \| server_down.
2.5 State Surface Contract
| State | Source | Surfaced to | Where |
|---|---|---|---|
currentUser | launchpad.sso_id cookie / authProfile.sso_id | SDK constructor | useCentralizedSession |
| session status | SDK event data.status | route guard / handler | useCentralizedSession |
msli (SDK-internal, do not touch) | localStorage timestamp | SDK's own fallback logic only | owned by the SDK, not by Launchpad code |
| company context | /users/me → authProfile.company_id | UI | authStore (reused) |
2.6 Local Storage & Cookies
| Key | Type | Owner | Notes |
|---|---|---|---|
msli (Mekari Session Logged In) | localStorage timestamp, 2h expiry | SDK internally — set on logged_in, cleared on logged_out | Launchpad must not read or write this key. A consumer write would collide with the SDK's own checkTimeout fallback (R7). By the time a consumer would observe server_down, the SDK's msli is already stale — there is no consumer-side fallback predicate to build. |
_mekari_account | cookie on sm.mekari.com | SSO (Rails) | not readable by Launchpad JS (cross-domain); the SDK's own iframe consumes it internally — Launchpad has no fallback logic keyed on it |
global_sso_token / _refresh_token | cookie .qontak.* | Launchpad existing | useAuthCookies.ts:5-7 |
launchpad.sso_id | cookie | Launchpad existing | useAuthCookies.ts:30 — currentUser source |
2.7 Sequence Diagrams
Happy path — logged_in (OAuth2 flow)
sequenceDiagram
participant B as Browser (Launchpad SPA)
participant SDK as mekari-account-web-sdk Session
participant IF as SM iframe (sm.mekari.com)
participant SM as Session Manager
participant R as Session Redis
participant API as Launchpad BE (/users/me)
B->>SDK: new Session({currentUser: launchpad.sso_id, interval: 5*60*1000})
SDK->>IF: open hidden iframe sm.mekari.com/current
IF->>SM: GET /current
SM->>R: validate + update last_request_at (<2h)
R-->>SM: ok
SM-->>IF: render page w/ ssoId (cache max 5s)
IF-->>SDK: postMessage({source:'mekari-account-web-sdk', ssoId})
SDK->>SDK: ssoId === currentUser -> set internal msli=now
SDK-->>B: session.on("event") -> {status:'logged_in'}
B->>API: GET /users/me (sync company) [reused]
API-->>B: profile {company_id}
Note over B: continue, no redirect
Failure path — server_down (SDK-internal fallback exhausted)
sequenceDiagram
participant B as Browser (Launchpad SPA)
participant SDK as mekari-account-web-sdk Session
participant IF as SM iframe (sm.mekari.com)
B->>SDK: new Session({currentUser, interval})
SDK->>IF: reload hidden iframe sm.mekari.com/current
IF--xSDK: no postMessage within checkTimeout (default 2000ms)
SDK->>SDK: internal msli check (SDK-owned, NOT consumer-owned)
alt internal msli < 2h
SDK-->>B: session.on("event") -> {status:'logged_in'} (SDK-internal degrade)
else
SDK-->>B: session.on("event") -> {status:'server_down'}
B->>B: fail-open (PRD 6.10) - no destructive action, no forced sign-out — log + monitor
end
An earlier draft also modeled a
switch_usersequence (destroy session + re-auth + "user changed" toast). Removed per R5 — the real SDK has noswitch_userstatus. Behavioral intent is preserved via thelogged_outpath above: an SSO account switch runs the normal sign-out flow; any post-re-login company re-sync is a separate BE/SSO concern (ADR-5/OQ-4), not SDK-driven.
2.8 Session State Machine
stateDiagram-v2
[*] --> Initializing: page load, flag on
Initializing --> LoggedIn: event logged_in
Initializing --> LoggedOut: event logged_out (incl. SSO account switch - R5)
Initializing --> ServerDown: event server_down
LoggedIn --> LoggedOut: event logged_out (2h idle / SSO logout / SSO account switch)
ServerDown --> LoggedIn: SDK-internal msli<2h (SDK-owned, not consumer)
ServerDown --> ServerDown: fail-open - no destructive action (R7 / PRD 6.10)
LoggedOut --> [*]: redirect to sign-out
2.9 Branch & Skip Catalog
| Branch | Condition | Action | Owner |
|---|---|---|---|
| Flag off | centralized_session false | SDK not loaded; behavior unchanged | FE |
| Excluded pages | sso-callback, login, expired (authenticated.global.ts:11-20) | skip session check | FE |
logged_out (incl. SSO account switch) | SDK cannot distinguish — see §2.4 | sign-out flow | FE |
server_down | SDK exhausted its internal checkTimeout + internal msli fallback | fail-open (PRD 6.10) — no destructive action, no forced sign-out; log + monitor | FE |
3. High-Availability & Security
The HA/perf targets for sm.mekari.com/current (6k RPS, p95 < 100ms, Redis <
2ms) are backend-owned (PRD §3) — Launchpad does not host that endpoint. FE
security:
| Area | Control | OWASP |
|---|---|---|
| SDK/iframe origin | The SDK is bundled (no script-src entry needed — R2). Page CSP frame-src/child-src whitelists sm.mekari.com for the hidden iframe; SM-side frame-ancestors whitelists launchpad.* (whitelist update = BE config repo; canonical host resolved — R8) | A05 Misconfig |
| Token handling | Tokens stay in existing cookies; no new token storage; never log tokens (AGENTS.md) | A02 Crypto / A09 Logging |
postMessage | Open security finding (upstream). The SDK's own window message listener checks only event.data.source === "mekari-account-web-sdk" — it does not validate event.origin. Launchpad cannot mitigate this: the SDK owns the listener and Launchpad only consumes session.on("event", ...). Recorded as an infosec finding to the SDK owners (Account & Launchpad) — tracked at OQ-5, not a "confirm" item (R9). | A08 Integrity / A07 Ident. & Auth. Failures |
| XSS via iframe content | iframe renders empty page w/ ssoId only; no untrusted HTML injected into Launchpad | A03 Injection |
| Referrer/origin (alt) | optional referrer/origin validation at SM (BE) | A05 |
Monitoring (FE-side): count SDK event types + server_down rate via existing
analytics plugin (plugins/mixpanel.ts); alert on server_down spike. Use
console.error/console.warn for unexpected SDK states per AGENTS.md (no
console.log in committed code).
4. Backwards Compatibility & Rollout Plan
4.A Feature flag contract (ADR-1)
Add "centralized_session": false to configs/development.json and
configs/production.json env blocks (this repo has no configs/local.json
— corrected from an earlier draft); it surfaces at
runtimeConfig.public.centralized_session via the existing spread
(nuxt.config.ts:148). Default off → zero behavior change. Flip per-env to
pilot. (No env-var-only or remote-config system exists — ADR-1.)
4.B Pre-merge verification (from package.json)
| Step | Command | Source |
|---|---|---|
| Lint | pnpm run lint | package.json:14 |
| Type check | pnpm run type-check | package.json:19 |
| Unit tests | pnpm run test | package.json:16 |
| Build | pnpm run build | package.json:7 |
4.C Agent Execution Plan (ordered chunks)
Blocked prerequisite: chunks 2–5 import
mekari-account-web-sdk— a resolved, installable git dependency (OQ-1 resolved; R1/R2/R12) — but full end-to-end verification still requires the live Session Manager (sm.mekari.com/current, OQ-2). The agent can now implement chunks 1–5 for real (add the real dependency, wire the realSessionconstructor/events) and unit-test them against a mockedSessionwhere the live SM isn't reachable, but cannot run an end-to-end integration test until OQ-2 resolves.
Chunk 1 — Feature flag (Config)
- Files:
configs/development.json,configs/production.json,nuxt.config.ts(expose typed key if needed) - Commands:
pnpm run type-check && pnpm run test - Acceptance:
runtimeConfig.public.centralized_session === falseby default; flag-off path renders app unchanged (spec).
Chunk 2 — SDK loader plugin (ADR-2)
- Files:
package.json(addmekari-account-web-sdkgit dependency); newapp/plugins/centralized-session.ts; modifynuxt.config.ts:89-95(register afterauth.ts) - Commands:
pnpm run lint && pnpm run build - Acceptance: when flag on,
mekari-account-web-sdkSessioninstantiated once withcurrentUserfromlaunchpad.sso_idandintervalset; when off, not instantiated (spec asserts noSessionconstruction side-effect).
Chunk 3 — Event router composable (ADR-6)
- Files (new):
app/common/composables/useCentralizedSession.ts+.spec.ts - Commands:
pnpm run test -- app/common/composables/useCentralizedSession.spec.ts - Acceptance (TDD, red first): spec mocks the SDK's single
"event"subscription and asserts —logged_out→ sign-out redirect (covers both plain logout and an SSO account switch, since the SDK cannot distinguish them — R5);logged_in→ no redirect;server_down→ fail-open (no redirect, no destructive action — R7). All 3 branches covered.
Chunk 4 — sign-out cleanup wiring
- Files:
app/common/composables/useCentralizedSession.ts, reuseauthStore.resetAuth(authStore.ts:142), reuse logout redirect (authenticated.global.ts:109) - Commands:
pnpm run test && pnpm run type-check - Acceptance: spec asserts token cookies cleared before the sign-out redirect. (No
msliremoval here — the SDK owns and clearsmsliinternally onlogged_out; Launchpad must not touch it — R7.)
Chunk 5 — periodic re-validation + server_down fail-open
- Files:
app/plugins/centralized-session.ts(passinterval: 5*60*1000to theSessionconstructor — no separate refresh call exists),useCentralizedSession.ts - Commands:
pnpm run test - Acceptance:
Sessionconstructed once withintervalset (fake timers assert the SDK's own periodic iframe re-check fires — there is no Launchpad-drivenrefresh()call); onserver_down, spec asserts no sign-out and no destructive action (fail-open, PRD 6.10). (OQ-6 is moot — R6.)
Chunk 6 — pilot enable
- Files:
configs/production.json(flip flag for pilot cohort) - Commands:
pnpm run build - Acceptance: post-deploy SDK event metrics emit;
server_downrate within alert threshold.
4.D Verification & Rollback Recipe
Post-deploy signals: SDK event counts (mixpanel), server_down rate, no
spike in session-expired toasts; manual: SSO logout in another tab → Launchpad
tab redirects to sign-out within one navigation.
Rollback (ordered):
- Set
centralized_session: falseinconfigs/production.jsonand redeploy (SDK stops being constructed and its iframe stops opening; app reverts to current behavior). - Confirm no iframe request to
sm.mekari.com/currentin prod (the SDK bundle itself always ships in the JS bundle regardless of flag — R2 — butSessionis only constructed, and the iframe only opened, when the flag is on). - Confirm session-expired toast/redirect rate returns to baseline.
- Revert the chunk PRs if code-level rollback needed (FE-only; safe — no DB/back-compat coupling).
4.E Rollout stages (PRD §4, FE scope = step 4)
| Stage | Audience | Go/No-go |
|---|---|---|
| 4 Taking Off | Launchpad pilot cohort (flag on) | SDK events healthy, server_down < threshold |
| 5 Next Chapter | CRM/Hub/HubChat (deferred) | per-repo RFC; add domain to CSP whitelist |
5. Concerns / Open Questions
- OQ-1 [RESOLVED]
— resolved by the real SDKmekari-account-web-sdk(formerly cited as@mekari/sdk) is not inpackage.json(:63-77) and needs a published CDN URLmekari-account-web-sdkv0.3.0 (session.ts): install vianpm install git+https://<user>:<pass>@bitbucket.org/mid-kelola-indonesia/mekari-account-web-sdk#<version>. It is a bundled git dependency — there is no CDN, no/sm/sdk.js, no<script>tag. Unblocks chunks 2–5 for real implementation (§4.C). - OQ-2 [critical — still open] Session Manager service + Redis + gateway routing to
sm.mekari.comnot present in any FE repo; Launchpad cannot self-verify the iframepostMessagecontract end-to-end. Need: a stagingsm.mekari.com/currentendpoint to integration-test against. This is the sole remaining hard blocker for chunks 2–6 end-to-end (§7). - OQ-3 [RESOLVED]
PRD names two iframe paths —— resolved: the SDK's default/sm/currentvs/sessionmanager/currentsessionUrlishttps://sm.mekari.com/current(session.ts). This is the canonical path — notaccount.mekari.com/sm/current, not/sessionmanager/current. - OQ-4 [important — still open] Launchpad currently derives company from
/users/me(authStore.ts:88), not/v1.1/users/me/current_company. Confirm whether reuse suffices (ADR-5) or the PRD'scurrent_companyendpoint is required for OAuth2 flow. This is a genuine BE/SSO dependency, independent of the SDK — the SDK exposes no company data at all (R10). - OQ-5 [SECURITY FINDING — open, upstream] The SDK does not validate
event.origin; it checks onlyevent.data.source === "mekari-account-web-sdk"(session.ts). Launchpad cannot mitigate this — the SDK owns thewindowmessagelistener. Recorded as an open infosec finding to the SDK owners (Account & Launchpad), OWASP A08/A07 (§3). This is a resolved-negative finding to escalate, not a "confirm" question. - OQ-6 [MOOT]
— moot: there is nosession.refresh()throttle interval "TBD"refresh()API. Periodic re-validation is achieved via the constructor'sintervaloption (recommended5*60*1000ms per PRD constraint 6.9). - OQ-7 [MOOT]
stale— moot: there is nocurrentUser/switch_userpre-detectionswitch_userstatus. A different SSO user surfaces aslogged_outregardless of whatcurrentUserwas passed at construction, and the SDK never exposes the other user'sssoIdto the consumer (R5).
6. Comment logs
| Date | Author | Comment |
|---|---|---|
| 2026-06-27 | Syafrizal Muhammad | Initial draft (FE Launchpad pilot scope). |
| 2026-07-02 | latest review (SDK 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:
- G-D1/Dependency: OQ-1 (SDK package) and OQ-3 (canonical iframe path) are now resolved —
mekari-account-web-sdkv0.3.0 is a real, installable git dependency and the canonical path issm.mekari.com/current. The remaining hard blocker is OQ-2 — a live/staging Session Manager (sm.mekari.com/current) to integration-test thepostMessagecontract end-to-end. - Chunks 1–5 can now be implemented for real (real dependency, real
Sessionconstructor/events) and unit-tested against a mockedSession; only true end-to-end verification (chunk 6 pilot enable, and any assertion depending on a live SM response) is gated on OQ-2. - A separate, non-blocking dependency remains open: OQ-4 (
current_company/ company-sync endpoint host) — it does not block chunks 1–5 coding, only the final endpoint choice for thelogged_incompany-resync assertion.
Resolve OQ-2 (stand up a staging Session Manager) → re-run §7. Optionally hand to
rfc-reviewer for a second-pass score.