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/sdk, centralized_session, current_company, msli, _mekari_account.
  • 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).
  • @mekari/sdk package version is [unverified — check repo] — not in package.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 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.52.5
Task 5 — Middleware integration (await SDK, timeout)10.51.5
Task 6 — logged_in + current-company sync ⚠️1.520.54.0
Task 7 — server_down + msli fallback 🚫1.50.52.0
Grand total1023.515.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/sdk API 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 (the server_down fallback 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

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({current_user}) (Load SDK with user_sso_id)

When centralized session is on, Launchpad loads the shared @mekari/sdk Session 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

ActionFileWhat changes
createapp/plugins/mekariSession.client.tstoggle-gated SDK load + new Session({current_user}) + .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/sdk dep [unverified — pending Q1] — only if npm distribution chosen

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 a fake Session class and vi.mock("#app"). Assert constructor called with { current_user: <sso_id> } when toggle on; not called when off. 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 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. Read current_user from useAuthCookies().LAUNCHPAD_SSO_ID.value.
  5. Implement behaviorconst session = new Session({ current_user }); register session.on("logged_in"|"logged_out"|"switch_user"|"server_down", handler) delegating to useCentralizedSession(). Guard against double-init (module singleton flag).
  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({current_user}) called with that id (mock SDK).
  • Toggle off ⇒ SDK is never loaded / constructed.
  • Init is idempotent (no duplicate Session on 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

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

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 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 handler map / handleSessionEvent(event, data, error) dispatcher; 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 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 event ⇒ clearTokenLaunchpad() called.
  • logged_out event ⇒ 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 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 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 useCentralizedSession consumed 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

ActionFileWhat changes
extendapp/common/composables/useCentralizedSession.tsswitch_userclearTokenLaunchpad() + resetAuth() + redirect to SSO_URL/auth?...response_type=code...
extendapp/features/sso-callback/composable/ssoCallback.tson post-switch callback, trigger toast.notify "your account has changed"
extendapp/common/composables/useCentralizedSession.spec.tsswitch_user clears tokens + builds correct authz URL
extendapp/features/sso-callback/composable/ssoCallback.spec.tstoast shown on post-switch return

Implementation steps

  1. Explore — Read app/middleware/authenticated.global.ts:79-80 for 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), and authStore.ts:117 for the toast.notify shape.
  2. Write failing tests (red) — Extend useCentralizedSession.spec.ts: switch_userclearTokenLaunchpad + resetAuth called, msli removed, and window.location set to the authz URL. In ssoCallback.spec.ts, assert toast.notify({ variant, title: /account has changed/ }) on a post-switch flag. Run both — red.
  3. Scaffold — Add the switch_user case to the useCentralizedSession dispatcher.
  4. 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-callback flow reads.
  5. Implement behaviorclearTokenLaunchpad()resetAuth()localStorage.removeItem("msli") → redirect. In ssoCallback.ts, after fetchOauthSSO(code)fetchAuthLaunchpad(), if the post-switch marker is set, fire toast.notify.
  6. Go greenpnpm run test -- app/common/composables/useCentralizedSession.spec.ts app/features/sso-callback/composable/ssoCallback.spec.ts.
  7. Quality gatepnpm run lint && pnpm run type-check && pnpm run build.

Acceptance criteria

  • switch_userclearTokenLaunchpad() + resetAuth() called and msli removed.
  • Browser redirected to SSO_URL/auth?client_id=&response_type=code&scope=sso:profile&redirect_uri=BASE_URL/sso-callback.
  • On post-switch /sso-callback return, 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

DisciplineDays
Frontend2
Backend
QA0.5
Total2.5

Assumptions: redirect builder + toast are direct reuse; the post-switch marker plumbing into ssoCallback is 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] (useCentralizedSession dispatcher 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

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, route into the server_down reaction (Task 7). 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/current cannot hang navigation: a hard client-side timeout falls through (to server_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

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 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

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 → write msli + call useCurrentCompany().sync()
extendapp/common/composables/useCentralizedSession.spec.tslogged_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

  1. Explore — Read app/common/store/authStore.ts:80 (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: logged_inlocalStorage msli written and 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: localStorage.setItem("msli", Date.now()) then useCurrentCompany().sync().
  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_inmsli timestamp written to localStorage.
  • logged_in ⇒ 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.

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

DisciplineDays
Frontend1.5
Backend2
QA0.5
Total4.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

ActionFileWhat changes
createapp/common/composables/useMsli.ts (or util)read/write msli timestamp in localStorage
createapp/common/composables/useMsli.spec.tswrite-then-read round-trip; age computation
extendapp/common/composables/useCentralizedSession.ts [predicate pending Q2]server_down → fallback predicate → keep-session vs sign-out
extendapp/common/composables/useCentralizedSession.spec.tsrecent msli ⇒ stay; stale/invalid ⇒ sign-out

Implementation steps

  1. Resolve Q2 first — Agree the FE-readable predicate (e.g. msli age < 2h AND global_sso_valid_until still valid — both FE-readable per useAuthCookies.ts). Do not depend on _mekari_account (cross-origin, unreadable).
  2. Explore — Read useAuthCookies.ts:7,21 (global_sso_valid_until is FE-readable) and the Task 3 sign-out branch to reuse it.
  3. Write failing tests (red) — Create useMsli.spec.ts (round-trip + age). Extend useCentralizedSession.spec.ts: recent msli ⇒ no sign-out; stale ⇒ clearTokenLaunchpad + resetAuth + sign-out redirect. Run — red.
  4. Scaffold — Create useMsli.ts with setMsli() / getMsliAge(); add server_down case to the dispatcher.
  5. Wire state — Evaluate the redefined predicate; reuse the Task 3 sign-out path on failure.
  6. Go greenpnpm run test -- app/common/composables/useMsli.spec.ts app/common/composables/useCentralizedSession.spec.ts.
  7. Quality gatepnpm run lint && pnpm run type-check && pnpm run build.

Acceptance criteria

  • msli helper round-trips a timestamp through localStorage and computes age.
  • (pending Q2) server_down + recent valid fallback ⇒ session preserved (treated as logged_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

DisciplineDays
Frontend1.5
Backend
QA0.5
Total2.0

Assumptions: msli helper 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 (useCentralizedSession dispatcher) → 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_down predicate — SSO + AL); these are the two items that keep the pilot from being feature-complete. The RFC §7 gate is no until 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 / 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.
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.