@gcm-cz/dmon-switcher
v1.3.0
Published
Headless JavaScript library for integrating the DMon user switcher into a relying party (RP).
Readme
@gcm-cz/dmon-switcher
Headless JavaScript library for integrating the DMon user switcher into a relying party (RP).
Lets your application discover which users the browser has already authenticated with DMon and switch between them — without leaving your page, without re-implementing the picker UI, and without touching DMon's cookies directly.
Internally the library embeds a hidden <iframe> that loads a DMon-hosted bridge page. The
bridge runs in DMon's first-party context, reads the dmon_auth session cookie, and exposes a
postMessage RPC interface. Your application calls listSessions(), renders its own picker,
and calls switchTo(). The library renders no UI of its own.
Zero runtime dependencies. Ships as ESM + CJS with full TypeScript declarations.
Prerequisites
- A DMon instance with the embed bridge feature enabled.
- A registered DMon client with
embed_originsconfigured to include your application's origin (e.g.https://app.example.com). Without this, the bridge iframe will be blocked by CSP and the library will not function. See the RP Integration Guide for the admin UI walk-through.
Installation
npm install @gcm-cz/dmon-switcher
# or
pnpm add @gcm-cz/dmon-switcherQuickstart
import { DmonSwitcher } from "@gcm-cz/dmon-switcher";
import type { Session } from "@gcm-cz/dmon-switcher";
// Instantiate once at component mount. A hidden iframe is created immediately.
const switcher = new DmonSwitcher({
dmonOrigin: "https://accounts.example.com",
realmSlug: "default",
clientId: "my-rp",
mode: "emit", // "emit" (default) or "redirect" — controls switchTo() behaviour
});
// Register a switch handler before awaiting ready().
switcher.on("switch", ({ loginHint }) => {
if (loginHint === "__new__") {
// Force interactive login to add a new session.
window.location.href = buildAuthorizeUrl({ prompt: "login" });
return;
}
// Attempt silent re-auth; fall back to interactive on login_required.
performSilentAuth({ loginHint, prompt: "none" })
.catch(() => { window.location.href = buildAuthorizeUrl({ loginHint }); });
});
// Wait for the bridge to signal ready, then fetch the session list.
await switcher.ready();
const sessions: Session[] = await switcher.listSessions();
renderMyUserMenu(sessions);
// Switch to a user when selected in your menu. Pass `session.sub` (the user id); the
// switcher resolves it to the login_hint (username) via the cached session list.
await switcher.switchTo(selectedSession.sub);
// Build the account-page URL for an anchor — the user decides how to open it.
const accountHref = switcher.accountPageUrl(currentSub);
// <a href={accountHref}>Account settings</a>
// Clean up when the component unmounts.
switcher.destroy();React example
import { useEffect, useRef, useState } from "react";
import { DmonSwitcher } from "@gcm-cz/dmon-switcher";
import type { Session } from "@gcm-cz/dmon-switcher";
function UserMenu({ currentSub }: { currentSub: string }) {
const switcherRef = useRef<DmonSwitcher | null>(null);
const [sessions, setSessions] = useState<Session[]>([]);
useEffect(() => {
const switcher = new DmonSwitcher({
dmonOrigin: "https://accounts.example.com",
realmSlug: "default",
clientId: "my-rp",
});
switcherRef.current = switcher;
switcher.on("switch", ({ loginHint }) => {
// your re-auth logic — loginHint is the username (or "__new__")
});
switcher.ready()
.then(() => switcher.listSessions())
.then(setSessions);
return () => switcher.destroy();
}, []);
return (
<menu>
{sessions.map(s => (
<li key={s.sub} onClick={() => switcherRef.current?.switchTo(s.sub)}>
{s.name}
</li>
))}
<li>
<a href={switcherRef.current?.accountPageUrl(currentSub)}>
Account settings
</a>
</li>
</menu>
);
}API Reference
Session
interface Session {
sub: string; // OIDC subject — the user id; pass to switchTo() / accountPageUrl()
name: string; // Username — used by the switcher as the login_hint (resolved from sub)
given_name?: string;
family_name?: string;
picture?: string; // Profile picture URL
}listSessions() returns only sessions the realm's can_authorize policy permits. Users
authenticated in DMon but blocked by policy are silently omitted.
DmonSwitcherOptions
interface DmonSwitcherOptions {
dmonOrigin: string; // Base URL of your DMon instance
realmSlug: string; // Realm slug the RP is registered in
clientId: string; // OAuth 2.0 client_id of the RP
mode?: "emit" | "redirect"; // default "emit"
debug?: boolean; // Log internal steps via console.debug; default false
}debug — when true, every internal step is logged via console.debug under the
[DmonSwitcher] prefix: construction, ready handshake, RPC send/receive, dropped messages,
switch, account-page URL construction, and teardown. Off by default. Useful during integration
and troubleshooting; do not enable in production.
DmonSwitcher
constructor(opts: DmonSwitcherOptions)
Creates a hidden <iframe>, appends it to document.body, and starts the bridge handshake.
Does not throw — errors are deferred and surfaced via ready() never resolving.
ready(): Promise<void>
Resolves when the bridge iframe sends its ready message. Also resolves immediately if
construction failed (e.g. DOM unavailable) or after destroy() is called — in both cases the
instance is inoperative and subsequent calls no-op.
listSessions() and switchTo() in "redirect" mode wait for ready() internally before
sending an RPC, so you do not have to await ready() before calling them. switchTo() in
"emit" mode fires local listeners immediately without waiting for the bridge. Awaiting
ready() explicitly is recommended when you want timeout-based error detection:
const TIMEOUT = 5000;
await Promise.race([
switcher.ready(),
new Promise((_, reject) => setTimeout(() => reject(new Error("bridge timeout")), TIMEOUT)),
]);listSessions(): Promise<Session[]>
Sends a list_sessions RPC to the bridge. Returns [] when there are no authenticated
sessions, when the cookie is absent, or when all sessions are policy-blocked.
switchTo(sub: string, opts?: { returnTo?: string }): Promise<void>
Initiates a user switch. sub is the target account's Session.sub (the user id), or the
"__new__" sentinel. The switcher resolves sub to the OIDC login_hint (the username,
Session.name) via the most recent listSessions() result; unresolved values are passed
through unchanged.
"emit"mode — fires allswitchlisteners with{ loginHint }(the resolved username) and resolves immediately. The RP is responsible for triggering OIDC re-authentication."redirect"mode — sendsredirect_authorizeto the bridge, which top-level-navigates to the DMon/authorizeendpoint withlogin_hint=<resolved username>,prompt=none, andredirect_uri=opts.returnTo ?? window.location.href.
__new__ sentinel: switchTo("__new__") uses prompt=login to force fresh credential
entry and append a new session to the cookie.
accountPageUrl(sub: string): string
Returns (does not open) the DMon account-page URL for the given account's Session.sub
(the user id), resolved to a login_hint (the username) via the cached listSessions() result:
{dmonOrigin}/account?login_hint={encodeURIComponent(resolvedUsername)}Pure and side-effect free — safe to call during render. Put the result in an anchor href so
the user controls how it opens. DMon will silently mount the account page if the cookie already
contains a session for that user; otherwise it falls back to the standard login flow.
destroy(): void
Removes the iframe, deregisters window message listeners, rejects all pending RPC promises
with AbortError, and resolves any pending ready() waiters so they do not hang. Idempotent —
safe to call multiple times, subsequent calls are no-ops. Call at component unmount.
on(event: "switch", handler: (payload: { loginHint: string }) => void): void
Registers a handler fired by switchTo() in "emit" mode.
on(event: "ready", handler: () => void): void
Registers a handler called when the bridge sends its ready message.
switchTo modes
| Mode | What switchTo() does | Who triggers re-auth |
|---|---|---|
| "emit" (default) | Fires the switch event | Your code |
| "redirect" | Bridge navigates the page to DMon /authorize | DMon → OIDC code callback |
"emit" is recommended for SPAs — it keeps your application in control of the authorization
flow and lets you discard tokens before starting the new grant.
