@xinosolutions/auth-sdk 0.1.1 → 0.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/dist/index.d.cts CHANGED
@@ -1,3 +1,50 @@
1
+ /** XS Auth browser session account (multi-account SSO on the Auth-Server domain). */
2
+ interface XSBrowserAccount {
3
+ sessionId: string;
4
+ userId: string;
5
+ email: string;
6
+ name: string;
7
+ isPrimary: boolean;
8
+ }
9
+ interface OpenSessionBridgeOptions {
10
+ clientId: string;
11
+ parentOrigin?: string;
12
+ timeoutMs?: number;
13
+ }
14
+ interface SessionBridge {
15
+ listAccounts(): Promise<XSBrowserAccount[]>;
16
+ revokeAccount(sessionId: string): Promise<{
17
+ accounts: XSBrowserAccount[];
18
+ remainingCount: number;
19
+ }>;
20
+ revokeAll(): Promise<void>;
21
+ setPrimary(sessionId: string): Promise<XSBrowserAccount[]>;
22
+ close(): void;
23
+ }
24
+ declare class SessionBridgeError extends Error {
25
+ readonly name = "SessionBridgeError";
26
+ readonly errorCode: string;
27
+ constructor(errorCode: string, message: string);
28
+ }
29
+ /** True when the user must pick another primary account before signing out. */
30
+ declare function isPrimaryAccountError(error: unknown): boolean;
31
+
32
+ interface SignOutWithXSOptions {
33
+ clientId: string;
34
+ parentOrigin?: string;
35
+ /** XS Auth user id (`sub`) for the account active in the host app. */
36
+ activeUserId?: string;
37
+ timeoutMs?: number;
38
+ /** Override Auth-Server base URL (e.g. same-origin `/xs-auth` proxy in local dev). */
39
+ authBaseUrl?: string;
40
+ }
41
+ type SignOutAction = 'revoked_one' | 'revoked_all' | 'cancelled';
42
+ interface SignOutWithXSResult {
43
+ action: SignOutAction;
44
+ revokedCurrent: boolean;
45
+ remainingCount: number;
46
+ }
47
+
1
48
  /** Popup was blocked by the browser or environment. */
