Task Breakdown — Centralized Web Session (Launchpad FE Integration)
Source RFC:
centralized-web-session.md(Account & Launchpad) · Slicing mode: Vertical (1 task = 1 story end-to-end) · Blocked tasks: included (full-picture). Target repo (verified):qontak-launchpad-fe— Nuxt 4 SPA (ssr: false), Vitest + happy-dom,pnpm run test=vitest --dom --pool=forks.
Reconnaissance notes (all RFC anchor paths verified against the repo):
- All files in the RFC §2.0 "Existing Code Anchors" table exist at the stated paths —
authenticated.global.ts,authStore.ts,ssoCallbackStore.ts,useAuthCookies.ts,useClient.ts,useToggleQontakOne.ts,ssoCallback.ts,SwitchAccountContent.vue,useAuthCookies.spec.ts,useErrorHandler.ts,nuxt.config.ts,app.vue,default.vue,plugins/auth.ts. - Net-new / blocked items confirmed absent by grep (matches RFC §2.0.1):
mekari-account-web-sdk,centralized_session,current_company. - Test convention: co-located
*.spec.ts, Vitest + happy-dom,vi.mock("#app")withvi.hoisted()spies (seeuseAuthCookies.spec.ts). Import alias is~/(e.g.~/common/composables/...). useClient<T>(url, opts)returns{ data: { value }, error: { value } }; response body is read atdata.value.data(useClient.ts:43,authStore.ts:88).- Toast pattern:
toast.notify({ position, variant, title })(@mekari/pixel3@1.0.8,authStore.ts:117) — reused elsewhere in the app but not wired by this feature: the "account changed" toast previously proposed here (Task 4) is deleted, since its trigger (switch_user) does not exist in the real SDK (R5). - SDK reconciled against real source (latest review, 2026-07-02): package is
mekari-account-web-sdkv0.3.0, installed as a bundled git dependency (npm install git+https://<user>:<pass>@bitbucket.org/mid-kelola-indonesia/mekari-account-web-sdk#<version>) — not npm-registry, not CDN. Constructor option iscurrentUser(camelCase). Subscription issession.on("event", (data) => { switch (data.status) { ... } })— the only valid event name is the literal"event"; there are exactly 3 statuses (logged_in,logged_out,server_down— noswitch_user). The SDK owns themslilocalStorage fallback internally; consumers must never read or write it. Q1 and Q5 (iframe hostsm.mekari.com/current) are RESOLVED by this verification — no longer open questions. - Current-company endpoint host (
/users/me/current_company) is[unverified — check repo]— does not exist in repo; host/owner is open question Q3 (genuine cross-layer BE dependency, unaffected by the SDK correction).
Effort Summary
| Story task | FE days | BE days | QA days | Total |
|---|---|---|---|---|
Task 1 — centralized_session toggle composable | 0.5 | — | 0.5 | 1.0 |
Task 2 — SDK loader plugin (instantiate Session) | 2 | — | 0.5 | 2.5 |
Task 3 — logged_out handler (useCentralizedSession) | 1.5 | — | 0.5 | 2.0 |
switch_user re-auth + "account changed" toast | DELETED (R5) | |||
| Task 5 — Middleware integration (await SDK, timeout) | 1 | — | 0.5 | 1.5 |
Task 6 — logged_in + current-company sync ⚠️ | 1.5 | 0.5 | 0.5 | 2.5 |
server_down + msli fallback | DELETED (R7) | |||
| Grand total (recomputed, latest review + BE grounding) | 6.5 | 0.5 | 2.5 | 9.5 |
BE grounding (latest review, verified against
qontak-launchpad, the Go/chi backend). Task 6's BE was reduced 2.0 → 0.5. The data the SDK needs already exists and is exposed:GET /users/mereturnssso_id+company_id+company_nameand live-syncs the company from Mekari SSO on every call (internal/app/service/users/get_info.go:63-83,138,147). So for Launchpad the "current-company sync" collapses to verification/wiring, not from-scratch BE — the 0.5 covers confirming the field feeds the SDK'scurrentUser/company seed. Caveat:/users/me'scompany_idis the external integer id, not the company SSO UUID — if a company SSO id is needed, that mapping is the only real BE task. Genuine net-new BE would appear only if the initiative later requires a selectable multi-company "current company" (Launchpad has no such concept today — company is single-valued on the user record).
Previous grand total was 15.5 (FE 10, BE 2, QA 3.5), including Task 4 (2.5) and Task 7 (2.0). Both are deleted: Task 4 (
switch_userre-auth + toast) because the real SDK has noswitch_userevent — an account switch surfaces aslogged_outand is already covered by Task 3, at no extra cost; Task 7 (server_down+mslifallback) because the SDK owns itsmslifallback internally and the product-level contract forserver_downis simply fail-open (PRD 6.10) — there is no consumer predicate or helper module to build. A consumerlocalStorage.setItem("msli", …)would collide with the SDK's ownmslikey and corrupt its internal fallback — this must never be implemented.Confidence: medium (was low). Only one [critical] blocker remains material to the estimate — Q3 (current-company endpoint host/owner; the 0.5 BE day in Task 6, reduced from 2.0 by BE grounding, is a placeholder — the Go backend already exposes and live-syncs company on
/users/me, so a dedicated BE proxy is likely unnecessary). Q1 (SDK distribution) and Q2 (server_downpredicate) are RESOLVED by real-SDK verification (latest review) and no longer add estimate risk. FE-only event handlers that reuse existing store actions (Tasks 1, 3, 5) are well-grounded and high-confidence.
Task 1: [FE] centralized_session toggle composable (Gate behind centralized_session)
A Launchpad engineer can flip one lever that turns all centralized-session behavior on or off; when off, the app behaves byte-for-byte as today.
Status: ✅ Actionable — Q4 (env-var vs backend source) only affects the internal TOGGLE_SOURCE default, not the composable's contract; ship with the env-var/const source the pilot prefers and leave the documented backend path as a TODO, exactly as useToggleQontakOne does.
Design reference: n/a — no UI; internal composable.
What to build
A new useCentralizedSessionToggle composable mirroring useToggleQontakOne.ts (module-singleton TOGGLE_SOURCE switch, ref + lazy init) that resolves to a boolean the plugin and middleware consult before doing any SDK work.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/common/composables/useCentralizedSessionToggle.ts | singleton toggle mirroring useToggleQontakOne; TOGGLE_SOURCE = "env" default for pilot, backend path stubbed |
| create | app/common/composables/useCentralizedSessionToggle.spec.ts | off → returns false (no side effects); on → true |
Implementation steps
- Explore — Open
app/common/composables/useToggleQontakOne.tsand copy its exact structure (module-levelTOGGLE_SOURCEconst, singletonref(false)+initializedref, lazyinitializeToggle()). - Write failing tests (red) — Create
app/common/composables/useCentralizedSessionToggle.spec.ts; mock#app(useRuntimeConfig) per theuseAuthCookies.spec.tsvi.hoisted()pattern. Assert: source off ⇒ exposed ref isfalse; source on ⇒true. Runpnpm run test -- app/common/composables/useCentralizedSessionToggle.spec.tsand confirm red. - Scaffold — Create
useCentralizedSessionToggle.tswith the singleton refs and the exporteduseCentralizedSessionToggle()returning{ centralizedSessionEnabled }. - Wire source — Implement
initializeToggle()readinguseRuntimeConfig().publicenv flag (e.g.CENTRALIZED_SESSION_ENABLED); keep abackendbranch stubbed returningPromise.resolve(false)(TODO, likeuseToggleQontakOne). - Implement behavior — Lazy-init on first use; idempotent.
- Go green —
pnpm run test -- app/common/composables/useCentralizedSessionToggle.spec.ts. - Quality gate —
pnpm run lint && pnpm run type-check && pnpm run build.
Acceptance criteria
- Toggle off → composable returns
false; on → returnstrue. - No SDK side effects occur when the toggle is off (asserted by the plugin/middleware consuming this — see Tasks 2, 5).
- Structure mirrors
useToggleQontakOne(singleton, lazy init).
Test strategy
Vitest + happy-dom, vi.mock("#app") for useRuntimeConfig. Key assertion: the exposed Ref<boolean> resolves to the value implied by TOGGLE_SOURCE; toggling the mocked env flag flips it.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | 0.5 |
| Total | 1.0 |
Assumptions: direct clone of the existing
useToggleQontakOnepattern; env-var source for the pilot (Q4) — no backend call built now.
Run to verify
pnpm run test -- app/common/composables/useCentralizedSessionToggle.spec.ts && pnpm run lint
Depends on
- None (unblocks every other task).
Task 2: [FE] SDK loader plugin — instantiate Session({currentUser}) (Load SDK with user_sso_id)
When centralized session is on, Launchpad loads the shared
mekari-account-web-sdkSession on page load with the current user's SSO id, so the app receives authoritative session events.
Status: ✅ Actionable — not blocked. Q1 (SDK distribution) and Q5 (iframe path) are RESOLVED via real-SDK verification (latest review): the package is mekari-account-web-sdk v0.3.0, installed as a bundled git dependency and imported directly (import { Session } from "mekari-account-web-sdk") — no CDN, no adapter/swap layer needed. The iframe host defaults to https://sm.mekari.com/current.
Design reference: n/a — no UI surface; iframe is injected by the SDK.
What to build
A new client-only Nuxt plugin mekariSession.client.ts that, only when the toggle is on, reads launchpad.sso_id (via useAuthCookies) and instantiates new Session({ currentUser, interval: 5 * 60 * 1000 }) (periodic re-check per PRD constraint 6.9), then subscribes once via session.on("event", (data) => { ... }) and forwards data.status into useCentralizedSession (Task 3+). The SDK is a singleton (a 2nd new Session() call returns the 1st instance and ignores new options), so teardown on toggle-flip/unmount must call session.destroy(), not just session.off().
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/plugins/mekariSession.client.ts | toggle-gated SDK load + new Session({currentUser}) + session.on("event", handler) wiring to useCentralizedSession |
| create | app/plugins/mekariSession.client.spec.ts | toggle on + launchpad.sso_id set ⇒ Session constructed with that id (mock SDK); toggle off ⇒ SDK never loaded |
| modify | package.json | add mekari-account-web-sdk as a git dependency: "mekari-account-web-sdk": "git+https://<user>:<pass>@bitbucket.org/mid-kelola-indonesia/mekari-account-web-sdk#<version>" |
Implementation steps
- Explore — Open
app/plugins/auth.tsto copy the plugin export shape (defineNuxtPlugin), andapp/common/composables/useAuthCookies.ts:30to seeLAUNCHPAD_SSO_ID(cookielaunchpad.sso_id). - Write failing tests (red) — Create
app/plugins/mekariSession.client.spec.ts;vi.mock("mekari-account-web-sdk")with a fakeSessionclass andvi.mock("#app"). Assert constructor called with{ currentUser: <sso_id> }when toggle on; not called when off; assertsession.onwas called exactly once with"event". Runpnpm run test -- app/plugins/mekariSession.client.spec.ts— red. - Scaffold — Create
mekariSession.client.tswithdefineNuxtPlugin; early-return whenuseCentralizedSessionToggle().centralizedSessionEnabledis false. - Wire SDK —
import { Session } from "mekari-account-web-sdk". ReadcurrentUserfromuseAuthCookies().LAUNCHPAD_SSO_ID.value. - Implement behavior —
const session = new Session({ currentUser, interval: 5 * 60 * 1000 }); registersession.on("event", (data) => { switch (data.status) { case "logged_in": ...; case "logged_out": ...; case "server_down": ...; } })delegating each case touseCentralizedSession(). Do not register per-status listeners ("logged_in","logged_out","server_down"are not valid event names — the SDK's only event name is the literal"event"; anything else is a silent no-op). On teardown, callsession.destroy(). - Go green —
pnpm run test -- app/plugins/mekariSession.client.spec.ts. - Quality gate —
pnpm run lint && pnpm run type-check && pnpm run build.
Acceptance criteria
- Toggle on +
launchpad.sso_idpresent ⇒new Session({currentUser})called with that id (mock SDK). - Toggle off ⇒ SDK is never loaded / constructed.
-
session.onis called exactly once, with the literal string"event"(never a per-status name). - Init is idempotent (no duplicate
Sessionconstruction on re-entry — also enforced by the SDK's own singleton behavior). - Teardown calls
session.destroy()(not justoff()).
Test strategy
Vitest + happy-dom; mock the mekari-account-web-sdk Session constructor (spy) and useAuthCookies. Key assertion: constructor invocation + the id passed; the single "event" subscription; off-path asserts zero constructions. Real network/iframe is out of unit scope.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 2 |
| Backend | — |
| QA | 0.5 |
| Total | 2.5 |
Assumptions: direct import of the real SDK (no adapter/swap layer needed now that distribution is resolved — latest review). Confidence raised from "low" (unknown API) to normal, since the constructor, event, and destroy contracts are now verified against
session.ts. No iframe/network asserted in unit tests.
Run to verify
pnpm run test -- app/plugins/mekariSession.client.spec.ts && pnpm run lint
Depends on
- [Task 1] (toggle). No external blockers — Q1 and Q5 resolved (latest review).
Task 3: [FE] logged_out handler — useCentralizedSession (Handle logged_out)
When SSO reports the user is logged out (explicit logout or 2h idle timeout), Launchpad revokes its own tokens and signs the user out to SSO.
Status: ✅ Actionable — reuses existing clearTokenLaunchpad, resetAuth, and the verified sign-out redirect; no external contract needed.
Design reference: n/a — no UI (redirect only).
What to build
The new useCentralizedSession.ts composable, beginning with its logged_out reaction: clear tokens, reset auth state, redirect to SSO_URL/sign_out?client_id=. This same branch also handles SSO account switches — the real SDK reports a switch as logged_out (it never exposes a dedicated switch_user status or the incoming ssoId, R5), so no separate case is needed.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/common/composables/useCentralizedSession.ts | event-dispatch composable; logged_out → clearTokenLaunchpad() + resetAuth() + redirect to SSO sign_out |
| create | app/common/composables/useCentralizedSession.spec.ts | logged_out calls both store resets + sets window.location to SSO_URL/sign_out?client_id=... |
Implementation steps
- Explore — Read
app/common/store/ssoCallbackStore.ts:46(clearTokenLaunchpad),app/common/store/authStore.ts:142(resetAuth), andapp/layouts/components/SwitchAccountContent.vue:110-117for the exact sign-out URL (${SSO_URL}/sign_out?client_id=${SSO_UNIFIED_CLIENT_ID}). - Write failing tests (red) — Create
useCentralizedSession.spec.ts; mock the two Pinia stores (@pinia/testing) anduseRuntimeConfig. Stub a{ status: "logged_out" }event and assertclearTokenLaunchpad+resetAuthcalled andwindow.location.hrefset. Runpnpm run test -- app/common/composables/useCentralizedSession.spec.ts— red. - Scaffold — Create
useCentralizedSession.tsexporting ahandleSessionEvent(data: { status })dispatcher (single-arg, matching the SDK'ssession.on("event", (data) => ...)callback — there is no seconderrorargument); implement thelogged_outbranch only. - Wire state — Import
useSsoCallbackStore(clearTokenLaunchpad) anduseAuthStore(resetAuth) from~/common/store/...; readSSO_URL/SSO_UNIFIED_CLIENT_IDfromuseRuntimeConfig().public. - Implement behavior — On
data.status === "logged_out":clearTokenLaunchpad()→resetAuth()→window.location.href = \${SSO_URL}/sign_out?client_id=${SSO_UNIFIED_CLIENT_ID}``. - Go green —
pnpm run test -- app/common/composables/useCentralizedSession.spec.ts. - Quality gate —
pnpm run lint && pnpm run type-check && pnpm run build.
Acceptance criteria
-
logged_outstatus ⇒clearTokenLaunchpad()called. -
logged_outstatus ⇒resetAuth()called (cookies cleared via existing reset). - Browser redirected to
SSO_URL/sign_out?client_id=<SSO_UNIFIED_CLIENT_ID>. - The same branch is exercised whether the
logged_outstatus originated from an explicit logout, idle timeout, or an SSO account switch — no separate handling exists or is needed (R5).
Test strategy
Vitest + happy-dom; @pinia/testing for store action spies, vi.mock("#app") for config. Key assertion: ordered calls to clearTokenLaunchpad → resetAuth and the final window.location.href value.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | — |
| QA | 0.5 |
| Total | 2.0 |
Assumptions: pure reuse of existing store actions and the verified sign-out URL; this task also establishes the
useCentralizedSessiondispatcher that Tasks 5 and 6 extend (was "Tasks 4, 6, 7" — Tasks 4 and 7 are deleted, latest review).
Run to verify
pnpm run test -- app/common/composables/useCentralizedSession.spec.ts && pnpm run lint
Depends on
- [Task 1] (toggle gating). Establishes
useCentralizedSessionconsumed by Task 2.
Task 4: DELETED (latest review, R5)
Originally:
[FE] switch_user re-auth + "account changed" toast (Handle switch_user).
Status: 🗑️ Deleted, not blocked. Real-SDK verification (mekari-account-web-sdk v0.3.0, session.ts) confirms there is no switch_user status — the SDK's Status type is exactly logged_in | logged_out | server_down. An SSO account switch is delivered as logged_out (the SDK cannot distinguish a switch from a plain logout and never exposes the incoming user's ssoId). That case is already fully handled by Task 3 — no dedicated re-auth flow, "account changed" toast, or post-switch marker plumbing into ssoCallback.ts is needed or should be built. The ~2.5 FE days previously estimated here (2 FE + 0.5 QA) are removed from the Effort Summary; see the recomputed grand total above.
If a future product requirement wants to surface "your account changed" copy specifically for the switch case (as opposed to a generic sign-out), that would require a new BE/SSO signal distinguishing switch from logout — out of scope for this SDK integration and not something the FE can infer today.
Task 5: [FE] Middleware integration — await SDK resolution with timeout (Middleware wiring)
When the toggle is on, the route guard waits for the SDK's initial session resolution (with a hard timeout) before allowing navigation; when off, the existing auth flow is unchanged.
Status: ✅ Actionable — touches the shared middleware (medium reversibility per ADR-4), but the await + timeout + toggle-gate logic needs no external contract. Reuses the existing single auth gate.
Design reference: [REQUIRED: confirm with design whether a loading state is needed] (RFC §1.3 / nice-to-have OQ) — n/a, design pending; ship without a visible loading state unless design confirms one.
What to build
Modify authenticated.global.ts so that, when the toggle is on, it awaits the initial SDK resolution exposed by useCentralizedSession with a client-side timeout that falls through to the server_down path; when off, the path is byte-for-byte unchanged.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| modify | app/middleware/authenticated.global.ts | toggle-gated await of initial SDK resolution + hard timeout; off → unchanged |
| create | app/middleware/authenticated.global.spec.ts | toggle on ⇒ awaits resolution; off ⇒ existing path unchanged; timeout ⇒ falls through |
Implementation steps
- Explore — Read
app/middleware/authenticated.global.tsfully: the excluded-pages list (:11-13sso-callback, etc.), the refresh path (:43), the unified-logout path (:89+), and the redirect builder (:79). Note where to insert the await without breaking excluded pages. - Write failing tests (red) — Create
authenticated.global.spec.ts(if neighboring middleware tests exist follow their setup; else mock#app+ the toggle +useCentralizedSession). Assert: toggle on ⇒ middleware awaits the resolution promise; toggle off ⇒ resolution never awaited; timeout ⇒ control proceeds toserver_downhandling. Run — red. - Scaffold — Add a toggle check at the top of the existing middleware body using
useCentralizedSessionToggle. - Wire state — Import
useCentralizedSessionto obtain anawaitInitialResolution(timeoutMs)promise (add this method to the composable). Skip on excluded routes and when toggle off. - Implement behavior —
if (centralizedSessionEnabled && !isExcluded) await Promise.race([resolution, timeout]); on timeout, treat it asserver_downand fail open per PRD 6.10 (no destructive action — this is a one-line log/observe branch inuseCentralizedSession, not a dedicated task; see the deleted Task 7 note below). Preserve the existing refresh/logout branches below untouched. - Go green —
pnpm run test -- app/middleware/authenticated.global.spec.tsand re-run any existing middleware specs to confirm the off-path stays green. - Quality gate —
pnpm run lint && pnpm run type-check && pnpm run build.
Acceptance criteria
- Toggle on ⇒ middleware awaits the initial SDK resolution before allowing navigation.
- A slow
sm.mekari.com/currentcannot hang navigation: a hard client-side timeout falls through (toserver_down, fail-open). - Toggle off ⇒ middleware path is byte-for-byte the current flow (existing specs still green).
- Excluded pages (login /
sso-callback) are never gated by the SDK await.
Test strategy
Vitest + happy-dom; mock the toggle and useCentralizedSession.awaitInitialResolution. Key assertion: presence/absence of the await per toggle state, and that Promise.race resolves on timeout. The off-path regression is guarded by re-running existing middleware specs.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1 |
| Backend | — |
| QA | 0.5 |
| Total | 1.5 |
Assumptions: the timeout + race is small; risk is in not regressing the shared gate, mitigated by the off-path test. Depends on the
useCentralizedSessionresolution promise existing.
Run to verify
pnpm run test -- app/middleware/authenticated.global.spec.ts && pnpm run lint
Depends on
- [Task 1] (toggle) · [Task 3] (
useCentralizedSessionexists; addawaitInitialResolution)
Task 6: [FE+BE] logged_in + current-company sync (Handle logged_in, Current-company sync)
After SSO confirms the session, Launchpad fetches/sets the user's authoritative current company so the displayed company always matches SSO.
Status: 🚫 Blocked — Q3 [critical]: the /users/me/current_company endpoint does not exist in the repo and has no verified FE-reachable host/owner (Launchpad BE proxy vs direct api.mekari.com with CORS/Kong is undecided). The company-sync core — the actual point of this story — cannot be built or tested without the endpoint contract. Unblock: Launchpad BE confirms the host + auth + payload shape for current-company (Q3). The 0.5 BE day below is a placeholder; BE grounding found the Go backend already exposes and live-syncs company on /users/me (get_info.go:60-83,138,147), so a new BE proxy is likely unnecessary (see Effort estimate). (This task no longer includes any msli write — see note below, R7.)
Design reference: n/a — no new UI (data sync only; company surfaced through existing store).
What to build
Extend useCentralizedSession.ts for the logged_in status to trigger company sync, and add useCurrentCompany.ts calling the current-company endpoint via useClient and setting it on the store. msli is NOT written here or anywhere by Launchpad — the real SDK owns that localStorage key internally (set on logged_in, cleared on logged_out); a consumer write would collide with and corrupt the SDK's own fallback bookkeeping (R7). The original plan to write msli=now on logged_in is removed.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/common/composables/useCurrentCompany.ts [unverified — endpoint host pending Q3] | useClient GET current-company; set on store |
| create | app/common/composables/useCurrentCompany.spec.ts | mocked useClient returns company ⇒ store updated; error routed via useErrorHandler |
| extend | app/common/composables/useCentralizedSession.ts | logged_in → call useCurrentCompany().sync() (no msli write) |
| extend | app/common/composables/useCentralizedSession.spec.ts | logged_in triggers company sync |
| create (?) | Launchpad BE current-company proxy [unverified — pending Q3] | only if FE cannot reach SSO directly |
Implementation steps
- Explore — Read
app/common/store/authStore.ts:81(existing/users/mecall viaapiBaseUrl) andapp/common/composables/useClient.ts:9,43(useClient<T>returns{data:{value}}, body atdata.value.data); readuseErrorHandler.tsfor the error-routing pattern. - Resolve Q3 first — Confirm the endpoint host/owner. Do not hardcode
api.mekari.comfrom the FE without confirming CORS/Kong routing (RFC §2.5). If a BE proxy is needed, that BE work is part of this task. - Write failing tests (red) — Create
useCurrentCompany.spec.ts: mockuseClientto return a company payload, assert the store setter is called; mock an error and assert it routes throughuseErrorHandler. ExtenduseCentralizedSession.spec.ts:data.status === "logged_in"⇒sync()called. Run — red. - Scaffold — Create
useCurrentCompany.tswith async()callinguseClient(() => \${host}/users/me/current_company`, { method: "GET" })`. - Wire state — Read
data.value.data, set the company on the appropriate store; routeerror.valuethroughuseErrorHandler. Add thelogged_inbranch inuseCentralizedSession: calluseCurrentCompany().sync()only — do NOT touchlocalStorage["msli"](R7). - Go green —
pnpm run test -- app/common/composables/useCurrentCompany.spec.ts app/common/composables/useCentralizedSession.spec.ts. - Quality gate —
pnpm run lint && pnpm run type-check && pnpm run build.
Acceptance criteria
-
logged_instatus ⇒ current company fetched viauseClientand set on the store. - API errors routed through
useErrorHandler(no raw throws, no token logging). - (pending Q3) endpoint host/owner confirmed; FE does not hardcode an unrouted host.
- No test or implementation code reads or writes
localStorage["msli"]— that key is exclusively owned by the SDK (R7).
Test strategy
Vitest + happy-dom; mock useClient (success + error). Key assertion: store setter called with data.value.data on logged_in. The real host is mocked until Q3 resolves.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | 0.5 |
| QA | 0.5 |
| Total | 2.5 |
Assumptions: BE reduced 2.0→0.5 (grounded against
qontak-launchpad, the Go backend) —/users/mealready returnssso_id+company_id+company_nameand live-syncs company from SSO (internal/app/service/users/get_info.go:60-83,138,147), so the sync is verification/wiring, not a new proxy. The 0.5 covers confirming the field feeds the SDK seed (caveat:company_idis the external integer id, not the SSO UUID). Real net-new BE appears only if a selectable multi-company "current company" is later required (Launchpad has none today). The FE-days estimate is unchanged by removing the (trivial, and incorrect)msli-write step.
Run to verify
pnpm run test -- app/common/composables/useCurrentCompany.spec.ts app/common/composables/useCentralizedSession.spec.ts && pnpm run lint
Depends on
- [Task 3] (dispatcher) · [External: current-company endpoint host/owner — Q3 (critical, pending)]
Task 7: DELETED (latest review, R7)
Originally:
[FE] server_down + msli fallback (Handle server_down, msli helper).
Status: 🗑️ Deleted, not blocked. Real-SDK verification (mekari-account-web-sdk v0.3.0, session.ts) confirms the SDK owns its msli localStorage fallback (2h expiry) internally — it sets msli on logged_in, clears it on logged_out, and uses it itself to decide whether a checkTimeout lapse should emit logged_in (fresh) or server_down (stale/absent). There is no FE-readable predicate to build, and no useMsli.ts helper should exist: a consumer localStorage.setItem("msli", …) would collide with and corrupt the SDK's own bookkeeping, silently breaking its fallback for every product on the shared session. The _mekari_account/global_sso_valid_until predicate the original task proposed is likewise removed — it depended on a premise (a consumer-evaluable fallback) that the real SDK does not support or need.
The actual product-level contract for server_down is simply fail-open (PRD 6.10): take no destructive action, do not force sign-out, log an observability counter. This is implemented as a one-line branch inside useCentralizedSession (Task 3's dispatcher extended in Task 5's middleware timeout path) — it does not warrant a separate task, helper module, or spec file. The ~2.0 days previously estimated here (1.5 FE + 0.5 QA) are removed from the Effort Summary.
Ordering rationale
- Build the spine first, in this order: Task 1 (toggle) → Task 3 (
useCentralizedSessiondispatcher) → Task 2 (SDK loader) → Task 5 (middleware). (Was "... → Task 4 (switch_user) → Task 5 ..." — Task 4 is deleted, latest review.) The toggle gates everything; the dispatcher composable is the shared event sink that Tasks 2, 5, 6 all wire into, so it must land early even though the RFC's chunk order lists the loader first. - Only one [critical] open question remains on the critical path, not three. Q1 (SDK distribution) and Q2 (
server_downpredicate) are RESOLVED by real-SDK verification (latest review) — they were correctness errors in the original contract, not genuine open decisions. Only Q3 (current-company endpoint) still gates a task (Task 6). Tasks 1, 2, 3, 5 are fully actionable today and deliver a working logout experience (including account switches, which the SDK reports aslogged_out— R5) behind the toggle. They are the safe, demonstrable pilot slice. - Task 2 (SDK loader) is no longer a gating dependency for end-to-end behavior. Q1 (bundled git dependency) and Q5 (
sm.mekari.com/current) are resolved — the realmekari-account-web-sdkpackage can be imported directly, no adapter/swap layer or mocked-SDK placeholder is needed. - Task 6 is the only remaining blocked task. Push externally to close Q3 (current-company endpoint host/owner — Launchpad BE); it is the one item that keeps the pilot from being feature-complete. (Task 7, previously blocked on Q2, is deleted — not merely unblocked, R7.)
- Lowest-risk demonstrable milestone: Tasks 1+2+3+5 give a pilot that correctly ends sessions on SSO logout (including account switches) — shippable behind the toggle using the real SDK, with no mocking or external dependency required beyond the SDK package install itself.
Skipped stories
(Full-scope mode: remaining blocked tasks and their unblock conditions. Tasks 1, 2, 3, 5 are fully actionable and appear above; Tasks 4 and 7 are deleted, not blocked — see their sections above.)
| Story / Task | Reason (unblock condition) |
|---|---|
Task 6 — logged_in + current-company sync | 🚫 Blocked on Q3 [critical] — /users/me/current_company endpoint host/owner unresolved (Launchpad BE proxy vs direct api.mekari.com via Kong/CORS). Unblock: Launchpad BE confirms host + auth + payload shape. Unaffected by the SDK correction (R10). |
switch_user re-auth + toast | DELETED (R5), not blocked — no switch_user event exists in the real SDK; fully covered by Task 3's logged_out handling. |
server_down + msli fallback | DELETED (R7), not blocked — the SDK owns msli internally; there is no FE-readable predicate to define. server_down is a fail-open, one-line branch (PRD 6.10), not a task. |
No longer blocked — Q1 (bundled git dependency) and Q5 (sm.mekari.com/current) resolved via real-SDK verification (latest review); see Task 2 above. |