@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.cjs +397 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -1
- package/dist/index.d.ts +81 -1
- package/dist/index.js +387 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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/
|
|
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
|
|
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 :
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|