Skip to main content

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") with vi.hoisted() spies (see useAuthCookies.spec.ts). Import alias is ~/ (e.g. ~/common/composables/...).
  • useClient<T>(url, opts) returns { data: { value }, error: { value } }; response body is read at data.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-sdk v0.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 is currentUser (camelCase). Subscription is session.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 — no switch_user). The SDK owns the msli localStorage fallback internally; consumers must never read or write it. Q1 and Q5 (iframe host sm.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 taskFE daysBE daysQA daysTotal
Task 1 — centralized_session toggle composable0.50.51.0
Task 2 — SDK loader plugin (instantiate Session)20.52.5
Task 3 — logged_out handler (useCentralizedSession)1.50.52.0
Task 4 — switch_user re-auth + "account changed" toast20.5DELETED (R5)
Task 5 — Middleware integration (await SDK, timeout)10.51.5
Task 6 — logged_in + current-company sync ⚠️1.50.50.52.5
Task 7 — server_down + msli fallback1.50.5DELETED (R7)
Grand total (recomputed, latest review + BE grounding)6.50.52.59.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/me returns sso_id + company_id + company_name and 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's currentUser/company seed. Caveat: /users/me's company_id is 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_user re-auth + toast) because the real SDK has no switch_user event — an account switch surfaces as logged_out and is already covered by Task 3, at no extra cost; Task 7 (server_down + msli fallback) because the SDK owns its msli fallback internally and the product-level contract for server_down is simply fail-open (PRD 6.10) — there is no consumer predicate or helper module to build. A consumer localStorage.setItem("msli", …) would collide with the SDK's own msli key 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_down predicate) 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

ActionFileWhat changes
createapp/common/composables/useCentralizedSessionToggle.tssingleton toggle mirroring useToggleQontakOne; TOGGLE_SOURCE = "env" default for pilot, backend path stubbed
createapp/common/composables/useCentralizedSessionToggle.spec.tsoff → returns false (no side effects); on → true

Implementation steps

  1. Explore — Open app/common/composables/useToggleQontakOne.ts and copy its exact structure (module-level TOGGLE_SOURCE const, singleton ref(false) + initialized ref, lazy initializeToggle()).
  2. Write failing tests (red) — Create app/common/composables/useCentralizedSessionToggle.spec.ts; mock #app (useRuntimeConfig) per the useAuthCookies.spec.ts vi.hoisted() pattern. Assert: source off ⇒ exposed ref is false; source on ⇒ true. Run pnpm run test -- app/common/composables/useCentralizedSessionToggle.spec.ts and confirm red.
  3. Scaffold — Create useCentralizedSessionToggle.ts with the singleton refs and the exported useCentralizedSessionToggle() returning { centralizedSessionEnabled }.
  4. Wire source — Implement initializeToggle() reading useRuntimeConfig().public env flag (e.g. CENTRALIZED_SESSION_ENABLED); keep a backend branch stubbed returning Promise.resolve(false) (TODO, like useToggleQontakOne).
  5. Implement behavior — Lazy-init on first use; idempotent.
  6. Go greenpnpm run test -- app/common/composables/useCentralizedSessionToggle.spec.ts.
  7. Quality gatepnpm run lint && pnpm run type-check && pnpm run build.

Acceptance criteria

  • Toggle off → composable returns false; on → returns true.
  • 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

DisciplineDays
Frontend0.5
Backend
QA0.5
Total1.0

Assumptions: direct clone of the existing useToggleQontakOne pattern; 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-sdk Session 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

