@gcm-cz/dmon-switcher 0.1.0
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/dist/index.cjs +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +149 -0
- package/package.json +28 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function h(s){if(typeof s!="object"||s===null)return!1;const i=s;return!(i.dmon_bridge!==!0||typeof i.type!="string")}function d(s){return typeof s.request_id=="string"}class l{constructor(i){this.iframe=null,this.nextRequestId=1,this.destroyed=!1,this.failed=!1,this.pending=new Map,this.switchListeners=[],this.readyListeners=[],this.handleMessage=e=>{if(this.iframe==null||!h(e.data))return;if(e.source!==this.iframe.contentWindow){this.log("message dropped: wrong source",{eventOrigin:e.origin,expectedOrigin:this.origin,hasSource:e.source!=null,sourceIsExpectedIframe:!1});return}if(e.origin!==this.origin){this.log("message dropped: wrong origin",{eventOrigin:e.origin,expectedOrigin:this.origin});return}const t=e.data;if(t.type==="ready"){this.log("ready received"),this.resolveReady();for(const o of this.readyListeners)try{o()}catch(n){this.logError("ready handler threw",n)}return}if(!d(t)){this.log("message dropped: no request_id");return}const r=this.pending.get(t.request_id);r!==void 0?(this.log("rpc reply",{request_id:t.request_id,type:t.type}),this.pending.delete(t.request_id),r.resolve(t)):this.log("rpc reply for unknown request_id — ignored",{request_id:t.request_id})},this.origin=i.dmonOrigin,this.mode=i.mode??"emit",this.debug=i.debug??!1,this.readyPromise=new Promise(e=>{this.resolveReady=e});try{const e=`${i.dmonOrigin}/api/v1/authorize/${i.realmSlug}/embed/bridge?client_id=${encodeURIComponent(i.clientId)}`,t=document.createElement("iframe");t.setAttribute("aria-hidden","true"),t.setAttribute("sandbox","allow-scripts allow-same-origin"),t.style.display="none",t.src=e,document.body.appendChild(t),this.iframe=t,window.addEventListener("message",this.handleMessage),this.log("constructed",{origin:this.origin,mode:this.mode,src:e})}catch(e){this.failed=!0,this.resolveReady(),this.logError("construction failed — switcher disabled",e)}}ready(){return this.readyPromise}async listSessions(){if(this.failed||this.destroyed||this.iframe==null)return this.log("listSessions skipped — switcher unavailable"),[];try{const e=(await this.rpc({type:"list_sessions"})).sessions;return Array.isArray(e)?(this.log("listSessions resolved",{count:e.length}),e):(this.log("listSessions resolved with no sessions array"),[])}catch(i){return this.logError("listSessions failed — returning empty list",i),[]}}async switchTo(i,e){if(this.failed||this.destroyed||this.iframe==null){this.log("switchTo skipped — switcher unavailable",{sub:i});return}try{if(this.mode==="emit"){this.log("switchTo emit",{sub:i,listeners:this.switchListeners.length}),this.emitSwitch(i);return}const t=i==="__new__"?"login":"none",r=e?.returnTo??window.location.href;this.log("switchTo redirect",{sub:i,prompt:t,return_to:r}),await this.rpc({type:"redirect_authorize",login_hint:i,prompt:t,return_to:r})}catch(t){this.logError("switchTo failed",t)}}accountPageUrl(i){const e=`${this.origin}/account?login_hint=${encodeURIComponent(i)}`;return this.log("accountPageUrl",{sub:i,url:e}),e}destroy(){if(this.destroyed){this.log("destroy called again — no-op");return}this.destroyed=!0;try{window.removeEventListener("message",this.handleMessage);const i=new DOMException("DmonSwitcher destroyed","AbortError");this.log("destroy",{rejectingPending:this.pending.size}),this.pending.forEach(({reject:e})=>e(i)),this.pending.clear(),this.iframe?.remove(),this.resolveReady()}catch(i){this.logError("destroy encountered an error",i)}}on(i,e){i==="switch"?this.switchListeners.push(e):this.readyListeners.push(e)}log(i,e){this.debug&&(e===void 0?console.debug(`[DmonSwitcher] ${i}`):console.debug(`[DmonSwitcher] ${i}`,e))}logError(i,e){e===void 0?console.error(`[DmonSwitcher] ${i}`):console.error(`[DmonSwitcher] ${i}`,e)}emitSwitch(i){for(const e of this.switchListeners)try{e({sub:i})}catch(t){this.logError("switch handler threw",t)}}async rpc(i){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 e=String(this.nextRequestId++);this.log("rpc send",{request_id:e,...i});const t=this.iframe;return new Promise((r,o)=>{this.pending.set(e,{resolve:r,reject:o}),t.contentWindow?.postMessage({...i,request_id:e},this.origin)})}}exports.DmonSwitcher=l;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
sub: string;
|
|
3
|
+
name: string;
|
|
4
|
+
given_name?: string;
|
|
5
|
+
family_name?: string;
|
|
6
|
+
picture?: string;
|
|
7
|
+
}
|
|
8
|
+
export type SwitcherMode = "emit" | "redirect";
|
|
9
|
+
export interface DmonSwitcherOptions {
|
|
10
|
+
dmonOrigin: string;
|
|
11
|
+
realmSlug: string;
|
|
12
|
+
clientId: string;
|
|
13
|
+
mode?: SwitcherMode;
|
|
14
|
+
/**
|
|
15
|
+
* When true, every internal step (construction, ready handshake, RPC send/receive,
|
|
16
|
+
* dropped messages, switch, account-page open, teardown) is logged via console.debug.
|
|
17
|
+
* Off by default.
|
|
18
|
+
*/
|
|
19
|
+
debug?: boolean;
|
|
20
|
+
}
|
|
21
|
+
type SwitchHandler = (payload: {
|
|
22
|
+
sub: string;
|
|
23
|
+
}) => void;
|
|
24
|
+
type ReadyHandler = () => void;
|
|
25
|
+
export declare class DmonSwitcher {
|
|
26
|
+
private readonly origin;
|
|
27
|
+
private readonly mode;
|
|
28
|
+
private readonly debug;
|
|
29
|
+
private readonly iframe;
|
|
30
|
+
private readonly readyPromise;
|
|
31
|
+
private resolveReady;
|
|
32
|
+
private nextRequestId;
|
|
33
|
+
private destroyed;
|
|
34
|
+
private failed;
|
|
35
|
+
private readonly pending;
|
|
36
|
+
private readonly switchListeners;
|
|
37
|
+
private readonly readyListeners;
|
|
38
|
+
constructor(opts: DmonSwitcherOptions);
|
|
39
|
+
ready(): Promise<void>;
|
|
40
|
+
listSessions(): Promise<Session[]>;
|
|
41
|
+
switchTo(sub: string, opts?: {
|
|
42
|
+
returnTo?: string;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Build the URL of the DMon account page for the given user, pre-filled with the
|
|
46
|
+
* `login_hint` so DMon can mount it silently if that user's session is in the cookie.
|
|
47
|
+
*
|
|
48
|
+
* This only constructs the string — it never navigates. Put it in an anchor's `href`
|
|
49
|
+
* so the user can choose how to open it (left click = same tab, middle/ctrl click =
|
|
50
|
+
* new tab). Pure and side-effect free; safe to call at any time.
|
|
51
|
+
*/
|
|
52
|
+
accountPageUrl(sub: string): string;
|
|
53
|
+
destroy(): void;
|
|
54
|
+
on(event: "switch", handler: SwitchHandler): void;
|
|
55
|
+
on(event: "ready", handler: ReadyHandler): void;
|
|
56
|
+
private log;
|
|
57
|
+
private logError;
|
|
58
|
+
private emitSwitch;
|
|
59
|
+
private rpc;
|
|
60
|
+
private readonly handleMessage;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
function h(s) {
|
|
2
|
+
if (typeof s != "object" || s === null) return !1;
|
|
3
|
+
const i = s;
|
|
4
|
+
return !(i.dmon_bridge !== !0 || typeof i.type != "string");
|
|
5
|
+
}
|
|
6
|
+
function d(s) {
|
|
7
|
+
return typeof s.request_id == "string";
|
|
8
|
+
}
|
|
9
|
+
class l {
|
|
10
|
+
constructor(i) {
|
|
11
|
+
this.iframe = null, this.nextRequestId = 1, this.destroyed = !1, this.failed = !1, this.pending = /* @__PURE__ */ new Map(), this.switchListeners = [], this.readyListeners = [], this.handleMessage = (e) => {
|
|
12
|
+
if (this.iframe == null || !h(e.data)) return;
|
|
13
|
+
if (e.source !== this.iframe.contentWindow) {
|
|
14
|
+
this.log("message dropped: wrong source", {
|
|
15
|
+
eventOrigin: e.origin,
|
|
16
|
+
expectedOrigin: this.origin,
|
|
17
|
+
hasSource: e.source != null,
|
|
18
|
+
sourceIsExpectedIframe: !1
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (e.origin !== this.origin) {
|
|
23
|
+
this.log("message dropped: wrong origin", { eventOrigin: e.origin, expectedOrigin: this.origin });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const t = e.data;
|
|
27
|
+
if (t.type === "ready") {
|
|
28
|
+
this.log("ready received"), this.resolveReady();
|
|
29
|
+
for (const o of this.readyListeners)
|
|
30
|
+
try {
|
|
31
|
+
o();
|
|
32
|
+
} catch (n) {
|
|
33
|
+
this.logError("ready handler threw", n);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!d(t)) {
|
|
38
|
+
this.log("message dropped: no request_id");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const r = this.pending.get(t.request_id);
|
|
42
|
+
r !== void 0 ? (this.log("rpc reply", { request_id: t.request_id, type: t.type }), this.pending.delete(t.request_id), r.resolve(t)) : this.log("rpc reply for unknown request_id — ignored", { request_id: t.request_id });
|
|
43
|
+
}, this.origin = i.dmonOrigin, this.mode = i.mode ?? "emit", this.debug = i.debug ?? !1, this.readyPromise = new Promise((e) => {
|
|
44
|
+
this.resolveReady = e;
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const e = `${i.dmonOrigin}/api/v1/authorize/${i.realmSlug}/embed/bridge?client_id=${encodeURIComponent(i.clientId)}`, t = document.createElement("iframe");
|
|
48
|
+
t.setAttribute("aria-hidden", "true"), t.setAttribute("sandbox", "allow-scripts allow-same-origin"), t.style.display = "none", t.src = e, document.body.appendChild(t), this.iframe = t, window.addEventListener("message", this.handleMessage), this.log("constructed", { origin: this.origin, mode: this.mode, src: e });
|
|
49
|
+
} catch (e) {
|
|
50
|
+
this.failed = !0, this.resolveReady(), this.logError("construction failed — switcher disabled", e);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
ready() {
|
|
54
|
+
return this.readyPromise;
|
|
55
|
+
}
|
|
56
|
+
async listSessions() {
|
|
57
|
+
if (this.failed || this.destroyed || this.iframe == null)
|
|
58
|
+
return this.log("listSessions skipped — switcher unavailable"), [];
|
|
59
|
+
try {
|
|
60
|
+
const e = (await this.rpc({ type: "list_sessions" })).sessions;
|
|
61
|
+
return Array.isArray(e) ? (this.log("listSessions resolved", { count: e.length }), e) : (this.log("listSessions resolved with no sessions array"), []);
|
|
62
|
+
} catch (i) {
|
|
63
|
+
return this.logError("listSessions failed — returning empty list", i), [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async switchTo(i, e) {
|
|
67
|
+
if (this.failed || this.destroyed || this.iframe == null) {
|
|
68
|
+
this.log("switchTo skipped — switcher unavailable", { sub: i });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
if (this.mode === "emit") {
|
|
73
|
+
this.log("switchTo emit", { sub: i, listeners: this.switchListeners.length }), this.emitSwitch(i);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const t = i === "__new__" ? "login" : "none", r = e?.returnTo ?? window.location.href;
|
|
77
|
+
this.log("switchTo redirect", { sub: i, prompt: t, return_to: r }), await this.rpc({
|
|
78
|
+
type: "redirect_authorize",
|
|
79
|
+
login_hint: i,
|
|
80
|
+
prompt: t,
|
|
81
|
+
return_to: r
|
|
82
|
+
});
|
|
83
|
+
} catch (t) {
|
|
84
|
+
this.logError("switchTo failed", t);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build the URL of the DMon account page for the given user, pre-filled with the
|
|
89
|
+
* `login_hint` so DMon can mount it silently if that user's session is in the cookie.
|
|
90
|
+
*
|
|
91
|
+
* This only constructs the string — it never navigates. Put it in an anchor's `href`
|
|
92
|
+
* so the user can choose how to open it (left click = same tab, middle/ctrl click =
|
|
93
|
+
* new tab). Pure and side-effect free; safe to call at any time.
|
|
94
|
+
*/
|
|
95
|
+
accountPageUrl(i) {
|
|
96
|
+
const e = `${this.origin}/account?login_hint=${encodeURIComponent(i)}`;
|
|
97
|
+
return this.log("accountPageUrl", { sub: i, url: e }), e;
|
|
98
|
+
}
|
|
99
|
+
destroy() {
|
|
100
|
+
if (this.destroyed) {
|
|
101
|
+
this.log("destroy called again — no-op");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.destroyed = !0;
|
|
105
|
+
try {
|
|
106
|
+
window.removeEventListener("message", this.handleMessage);
|
|
107
|
+
const i = new DOMException("DmonSwitcher destroyed", "AbortError");
|
|
108
|
+
this.log("destroy", { rejectingPending: this.pending.size }), this.pending.forEach(({ reject: e }) => e(i)), this.pending.clear(), this.iframe?.remove(), this.resolveReady();
|
|
109
|
+
} catch (i) {
|
|
110
|
+
this.logError("destroy encountered an error", i);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
on(i, e) {
|
|
114
|
+
i === "switch" ? this.switchListeners.push(e) : this.readyListeners.push(e);
|
|
115
|
+
}
|
|
116
|
+
log(i, e) {
|
|
117
|
+
this.debug && (e === void 0 ? console.debug(`[DmonSwitcher] ${i}`) : console.debug(`[DmonSwitcher] ${i}`, e));
|
|
118
|
+
}
|
|
119
|
+
// Errors are always surfaced (not gated by `debug`) but never thrown — the library
|
|
120
|
+
// must not crash the consuming application.
|
|
121
|
+
logError(i, e) {
|
|
122
|
+
e === void 0 ? console.error(`[DmonSwitcher] ${i}`) : console.error(`[DmonSwitcher] ${i}`, e);
|
|
123
|
+
}
|
|
124
|
+
// Invoke each consumer "switch" handler in isolation: a throwing handler is logged and
|
|
125
|
+
// skipped, never allowed to break the loop or propagate out of the library.
|
|
126
|
+
emitSwitch(i) {
|
|
127
|
+
for (const e of this.switchListeners)
|
|
128
|
+
try {
|
|
129
|
+
e({ sub: i });
|
|
130
|
+
} catch (t) {
|
|
131
|
+
this.logError("switch handler threw", t);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async rpc(i) {
|
|
135
|
+
if (this.destroyed || this.iframe == null)
|
|
136
|
+
throw new DOMException("DmonSwitcher destroyed", "AbortError");
|
|
137
|
+
if (await this.readyPromise, this.destroyed || this.iframe == null)
|
|
138
|
+
throw new DOMException("DmonSwitcher destroyed", "AbortError");
|
|
139
|
+
const e = String(this.nextRequestId++);
|
|
140
|
+
this.log("rpc send", { request_id: e, ...i });
|
|
141
|
+
const t = this.iframe;
|
|
142
|
+
return new Promise((r, o) => {
|
|
143
|
+
this.pending.set(e, { resolve: r, reject: o }), t.contentWindow?.postMessage({ ...i, request_id: e }, this.origin);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
l as DmonSwitcher
|
|
149
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gcm-cz/dmon-switcher",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "vite build && tsc --emitDeclarationOnly",
|
|
18
|
+
"prepare": "vite build && tsc --emitDeclarationOnly",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vite": "^7.3.1",
|
|
25
|
+
"vitest": "^4.1.6",
|
|
26
|
+
"jsdom": "^29.1.1"
|
|
27
|
+
}
|
|
28
|
+
}
|