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/sdk,centralized_session,current_company,msli,_mekari_account. - 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). @mekari/sdkpackage version is[unverified — check repo]— not inpackage.json; distribution (npm vs CDN) is open question Q1.- Current-company endpoint host (
/users/me/current_company) is[unverified — check repo]— does not exist in repo; host/owner is open question Q3.
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 |
Task 4 — switch_user re-auth + "account changed" toast | 2 | — | 0.5 | 2.5 |
| Task 5 — Middleware integration (await SDK, timeout) | 1 | — | 0.5 | 1.5 |
Task 6 — logged_in + current-company sync ⚠️ | 1.5 | 2 | 0.5 | 4.0 |
Task 7 — server_down + msli fallback 🚫 | 1.5 | — | 0.5 | 2.0 |
| Grand total | 10 | 2 | 3.5 | 15.5 |
Confidence: low. Key assumptions: three [critical] blockers move this estimate materially — Q1 (SDK distribution npm-vs-CDN drives Task 2's loader shape and the unknown
@mekari/sdkAPI surface), Q3 (current-company endpoint host/owner; the 2 BE days in Task 6 are a placeholder for a Launchpad BE proxy that may or may not be needed), and Q2 (theserver_downfallback predicate is presently unimplementable from the Launchpad origin). FE-only event handlers that reuse existing store actions (Tasks 1, 3, 4, 5) are well-grounded and high-confidence within this low-overall envelope.
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({current_user}) (Load SDK with user_sso_id)
When centralized session is on, Launchpad loads the shared
@mekari/sdkSession on page load with the current user's SSO id, so the app receives authoritative session events.
Status: ⚠️ Partially blocked — the plugin shell, toggle gating, cookie read, and the new Session({current_user}) call against a mocked SDK are fully actionable now. Blocked pieces: Q1 (npm @mekari/sdk vs CDN account.mekari.com/sm/sdk.js — drives whether you import or inject a <script>) and Q5 (canonical iframe path /sm/current vs /sessionmanager/current). The exact @mekari/sdk API is unverified until Q1 resolves — build against the RFC §2.1 contract behind a thin adapter.
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({ current_user }), then forwards the four events into useCentralizedSession (Task 3+). Init must be idempotent.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/plugins/mekariSession.client.ts | toggle-gated SDK load + new Session({current_user}) + .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/sdk dep [unverified — pending Q1] — only if npm distribution chosen |
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.mocka fakeSessionclass andvi.mock("#app"). Assert constructor called with{ current_user: <sso_id> }when toggle on; not called when off. Runpnpm run test -- app/plugins/mekariSession.client.spec.ts— red. - Scaffold — Create
mekariSession.client.tswithdefineNuxtPlugin; early-return whenuseCentralizedSessionToggle().centralizedSessionEnabledis false. - Wire SDK adapter — Behind a thin local adapter (so Q1 npm-vs-CDN is swappable), obtain
Session: stub with the mock for now; real import/CDN injection added once Q1 resolves. Readcurrent_userfromuseAuthCookies().LAUNCHPAD_SSO_ID.value. - Implement behavior —
const session = new Session({ current_user }); registersession.on("logged_in"|"logged_out"|"switch_user"|"server_down", handler)delegating touseCentralizedSession(). Guard against double-init (module singleton flag). - 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({current_user})called with that id (mock SDK). - Toggle off ⇒ SDK is never loaded / constructed.
- Init is idempotent (no duplicate
Sessionon re-entry). - (pending Q1) Real SDK obtained via the confirmed distribution channel.
- (pending Q5) Iframe path matches the canonical Session Manager URL.
Test strategy
Vitest + happy-dom; mock Session constructor (spy) and useAuthCookies. Key assertion: constructor invocation + the id passed; 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: SDK accessed behind a one-file adapter so the Q1 distribution choice is a localized change; the unknown SDK API adds risk (low confidence). 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) · [External: SDK distribution — Q1 (pending)] · [External: iframe path — Q5 (pending)]
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=.
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 alogged_outevent and assertclearTokenLaunchpad+resetAuthcalled andwindow.location.hrefset. Runpnpm run test -- app/common/composables/useCentralizedSession.spec.ts— red. - Scaffold — Create
useCentralizedSession.tsexporting a handler map /handleSessionEvent(event, data, error)dispatcher; implement thelogged_outbranch only. - Wire state — Import
useSsoCallbackStore(clearTokenLaunchpad) anduseAuthStore(resetAuth) from~/common/store/...; readSSO_URL/SSO_UNIFIED_CLIENT_IDfromuseRuntimeConfig().public. - Implement behavior — On
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_outevent ⇒clearTokenLaunchpad()called. -
logged_outevent ⇒resetAuth()called (cookies cleared via existing reset). - Browser redirected to
SSO_URL/sign_out?client_id=<SSO_UNIFIED_CLIENT_ID>.
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 4, 6, 7 extend.
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: [FE] switch_user re-auth + "account changed" toast (Handle switch_user)
When SSO reports a different user (account switch), Launchpad clears its tokens, re-authenticates via the existing OAuth2 authorization-code flow, and shows a "your account has changed" toast on return.
Status: ✅ Actionable for the FE flow — reuses the verified authz-code redirect builder, clearTokenLaunchpad/resetAuth, and toast.notify. The post-return company re-sync piece is deferred to Task 6 (Q3); this task covers token-clear → re-auth redirect → toast.
Design reference: n/a — reuse existing Pixel toast, no new Figma (RFC §1.3). DS version: @mekari/pixel3@1.0.8. Frame: text-only toast. Design QA: TBD.
What to build
Extend useCentralizedSession.ts with the switch_user branch: clear tokens + resetAuth() + remove msli, redirect to the SSO authz-code URL, and on /sso-callback return fire the "account changed" toast.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | app/common/composables/useCentralizedSession.ts | switch_user → clearTokenLaunchpad() + resetAuth() + redirect to SSO_URL/auth?...response_type=code... |
| extend | app/features/sso-callback/composable/ssoCallback.ts | on post-switch callback, trigger toast.notify "your account has changed" |
| extend | app/common/composables/useCentralizedSession.spec.ts | switch_user clears tokens + builds correct authz URL |
| extend | app/features/sso-callback/composable/ssoCallback.spec.ts | toast shown on post-switch return |
Implementation steps
- Explore — Read
app/middleware/authenticated.global.ts:79-80for the exact authz URL builder (${SSO_URL}/auth/?client_id=${SSO_UNIFIED_CLIENT_ID}&response_type=code&scope=sso:profile&redirect_uri=${BASE_URL}/sso-callback),app/features/sso-callback/composable/ssoCallback.ts(handleRedirection,fetchOauthSSO), andauthStore.ts:117for thetoast.notifyshape. - Write failing tests (red) — Extend
useCentralizedSession.spec.ts:switch_user⇒clearTokenLaunchpad+resetAuthcalled,msliremoved, andwindow.locationset to the authz URL. InssoCallback.spec.ts, asserttoast.notify({ variant, title: /account has changed/ })on a post-switch flag. Run both — red. - Scaffold — Add the
switch_usercase to theuseCentralizedSessiondispatcher. - Wire state — Reuse the redirect builder string from middleware; import stores from
~/common/store/...; set a "post-switch" marker (e.g. session-storage flag or query param) the/sso-callbackflow reads. - Implement behavior —
clearTokenLaunchpad()→resetAuth()→localStorage.removeItem("msli")→ redirect. InssoCallback.ts, afterfetchOauthSSO(code)→fetchAuthLaunchpad(), if the post-switch marker is set, firetoast.notify. - Go green —
pnpm run test -- app/common/composables/useCentralizedSession.spec.ts app/features/sso-callback/composable/ssoCallback.spec.ts. - Quality gate —
pnpm run lint && pnpm run type-check && pnpm run build.
Acceptance criteria
-
switch_user⇒clearTokenLaunchpad()+resetAuth()called andmsliremoved. - Browser redirected to
SSO_URL/auth?client_id=&response_type=code&scope=sso:profile&redirect_uri=BASE_URL/sso-callback. - On post-switch
/sso-callbackreturn,toast.notify"your account has changed" is shown. - (deferred to Task 6) current company is re-synced after the switch — pending Q3.
Test strategy
Vitest + happy-dom; @pinia/testing spies, mocked #app/window.location. Key assertion: the constructed authz URL string and the toast call args (title matches "account changed", variant per existing pattern).
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 2 |
| Backend | — |
| QA | 0.5 |
| Total | 2.5 |
Assumptions: redirect builder + toast are direct reuse; the post-switch marker plumbing into
ssoCallbackis the only net-new wiring. Company re-sync excluded here (Task 6, Q3).
Run to verify
pnpm run test -- app/common/composables/useCentralizedSession.spec.ts app/features/sso-callback/composable/ssoCallback.spec.ts && pnpm run lint
Depends on
- [Task 1] (toggle) · [Task 3] (
useCentralizedSessiondispatcher must exist)
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, route into theserver_downreaction (Task 7). 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/currentcannot hang navigation: a hard client-side timeout falls through (toserver_down). - 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 records liveness (
msli) and 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 logged_in → write msli portion is small and could ship, but 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 2 BE days below are a placeholder for a possible BE proxy.
Design reference: n/a — no new UI (data sync only; company surfaced through existing store).
What to build
Extend useCentralizedSession.ts for logged_in (write msli=now, trigger company sync) and add useCurrentCompany.ts calling the current-company endpoint via useClient and setting it on the store.
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 → write msli + call useCurrentCompany().sync() |
| extend | app/common/composables/useCentralizedSession.spec.ts | logged_in writes msli and 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:80(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:logged_in⇒localStoragemsliwritten andsync()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:localStorage.setItem("msli", Date.now())thenuseCurrentCompany().sync(). - 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_in⇒mslitimestamp written to localStorage. -
logged_in⇒ 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.
Test strategy
Vitest + happy-dom; mock useClient (success + error) and localStorage. Key assertion: store setter called with data.value.data, and msli written on logged_in. The real host is mocked until Q3 resolves.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | 2 |
| QA | 0.5 |
| Total | 4.0 |
Assumptions: BE 2 days is a placeholder for a possible Launchpad BE current-company proxy (Q3) — drops to 0 if FE can call SSO directly through Kong. The
msli-write portion is trivial; the company sync is the blocked core.
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: [FE] server_down + msli fallback (Handle server_down, msli helper)
When the SDK exhausts its backoff and reports the session service is down, Launchpad applies a local fallback (recent
msli) to keep the user in, or signs them out if the fallback is stale.
Status: 🚫 Blocked — Q2 [critical]: the RFC's fallback predicate references _mekari_account validity, but that cookie is account.mekari.com-scoped and unreadable from the Launchpad origin (useAuthCookies.ts has no such cookie — verified). The predicate must be redefined as FE-readable (e.g. msli age + global_sso_valid_until) before the core decision logic can be built. The msli write helper itself is trivial (and is exercised by Task 6's logged_in), but the server_down decision — the point of this story — is unimplementable as specified. Unblock: SSO + Account & Launchpad agree an FE-only fallback predicate (Q2).
Design reference: n/a — no UI (sign-out redirect on fallback failure).
What to build
Extend useCentralizedSession.ts with the server_down branch and add an msli local-storage helper: on server_down, evaluate the (redefined) FE-readable predicate — recent enough ⇒ treat as logged_in; else sign out via the existing flow.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/common/composables/useMsli.ts (or util) | read/write msli timestamp in localStorage |
| create | app/common/composables/useMsli.spec.ts | write-then-read round-trip; age computation |
| extend | app/common/composables/useCentralizedSession.ts [predicate pending Q2] | server_down → fallback predicate → keep-session vs sign-out |
| extend | app/common/composables/useCentralizedSession.spec.ts | recent msli ⇒ stay; stale/invalid ⇒ sign-out |
Implementation steps
- Resolve Q2 first — Agree the FE-readable predicate (e.g.
msliage < 2h ANDglobal_sso_valid_untilstill valid — both FE-readable peruseAuthCookies.ts). Do not depend on_mekari_account(cross-origin, unreadable). - Explore — Read
useAuthCookies.ts:7,21(global_sso_valid_untilis FE-readable) and the Task 3 sign-out branch to reuse it. - Write failing tests (red) — Create
useMsli.spec.ts(round-trip + age). ExtenduseCentralizedSession.spec.ts: recentmsli⇒ no sign-out; stale ⇒clearTokenLaunchpad+resetAuth+ sign-out redirect. Run — red. - Scaffold — Create
useMsli.tswithsetMsli()/getMsliAge(); addserver_downcase to the dispatcher. - Wire state — Evaluate the redefined predicate; reuse the Task 3 sign-out path on failure.
- Go green —
pnpm run test -- app/common/composables/useMsli.spec.ts app/common/composables/useCentralizedSession.spec.ts. - Quality gate —
pnpm run lint && pnpm run type-check && pnpm run build.
Acceptance criteria
-
mslihelper round-trips a timestamp through localStorage and computes age. - (pending Q2)
server_down+ recent valid fallback ⇒ session preserved (treated aslogged_in). -
server_down+ stale/invalid fallback ⇒ product sign-out (reuses Task 3 path). - Predicate uses only FE-readable signals (
msli,global_sso_valid_until) — never_mekari_account.
Test strategy
Vitest + happy-dom; mock localStorage and cookies. Key assertion: branch selection on a recent vs stale msli, and the sign-out call set on the stale branch. Predicate logic is gated on Q2.
Effort estimate
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | — |
| QA | 0.5 |
| Total | 2.0 |
Assumptions:
mslihelper is trivial; the estimate assumes Q2 yields a purely FE-readable predicate so no BE work is added. If a new BE signal is required this grows.
Run to verify
pnpm run test -- app/common/composables/useMsli.spec.ts app/common/composables/useCentralizedSession.spec.ts && pnpm run lint
Depends on
- [Task 3] (dispatcher + reusable sign-out) · [External: FE-readable fallback predicate — Q2 (critical, pending)]
Ordering rationale
- Build the spine first, in this order: Task 1 (toggle) → Task 3 (
useCentralizedSessiondispatcher) → Task 2 (SDK loader) → Task 4 (switch_user) → Task 5 (middleware). The toggle gates everything; the dispatcher composable is the shared event sink that Tasks 2, 4, 5, 6, 7 all wire into, so it must land early even though the RFC's chunk order lists the loader first. - The critical path runs through three [critical] open questions, not through code. Tasks 1, 3, 4, 5 are fully actionable today and deliver a working logout + account-switch experience behind the toggle. They are the safe, demonstrable pilot slice.
- Task 2 (SDK loader) is the gating dependency for any real end-to-end behavior and is partially blocked by Q1 (npm vs CDN) and Q5 (iframe path). Build it against a mocked SDK behind a thin adapter now so the real-distribution swap is a one-file change once Q1 closes.
- Tasks 6 and 7 are fully blocked and should not be started until their contracts resolve. Push externally to close Q3 (current-company endpoint host/owner — Launchpad BE) and Q2 (FE-readable
server_downpredicate — SSO + AL); these are the two items that keep the pilot from being feature-complete. The RFC §7 gate isnountil Q1–Q3 close. - Lowest-risk demonstrable milestone: Tasks 1+3+4+5 give a pilot that correctly ends sessions on SSO logout and re-auths on account switch — shippable behind the toggle even before the SDK distribution and company-sync contracts land (using a mocked SDK in staging).
Skipped stories
(Full-scope mode: every task marked 🚫 Blocked, with its unblock condition. Tasks 1–5 are actionable/partially-blocked and appear 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. |
Task 7 — server_down + msli fallback | 🚫 Blocked on Q2 [critical] — fallback predicate depends on _mekari_account, which is cross-origin and unreadable from the Launchpad origin. Unblock: SSO + AL redefine an FE-readable predicate (msli age + global_sso_valid_until). |
| Task 2 — SDK loader plugin (real SDK) | ⚠️ Partially blocked on Q1 [critical] (npm @mekari/sdk vs CDN sm/sdk.js) and Q5 [important] (iframe path /sm/current vs /sessionmanager/current). Shell + mocked-SDK construction is actionable now; real-distribution wiring waits on Q1/Q5. |