ActionFileWhat changes
createapp/plugins/mekariSession.client.tstoggle-gated SDK load + new Session({currentUser}) + session.on("event", handler) wiring to useCentralizedSession
createapp/plugins/mekariSession.client.spec.tstoggle on + launchpad.sso_id set ⇒ Session constructed with that id (mock SDK); toggle off ⇒ SDK never loaded
modifypackage.jsonadd 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

  1. Explore — Open app/plugins/auth.ts to copy the plugin export shape (defineNuxtPlugin), and app/common/composables/useAuthCookies.ts:30 to see LAUNCHPAD_SSO_ID (cookie launchpad.sso_id).
  2. Write failing tests (red) — Create app/plugins/mekariSession.client.spec.ts; vi.mock("mekari-account-web-sdk") with a fake Session class and vi.mock("#app"). Assert constructor called with { currentUser: <sso_id> } when toggle on; not called when off; assert session.on was called exactly once with "event". Run pnpm run test -- app/plugins/mekariSession.client.spec.ts — red.
  3. Scaffold — Create mekariSession.client.ts with defineNuxtPlugin; early-return when useCentralizedSessionToggle().centralizedSessionEnabled is false.
  4. Wire SDKimport { Session } from "mekari-account-web-sdk". Read currentUser from useAuthCookies().LAUNCHPAD_SSO_ID.value.
  5. Implement behaviorconst session = new Session({ currentUser, interval: 5 * 60 * 1000 }); register session.on("event", (data) => { switch (data.status) { case "logged_in": ...; case "logged_out": ...; case "server_down": ...; } }) delegating each case to useCentralizedSession(). 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, call session.destroy().
  6. Go greenpnpm run test -- app/plugins/mekariSession.client.spec.ts.
  7. Quality gatepnpm run lint && pnpm run type-check && pnpm run build.

