@xinosolutions/auth-sdk 0.1.0 → 0.3.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 +382 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -1
- package/dist/index.d.ts +75 -1
- package/dist/index.js +374 -7
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
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;
|
|
@@ -34,6 +90,19 @@ interface LoginWithXSOptions {
|
|
|
34
90
|
* Request a `nonce` inside `idToken` — your backend should verify it matches when validating the JWT.
|
|
35
91
|
*/
|
|
36
92
|
useNonce?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* OAuth-style prompt for multi-account SSO on the Auth-Server domain.
|
|
95
|
+
* - `none`: silent sign-in with the primary browser session (fails with `login_required` if absent)
|
|
96
|
+
* - `select_account`: always show the account chooser when sessions exist
|
|
97
|
+
* - `login`: force email/password (add another account)
|
|
98
|
+
*/
|
|
99
|
+
prompt?: 'none' | 'select_account' | 'login';
|
|
100
|
+
/** Prefer a specific XS Auth user id (`sub`) when continuing an existing browser session. */
|
|
101
|
+
accountId?: string;
|
|
102
|
+
/** When true and `prompt` is unset, defaults to `none` instead of account chooser. */
|
|
103
|
+
preferPrimary?: boolean;
|
|
104
|
+
/** Override Auth-Server base URL (e.g. same-origin `/xs-auth` proxy in local dev). */
|
|
105
|
+
authBaseUrl?: string;
|
|
37
106
|
}
|
|
38
107
|
interface LoginWithXSResult {
|
|
39
108
|
user: XSAuthUser;
|
|
@@ -47,7 +116,12 @@ declare function createPkcePair(): Promise<{
|
|
|
47
116
|
}>;
|
|
48
117
|
/**
|
|
49
118
|
* Opens the XS Auth sign-in popup and returns the signed-in user plus a verifiable `idToken`.
|
|
119
|
+
*
|
|
120
|
+
* All Auth-Server HTTP calls happen inside the popup (same origin). The host app only
|
|
121
|
+
* opens the popup and exchanges postMessages with the PKCE verifier — no fetch to Auth-Server.
|
|
50
122
|
*/
|
|
51
123
|
declare function loginWithXS(opts: LoginWithXSOptions): Promise<LoginWithXSResult>;
|
|
124
|
+
/** True when Auth-Server has no active browser session for silent sign-in. */
|
|
125
|
+
declare function isLoginRequiredError(error: unknown): boolean;
|
|
52
126
|
|
|
53
|
-
export { LoginTimeoutError, type LoginWithXSOptions, type LoginWithXSResult, PopupBlockedError, PopupClosedError, XSAuthDeniedError, type XSAuthUser, createPkcePair, loginWithXS };
|
|
127
|
+
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, isLoginRequiredError, isPrimaryAccountError, loginWithXS, openSessionBridge, 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;
|
|
@@ -34,6 +90,19 @@ interface LoginWithXSOptions {
|
|
|
34
90
|
* Request a `nonce` inside `idToken` — your backend should verify it matches when validating the JWT.
|
|
35
91
|
*/
|
|
36
92
|
useNonce?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* OAuth-style prompt for multi-account SSO on the Auth-Server domain.
|
|
95
|
+
* - `none`: silent sign-in with the primary browser session (fails with `login_required` if absent)
|
|
96
|
+
* - `select_account`: always show the account chooser when sessions exist
|
|
97
|
+
* - `login`: force email/password (add another account)
|
|
98
|
+
*/
|
|
99
|
+
prompt?: 'none' | 'select_account' | 'login';
|
|
100
|
+
/** Prefer a specific XS Auth user id (`sub`) when continuing an existing browser session. */
|
|
101
|
+
accountId?: string;
|
|
102
|
+
/** When true and `prompt` is unset, defaults to `none` instead of account chooser. */
|
|
103
|
+
preferPrimary?: boolean;
|
|
104
|
+
/** Override Auth-Server base URL (e.g. same-origin `/xs-auth` proxy in local dev). */
|
|
105
|
+
authBaseUrl?: string;
|
|
37
106
|
}
|
|
38
107
|
interface LoginWithXSResult {
|
|
39
108
|
user: XSAuthUser;
|
|
@@ -47,7 +116,12 @@ declare function createPkcePair(): Promise<{
|
|
|
47
116
|
}>;
|
|
48
117
|
/**
|
|
49
118
|
* Opens the XS Auth sign-in popup and returns the signed-in user plus a verifiable `idToken`.
|
|
119
|
+
*
|
|
120
|
+
* All Auth-Server HTTP calls happen inside the popup (same origin). The host app only
|
|
121
|
+
* opens the popup and exchanges postMessages with the PKCE verifier — no fetch to Auth-Server.
|
|
50
122
|
*/
|
|
51
123
|
declare function loginWithXS(opts: LoginWithXSOptions): Promise<LoginWithXSResult>;
|
|
124
|
+
/** True when Auth-Server has no active browser session for silent sign-in. */
|
|
125
|
+
declare function isLoginRequiredError(error: unknown): boolean;
|
|
52
126
|
|
|
53
|
-
export { LoginTimeoutError, type LoginWithXSOptions, type LoginWithXSResult, PopupBlockedError, PopupClosedError, XSAuthDeniedError, type XSAuthUser, createPkcePair, loginWithXS };
|
|
127
|
+
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, isLoginRequiredError, isPrimaryAccountError, loginWithXS, openSessionBridge, 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") {
|
|
@@ -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) => {
|
|
@@ -126,6 +468,22 @@ async function loginWithXS(opts) {
|
|
|
126
468
|
);
|
|
127
469
|
return;
|
|
128
470
|
}
|
|
471
|
+
if (d.type === "need_verifier") {
|
|
472
|
+
try {
|
|
473
|
+
popup.postMessage(
|
|
474
|
+
{
|
|
475
|
+
source: "xs-auth-opener",
|
|
476
|
+
state: st,
|
|
477
|
+
code_verifier: pair.verifier,
|
|
478
|
+
client_id: opts.clientId
|
|
479
|
+
},
|
|
480
|
+
authOrigin
|
|
481
|
+
);
|
|
482
|
+
} catch {
|
|
483
|
+
fail(new XSAuthDeniedError("exchange_failed", "Sign in could not be completed."));
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
129
487
|
const idToken = typeof d.id_token === "string" ? d.id_token : typeof d.idToken === "string" ? d.idToken : "";
|
|
130
488
|
const user = parseSdkUser(d.user);
|
|
131
489
|
if (!idToken || !user) return;
|
|
@@ -156,7 +514,7 @@ function randomBase64State() {
|
|
|
156
514
|
function normalizeBaseUrl(s) {
|
|
157
515
|
return new URL(s);
|
|
158
516
|
}
|
|
159
|
-
function
|
|
517
|
+
function popupWindowFeatures2(width, height) {
|
|
160
518
|
const win = window;
|
|
161
519
|
const outerW = win.outerWidth > 0 ? win.outerWidth : win.innerWidth;
|
|
162
520
|
const outerH = win.outerHeight > 0 ? win.outerHeight : win.innerHeight;
|
|
@@ -166,12 +524,21 @@ function popupWindowFeatures(width, height) {
|
|
|
166
524
|
const top = Math.round(screenY + Math.max(0, (outerH - height) / 2));
|
|
167
525
|
return `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
|
168
526
|
}
|
|
527
|
+
function isLoginRequiredError(error) {
|
|
528
|
+
return error instanceof XSAuthDeniedError && error.errorCode === "login_required";
|
|
529
|
+
}
|
|
169
530
|
export {
|
|
531
|
+
LoginBridgeTimeoutError,
|
|
170
532
|
LoginTimeoutError,
|
|
171
533
|
PopupBlockedError,
|
|
172
534
|
PopupClosedError,
|
|
535
|
+
SessionBridgeError,
|
|
173
536
|
XSAuthDeniedError,
|
|
174
537
|
createPkcePair,
|
|
175
|
-
|
|
538
|
+
isLoginRequiredError,
|
|
539
|
+
isPrimaryAccountError,
|
|
540
|
+
loginWithXS,
|
|
541
|
+
openSessionBridge2 as openSessionBridge,
|
|
542
|
+
signOutWithXS2 as signOutWithXS
|
|
176
543
|
};
|
|
177
544
|
//# sourceMappingURL=index.js.map
|