2
49
  declare class PopupBlockedError extends Error {
3
50
  readonly name = "PopupBlockedError";
@@ -17,6 +64,15 @@ declare class XSAuthDeniedError extends Error {
17
64
  readonly description: string;
18
65
  constructor(errorCode: string, description: string);
19
66
  }
67
+ /** Session bridge did not respond within timeout. */
68
+ declare class LoginBridgeTimeoutError extends Error {
69
+ readonly name = "LoginBridgeTimeoutError";
70
+ }
71
+
72
+ /** Opens the Auth-Server session bridge popup (multi-account sign-out). */
73
+ declare function openSessionBridge(opts: OpenSessionBridgeOptions): Promise<SessionBridge>;
74
+ /** Google-style sign-out popup on the Auth-Server domain. */
75
+ declare function signOutWithXS(opts: SignOutWithXSOptions): Promise<SignOutWithXSResult>;
20
76
  /** Signed-in user profile from XS Auth. */
21
77
  interface XSAuthUser {
22
78
  email: string;
@@ -28,17 +84,35 @@ interface LoginWithXSOptions {
28
84
  clientId: string;
29
85
  /** Must be on your XS Auth client's allowed domains list (normally `window.location.origin`). */
30
86
  parentOrigin?: string;
87
+ /**
88
+ * Sign-in timeout in ms. Defaults to 180s to allow MFA verification in the Auth-Server popup.
89
+ */
31
90
  timeoutMs?: number;
32
91
  scope?: string;
33
92
  /**
34
93
  * Request a `nonce` inside `idToken` — your backend should verify it matches when validating the JWT.
35
94
  */
36
95
  useNonce?: boolean;
96
+ /**
97
+ * OAuth-style prompt for multi-account SSO on the Auth-Server domain.
98
+ * - `none`: silent sign-in with the primary browser session (fails with `login_required` if absent)
99
+ * - `select_account`: always show the account chooser when sessions exist
100
+ * - `login`: force email/password (add another account)
101
+ */
102
+ prompt?: 'none' | 'select_account' | 'login';
103
+ /** Prefer a specific XS Auth user id (`sub`) when continuing an existing browser session. */
104
+ accountId?: string;
105
+ /** When true and `prompt` is unset, defaults to `none` instead of account chooser. */
106
+ preferPrimary?: boolean;
107
+ /** Override Auth-Server base URL (e.g. same-origin `/xs-auth` proxy in local dev). */
108
+ authBaseUrl?: string;
37
109
  }
38
110
  interface LoginWithXSResult {
39
111
  user: XSAuthUser;
40
112
  /** Short-lived signed JWT — send to your backend to create a session. */
41
113
  idToken: string;
114
+ /** True when the user completed MFA during this sign-in (`amr` includes `mfa`). */
115
+ mfaVerified: boolean;
42
116
  }
43
117
  /** PKCE helpers for tests / custom callers. */
44
118
  declare function createPkcePair(): Promise<{
@@ -52,5 +126,11 @@ declare function createPkcePair(): Promise<{
52
126
  * opens the popup and exchanges postMessages with the PKCE verifier — no fetch to Auth-Server.
53
127
  */
54
128
  declare function loginWithXS(opts: LoginWithXSOptions): Promise<LoginWithXSResult>;
129
+ /** True when an XS Auth idToken indicates MFA was completed at sign-in. */
130
+ declare function idTokenIncludesMfa(idToken: string): boolean;
131
+ /** Read the `amr` claim from an XS Auth idToken payload (no signature verification). */
132
+ declare function parseIdTokenAmr(idToken: string): string[] | null;
133
+ /** True when Auth-Server has no active browser session for silent sign-in. */
134
+ declare function isLoginRequiredError(error: unknown): boolean;
55
135
 
56
- export { LoginTimeoutError, type LoginWithXSOptions, type LoginWithXSResult, PopupBlockedError, PopupClosedError, XSAuthDeniedError, type XSAuthUser, createPkcePair, loginWithXS };
136
+ export { LoginBridgeTimeoutError, LoginTimeoutError, type LoginWithXSOptions, type LoginWithXSResult, type OpenSessionBridgeOptions, PopupBlockedError, PopupClosedError, type SessionBridge, SessionBridgeError, type SignOutAction, type SignOutWithXSOptions, type SignOutWithXSResult, XSAuthDeniedError, type XSAuthUser, type XSBrowserAccount, createPkcePair, idTokenIncludesMfa, isLoginRequiredError, isPrimaryAccountError, loginWithXS, openSessionBridge, parseIdTokenAmr, signOutWithXS };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,50 @@
1
+ /** XS Auth browser session account (multi-account SSO on the Auth-Server domain). */
2
+ interface XSBrowserAccount {
3
+ sessionId: string;
4
+ userId: string;
5
+ email: string;
6
+ name: string;
7
+ isPrimary: boolean;
8
+ }
9
+ interface OpenSessionBridgeOptions {
10
+ clientId: string;
11
+ parentOrigin?: string;
12
+ timeoutMs?: number;
13
+ }
14
+ interface SessionBridge {
15
+ listAccounts(): Promise<XSBrowserAccount[]>;
16
+ revokeAccount(sessionId: string): Promise<{
17
+ accounts: XSBrowserAccount[];
18
+ remainingCount: number;
19
+ }>;
20
+ revokeAll(): Promise<void>;
21
+ setPrimary(sessionId: string): Promise<XSBrowserAccount[]>;
22
+ close(): void;
23
+ }
24
+ declare class SessionBridgeError extends Error {
25
+ readonly name = "SessionBridgeError";
26
+ readonly errorCode: string;
27
+ constructor(errorCode: string, message: string);
28
+ }
29
+ /** True when the user must pick another primary account before signing out. */
30
+ declare function isPrimaryAccountError(error: unknown): boolean;
31
+
32
+ interface SignOutWithXSOptions {
33
+ clientId: string;
34
+ parentOrigin?: string;
35
+ /** XS Auth user id (`sub`) for the account active in the host app. */
36
+ activeUserId?: string;
37
+ timeoutMs?: number;
38
+ /** Override Auth-Server base URL (e.g. same-origin `/xs-auth` proxy in local dev). */
39
+ authBaseUrl?: string;
40
+ }
41
+ type SignOutAction = 'revoked_one' | 'revoked_all' | 'cancelled';
42
+ interface SignOutWithXSResult {
43
+ action: SignOutAction;
44
+ revokedCurrent: boolean;
45
+ remainingCount: number;
46
+ }
47
+
1
48
  /** Popup was blocked by the browser or environment. */
2
49
  declare class PopupBlockedError extends Error {
3
50
  readonly name = "PopupBlockedError";
@@ -17,6 +64,15 @@ declare class XSAuthDeniedError extends Error {
17
64
  readonly description: string;
18
65
  constructor(errorCode: string, description: string);
19
66
  }
67
+ /** Session bridge did not respond within timeout. */
68
+ declare class LoginBridgeTimeoutError extends Error {
69
+ readonly name = "LoginBridgeTimeoutError";
70
+ }
71
+
72
+ /** Opens the Auth-Server session bridge popup (multi-account sign-out). */
73
+ declare function openSessionBridge(opts: OpenSessionBridgeOptions): Promise<SessionBridge>;
74
+ /** Google-style sign-out popup on the Auth-Server domain. */
75
+ declare function signOutWithXS(opts: SignOutWithXSOptions): Promise<SignOutWithXSResult>;
20
76
  /** Signed-in user profile from XS Auth. */
21
77
  interface XSAuthUser {
22
78
  email: string;
@@ -28,17 +84,35 @@ interface LoginWithXSOptions {
28
84
  clientId: string;
29
85
  /** Must be on your XS Auth client's allowed domains list (normally `window.location.origin`). */
30
86
  parentOrigin?: string;
87
+ /**
88
+ * Sign-in timeout in ms. Defaults to 180s to allow MFA verification in the Auth-Server popup.
89
+ */
31
90
  timeoutMs?: number;
32
91
  scope?: string;
33
92
  /**
34
93
  * Request a `nonce` inside `idToken` — your backend should verify it matches when validating the JWT.
35
94
  */
36
95
  useNonce?: boolean;
96
+ /**
97
+ * OAuth-style prompt for multi-account SSO on the Auth-Server domain.
98
+ * - `none`: silent sign-in with the primary browser session (fails with `login_required` if absent)
99
+ * - `select_account`: always show the account chooser when sessions exist
100
+ * - `login`: force email/password (add another account)
101
+ */
102
+ prompt?: 'none' | 'select_account' | 'login';
103
+ /** Prefer a specific XS Auth user id (`sub`) when continuing an existing browser session. */
104
+ accountId?: string;
105
+ /** When true and `prompt` is unset, defaults to `none` instead of account chooser. */
106
+ preferPrimary?: boolean;
107
+ /** Override Auth-Server base URL (e.g. same-origin `/xs-auth` proxy in local dev). */
108
+ authBaseUrl?: string;
37
109
  }
38
110
  interface LoginWithXSResult {
39
111
  user: XSAuthUser;
40
112
  /** Short-lived signed JWT — send to your backend to create a session. */
41
113
  idToken: string;
114
+ /** True when the user completed MFA during this sign-in (`amr` includes `mfa`). */
115
+ mfaVerified: boolean;
42
116
  }
43
117
  /** PKCE helpers for tests / custom callers. */
44
118
  declare function createPkcePair(): Promise<{
@@ -52,5 +126,11 @@ declare function createPkcePair(): Promise<{
52
126
  * opens the popup and exchanges postMessages with the PKCE verifier — no fetch to Auth-Server.
53
127
  */
54
128
  declare function loginWithXS(opts: LoginWithXSOptions): Promise<LoginWithXSResult>;
129
+ /** True when an XS Auth idToken indicates MFA was completed at sign-in. */
130
+ declare function idTokenIncludesMfa(idToken: string): boolean;
131
+ /** Read the `amr` claim from an XS Auth idToken payload (no signature verification). */
132
+ declare function parseIdTokenAmr(idToken: string): string[] | null;
133
+ /** True when Auth-Server has no active browser session for silent sign-in. */
134
+ declare function isLoginRequiredError(error: unknown): boolean;
55
135
 
56
- export { LoginTimeoutError, type LoginWithXSOptions, type LoginWithXSResult, PopupBlockedError, PopupClosedError, XSAuthDeniedError, type XSAuthUser, createPkcePair, loginWithXS };
136
+ export { LoginBridgeTimeoutError, LoginTimeoutError, type LoginWithXSOptions, type LoginWithXSResult, type OpenSessionBridgeOptions, PopupBlockedError, PopupClosedError, type SessionBridge, SessionBridgeError, type SignOutAction, type SignOutWithXSOptions, type SignOutWithXSResult, XSAuthDeniedError, type XSAuthUser, type XSBrowserAccount, createPkcePair, idTokenIncludesMfa, isLoginRequiredError, isPrimaryAccountError, loginWithXS, openSessionBridge, parseIdTokenAmr, signOutWithXS };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // src/index.ts
1
+ // src/errors.ts
2
2
  var PopupBlockedError = class extends Error {
3
3
  name = "PopupBlockedError";
4
4
  };
@@ -18,7 +18,327 @@ var XSAuthDeniedError = class extends Error {
18
18
  this.description = description;
19
19
  }
20
20
  };
21
+ var LoginBridgeTimeoutError = class extends Error {
22
+ name = "LoginBridgeTimeoutError";
23
+ };
24
+
25
+ // src/session-bridge.ts
26
+ var SessionBridgeError = class extends Error {
27
+ name = "SessionBridgeError";
28
+ errorCode;
29
+ constructor(errorCode, message) {
30
+ super(message || errorCode);
31
+ this.errorCode = errorCode;
32
+ }
33
+ };
34
+ var BRIDGE_STATE_KEY = "__xs_bridge_state";
35
+ function parseAccounts(raw) {
36
+ if (!Array.isArray(raw)) return [];
37
+ return raw.map((item) => {
38
+ if (!item || typeof item !== "object") return null;
39
+ const row = item;
40
+ const sessionId = typeof row.sessionId === "string" ? row.sessionId : typeof row.session_id === "string" ? row.session_id : "";
41
+ const userId = typeof row.userId === "string" ? row.userId : typeof row.user_id === "string" ? row.user_id : "";
42
+ const email = typeof row.email === "string" ? row.email : "";
43
+ const name = typeof row.name === "string" ? row.name : email;
44
+ if (!sessionId || !email) return null;
45
+ return {
46
+ sessionId,
47
+ userId,
48
+ email,
49
+ name,
50
+ isPrimary: row.isPrimary === true || row.is_primary === true
51
+ };
52
+ }).filter((x) => x !== null);
53
+ }
54
+ function hiddenBridgePopupFeatures() {
55
+ return "width=1,height=1,left=-10000,top=-10000,resizable=no,scrollbars=no";
56
+ }
57
+ function randomBridgeState() {
58
+ const bytes = new Uint8Array(32);
59
+ crypto.getRandomValues(bytes);
60
+ let binary = "";
61
+ for (let i = 0; i < bytes.length; i++) {
62
+ binary += String.fromCharCode(bytes[i]);
63
+ }
64
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/u, "");
65
+ }
66
+ async function openSessionBridge(opts, authBaseUrl) {
67
+ const parentOrigin = opts.parentOrigin ?? globalThis.location?.origin;
68
+ if (!parentOrigin || typeof window === "undefined") {
69
+ throw new Error(
70
+ "openSessionBridge requires a browser Window and explicit parentOrigin (or window.location)."
71
+ );
72
+ }
73
+ const normalizedBase = new URL(authBaseUrl);
74
+ const authOrigin = normalizedBase.origin;
75
+ const state = randomBridgeState();
76
+ sessionStorage.setItem(BRIDGE_STATE_KEY, state);
77
+ const params = new URLSearchParams({
78
+ client_id: opts.clientId,
79
+ parent_origin: parentOrigin,
80
+ state
81
+ });
82
+ const bridgeUrl = `${normalizedBase.href.replace(/\/?$/u, "")}/session/bridge?${params.toString()}`;
83
+ const popup = window.open(
84
+ bridgeUrl,
85
+ `xs-auth-bridge-${crypto.randomUUID()}`,
86
+ hiddenBridgePopupFeatures()
87
+ );
88
+ if (!popup) throw new PopupBlockedError();
89
+ const timeoutMs = opts.timeoutMs !== void 0 ? opts.timeoutMs : 12e4;
90
+ let cleaned = false;
91
+ let closePoll;
92
+ const teardown = () => {
93
+ if (cleaned) return;
94
+ cleaned = true;
95
+ window.removeEventListener("message", handleMessage);
96
+ if (closePoll !== void 0) clearInterval(closePoll);
97
+ sessionStorage.removeItem(BRIDGE_STATE_KEY);
98
+ try {
99
+ popup.close();
100
+ } catch {
101
+ }
102
+ };
103
+ const pending = /* @__PURE__ */ new Map();
104
+ let bridgeReady = false;
105
+ let readyResolve = null;
106
+ let readyReject = null;
107
+ const readyPromise = new Promise((resolve, reject) => {
108
+ readyResolve = resolve;
109
+ readyReject = reject;
110
+ });
111
+ const handleMessage = (event) => {
112
+ if (event.origin !== authOrigin) return;
113
+ const d = event.data;
114
+ if (d?.source !== "xs-auth") return;
115
+ const st = typeof d.state === "string" ? d.state : "";
116
+ if (!st || st !== sessionStorage.getItem(BRIDGE_STATE_KEY)) return;
117
+ if (d.type === "bridge_ready") {
118
+ bridgeReady = true;
119
+ readyResolve?.();
120
+ return;
121
+ }
122
+ const requestId = typeof d.requestId === "string" ? d.requestId : "";
123
+ if (!requestId) return;
124
+ const entry = pending.get(requestId);
125
+ if (!entry) return;
126
+ pending.delete(requestId);
127
+ if (typeof d.error === "string" && d.error.length > 0) {
128
+ entry.reject(
129
+ new SessionBridgeError(
130
+ String(d.error),
131
+ typeof d.error_description === "string" ? d.error_description : ""
132
+ )
133
+ );
134
+ return;
135
+ }
136
+ if (d.type === "result") {
137
+ entry.resolve(d);
138
+ }
139
+ };
140
+ window.addEventListener("message", handleMessage);
141
+ closePoll = window.setInterval(() => {
142
+ if (!popup.closed) return;
143
+ if (cleaned) return;
144
+ for (const [, entry] of pending) {
145
+ entry.reject(new PopupClosedError());
146
+ }
147
+ pending.clear();
148
+ readyReject?.(new PopupClosedError());
149
+ teardown();
150
+ }, 280);
151
+ const readyTimer = window.setTimeout(() => {
152
+ if (bridgeReady) return;
153
+ readyReject?.(new LoginBridgeTimeoutError());
154
+ teardown();
155
+ }, timeoutMs);
156
+ try {
157
+ await readyPromise;
158
+ } catch (error) {
159
+ clearTimeout(readyTimer);
160
+ throw error;
161
+ }
162
+ clearTimeout(readyTimer);
163
+ const sendRequest = (type, extra = {}) => {
164
+ if (popup.closed) {
165
+ return Promise.reject(new PopupClosedError());
166
+ }
167
+ const requestId = crypto.randomUUID();
168
+ return new Promise((resolve, reject) => {
169
+ const timer = window.setTimeout(() => {
170
+ pending.delete(requestId);
171
+ reject(new LoginBridgeTimeoutError());
172
+ }, timeoutMs);
173
+ pending.set(requestId, {
174
+ resolve: (value) => {
175
+ clearTimeout(timer);
176
+ resolve(value);
177
+ },
178
+ reject: (error) => {
179
+ clearTimeout(timer);
180
+ reject(error);
181
+ }
182
+ });
183
+ try {
184
+ popup.postMessage(
185
+ {
186
+ source: "xs-auth-opener",
187
+ state,
188
+ client_id: opts.clientId,
189
+ requestId,
190
+ type,
191
+ ...extra
192
+ },
193
+ authOrigin
194
+ );
195
+ } catch {
196
+ pending.delete(requestId);
197
+ clearTimeout(timer);
198
+ reject(new SessionBridgeError("post_message_failed", "Could not reach the session bridge."));
199
+ }
200
+ });
201
+ };
202
+ return {
203
+ async listAccounts() {
204
+ const result = await sendRequest("list_accounts");
205
+ return parseAccounts(result.accounts);
206
+ },
207
+ async revokeAccount(sessionId) {
208
+ const result = await sendRequest("revoke", { session_id: sessionId.trim() });
209
+ const accounts = parseAccounts(result.accounts);
210
+ const remainingCount = typeof result.remainingCount === "number" ? result.remainingCount : accounts.length;
211
+ return { accounts, remainingCount };
212
+ },
213
+ async revokeAll() {
214
+ await sendRequest("revoke_all");
215
+ },
216
+ async setPrimary(sessionId) {
217
+ const result = await sendRequest("set_primary", { session_id: sessionId.trim() });
218
+ return parseAccounts(result.accounts);
219
+ },
220
+ close() {
221
+ teardown();
222
+ }
223
+ };
224
+ }
225
+ function isPrimaryAccountError(error) {
226
+ return error instanceof SessionBridgeError && error.errorCode === "primary_account";
227
+ }
228
+
229
+ // src/sign-out.ts
230
+ var SIGN_OUT_STATE_KEY = "__xs_sign_out_state";
231
+ function popupWindowFeatures(width, height) {
232
+ const win = window;
233
+ const outerW = win.outerWidth > 0 ? win.outerWidth : win.innerWidth;
234
+ const outerH = win.outerHeight > 0 ? win.outerHeight : win.innerHeight;
235
+ const screenX = win.screenX ?? win.screenLeft ?? 0;
236
+ const screenY = win.screenY ?? win.screenTop ?? 0;
237
+ const left = Math.round(screenX + Math.max(0, (outerW - width) / 2));
238
+ const top = Math.round(screenY + Math.max(0, (outerH - height) / 2));
239
+ return `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
240
+ }
241
+ function randomState() {
242
+ const bytes = new Uint8Array(32);
243
+ crypto.getRandomValues(bytes);
244
+ let binary = "";
245
+ for (let i = 0; i < bytes.length; i++) {
246
+ binary += String.fromCharCode(bytes[i]);
247
+ }
248
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/u, "");
249
+ }
250
+ function parseSignOutResult(raw, data) {
251
+ return {
252
+ action: raw,
253
+ revokedCurrent: data.revokedCurrent === true,
254
+ remainingCount: typeof data.remainingCount === "number" && Number.isFinite(data.remainingCount) ? Math.max(0, data.remainingCount) : 0
255
+ };
256
+ }
257
+ async function signOutWithXS(opts, authBaseUrl) {
258
+ const parentOrigin = opts.parentOrigin ?? globalThis.location?.origin;
259
+ if (!parentOrigin || typeof window === "undefined") {
260
+ throw new Error(
261
+ "signOutWithXS requires a browser Window and explicit parentOrigin (or window.location)."
262
+ );
263
+ }
264
+ const normalizedBase = new URL(authBaseUrl);
265
+ const authOrigin = normalizedBase.origin;
266
+ const state = randomState();
267
+ sessionStorage.setItem(SIGN_OUT_STATE_KEY, state);
268
+ const params = new URLSearchParams({
269
+ client_id: opts.clientId,
270
+ parent_origin: parentOrigin,
271
+ state
272
+ });
273
+ if (opts.activeUserId?.trim()) {
274
+ params.set("active_user_id", opts.activeUserId.trim());
275
+ }
276
+ const url = `${normalizedBase.href.replace(/\/?$/u, "")}/session/sign-out?${params.toString()}`;
277
+ const popup = window.open(
278
+ url,
279
+ `xs-auth-signout-${crypto.randomUUID()}`,
280
+ popupWindowFeatures(480, 640)
281
+ );
282
+ if (!popup) throw new PopupBlockedError();
283
+ const timeoutMs = opts.timeoutMs !== void 0 ? opts.timeoutMs : 12e4;
284
+ return await new Promise((resolve, reject) => {
285
+ let cleaned = false;
286
+ const teardown = () => {
287
+ if (cleaned) return;
288
+ cleaned = true;
289
+ window.removeEventListener("message", handleMessage);
290
+ clearInterval(closePoll);
291
+ clearTimeout(timer);
292
+ sessionStorage.removeItem(SIGN_OUT_STATE_KEY);
293
+ try {
294
+ popup.close();
295
+ } catch {
296
+ }
297
+ };
298
+ const fail = (error) => {
299
+ teardown();
300
+ reject(error);
301
+ };
302
+ const timer = window.setTimeout(() => {
303
+ fail(new LoginBridgeTimeoutError());
304
+ }, timeoutMs);
305
+ const handleMessage = (event) => {
306
+ if (event.origin !== authOrigin) return;
307
+ const d = event.data;
308
+ if (d?.source !== "xs-auth") return;
309
+ const st = typeof d.state === "string" ? d.state : "";
310
+ if (!st || st !== sessionStorage.getItem(SIGN_OUT_STATE_KEY)) return;
311
+ if (d.type === "sign_out_cancelled") {
312
+ teardown();
313
+ resolve(parseSignOutResult("cancelled", d));
314
+ return;
315
+ }
316
+ if (d.type === "sign_out_complete") {
317
+ teardown();
318
+ const actionRaw = typeof d.action === "string" ? d.action : "revoked_one";
319
+ const action = actionRaw === "revoked_all" ? "revoked_all" : actionRaw === "cancelled" ? "cancelled" : "revoked_one";
320
+ resolve(parseSignOutResult(action, d));
321
+ }
322
+ };
323
+ window.addEventListener("message", handleMessage);
324
+ const closePoll = window.setInterval(() => {
325
+ if (!popup.closed) return;
326
+ if (cleaned) return;
327
+ if (!sessionStorage.getItem(SIGN_OUT_STATE_KEY)) return;
328
+ fail(new PopupClosedError());
329
+ }, 280);
330
+ });
331
+ }
332
+
333
+ // src/index.ts
21
334
  var XS_AUTH_BASE_URL = "https://auth.xinosolutions.com";
335
+ function openSessionBridge2(opts) {
336
+ return openSessionBridge(opts, XS_AUTH_BASE_URL);
337
+ }
338
+ function signOutWithXS2(opts) {
339
+ const authBase = opts.authBaseUrl?.trim() || XS_AUTH_BASE_URL;
340
+ return signOutWithXS(opts, authBase);
341
+ }
22
342
  function bytesBuffer(len) {
23
343
  const out = new Uint8Array(len);
24
344
  crypto.getRandomValues(out);
@@ -57,8 +377,26 @@ function parseSdkUser(raw) {
57
377
  if (lastName) user.lastName = lastName;
58
378
  return user;
59
379
  }
380
+ function resolvePrompt(opts) {
381
+ if (opts.prompt) return opts.prompt;
382
+ if (opts.preferPrimary) return "none";
383
+ return void 0;
384
+ }
385
+ function authPublicPathFromBase(authBaseUrl) {
386
+ try {
387
+ const pathname = new URL(authBaseUrl).pathname.replace(/\/$/u, "");
388
+ return pathname === "/" ? "" : pathname;
389
+ } catch {
390
+ return "";
391
+ }
392
+ }
393
+ function maybeSetPublicPath(params, authBaseUrl) {
394
+ const publicPath = authPublicPathFromBase(authBaseUrl);
395
+ if (publicPath) params.set("public_path", publicPath);
396
+ }
60
397
  async function loginWithXS(opts) {
61
- const normalizedBase = normalizeBaseUrl(XS_AUTH_BASE_URL);
398
+ const authBase = opts.authBaseUrl?.trim() || XS_AUTH_BASE_URL;
399
+ const normalizedBase = normalizeBaseUrl(authBase);
62
400
  const authOrigin = normalizedBase.origin;
63
401
  const parentOrigin = opts.parentOrigin ?? globalThis.location?.origin;
64
402
  if (!parentOrigin || typeof window === "undefined") {
@@ -66,7 +404,7 @@ async function loginWithXS(opts) {
66
404
  "loginWithXS requires a browser Window and explicit parentOrigin (or window.location)."
67
405
  );
68
406
  }
69
- const timeoutMs = opts.timeoutMs !== void 0 ? opts.timeoutMs : 12e4;
407
+ const timeoutMs = opts.timeoutMs !== void 0 ? opts.timeoutMs : 18e4;
70
408
  const state = randomBase64State();
71
409
  sessionStorage.setItem(STATE_KEY_STORAGE, state);
72
410
  const nonce = opts.useNonce ? randomBase64State() : void 0;
@@ -81,13 +419,17 @@ async function loginWithXS(opts) {
81
419
  });
82
420
  if (opts.scope?.trim()) params.set("scope", opts.scope.trim());
83
421
  if (nonce) params.set("nonce", nonce);
422
+ const prompt = resolvePrompt(opts);
423
+ if (prompt) params.set("prompt", prompt);
424
+ if (opts.accountId?.trim()) params.set("account_id", opts.accountId.trim());
425
+ maybeSetPublicPath(params, authBase);
84
426
  const authorizeUrl = `${normalizedBase.href.replace(/\/?$/u, "")}/authorize?${params.toString()}`;
85
- const popupWidth = 480;
86
- const popupHeight = 640;
427
+ const popupWidth = prompt === "none" ? 420 : 480;
428
+ const popupHeight = prompt === "none" ? 120 : 640;
87
429
  const popup = window.open(
88
430
  authorizeUrl,
89
431
  `xs-auth-${crypto.randomUUID()}`,
90
- popupWindowFeatures(popupWidth, popupHeight)
432
+ popupWindowFeatures2(popupWidth, popupHeight)
91
433
  );
92
434
  if (!popup) throw new PopupBlockedError();
93
435
  return await new Promise((resolve, reject) => {
@@ -150,7 +492,7 @@ async function loginWithXS(opts) {
150
492
  window.removeEventListener("message", handleMessage);
151
493
  clearInterval(closePoll);
152
494
  cleaned = true;
153
- resolve({ user, idToken });
495
+ resolve({ user, idToken, mfaVerified: idTokenIncludesMfa(idToken) });
154
496
  try {
155
497
  popup.close();
156
498
  } catch {
@@ -166,13 +508,38 @@ async function loginWithXS(opts) {
166
508
  }, 280);
167
509
  });
168
510
  }
511
+ function idTokenIncludesMfa(idToken) {
512
+ const amr = parseIdTokenAmr(idToken);
513
+ return Array.isArray(amr) && amr.includes("mfa");
514
+ }
515
+ function parseIdTokenAmr(idToken) {
516
+ const payload = parseJwtPayload(idToken);
517
+ if (!payload || !("amr" in payload)) return null;
518
+ const amr = payload.amr;
519
+ if (Array.isArray(amr)) {
520
+ return amr.filter((v) => typeof v === "string");
521
+ }
522
+ if (typeof amr === "string" && amr.trim()) return [amr.trim()];
523
+ return null;
524
+ }
525
+ function parseJwtPayload(token) {
526
+ const parts = token.split(".");
527
+ if (parts.length < 2) return null;
528
+ try {
529
+ const json = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
530
+ const parsed = JSON.parse(json);
531
+ return parsed && typeof parsed === "object" ? parsed : null;
532
+ } catch {
533
+ return null;
534
+ }
535
+ }
169
536
  function randomBase64State() {
170
537
  return base64UrlEncode(bytesBuffer(32));
171
538
  }
172
539
  function normalizeBaseUrl(s) {
173
540
  return new URL(s);
174
541
  }
175
- function popupWindowFeatures(width, height) {
542
+ function popupWindowFeatures2(width, height) {
176
543
  const win = window;
177
544
  const outerW = win.outerWidth > 0 ? win.outerWidth : win.innerWidth;
178
545
  const outerH = win.outerHeight > 0 ? win.outerHeight : win.innerHeight;
@@ -182,12 +549,23 @@ function popupWindowFeatures(width, height) {
182
549
  const top = Math.round(screenY + Math.max(0, (outerH - height) / 2));
183
550
  return `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
184
551
  }
552
+ function isLoginRequiredError(error) {
553
+ return error instanceof XSAuthDeniedError && error.errorCode === "login_required";
554
+ }
185
555
  export {
556
+ LoginBridgeTimeoutError,
186
557
  LoginTimeoutError,
187
558
  PopupBlockedError,
188
559
  PopupClosedError,
560
+ SessionBridgeError,
189
561
  XSAuthDeniedError,
190
562
  createPkcePair,
191
- loginWithXS
563
+ idTokenIncludesMfa,
564
+ isLoginRequiredError,
565
+ isPrimaryAccountError,
566
+ loginWithXS,
567
+ openSessionBridge2 as openSessionBridge,
568
+ parseIdTokenAmr,
569
+ signOutWithXS2 as signOutWithXS
192
570
  };
193
571
  //# sourceMappingURL=index.js.map