Acceptance criteria

  • Toggle on + launchpad.sso_id present ⇒ new Session({currentUser}) called with that id (mock SDK).
  • Toggle off ⇒ SDK is never loaded / constructed.
  • session.on is called exactly once, with the literal string "event" (never a per-status name).
  • Init is idempotent (no duplicate Session construction on re-entry — also enforced by the SDK's own singleton behavior).
  • Teardown calls session.destroy() (not just off()).

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

DisciplineDays
Frontend2
Backend
QA0.5
Total2.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

ActionFileWhat changes
createapp/common/composables/useCentralizedSession.tsevent-dispatch composable; logged_outclearTokenLaunchpad() + resetAuth() + redirect to SSO sign_out
createapp/common/composables/useCentralizedSession.spec.tslogged_out calls both store resets + sets window.location to SSO_URL/sign_out?client_id=...

Implementation steps

  1. Explore — Read app/common/store/ssoCallbackStore.ts:46 (clearTokenLaunchpad), app/common/store/authStore.ts:142 (resetAuth), and app/layouts/components/SwitchAccountContent.vue:110-117 for the exact sign-out URL (${SSO_URL}/sign_out?client_id=${SSO_UNIFIED_CLIENT_ID}).
  2. Write failing tests (red) — Create useCentralizedSession.spec.ts; mock the two Pinia stores (@pinia/testing) and useRuntimeConfig. Stub a { status: "logged_out" } event and assert clearTokenLaunchpad + resetAuth called and window.location.href set. Run pnpm run test -- app/common/composables/useCentralizedSession.spec.ts — red.
  3. Scaffold — Create useCentralizedSession.ts exporting a handleSessionEvent(data: { status }) dispatcher (single-arg, matching the SDK's session.on("event", (data) => ...) callback — there is no second error argument); implement the logged_out branch only.
  4. Wire state — Import useSsoCallbackStore (clearTokenLaunchpad) and useAuthStore (resetAuth) from ~/common/store/...; read SSO_URL/SSO_UNIFIED_CLIENT_ID from useRuntimeConfig().public.
  5. Implement behavior — On data.status === "logged_out": clearTokenLaunchpad()resetAuth()window.location.href = \${SSO_URL}/sign_out?client_id=${SSO_UNIFIED_CLIENT_ID}``.
  6. Go greenpnpm run test -- app/common/composables/useCentralizedSession.spec.ts.
  7. Quality gatepnpm run lint && pnpm run type-check && pnpm run build.

Acceptance criteria

  • logged_out status ⇒ clearTokenLaunchpad() called.
  • logged_out status ⇒ 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_out status 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 clearTokenLaunchpadresetAuth and the final window.location.href value.

Effort estimate

DisciplineDays
Frontend1.5
Backend
QA0.5
Total2.0

Assumptions: pure reuse of existing store actions and the verified sign-out URL; this task also establishes the useCentralizedSession dispatcher 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 useCentralizedSession consumed 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

ActionFileWhat changes
modifyapp/middleware/authenticated.global.tstoggle-gated await of initial SDK resolution + hard timeout; off → unchanged
createapp/middleware/authenticated.global.spec.tstoggle on ⇒ awaits resolution; off ⇒ existing path unchanged; timeout ⇒ falls through

Implementation steps

  1. Explore — Read app/middleware/authenticated.global.ts fully: the excluded-pages list (:11-13 sso-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.
  2. 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 to server_down handling. Run — red.
  3. Scaffold — Add a toggle check at the top of the existing middleware body using useCentralizedSessionToggle.
  4. Wire state — Import useCentralizedSession to obtain an awaitInitialResolution(timeoutMs) promise (add this method to the composable). Skip on excluded routes and when toggle off.
  5. Implement behaviorif (centralizedSessionEnabled && !isExcluded) await Promise.race([resolution, timeout]); on timeout, treat it as server_down and fail open per PRD 6.10 (no destructive action — this is a one-line log/observe branch in useCentralizedSession, not a dedicated task; see the deleted Task 7 note below). Preserve the existing refresh/logout branches below untouched.
  6. Go greenpnpm run test -- app/middleware/authenticated.global.spec.ts and re-run any existing middleware specs to confirm the off-path stays green.
  7. Quality gatepnpm 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/current cannot hang navigation: a hard client-side timeout falls through (to server_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

DisciplineDays
Frontend1
Backend
QA0.5
Total1.5

Assumptions: the timeout + race is small; risk is in not regressing the shared gate, mitigated by the off-path test. Depends on the useCentralizedSession resolution promise existing.

Run to verify

pnpm run test -- app/middleware/authenticated.global.spec.ts && pnpm run lint

Depends on

  • [Task 1] (toggle) · [Task 3] (useCentralizedSession exists; add awaitInitialResolution)

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

ActionFileWhat changes
createapp/common/composables/useCurrentCompany.ts [unverified — endpoint host pending Q3]useClient GET current-company; set on store
createapp/common/composables/useCurrentCompany.spec.tsmocked useClient returns company ⇒ store updated; error routed via useErrorHandler
extendapp/common/composables/useCentralizedSession.tslogged_in → call useCurrentCompany().sync() (no msli write)
extendapp/common/composables/useCentralizedSession.spec.tslogged_in triggers company sync
create (?)Launchpad BE current-company proxy [unverified — pending Q3]only if FE cannot reach SSO directly

Implementation steps

  1. Explore — Read app/common/store/authStore.ts:81 (existing /users/me call via apiBaseUrl) and app/common/composables/useClient.ts:9,43 (useClient<T> returns {data:{value}}, body at data.value.data); read useErrorHandler.ts for the error-routing pattern.
  2. Resolve Q3 first — Confirm the endpoint host/owner. Do not hardcode api.mekari.com from the FE without confirming CORS/Kong routing (RFC §2.5). If a BE proxy is needed, that BE work is part of this task.
  3. Write failing tests (red) — Create useCurrentCompany.spec.ts: mock useClient to return a company payload, assert the store setter is called; mock an error and assert it routes through useErrorHandler. Extend useCentralizedSession.spec.ts: data.status === "logged_in"sync() called. Run — red.
  4. Scaffold — Create useCurrentCompany.ts with a sync() calling useClient(() => \${host}/users/me/current_company`, { method: "GET" })`.
  5. Wire state — Read data.value.data, set the company on the appropriate store; route error.value through useErrorHandler. Add the logged_in branch in useCentralizedSession: call useCurrentCompany().sync() only — do NOT touch localStorage["msli"] (R7).
  6. Go greenpnpm run test -- app/common/composables/useCurrentCompany.spec.ts app/common/composables/useCentralizedSession.spec.ts.
  7. Quality gatepnpm run lint && pnpm run type-check && pnpm run build.

Acceptance criteria

  • logged_in status ⇒ current company fetched via useClient and 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

DisciplineDays
Frontend1.5
Backend0.5
QA0.5
Total2.5

Assumptions: BE reduced 2.0→0.5 (grounded against qontak-launchpad, the Go backend) — /users/me already returns sso_id+company_id+company_name and 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_id is 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 (useCentralizedSession dispatcher) → 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_down predicate) 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 as logged_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 real mekari-account-web-sdk package 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 / TaskReason (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).
Task 4 — switch_user re-auth + toastDELETED (R5), not blocked — no switch_user event exists in the real SDK; fully covered by Task 3's logged_out handling.
Task 7 — server_down + msli fallbackDELETED (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.
Task 2 — SDK loader plugin (real SDK)No longer blocked — Q1 (bundled git dependency) and Q5 (sm.mekari.com/current) resolved via real-SDK verification (latest review); see Task 2 above.