@gcm-cz/dmon-switcher 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -19
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +31 -2
- package/dist/index.js +85 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,15 +51,15 @@ const switcher = new DmonSwitcher({
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
// Register a switch handler before awaiting ready().
|
|
54
|
-
switcher.on("switch", ({
|
|
55
|
-
if (
|
|
54
|
+
switcher.on("switch", ({ loginHint }) => {
|
|
55
|
+
if (loginHint === "__new__") {
|
|
56
56
|
// Force interactive login to add a new session.
|
|
57
57
|
window.location.href = buildAuthorizeUrl({ prompt: "login" });
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
// Attempt silent re-auth; fall back to interactive on login_required.
|
|
61
|
-
performSilentAuth({ loginHint
|
|
62
|
-
.catch(() => { window.location.href = buildAuthorizeUrl({ loginHint
|
|
61
|
+
performSilentAuth({ loginHint, prompt: "none" })
|
|
62
|
+
.catch(() => { window.location.href = buildAuthorizeUrl({ loginHint }); });
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
// Wait for the bridge to signal ready, then fetch the session list.
|
|
@@ -67,11 +67,12 @@ await switcher.ready();
|
|
|
67
67
|
const sessions: Session[] = await switcher.listSessions();
|
|
68
68
|
renderMyUserMenu(sessions);
|
|
69
69
|
|
|
70
|
-
// Switch to a user when selected in your menu.
|
|
71
|
-
|
|
70
|
+
// Switch to a user when selected in your menu. Pass `session.sub` (the user id); the
|
|
71
|
+
// switcher resolves it to the login_hint (username) via the cached session list.
|
|
72
|
+
await switcher.switchTo(selectedSession.sub);
|
|
72
73
|
|
|
73
74
|
// Build the account-page URL for an anchor — the user decides how to open it.
|
|
74
|
-
const accountHref = switcher.accountPageUrl(
|
|
75
|
+
const accountHref = switcher.accountPageUrl(currentSub);
|
|
75
76
|
// <a href={accountHref}>Account settings</a>
|
|
76
77
|
|
|
77
78
|
// Clean up when the component unmounts.
|
|
@@ -97,8 +98,8 @@ function UserMenu({ currentSub }: { currentSub: string }) {
|
|
|
97
98
|
});
|
|
98
99
|
switcherRef.current = switcher;
|
|
99
100
|
|
|
100
|
-
switcher.on("switch", ({
|
|
101
|
-
// your re-auth logic
|
|
101
|
+
switcher.on("switch", ({ loginHint }) => {
|
|
102
|
+
// your re-auth logic — loginHint is the username (or "__new__")
|
|
102
103
|
});
|
|
103
104
|
|
|
104
105
|
switcher.ready()
|
|
@@ -133,8 +134,8 @@ function UserMenu({ currentSub }: { currentSub: string }) {
|
|
|
133
134
|
|
|
134
135
|
```typescript
|
|
135
136
|
interface Session {
|
|
136
|
-
sub: string; //
|
|
137
|
-
name: string; //
|
|
137
|
+
sub: string; // OIDC subject — the user id; pass to switchTo() / accountPageUrl()
|
|
138
|
+
name: string; // Username — used by the switcher as the login_hint (resolved from sub)
|
|
138
139
|
given_name?: string;
|
|
139
140
|
family_name?: string;
|
|
140
141
|
picture?: string; // Profile picture URL
|
|
@@ -194,12 +195,15 @@ sessions, when the cookie is absent, or when all sessions are policy-blocked.
|
|
|
194
195
|
|
|
195
196
|
#### `switchTo(sub: string, opts?: { returnTo?: string }): Promise<void>`
|
|
196
197
|
|
|
197
|
-
Initiates a user switch.
|
|
198
|
+
Initiates a user switch. `sub` is the target account's `Session.sub` (the user id), or the
|
|
199
|
+
`"__new__"` sentinel. The switcher resolves `sub` to the OIDC `login_hint` (the username,
|
|
200
|
+
`Session.name`) via the most recent `listSessions()` result; unresolved values are passed
|
|
201
|
+
through unchanged.
|
|
198
202
|
|
|
199
|
-
- **`"emit"` mode** — fires all `switch` listeners with `{
|
|
200
|
-
The RP is responsible for triggering OIDC re-authentication.
|
|
203
|
+
- **`"emit"` mode** — fires all `switch` listeners with `{ loginHint }` (the resolved
|
|
204
|
+
username) and resolves immediately. The RP is responsible for triggering OIDC re-authentication.
|
|
201
205
|
- **`"redirect"` mode** — sends `redirect_authorize` to the bridge, which top-level-navigates
|
|
202
|
-
to the DMon `/authorize` endpoint with `login_hint
|
|
206
|
+
to the DMon `/authorize` endpoint with `login_hint=<resolved username>`, `prompt=none`, and
|
|
203
207
|
`redirect_uri=opts.returnTo ?? window.location.href`.
|
|
204
208
|
|
|
205
209
|
**`__new__` sentinel:** `switchTo("__new__")` uses `prompt=login` to force fresh credential
|
|
@@ -207,15 +211,16 @@ entry and append a new session to the cookie.
|
|
|
207
211
|
|
|
208
212
|
#### `accountPageUrl(sub: string): string`
|
|
209
213
|
|
|
210
|
-
Returns (does **not** open) the DMon account-page URL for `sub
|
|
214
|
+
Returns (does **not** open) the DMon account-page URL for the given account's `Session.sub`
|
|
215
|
+
(the user id), resolved to a `login_hint` (the username) via the cached `listSessions()` result:
|
|
211
216
|
|
|
212
217
|
```
|
|
213
|
-
{dmonOrigin}/account?login_hint={encodeURIComponent(
|
|
218
|
+
{dmonOrigin}/account?login_hint={encodeURIComponent(resolvedUsername)}
|
|
214
219
|
```
|
|
215
220
|
|
|
216
221
|
Pure and side-effect free — safe to call during render. Put the result in an anchor `href` so
|
|
217
222
|
the user controls how it opens. DMon will silently mount the account page if the cookie already
|
|
218
|
-
contains
|
|
223
|
+
contains a session for that user; otherwise it falls back to the standard login flow.
|
|
219
224
|
|
|
220
225
|
#### `destroy(): void`
|
|
221
226
|
|
|
@@ -223,7 +228,7 @@ Removes the iframe, deregisters `window` message listeners, rejects all pending
|
|
|
223
228
|
with `AbortError`, and resolves any pending `ready()` waiters so they do not hang. Idempotent —
|
|
224
229
|
safe to call multiple times, subsequent calls are no-ops. Call at component unmount.
|
|
225
230
|
|
|
226
|
-
#### `on(event: "switch", handler: (payload: {
|
|
231
|
+
#### `on(event: "switch", handler: (payload: { loginHint: string }) => void): void`
|
|
227
232
|
|
|
228
233
|
Registers a handler fired by `switchTo()` in `"emit"` mode.
|
|
229
234
|
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function h(s){if(typeof s!="object"||s===null)return!1;const e=s;return!(e.dmon_bridge!==!0||typeof e.type!="string")}function d(s){return typeof s.request_id=="string"}class a{constructor(e){this.iframe=null,this.nextRequestId=1,this.destroyed=!1,this.failed=!1,this.bridgeFailed=!1,this.readyReceived=!1,this.loadGraceTimer=null,this.pending=new Map,this.switchListeners=[],this.readyListeners=[],this.errorListeners=[],this.lastSessions=[],this.handleIframeLoad=()=>{this.log("iframe load event"),!(this.readyReceived||this.destroyed||this.failed||this.bridgeFailed)&&(this.loadGraceTimer=setTimeout(()=>{this.rejectBridge("DmonSwitcher: bridge iframe loaded but sent no ready message (CSP frame-ancestors block?)")},this.postLoadGraceMs))},this.handleIframeError=()=>{this.log("iframe error event"),this.rejectBridge("DmonSwitcher: bridge iframe failed to load (network error)")},this.handleMessage=i=>{if(this.iframe==null||!h(i.data))return;if(i.source!==this.iframe.contentWindow){this.log("message dropped: wrong source",{eventOrigin:i.origin,expectedOrigin:this.origin,hasSource:i.source!=null,sourceIsExpectedIframe:!1});return}if(i.origin!==this.origin){this.log("message dropped: wrong origin",{eventOrigin:i.origin,expectedOrigin:this.origin});return}const r=i.data;if(r.type==="ready"){this.log("ready received"),this.readyReceived=!0,this.loadGraceTimer!=null&&(clearTimeout(this.loadGraceTimer),this.loadGraceTimer=null),this.resolveReady();for(const o of this.readyListeners)try{o()}catch(n){this.logError("ready handler threw",n)}return}if(!d(r)){this.log("message dropped: no request_id");return}const t=this.pending.get(r.request_id);t!==void 0?(this.log("rpc reply",{request_id:r.request_id,type:r.type}),this.pending.delete(r.request_id),t.resolve(r)):this.log("rpc reply for unknown request_id — ignored",{request_id:r.request_id})},this.origin=e.dmonOrigin,this.mode=e.mode??"emit",this.debug=e.debug??!1,this.postLoadGraceMs=e.postLoadGraceMs??200,this.readyPromise=new Promise((i,r)=>{this.resolveReady=i,this.rejectReady=r});try{const i=`${e.dmonOrigin}/api/v1/authorize/${e.realmSlug}/embed/bridge?client_id=${encodeURIComponent(e.clientId)}`,r=document.createElement("iframe");r.setAttribute("aria-hidden","true"),r.setAttribute("sandbox","allow-scripts allow-same-origin"),r.style.display="none",r.src=i,document.body.appendChild(r),r.onload=this.handleIframeLoad,r.onerror=this.handleIframeError,this.iframe=r,window.addEventListener("message",this.handleMessage),this.log("constructed",{origin:this.origin,mode:this.mode,src:i})}catch(i){this.failed=!0,this.resolveReady(),this.logError("construction failed — switcher disabled",i)}}ready(){return this.readyPromise}async listSessions(){if(this.failed||this.destroyed||this.iframe==null)return this.log("listSessions skipped — switcher unavailable"),[];try{const i=(await this.rpc({type:"list_sessions"})).sessions;return Array.isArray(i)?(this.log("listSessions resolved",{count:i.length}),this.lastSessions=i,this.lastSessions):(this.log("listSessions resolved with no sessions array"),[])}catch{return[]}}async switchTo(e,i){if(this.failed||this.destroyed||this.iframe==null){this.log("switchTo skipped — switcher unavailable",{sub:e});return}try{const r=this.resolveLoginHint(e);if(this.mode==="emit"){this.log("switchTo emit",{sub:e,loginHint:r,listeners:this.switchListeners.length}),this.emitSwitch(r);return}const t=e==="__new__"?"login":"none",o=i?.returnTo??window.location.href;this.log("switchTo redirect",{sub:e,loginHint:r,prompt:t,return_to:o}),await this.rpc({type:"redirect_authorize",login_hint:r,prompt:t,return_to:o})}catch(r){this.logError("switchTo failed",r)}}accountPageUrl(e){const i=this.resolveLoginHint(e),r=`${this.origin}/account?login_hint=${encodeURIComponent(i)}`;return this.log("accountPageUrl",{sub:e,loginHint:i,url:r}),r}resolveLoginHint(e){return e==="__new__"?e:this.lastSessions.find(r=>r.sub===e)?.name??e}destroy(){if(this.destroyed){this.log("destroy called again — no-op");return}this.destroyed=!0;try{this.loadGraceTimer!=null&&(clearTimeout(this.loadGraceTimer),this.loadGraceTimer=null),window.removeEventListener("message",this.handleMessage);const e=new DOMException("DmonSwitcher destroyed","AbortError");this.log("destroy",{rejectingPending:this.pending.size}),this.pending.forEach(({reject:i})=>i(e)),this.pending.clear(),this.iframe?.remove(),this.resolveReady()}catch(e){this.logError("destroy encountered an error",e)}}on(e,i){e==="switch"?this.switchListeners.push(i):e==="ready"?this.readyListeners.push(i):this.errorListeners.push(i)}rejectBridge(e){if(this.bridgeFailed||this.destroyed||this.failed)return;this.bridgeFailed=!0;const i=new Error(e);this.log(`bridge failure: ${e}`),this.rejectReady(i),this.pending.forEach(({reject:r})=>{r(i)}),this.pending.clear();for(const r of this.errorListeners)try{r(i)}catch(t){this.logError("error handler threw",t)}}log(e,i){this.debug&&(i===void 0?console.debug(`[DmonSwitcher] ${e}`):console.debug(`[DmonSwitcher] ${e}`,i))}logError(e,i){i===void 0?console.error(`[DmonSwitcher] ${e}`):console.error(`[DmonSwitcher] ${e}`,i)}emitSwitch(e){for(const i of this.switchListeners)try{i({loginHint:e})}catch(r){this.logError("switch handler threw",r)}}async rpc(e){if(this.destroyed||this.iframe==null)throw new DOMException("DmonSwitcher destroyed","AbortError");if(await this.readyPromise,this.destroyed||this.iframe==null)throw new DOMException("DmonSwitcher destroyed","AbortError");const i=String(this.nextRequestId++);this.log("rpc send",{request_id:i,...e});const r=this.iframe;return new Promise((t,o)=>{this.pending.set(i,{resolve:t,reject:o}),r.contentWindow?.postMessage({...e,request_id:i},this.origin)})}}exports.DmonSwitcher=a;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export interface Session {
|
|
2
|
+
/**
|
|
3
|
+
* OIDC subject identifier — the DMon user id as a string. Equals the `sub`
|
|
4
|
+
* claim of the access/ID tokens issued for this user, so relying parties can
|
|
5
|
+
* correlate a switcher account with its tokens.
|
|
6
|
+
*/
|
|
2
7
|
sub: string;
|
|
8
|
+
/**
|
|
9
|
+
* Username / login identifier. Pass this to {@link DmonSwitcher.switchTo} and
|
|
10
|
+
* {@link DmonSwitcher.accountPageUrl} as the OIDC `login_hint` when switching
|
|
11
|
+
* to this account.
|
|
12
|
+
*/
|
|
3
13
|
name: string;
|
|
4
14
|
given_name?: string;
|
|
5
15
|
family_name?: string;
|
|
@@ -27,7 +37,7 @@ export interface DmonSwitcherOptions {
|
|
|
27
37
|
postLoadGraceMs?: number;
|
|
28
38
|
}
|
|
29
39
|
type SwitchHandler = (payload: {
|
|
30
|
-
|
|
40
|
+
loginHint: string;
|
|
31
41
|
}) => void;
|
|
32
42
|
type ReadyHandler = () => void;
|
|
33
43
|
type ErrorHandler = (err: Error) => void;
|
|
@@ -50,21 +60,40 @@ export declare class DmonSwitcher {
|
|
|
50
60
|
private readonly switchListeners;
|
|
51
61
|
private readonly readyListeners;
|
|
52
62
|
private readonly errorListeners;
|
|
63
|
+
/** Most recent listSessions() result, used to resolve a `sub` (user id) to its login_hint. */
|
|
64
|
+
private lastSessions;
|
|
53
65
|
constructor(opts: DmonSwitcherOptions);
|
|
54
66
|
ready(): Promise<void>;
|
|
55
67
|
listSessions(): Promise<Session[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Switch to a previously authenticated account.
|
|
70
|
+
*
|
|
71
|
+
* @param sub - The {@link Session.sub} of the target account (the user id), or the
|
|
72
|
+
* `"__new__"` sentinel to force interactive login. The switcher resolves it to the
|
|
73
|
+
* OIDC `login_hint` (username) via the cached {@link listSessions} result.
|
|
74
|
+
*/
|
|
56
75
|
switchTo(sub: string, opts?: {
|
|
57
76
|
returnTo?: string;
|
|
58
77
|
}): Promise<void>;
|
|
59
78
|
/**
|
|
60
|
-
* Build the URL of the DMon account page for the given
|
|
79
|
+
* Build the URL of the DMon account page for the given account, pre-filled with the
|
|
61
80
|
* `login_hint` so DMon can mount it silently if that user's session is in the cookie.
|
|
62
81
|
*
|
|
82
|
+
* @param sub - The {@link Session.sub} of the target account (the user id); resolved to
|
|
83
|
+
* the OIDC `login_hint` (username) via the cached {@link listSessions} result.
|
|
84
|
+
*
|
|
63
85
|
* This only constructs the string — it never navigates. Put it in an anchor's `href`
|
|
64
86
|
* so the user can choose how to open it (left click = same tab, middle/ctrl click =
|
|
65
87
|
* new tab). Pure and side-effect free; safe to call at any time.
|
|
66
88
|
*/
|
|
67
89
|
accountPageUrl(sub: string): string;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a {@link Session.sub} (the user id) to the OIDC `login_hint` (the username)
|
|
92
|
+
* using the most recent {@link listSessions} result. The `"__new__"` sentinel is passed
|
|
93
|
+
* through unchanged. When no cached session matches (e.g. `listSessions` has not run
|
|
94
|
+
* yet), the input is returned unchanged as a best-effort fallback.
|
|
95
|
+
*/
|
|
96
|
+
private resolveLoginHint;
|
|
68
97
|
destroy(): void;
|
|
69
98
|
on(event: "switch", handler: SwitchHandler): void;
|
|
70
99
|
on(event: "ready", handler: ReadyHandler): void;
|
package/dist/index.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
function
|
|
1
|
+
function h(s) {
|
|
2
2
|
if (typeof s != "object" || s === null) return !1;
|
|
3
|
-
const
|
|
4
|
-
return !(
|
|
3
|
+
const e = s;
|
|
4
|
+
return !(e.dmon_bridge !== !0 || typeof e.type != "string");
|
|
5
5
|
}
|
|
6
|
-
function
|
|
6
|
+
function d(s) {
|
|
7
7
|
return typeof s.request_id == "string";
|
|
8
8
|
}
|
|
9
9
|
class a {
|
|
10
|
-
constructor(
|
|
11
|
-
this.iframe = null, this.nextRequestId = 1, this.destroyed = !1, this.failed = !1, this.bridgeFailed = !1, this.readyReceived = !1, this.loadGraceTimer = null, this.pending = /* @__PURE__ */ new Map(), this.switchListeners = [], this.readyListeners = [], this.errorListeners = [], this.handleIframeLoad = () => {
|
|
10
|
+
constructor(e) {
|
|
11
|
+
this.iframe = null, this.nextRequestId = 1, this.destroyed = !1, this.failed = !1, this.bridgeFailed = !1, this.readyReceived = !1, this.loadGraceTimer = null, this.pending = /* @__PURE__ */ new Map(), this.switchListeners = [], this.readyListeners = [], this.errorListeners = [], this.lastSessions = [], this.handleIframeLoad = () => {
|
|
12
12
|
this.log("iframe load event"), !(this.readyReceived || this.destroyed || this.failed || this.bridgeFailed) && (this.loadGraceTimer = setTimeout(() => {
|
|
13
13
|
this.rejectBridge("DmonSwitcher: bridge iframe loaded but sent no ready message (CSP frame-ancestors block?)");
|
|
14
14
|
}, this.postLoadGraceMs));
|
|
15
15
|
}, this.handleIframeError = () => {
|
|
16
16
|
this.log("iframe error event"), this.rejectBridge("DmonSwitcher: bridge iframe failed to load (network error)");
|
|
17
|
-
}, this.handleMessage = (
|
|
18
|
-
if (this.iframe == null || !
|
|
19
|
-
if (
|
|
17
|
+
}, this.handleMessage = (i) => {
|
|
18
|
+
if (this.iframe == null || !h(i.data)) return;
|
|
19
|
+
if (i.source !== this.iframe.contentWindow) {
|
|
20
20
|
this.log("message dropped: wrong source", {
|
|
21
|
-
eventOrigin:
|
|
21
|
+
eventOrigin: i.origin,
|
|
22
22
|
expectedOrigin: this.origin,
|
|
23
|
-
hasSource:
|
|
23
|
+
hasSource: i.source != null,
|
|
24
24
|
sourceIsExpectedIframe: !1
|
|
25
25
|
});
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
-
if (
|
|
29
|
-
this.log("message dropped: wrong origin", { eventOrigin:
|
|
28
|
+
if (i.origin !== this.origin) {
|
|
29
|
+
this.log("message dropped: wrong origin", { eventOrigin: i.origin, expectedOrigin: this.origin });
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
32
|
+
const r = i.data;
|
|
33
|
+
if (r.type === "ready") {
|
|
34
34
|
this.log("ready received"), this.readyReceived = !0, this.loadGraceTimer != null && (clearTimeout(this.loadGraceTimer), this.loadGraceTimer = null), this.resolveReady();
|
|
35
35
|
for (const o of this.readyListeners)
|
|
36
36
|
try {
|
|
@@ -40,20 +40,20 @@ class a {
|
|
|
40
40
|
}
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
-
if (!
|
|
43
|
+
if (!d(r)) {
|
|
44
44
|
this.log("message dropped: no request_id");
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
const t = this.pending.get(
|
|
48
|
-
t !== void 0 ? (this.log("rpc reply", { request_id:
|
|
49
|
-
}, this.origin =
|
|
50
|
-
this.resolveReady =
|
|
47
|
+
const t = this.pending.get(r.request_id);
|
|
48
|
+
t !== void 0 ? (this.log("rpc reply", { request_id: r.request_id, type: r.type }), this.pending.delete(r.request_id), t.resolve(r)) : this.log("rpc reply for unknown request_id — ignored", { request_id: r.request_id });
|
|
49
|
+
}, this.origin = e.dmonOrigin, this.mode = e.mode ?? "emit", this.debug = e.debug ?? !1, this.postLoadGraceMs = e.postLoadGraceMs ?? 200, this.readyPromise = new Promise((i, r) => {
|
|
50
|
+
this.resolveReady = i, this.rejectReady = r;
|
|
51
51
|
});
|
|
52
52
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
} catch (
|
|
56
|
-
this.failed = !0, this.resolveReady(), this.logError("construction failed — switcher disabled",
|
|
53
|
+
const i = `${e.dmonOrigin}/api/v1/authorize/${e.realmSlug}/embed/bridge?client_id=${encodeURIComponent(e.clientId)}`, r = document.createElement("iframe");
|
|
54
|
+
r.setAttribute("aria-hidden", "true"), r.setAttribute("sandbox", "allow-scripts allow-same-origin"), r.style.display = "none", r.src = i, document.body.appendChild(r), r.onload = this.handleIframeLoad, r.onerror = this.handleIframeError, this.iframe = r, window.addEventListener("message", this.handleMessage), this.log("constructed", { origin: this.origin, mode: this.mode, src: i });
|
|
55
|
+
} catch (i) {
|
|
56
|
+
this.failed = !0, this.resolveReady(), this.logError("construction failed — switcher disabled", i);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
ready() {
|
|
@@ -63,44 +63,64 @@ class a {
|
|
|
63
63
|
if (this.failed || this.destroyed || this.iframe == null)
|
|
64
64
|
return this.log("listSessions skipped — switcher unavailable"), [];
|
|
65
65
|
try {
|
|
66
|
-
const
|
|
67
|
-
return Array.isArray(
|
|
66
|
+
const i = (await this.rpc({ type: "list_sessions" })).sessions;
|
|
67
|
+
return Array.isArray(i) ? (this.log("listSessions resolved", { count: i.length }), this.lastSessions = i, this.lastSessions) : (this.log("listSessions resolved with no sessions array"), []);
|
|
68
68
|
} catch {
|
|
69
69
|
return [];
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Switch to a previously authenticated account.
|
|
74
|
+
*
|
|
75
|
+
* @param sub - The {@link Session.sub} of the target account (the user id), or the
|
|
76
|
+
* `"__new__"` sentinel to force interactive login. The switcher resolves it to the
|
|
77
|
+
* OIDC `login_hint` (username) via the cached {@link listSessions} result.
|
|
78
|
+
*/
|
|
79
|
+
async switchTo(e, i) {
|
|
73
80
|
if (this.failed || this.destroyed || this.iframe == null) {
|
|
74
|
-
this.log("switchTo skipped — switcher unavailable", { sub:
|
|
81
|
+
this.log("switchTo skipped — switcher unavailable", { sub: e });
|
|
75
82
|
return;
|
|
76
83
|
}
|
|
77
84
|
try {
|
|
85
|
+
const r = this.resolveLoginHint(e);
|
|
78
86
|
if (this.mode === "emit") {
|
|
79
|
-
this.log("switchTo emit", { sub: r, listeners: this.switchListeners.length }), this.emitSwitch(r);
|
|
87
|
+
this.log("switchTo emit", { sub: e, loginHint: r, listeners: this.switchListeners.length }), this.emitSwitch(r);
|
|
80
88
|
return;
|
|
81
89
|
}
|
|
82
|
-
const
|
|
83
|
-
this.log("switchTo redirect", { sub: r, prompt:
|
|
90
|
+
const t = e === "__new__" ? "login" : "none", o = i?.returnTo ?? window.location.href;
|
|
91
|
+
this.log("switchTo redirect", { sub: e, loginHint: r, prompt: t, return_to: o }), await this.rpc({
|
|
84
92
|
type: "redirect_authorize",
|
|
85
93
|
login_hint: r,
|
|
86
|
-
prompt:
|
|
87
|
-
return_to:
|
|
94
|
+
prompt: t,
|
|
95
|
+
return_to: o
|
|
88
96
|
});
|
|
89
|
-
} catch (
|
|
90
|
-
this.logError("switchTo failed",
|
|
97
|
+
} catch (r) {
|
|
98
|
+
this.logError("switchTo failed", r);
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
/**
|
|
94
|
-
* Build the URL of the DMon account page for the given
|
|
102
|
+
* Build the URL of the DMon account page for the given account, pre-filled with the
|
|
95
103
|
* `login_hint` so DMon can mount it silently if that user's session is in the cookie.
|
|
96
104
|
*
|
|
105
|
+
* @param sub - The {@link Session.sub} of the target account (the user id); resolved to
|
|
106
|
+
* the OIDC `login_hint` (username) via the cached {@link listSessions} result.
|
|
107
|
+
*
|
|
97
108
|
* This only constructs the string — it never navigates. Put it in an anchor's `href`
|
|
98
109
|
* so the user can choose how to open it (left click = same tab, middle/ctrl click =
|
|
99
110
|
* new tab). Pure and side-effect free; safe to call at any time.
|
|
100
111
|
*/
|
|
101
|
-
accountPageUrl(
|
|
102
|
-
const e = `${this.origin}/account?login_hint=${encodeURIComponent(
|
|
103
|
-
return this.log("accountPageUrl", { sub:
|
|
112
|
+
accountPageUrl(e) {
|
|
113
|
+
const i = this.resolveLoginHint(e), r = `${this.origin}/account?login_hint=${encodeURIComponent(i)}`;
|
|
114
|
+
return this.log("accountPageUrl", { sub: e, loginHint: i, url: r }), r;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a {@link Session.sub} (the user id) to the OIDC `login_hint` (the username)
|
|
118
|
+
* using the most recent {@link listSessions} result. The `"__new__"` sentinel is passed
|
|
119
|
+
* through unchanged. When no cached session matches (e.g. `listSessions` has not run
|
|
120
|
+
* yet), the input is returned unchanged as a best-effort fallback.
|
|
121
|
+
*/
|
|
122
|
+
resolveLoginHint(e) {
|
|
123
|
+
return e === "__new__" ? e : this.lastSessions.find((r) => r.sub === e)?.name ?? e;
|
|
104
124
|
}
|
|
105
125
|
destroy() {
|
|
106
126
|
if (this.destroyed) {
|
|
@@ -110,57 +130,57 @@ class a {
|
|
|
110
130
|
this.destroyed = !0;
|
|
111
131
|
try {
|
|
112
132
|
this.loadGraceTimer != null && (clearTimeout(this.loadGraceTimer), this.loadGraceTimer = null), window.removeEventListener("message", this.handleMessage);
|
|
113
|
-
const
|
|
114
|
-
this.log("destroy", { rejectingPending: this.pending.size }), this.pending.forEach(({ reject:
|
|
115
|
-
} catch (
|
|
116
|
-
this.logError("destroy encountered an error",
|
|
133
|
+
const e = new DOMException("DmonSwitcher destroyed", "AbortError");
|
|
134
|
+
this.log("destroy", { rejectingPending: this.pending.size }), this.pending.forEach(({ reject: i }) => i(e)), this.pending.clear(), this.iframe?.remove(), this.resolveReady();
|
|
135
|
+
} catch (e) {
|
|
136
|
+
this.logError("destroy encountered an error", e);
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
|
-
on(
|
|
120
|
-
|
|
139
|
+
on(e, i) {
|
|
140
|
+
e === "switch" ? this.switchListeners.push(i) : e === "ready" ? this.readyListeners.push(i) : this.errorListeners.push(i);
|
|
121
141
|
}
|
|
122
|
-
rejectBridge(
|
|
142
|
+
rejectBridge(e) {
|
|
123
143
|
if (this.bridgeFailed || this.destroyed || this.failed) return;
|
|
124
144
|
this.bridgeFailed = !0;
|
|
125
|
-
const
|
|
126
|
-
this.log(`bridge failure: ${
|
|
127
|
-
i
|
|
145
|
+
const i = new Error(e);
|
|
146
|
+
this.log(`bridge failure: ${e}`), this.rejectReady(i), this.pending.forEach(({ reject: r }) => {
|
|
147
|
+
r(i);
|
|
128
148
|
}), this.pending.clear();
|
|
129
|
-
for (const
|
|
149
|
+
for (const r of this.errorListeners)
|
|
130
150
|
try {
|
|
131
|
-
i
|
|
151
|
+
r(i);
|
|
132
152
|
} catch (t) {
|
|
133
153
|
this.logError("error handler threw", t);
|
|
134
154
|
}
|
|
135
155
|
}
|
|
136
|
-
log(
|
|
137
|
-
this.debug && (
|
|
156
|
+
log(e, i) {
|
|
157
|
+
this.debug && (i === void 0 ? console.debug(`[DmonSwitcher] ${e}`) : console.debug(`[DmonSwitcher] ${e}`, i));
|
|
138
158
|
}
|
|
139
159
|
// Errors are always surfaced (not gated by `debug`) but never thrown — the library
|
|
140
160
|
// must not crash the consuming application.
|
|
141
|
-
logError(
|
|
142
|
-
|
|
161
|
+
logError(e, i) {
|
|
162
|
+
i === void 0 ? console.error(`[DmonSwitcher] ${e}`) : console.error(`[DmonSwitcher] ${e}`, i);
|
|
143
163
|
}
|
|
144
164
|
// Invoke each consumer "switch" handler in isolation: a throwing handler is logged and
|
|
145
165
|
// skipped, never allowed to break the loop or propagate out of the library.
|
|
146
|
-
emitSwitch(
|
|
147
|
-
for (const
|
|
166
|
+
emitSwitch(e) {
|
|
167
|
+
for (const i of this.switchListeners)
|
|
148
168
|
try {
|
|
149
|
-
|
|
150
|
-
} catch (
|
|
151
|
-
this.logError("switch handler threw",
|
|
169
|
+
i({ loginHint: e });
|
|
170
|
+
} catch (r) {
|
|
171
|
+
this.logError("switch handler threw", r);
|
|
152
172
|
}
|
|
153
173
|
}
|
|
154
|
-
async rpc(
|
|
174
|
+
async rpc(e) {
|
|
155
175
|
if (this.destroyed || this.iframe == null)
|
|
156
176
|
throw new DOMException("DmonSwitcher destroyed", "AbortError");
|
|
157
177
|
if (await this.readyPromise, this.destroyed || this.iframe == null)
|
|
158
178
|
throw new DOMException("DmonSwitcher destroyed", "AbortError");
|
|
159
|
-
const
|
|
160
|
-
this.log("rpc send", { request_id:
|
|
161
|
-
const
|
|
179
|
+
const i = String(this.nextRequestId++);
|
|
180
|
+
this.log("rpc send", { request_id: i, ...e });
|
|
181
|
+
const r = this.iframe;
|
|
162
182
|
return new Promise((t, o) => {
|
|
163
|
-
this.pending.set(
|
|
183
|
+
this.pending.set(i, { resolve: t, reject: o }), r.contentWindow?.postMessage({ ...e, request_id: i }, this.origin);
|
|
164
184
|
});
|
|
165
185
|
}
|
|
166
186
|
}
|