@xinosolutions/auth-sdk 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Xino Solutions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ <div align="center">
2
+
3
+ <img src="https://camo.githubusercontent.com/cfe32456bb3319eb01699f6e37171b62fdef7878a4370eaa899f7ba17457fa0f/68747470733a2f2f78696e6f736f6c7574696f6e732e636f6d2f6c6f676f732f78696e6f2d6c6f676f2e706e67" alt="XinoSolutions Logo" width="120" />
4
+
5
+ # @xinosolutions/auth-sdk
6
+
7
+ Sign in with XS Auth — like Google or Microsoft, one button and you get the user back.
8
+
9
+ </div>
10
+
11
+ ---
12
+
13
+ ## About XinoSolutions
14
+
15
+ **XinoSolutions** is a software development company dedicated to creating high-quality, developer-friendly solutions. We build modern tools and components that help teams ship secure, polished products faster.
16
+
17
+ `@xinosolutions/auth-sdk` is part of our open-source initiative to make **XS Auth** easy to integrate into any web application.
18
+
19
+ ---
20
+
21
+ Add **Login with XS** next to Google and Microsoft. Call `loginWithXS` with your `clientId` and get the signed-in user back.
22
+
23
+ **Browser-only.** Requires `window`, `sessionStorage`, and `crypto`. Does not run in Node.js.
24
+
25
+ ## Why use this package
26
+
27
+ - **One line to sign in** — `loginWithXS({ clientId: 'xs_your_app' })`
28
+ - **User profile included** — `email`, `firstName`, `lastName`
29
+ - **Popup login** — users stay on your page
30
+ - **Typed errors** — popup blocked, cancelled, timeout, denied
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install @xinosolutions/auth-sdk
36
+ ```
37
+
38
+ ```bash
39
+ yarn add @xinosolutions/auth-sdk
40
+ ```
41
+
42
+ ```bash
43
+ pnpm add @xinosolutions/auth-sdk
44
+ ```
45
+
46
+ ## Before you start
47
+
48
+ Get your **`clientId`** from Xino Solutions (e.g. `xs_your_app`).
49
+
50
+ Register **allowed domains** for every URL where your app runs (`http://localhost:3000`, `https://app.example.com`, etc.).
51
+
52
+ ## Usage
53
+
54
+ ```typescript
55
+ import { loginWithXS, PopupBlockedError } from '@xinosolutions/auth-sdk';
56
+
57
+ async function signInWithXS() {
58
+ try {
59
+ const { user, idToken } = await loginWithXS({
60
+ clientId: 'xs_your_app',
61
+ });
62
+
63
+ console.log(user.email, user.firstName, user.lastName);
64
+
65
+ await fetch('/api/auth/session', {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ credentials: 'include',
69
+ body: JSON.stringify({ idToken }),
70
+ });
71
+ } catch (e) {
72
+ if (e instanceof PopupBlockedError) {
73
+ alert('Please allow popups for this site.');
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Result shape
80
+
81
+ ```json
82
+ {
83
+ "user": {
84
+ "email": "user@example.com",
85
+ "firstName": "Jane",
86
+ "lastName": "Doe"
87
+ },
88
+ "idToken": "eyJhbGciOiJSUzI1NiIs..."
89
+ }
90
+ ```
91
+
92
+ - **`user`** — display in your UI immediately
93
+ - **`idToken`** — send to **your backend** to create a session
94
+
95
+ ### Handle errors
96
+
97
+ ```typescript
98
+ import {
99
+ loginWithXS,
100
+ PopupBlockedError,
101
+ PopupClosedError,
102
+ LoginTimeoutError,
103
+ XSAuthDeniedError,
104
+ } from '@xinosolutions/auth-sdk';
105
+
106
+ try {
107
+ const { user, idToken } = await loginWithXS({ clientId: 'xs_your_app' });
108
+ } catch (e) {
109
+ if (e instanceof PopupBlockedError) { /* allow popups */ }
110
+ else if (e instanceof PopupClosedError) { /* user cancelled */ }
111
+ else if (e instanceof LoginTimeoutError) { /* retry */ }
112
+ else if (e instanceof XSAuthDeniedError) { /* e.errorCode, e.description */ }
113
+ }
114
+ ```
115
+
116
+ ## React example
117
+
118
+ ```tsx
119
+ import { useState } from 'react';
120
+ import { loginWithXS, PopupBlockedError } from '@xinosolutions/auth-sdk';
121
+
122
+ export function LoginWithXSButton() {
123
+ const [user, setUser] = useState(null);
124
+
125
+ async function handleSignIn() {
126
+ try {
127
+ const result = await loginWithXS({ clientId: 'xs_your_app' });
128
+ setUser(result.user);
129
+ await fetch('/api/auth/session', {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' },
132
+ credentials: 'include',
133
+ body: JSON.stringify({ idToken: result.idToken }),
134
+ });
135
+ } catch (e) {
136
+ if (e instanceof PopupBlockedError) alert('Allow popups and try again.');
137
+ }
138
+ }
139
+
140
+ return (
141
+ <>
142
+ <button type="button" onClick={handleSignIn}>
143
+ Login with XS
144
+ </button>
145
+ {user ? <p>Signed in as {user.email}</p> : null}
146
+ </>
147
+ );
148
+ }
149
+ ```
150
+
151
+ ## API reference
152
+
153
+ ### `loginWithXS(options)`
154
+
155
+ | Option | Type | Default | Description |
156
+ |---|---|---|---|
157
+ | `clientId` | `string` | — | Your public client id |
158
+ | `parentOrigin` | `string` | `window.location.origin` | Must be on your **allowed domains** list |
159
+ | `timeoutMs` | `number` | `120000` | Max wait for login (ms) |
160
+ | `scope` | `string` | — | Optional scope |
161
+ | `useNonce` | `boolean` | `false` | Optional nonce for backend session setup |
162
+
163
+ **Returns:** `Promise<{ user: XSAuthUser; idToken: string }>`
164
+
165
+ ### Error classes
166
+
167
+ | Error | When |
168
+ |---|---|
169
+ | `PopupBlockedError` | Popup blocked |
170
+ | `PopupClosedError` | User closed popup |
171
+ | `LoginTimeoutError` | Timed out |
172
+ | `XSAuthDeniedError` | Sign-in denied |
173
+
174
+ ## Tips
175
+
176
+ - Trigger login from a **button click** (popup blockers).
177
+ - Register **all domains** (local + production) as allowed domains.
178
+
179
+ ## Troubleshooting
180
+
181
+ | Symptom | What to check |
182
+ |---|---|
183
+ | Popup blocked | User gesture + allow popups |
184
+ | Domain / origin error | URL not on allowed domains list |
185
+ | Times out | Increase `timeoutMs` or try again |
186
+
187
+ ## License
188
+
189
+ MIT
190
+
191
+ ## Support
192
+
193
+ For `clientId`, allowed domains, or integration help, contact **Xino Solutions**.
package/dist/index.cjs ADDED
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ LoginTimeoutError: () => LoginTimeoutError,
24
+ PopupBlockedError: () => PopupBlockedError,
25
+ PopupClosedError: () => PopupClosedError,
26
+ XSAuthDeniedError: () => XSAuthDeniedError,
27
+ createPkcePair: () => createPkcePair,
28
+ loginWithXS: () => loginWithXS
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var PopupBlockedError = class extends Error {
32
+ name = "PopupBlockedError";
33
+ };
34
+ var PopupClosedError = class extends Error {
35
+ name = "PopupClosedError";
36
+ };
37
+ var LoginTimeoutError = class extends Error {
38
+ name = "LoginTimeoutError";
39
+ };
40
+ var XSAuthDeniedError = class extends Error {
41
+ name = "XSAuthDeniedError";
42
+ errorCode;
43
+ description;
44
+ constructor(errorCode, description) {
45
+ super(description || errorCode || "xs_auth_denied");
46
+ this.errorCode = errorCode;
47
+ this.description = description;
48
+ }
49
+ };
50
+ var XS_AUTH_BASE_URL = "https://auth.xinosolutions.com";
51
+ function bytesBuffer(len) {
52
+ const out = new Uint8Array(len);
53
+ crypto.getRandomValues(out);
54
+ return out;
55
+ }
56
+ function base64UrlEncode(buf) {
57
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
58
+ let binary = "";
59
+ const chunkSize = 32768;
60
+ for (let i = 0; i < bytes.length; i += chunkSize) {
61
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
62
+ }
63
+ const b64 = btoa(binary);
64
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/u, "");
65
+ }
66
+ async function pkceVerifierAndChallenge() {
67
+ const verifier = base64UrlEncode(bytesBuffer(64));
68
+ const data = new TextEncoder().encode(verifier);
69
+ const digest = await crypto.subtle.digest("SHA-256", data);
70
+ const challenge = base64UrlEncode(digest);
71
+ return { verifier, challenge };
72
+ }
73
+ async function createPkcePair() {
74
+ return pkceVerifierAndChallenge();
75
+ }
76
+ var STATE_KEY_STORAGE = "__xs_login_state";
77
+ function parseSdkUser(raw) {
78
+ if (!raw || typeof raw !== "object") return null;
79
+ const u = raw;
80
+ const email = typeof u.email === "string" ? u.email.trim() : "";
81
+ if (!email) return null;
82
+ const user = { email };
83
+ const firstName = typeof u.firstName === "string" ? u.firstName.trim() : "";
84
+ const lastName = typeof u.lastName === "string" ? u.lastName.trim() : "";
85
+ if (firstName) user.firstName = firstName;
86
+ if (lastName) user.lastName = lastName;
87
+ return user;
88
+ }
89
+ async function loginWithXS(opts) {
90
+ const normalizedBase = normalizeBaseUrl(XS_AUTH_BASE_URL);
91
+ const authOrigin = normalizedBase.origin;
92
+ const parentOrigin = opts.parentOrigin ?? globalThis.location?.origin;
93
+ if (!parentOrigin || typeof window === "undefined") {
94
+ throw new Error(
95
+ "loginWithXS requires a browser Window and explicit parentOrigin (or window.location)."
96
+ );
97
+ }
98
+ const timeoutMs = opts.timeoutMs !== void 0 ? opts.timeoutMs : 12e4;
99
+ const state = randomBase64State();
100
+ sessionStorage.setItem(STATE_KEY_STORAGE, state);
101
+ const nonce = opts.useNonce ? randomBase64State() : void 0;
102
+ const pair = await pkceVerifierAndChallenge();
103
+ const params = new URLSearchParams({
104
+ client_id: opts.clientId,
105
+ state,
106
+ code_challenge: pair.challenge,
107
+ code_challenge_method: "S256",
108
+ parent_origin: parentOrigin,
109
+ response_mode: "popup"
110
+ });
111
+ if (opts.scope?.trim()) params.set("scope", opts.scope.trim());
112
+ if (nonce) params.set("nonce", nonce);
113
+ const authorizeUrl = `${normalizedBase.href.replace(/\/?$/u, "")}/authorize?${params.toString()}`;
114
+ const popupWidth = 480;
115
+ const popupHeight = 640;
116
+ const popup = window.open(
117
+ authorizeUrl,
118
+ `xs-auth-${crypto.randomUUID()}`,
119
+ popupWindowFeatures(popupWidth, popupHeight)
120
+ );
121
+ if (!popup) throw new PopupBlockedError();
122
+ return await new Promise((resolve, reject) => {
123
+ let cleaned = false;
124
+ const teardown = () => {
125
+ if (cleaned) return;
126
+ cleaned = true;
127
+ window.removeEventListener("message", handleMessage);
128
+ clearInterval(closePoll);
129
+ clearTimeout(timer);
130
+ try {
131
+ popup.close();
132
+ } catch {
133
+ }
134
+ };
135
+ const fail = (e) => {
136
+ teardown();
137
+ reject(e instanceof Error ? e : new Error(String(e)));
138
+ };
139
+ const timer = window.setTimeout(() => {
140
+ fail(new LoginTimeoutError());
141
+ }, timeoutMs);
142
+ const handleMessage = (event) => {
143
+ if (event.origin !== authOrigin) return;
144
+ const d = event.data;
145
+ if (d?.source !== "xs-auth") return;
146
+ const st = typeof d.state === "string" ? d.state : "";
147
+ if (!st || st !== sessionStorage.getItem(STATE_KEY_STORAGE)) return;
148
+ if (typeof d.error === "string" && d.error.length > 0) {
149
+ teardown();
150
+ reject(
151
+ new XSAuthDeniedError(
152
+ String(d.error),
153
+ typeof d.error_description === "string" ? d.error_description : ""
154
+ )
155
+ );
156
+ return;
157
+ }
158
+ const idToken = typeof d.id_token === "string" ? d.id_token : typeof d.idToken === "string" ? d.idToken : "";
159
+ const user = parseSdkUser(d.user);
160
+ if (!idToken || !user) return;
161
+ sessionStorage.removeItem(STATE_KEY_STORAGE);
162
+ clearTimeout(timer);
163
+ window.removeEventListener("message", handleMessage);
164
+ clearInterval(closePoll);
165
+ cleaned = true;
166
+ resolve({ user, idToken });
167
+ try {
168
+ popup.close();
169
+ } catch {
170
+ }
171
+ };
172
+ window.addEventListener("message", handleMessage);
173
+ const closePoll = window.setInterval(() => {
174
+ if (!popup.closed) return;
175
+ if (cleaned) return;
176
+ const pending = sessionStorage.getItem(STATE_KEY_STORAGE);
177
+ if (!pending) return;
178
+ fail(new PopupClosedError());
179
+ }, 280);
180
+ });
181
+ }
182
+ function randomBase64State() {
183
+ return base64UrlEncode(bytesBuffer(32));
184
+ }
185
+ function normalizeBaseUrl(s) {
186
+ return new URL(s);
187
+ }
188
+ function popupWindowFeatures(width, height) {
189
+ const win = window;
190
+ const outerW = win.outerWidth > 0 ? win.outerWidth : win.innerWidth;
191
+ const outerH = win.outerHeight > 0 ? win.outerHeight : win.innerHeight;
192
+ const screenX = win.screenX ?? win.screenLeft ?? 0;
193
+ const screenY = win.screenY ?? win.screenTop ?? 0;
194
+ const left = Math.round(screenX + Math.max(0, (outerW - width) / 2));
195
+ const top = Math.round(screenY + Math.max(0, (outerH - height) / 2));
196
+ return `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
197
+ }
198
+ // Annotate the CommonJS export names for ESM import in node:
199
+ 0 && (module.exports = {
200
+ LoginTimeoutError,
201
+ PopupBlockedError,
202
+ PopupClosedError,
203
+ XSAuthDeniedError,
204
+ createPkcePair,
205
+ loginWithXS
206
+ });
207
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/** Popup was blocked by the browser or environment. */\r\nexport class PopupBlockedError extends Error {\r\n readonly name = 'PopupBlockedError';\r\n}\r\n\r\n/** User closed the popup before completing login. */\r\nexport class PopupClosedError extends Error {\r\n readonly name = 'PopupClosedError';\r\n}\r\n\r\n/** XS Auth popup did not complete within timeout. */\r\nexport class LoginTimeoutError extends Error {\r\n readonly name = 'LoginTimeoutError';\r\n}\r\n\r\n/** XS Auth returned an OAuth-style error inside postMessage. */\r\nexport class XSAuthDeniedError extends Error {\r\n readonly name = 'XSAuthDeniedError';\r\n readonly errorCode: string;\r\n readonly description: string;\r\n\r\n constructor(errorCode: string, description: string) {\r\n super(description || errorCode || 'xs_auth_denied');\r\n this.errorCode = errorCode;\r\n this.description = description;\r\n }\r\n}\r\n\r\n/** XS Auth server — baked in at build time (local vs production). */\r\nconst XS_AUTH_BASE_URL = __XS_AUTH_BASE_URL__;\r\n\r\n/** Signed-in user profile from XS Auth. */\r\nexport interface XSAuthUser {\r\n email: string;\r\n firstName?: string;\r\n lastName?: string;\r\n}\r\n\r\nexport interface LoginWithXSOptions {\r\n /** Your app's public client id from Xino Solutions. */\r\n clientId: string;\r\n /** Must be on your XS Auth client's allowed domains list (normally `window.location.origin`). */\r\n parentOrigin?: string;\r\n timeoutMs?: number;\r\n scope?: string;\r\n /**\r\n * Request a `nonce` inside `idToken` — your backend should verify it matches when validating the JWT.\r\n */\r\n useNonce?: boolean;\r\n}\r\n\r\nexport interface LoginWithXSResult {\r\n user: XSAuthUser;\r\n /** Short-lived signed JWT — send to your backend to create a session. */\r\n idToken: string;\r\n}\r\n\r\nfunction bytesBuffer(len: number): Uint8Array {\r\n const out = new Uint8Array(len);\r\n crypto.getRandomValues(out);\r\n return out;\r\n}\r\n\r\nfunction base64UrlEncode(buf: Uint8Array | ArrayBuffer): string {\r\n const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);\r\n let binary = '';\r\n const chunkSize = 0x8000;\r\n for (let i = 0; i < bytes.length; i += chunkSize) {\r\n binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));\r\n }\r\n const b64 = btoa(binary);\r\n return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/u, '');\r\n}\r\n\r\nasync function pkceVerifierAndChallenge(): Promise<{\r\n verifier: string;\r\n challenge: string;\r\n}> {\r\n const verifier = base64UrlEncode(bytesBuffer(64));\r\n const data = new TextEncoder().encode(verifier);\r\n const digest = await crypto.subtle.digest('SHA-256', data);\r\n const challenge = base64UrlEncode(digest);\r\n return { verifier, challenge };\r\n}\r\n\r\n/** PKCE helpers for tests / custom callers. */\r\nexport async function createPkcePair(): Promise<{\r\n verifier: string;\r\n challenge: string;\r\n}> {\r\n return pkceVerifierAndChallenge();\r\n}\r\n\r\nconst STATE_KEY_STORAGE = '__xs_login_state';\r\n\r\nfunction parseSdkUser(raw: unknown): XSAuthUser | null {\r\n if (!raw || typeof raw !== 'object') return null;\r\n const u = raw as Record<string, unknown>;\r\n const email = typeof u.email === 'string' ? u.email.trim() : '';\r\n if (!email) return null;\r\n const user: XSAuthUser = { email };\r\n const firstName =\r\n typeof u.firstName === 'string' ? u.firstName.trim() : '';\r\n const lastName = typeof u.lastName === 'string' ? u.lastName.trim() : '';\r\n if (firstName) user.firstName = firstName;\r\n if (lastName) user.lastName = lastName;\r\n return user;\r\n}\r\n\r\n/**\r\n * Opens the XS Auth sign-in popup and returns the signed-in user plus a verifiable `idToken`.\r\n */\r\nexport async function loginWithXS(\r\n opts: LoginWithXSOptions,\r\n): Promise<LoginWithXSResult> {\r\n const normalizedBase = normalizeBaseUrl(XS_AUTH_BASE_URL);\r\n const authOrigin = normalizedBase.origin;\r\n const parentOrigin = (opts.parentOrigin ?? globalThis.location?.origin) as\r\n | string\r\n | undefined;\r\n if (!parentOrigin || typeof window === 'undefined') {\r\n throw new Error(\r\n 'loginWithXS requires a browser Window and explicit parentOrigin (or window.location).',\r\n );\r\n }\r\n\r\n const timeoutMs =\r\n opts.timeoutMs !== undefined ? opts.timeoutMs : 120_000;\r\n\r\n const state = randomBase64State();\r\n sessionStorage.setItem(STATE_KEY_STORAGE, state);\r\n\r\n const nonce = opts.useNonce ? randomBase64State() : undefined;\r\n const pair = await pkceVerifierAndChallenge();\r\n\r\n const params = new URLSearchParams({\r\n client_id: opts.clientId,\r\n state,\r\n code_challenge: pair.challenge,\r\n code_challenge_method: 'S256',\r\n parent_origin: parentOrigin,\r\n response_mode: 'popup',\r\n });\r\n if (opts.scope?.trim()) params.set('scope', opts.scope.trim());\r\n if (nonce) params.set('nonce', nonce);\r\n\r\n const authorizeUrl = `${normalizedBase.href.replace(/\\/?$/u, '')}/authorize?${params.toString()}`;\r\n\r\n const popupWidth = 480;\r\n const popupHeight = 640;\r\n const popup = window.open(\r\n authorizeUrl,\r\n `xs-auth-${crypto.randomUUID()}`,\r\n popupWindowFeatures(popupWidth, popupHeight),\r\n );\r\n if (!popup) throw new PopupBlockedError();\r\n\r\n return await new Promise<LoginWithXSResult>((resolve, reject) => {\r\n let cleaned = false;\r\n const teardown = (): void => {\r\n if (cleaned) return;\r\n cleaned = true;\r\n window.removeEventListener('message', handleMessage as EventListener);\r\n clearInterval(closePoll);\r\n clearTimeout(timer);\r\n try {\r\n popup.close();\r\n } catch {\r\n // ignore\r\n }\r\n };\r\n\r\n const fail = (e: unknown): void => {\r\n teardown();\r\n reject(e instanceof Error ? e : new Error(String(e)));\r\n };\r\n\r\n const timer = window.setTimeout(() => {\r\n fail(new LoginTimeoutError());\r\n }, timeoutMs);\r\n\r\n const handleMessage = (event: MessageEvent): void => {\r\n if (event.origin !== authOrigin) return;\r\n const d = event.data as Partial<Record<string, unknown>>;\r\n if (d?.source !== 'xs-auth') return;\r\n\r\n const st = typeof d.state === 'string' ? d.state : '';\r\n if (!st || st !== sessionStorage.getItem(STATE_KEY_STORAGE)) return;\r\n\r\n if (typeof d.error === 'string' && d.error.length > 0) {\r\n teardown();\r\n reject(\r\n new XSAuthDeniedError(\r\n String(d.error),\r\n typeof d.error_description === 'string'\r\n ? d.error_description\r\n : '',\r\n ),\r\n );\r\n return;\r\n }\r\n\r\n const idToken =\r\n typeof d.id_token === 'string'\r\n ? d.id_token\r\n : typeof d.idToken === 'string'\r\n ? d.idToken\r\n : '';\r\n const user = parseSdkUser(d.user);\r\n if (!idToken || !user) return;\r\n\r\n sessionStorage.removeItem(STATE_KEY_STORAGE);\r\n clearTimeout(timer);\r\n window.removeEventListener('message', handleMessage as EventListener);\r\n clearInterval(closePoll);\r\n cleaned = true;\r\n\r\n resolve({ user, idToken });\r\n\r\n try {\r\n popup.close();\r\n } catch {\r\n // ignore\r\n }\r\n };\r\n\r\n window.addEventListener('message', handleMessage as EventListener);\r\n\r\n const closePoll = window.setInterval(() => {\r\n if (!popup.closed) return;\r\n if (cleaned) return;\r\n const pending = sessionStorage.getItem(STATE_KEY_STORAGE);\r\n if (!pending) return;\r\n fail(new PopupClosedError());\r\n }, 280);\r\n });\r\n}\r\n\r\nfunction randomBase64State(): string {\r\n return base64UrlEncode(bytesBuffer(32));\r\n}\r\n\r\nfunction normalizeBaseUrl(s: string): URL {\r\n return new URL(s);\r\n}\r\n\r\n/** Center popup over the opener window (falls back to screen center). */\r\nfunction popupWindowFeatures(width: number, height: number): string {\r\n const win = window;\r\n const outerW = win.outerWidth > 0 ? win.outerWidth : win.innerWidth;\r\n const outerH = win.outerHeight > 0 ? win.outerHeight : win.innerHeight;\r\n const screenX = win.screenX ?? win.screenLeft ?? 0;\r\n const screenY = win.screenY ?? win.screenTop ?? 0;\r\n const left = Math.round(screenX + Math.max(0, (outerW - width) / 2));\r\n const top = Math.round(screenY + Math.max(0, (outerH - height) / 2));\r\n return `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;\r\n}\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,53 @@
1
+ /** Popup was blocked by the browser or environment. */
2
+ declare class PopupBlockedError extends Error {
3
+ readonly name = "PopupBlockedError";
4
+ }
5
+ /** User closed the popup before completing login. */
6
+ declare class PopupClosedError extends Error {
7
+ readonly name = "PopupClosedError";
8
+ }
9
+ /** XS Auth popup did not complete within timeout. */
10
+ declare class LoginTimeoutError extends Error {
11
+ readonly name = "LoginTimeoutError";
12
+ }
13
+ /** XS Auth returned an OAuth-style error inside postMessage. */
14
+ declare class XSAuthDeniedError extends Error {
15
+ readonly name = "XSAuthDeniedError";
16
+ readonly errorCode: string;
17
+ readonly description: string;
18
+ constructor(errorCode: string, description: string);
19
+ }
20
+ /** Signed-in user profile from XS Auth. */
21
+ interface XSAuthUser {
22
+ email: string;
23
+ firstName?: string;
24
+ lastName?: string;
25
+ }
26
+ interface LoginWithXSOptions {
27
+ /** Your app's public client id from Xino Solutions. */
28
+ clientId: string;
29
+ /** Must be on your XS Auth client's allowed domains list (normally `window.location.origin`). */
30
+ parentOrigin?: string;
31
+ timeoutMs?: number;
32
+ scope?: string;
33
+ /**
34
+ * Request a `nonce` inside `idToken` — your backend should verify it matches when validating the JWT.
35
+ */
36
+ useNonce?: boolean;
37
+ }
38
+ interface LoginWithXSResult {
39
+ user: XSAuthUser;
40
+ /** Short-lived signed JWT — send to your backend to create a session. */
41
+ idToken: string;
42
+ }
43
+ /** PKCE helpers for tests / custom callers. */
44
+ declare function createPkcePair(): Promise<{
45
+ verifier: string;
46
+ challenge: string;
47
+ }>;
48
+ /**
49
+ * Opens the XS Auth sign-in popup and returns the signed-in user plus a verifiable `idToken`.
50
+ */
51
+ declare function loginWithXS(opts: LoginWithXSOptions): Promise<LoginWithXSResult>;
52
+
53
+ export { LoginTimeoutError, type LoginWithXSOptions, type LoginWithXSResult, PopupBlockedError, PopupClosedError, XSAuthDeniedError, type XSAuthUser, createPkcePair, loginWithXS };
@@ -0,0 +1,53 @@
1
+ /** Popup was blocked by the browser or environment. */
2
+ declare class PopupBlockedError extends Error {
3
+ readonly name = "PopupBlockedError";
4
+ }
5
+ /** User closed the popup before completing login. */
6
+ declare class PopupClosedError extends Error {
7
+ readonly name = "PopupClosedError";
8
+ }
9
+ /** XS Auth popup did not complete within timeout. */
10
+ declare class LoginTimeoutError extends Error {
11
+ readonly name = "LoginTimeoutError";
12
+ }
13
+ /** XS Auth returned an OAuth-style error inside postMessage. */
14
+ declare class XSAuthDeniedError extends Error {
15
+ readonly name = "XSAuthDeniedError";
16
+ readonly errorCode: string;
17
+ readonly description: string;
18
+ constructor(errorCode: string, description: string);
19
+ }
20
+ /** Signed-in user profile from XS Auth. */
21
+ interface XSAuthUser {
22
+ email: string;
23
+ firstName?: string;
24
+ lastName?: string;
25
+ }
26
+ interface LoginWithXSOptions {
27
+ /** Your app's public client id from Xino Solutions. */
28
+ clientId: string;
29
+ /** Must be on your XS Auth client's allowed domains list (normally `window.location.origin`). */
30
+ parentOrigin?: string;
31
+ timeoutMs?: number;
32
+ scope?: string;
33
+ /**
34
+ * Request a `nonce` inside `idToken` — your backend should verify it matches when validating the JWT.
35
+ */
36
+ useNonce?: boolean;
37
+ }
38
+ interface LoginWithXSResult {
39
+ user: XSAuthUser;
40
+ /** Short-lived signed JWT — send to your backend to create a session. */
41
+ idToken: string;
42
+ }
43
+ /** PKCE helpers for tests / custom callers. */
44
+ declare function createPkcePair(): Promise<{
45
+ verifier: string;
46
+ challenge: string;
47
+ }>;
48
+ /**
49
+ * Opens the XS Auth sign-in popup and returns the signed-in user plus a verifiable `idToken`.
50
+ */
51
+ declare function loginWithXS(opts: LoginWithXSOptions): Promise<LoginWithXSResult>;
52
+
53
+ export { LoginTimeoutError, type LoginWithXSOptions, type LoginWithXSResult, PopupBlockedError, PopupClosedError, XSAuthDeniedError, type XSAuthUser, createPkcePair, loginWithXS };
package/dist/index.js ADDED
@@ -0,0 +1,177 @@
1
+ // src/index.ts
2
+ var PopupBlockedError = class extends Error {
3
+ name = "PopupBlockedError";
4
+ };
5
+ var PopupClosedError = class extends Error {
6
+ name = "PopupClosedError";
7
+ };
8
+ var LoginTimeoutError = class extends Error {
9
+ name = "LoginTimeoutError";
10
+ };
11
+ var XSAuthDeniedError = class extends Error {
12
+ name = "XSAuthDeniedError";
13
+ errorCode;
14
+ description;
15
+ constructor(errorCode, description) {
16
+ super(description || errorCode || "xs_auth_denied");
17
+ this.errorCode = errorCode;
18
+ this.description = description;
19
+ }
20
+ };
21
+ var XS_AUTH_BASE_URL = "https://auth.xinosolutions.com";
22
+ function bytesBuffer(len) {
23
+ const out = new Uint8Array(len);
24
+ crypto.getRandomValues(out);
25
+ return out;
26
+ }
27
+ function base64UrlEncode(buf) {
28
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
29
+ let binary = "";
30
+ const chunkSize = 32768;
31
+ for (let i = 0; i < bytes.length; i += chunkSize) {
32
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
33
+ }
34
+ const b64 = btoa(binary);
35
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/u, "");
36
+ }
37
+ async function pkceVerifierAndChallenge() {
38
+ const verifier = base64UrlEncode(bytesBuffer(64));
39
+ const data = new TextEncoder().encode(verifier);
40
+ const digest = await crypto.subtle.digest("SHA-256", data);
41
+ const challenge = base64UrlEncode(digest);
42
+ return { verifier, challenge };
43
+ }
44
+ async function createPkcePair() {
45
+ return pkceVerifierAndChallenge();
46
+ }
47
+ var STATE_KEY_STORAGE = "__xs_login_state";
48
+ function parseSdkUser(raw) {
49
+ if (!raw || typeof raw !== "object") return null;
50
+ const u = raw;
51
+ const email = typeof u.email === "string" ? u.email.trim() : "";
52
+ if (!email) return null;
53
+ const user = { email };
54
+ const firstName = typeof u.firstName === "string" ? u.firstName.trim() : "";
55
+ const lastName = typeof u.lastName === "string" ? u.lastName.trim() : "";
56
+ if (firstName) user.firstName = firstName;
57
+ if (lastName) user.lastName = lastName;
58
+ return user;
59
+ }
60
+ async function loginWithXS(opts) {
61
+ const normalizedBase = normalizeBaseUrl(XS_AUTH_BASE_URL);
62
+ const authOrigin = normalizedBase.origin;
63
+ const parentOrigin = opts.parentOrigin ?? globalThis.location?.origin;
64
+ if (!parentOrigin || typeof window === "undefined") {
65
+ throw new Error(
66
+ "loginWithXS requires a browser Window and explicit parentOrigin (or window.location)."
67
+ );
68
+ }
69
+ const timeoutMs = opts.timeoutMs !== void 0 ? opts.timeoutMs : 12e4;
70
+ const state = randomBase64State();
71
+ sessionStorage.setItem(STATE_KEY_STORAGE, state);
72
+ const nonce = opts.useNonce ? randomBase64State() : void 0;
73
+ const pair = await pkceVerifierAndChallenge();
74
+ const params = new URLSearchParams({
75
+ client_id: opts.clientId,
76
+ state,
77
+ code_challenge: pair.challenge,
78
+ code_challenge_method: "S256",
79
+ parent_origin: parentOrigin,
80
+ response_mode: "popup"
81
+ });
82
+ if (opts.scope?.trim()) params.set("scope", opts.scope.trim());
83
+ if (nonce) params.set("nonce", nonce);
84
+ const authorizeUrl = `${normalizedBase.href.replace(/\/?$/u, "")}/authorize?${params.toString()}`;
85
+ const popupWidth = 480;
86
+ const popupHeight = 640;
87
+ const popup = window.open(
88
+ authorizeUrl,
89
+ `xs-auth-${crypto.randomUUID()}`,
90
+ popupWindowFeatures(popupWidth, popupHeight)
91
+ );
92
+ if (!popup) throw new PopupBlockedError();
93
+ return await new Promise((resolve, reject) => {
94
+ let cleaned = false;
95
+ const teardown = () => {
96
+ if (cleaned) return;
97
+ cleaned = true;
98
+ window.removeEventListener("message", handleMessage);
99
+ clearInterval(closePoll);
100
+ clearTimeout(timer);
101
+ try {
102
+ popup.close();
103
+ } catch {
104
+ }
105
+ };
106
+ const fail = (e) => {
107
+ teardown();
108
+ reject(e instanceof Error ? e : new Error(String(e)));
109
+ };
110
+ const timer = window.setTimeout(() => {
111
+ fail(new LoginTimeoutError());
112
+ }, timeoutMs);
113
+ const handleMessage = (event) => {
114
+ if (event.origin !== authOrigin) return;
115
+ const d = event.data;
116
+ if (d?.source !== "xs-auth") return;
117
+ const st = typeof d.state === "string" ? d.state : "";
118
+ if (!st || st !== sessionStorage.getItem(STATE_KEY_STORAGE)) return;
119
+ if (typeof d.error === "string" && d.error.length > 0) {
120
+ teardown();
121
+ reject(
122
+ new XSAuthDeniedError(
123
+ String(d.error),
124
+ typeof d.error_description === "string" ? d.error_description : ""
125
+ )
126
+ );
127
+ return;
128
+ }
129
+ const idToken = typeof d.id_token === "string" ? d.id_token : typeof d.idToken === "string" ? d.idToken : "";
130
+ const user = parseSdkUser(d.user);
131
+ if (!idToken || !user) return;
132
+ sessionStorage.removeItem(STATE_KEY_STORAGE);
133
+ clearTimeout(timer);
134
+ window.removeEventListener("message", handleMessage);
135
+ clearInterval(closePoll);
136
+ cleaned = true;
137
+ resolve({ user, idToken });
138
+ try {
139
+ popup.close();
140
+ } catch {
141
+ }
142
+ };
143
+ window.addEventListener("message", handleMessage);
144
+ const closePoll = window.setInterval(() => {
145
+ if (!popup.closed) return;
146
+ if (cleaned) return;
147
+ const pending = sessionStorage.getItem(STATE_KEY_STORAGE);
148
+ if (!pending) return;
149
+ fail(new PopupClosedError());
150
+ }, 280);
151
+ });
152
+ }
153
+ function randomBase64State() {
154
+ return base64UrlEncode(bytesBuffer(32));
155
+ }
156
+ function normalizeBaseUrl(s) {
157
+ return new URL(s);
158
+ }
159
+ function popupWindowFeatures(width, height) {
160
+ const win = window;
161
+ const outerW = win.outerWidth > 0 ? win.outerWidth : win.innerWidth;
162
+ const outerH = win.outerHeight > 0 ? win.outerHeight : win.innerHeight;
163
+ const screenX = win.screenX ?? win.screenLeft ?? 0;
164
+ const screenY = win.screenY ?? win.screenTop ?? 0;
165
+ const left = Math.round(screenX + Math.max(0, (outerW - width) / 2));
166
+ const top = Math.round(screenY + Math.max(0, (outerH - height) / 2));
167
+ return `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
168
+ }
169
+ export {
170
+ LoginTimeoutError,
171
+ PopupBlockedError,
172
+ PopupClosedError,
173
+ XSAuthDeniedError,
174
+ createPkcePair,
175
+ loginWithXS
176
+ };
177
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/** Popup was blocked by the browser or environment. */\r\nexport class PopupBlockedError extends Error {\r\n readonly name = 'PopupBlockedError';\r\n}\r\n\r\n/** User closed the popup before completing login. */\r\nexport class PopupClosedError extends Error {\r\n readonly name = 'PopupClosedError';\r\n}\r\n\r\n/** XS Auth popup did not complete within timeout. */\r\nexport class LoginTimeoutError extends Error {\r\n readonly name = 'LoginTimeoutError';\r\n}\r\n\r\n/** XS Auth returned an OAuth-style error inside postMessage. */\r\nexport class XSAuthDeniedError extends Error {\r\n readonly name = 'XSAuthDeniedError';\r\n readonly errorCode: string;\r\n readonly description: string;\r\n\r\n constructor(errorCode: string, description: string) {\r\n super(description || errorCode || 'xs_auth_denied');\r\n this.errorCode = errorCode;\r\n this.description = description;\r\n }\r\n}\r\n\r\n/** XS Auth server — baked in at build time (local vs production). */\r\nconst XS_AUTH_BASE_URL = __XS_AUTH_BASE_URL__;\r\n\r\n/** Signed-in user profile from XS Auth. */\r\nexport interface XSAuthUser {\r\n email: string;\r\n firstName?: string;\r\n lastName?: string;\r\n}\r\n\r\nexport interface LoginWithXSOptions {\r\n /** Your app's public client id from Xino Solutions. */\r\n clientId: string;\r\n /** Must be on your XS Auth client's allowed domains list (normally `window.location.origin`). */\r\n parentOrigin?: string;\r\n timeoutMs?: number;\r\n scope?: string;\r\n /**\r\n * Request a `nonce` inside `idToken` — your backend should verify it matches when validating the JWT.\r\n */\r\n useNonce?: boolean;\r\n}\r\n\r\nexport interface LoginWithXSResult {\r\n user: XSAuthUser;\r\n /** Short-lived signed JWT — send to your backend to create a session. */\r\n idToken: string;\r\n}\r\n\r\nfunction bytesBuffer(len: number): Uint8Array {\r\n const out = new Uint8Array(len);\r\n crypto.getRandomValues(out);\r\n return out;\r\n}\r\n\r\nfunction base64UrlEncode(buf: Uint8Array | ArrayBuffer): string {\r\n const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);\r\n let binary = '';\r\n const chunkSize = 0x8000;\r\n for (let i = 0; i < bytes.length; i += chunkSize) {\r\n binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));\r\n }\r\n const b64 = btoa(binary);\r\n return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/u, '');\r\n}\r\n\r\nasync function pkceVerifierAndChallenge(): Promise<{\r\n verifier: string;\r\n challenge: string;\r\n}> {\r\n const verifier = base64UrlEncode(bytesBuffer(64));\r\n const data = new TextEncoder().encode(verifier);\r\n const digest = await crypto.subtle.digest('SHA-256', data);\r\n const challenge = base64UrlEncode(digest);\r\n return { verifier, challenge };\r\n}\r\n\r\n/** PKCE helpers for tests / custom callers. */\r\nexport async function createPkcePair(): Promise<{\r\n verifier: string;\r\n challenge: string;\r\n}> {\r\n return pkceVerifierAndChallenge();\r\n}\r\n\r\nconst STATE_KEY_STORAGE = '__xs_login_state';\r\n\r\nfunction parseSdkUser(raw: unknown): XSAuthUser | null {\r\n if (!raw || typeof raw !== 'object') return null;\r\n const u = raw as Record<string, unknown>;\r\n const email = typeof u.email === 'string' ? u.email.trim() : '';\r\n if (!email) return null;\r\n const user: XSAuthUser = { email };\r\n const firstName =\r\n typeof u.firstName === 'string' ? u.firstName.trim() : '';\r\n const lastName = typeof u.lastName === 'string' ? u.lastName.trim() : '';\r\n if (firstName) user.firstName = firstName;\r\n if (lastName) user.lastName = lastName;\r\n return user;\r\n}\r\n\r\n/**\r\n * Opens the XS Auth sign-in popup and returns the signed-in user plus a verifiable `idToken`.\r\n */\r\nexport async function loginWithXS(\r\n opts: LoginWithXSOptions,\r\n): Promise<LoginWithXSResult> {\r\n const normalizedBase = normalizeBaseUrl(XS_AUTH_BASE_URL);\r\n const authOrigin = normalizedBase.origin;\r\n const parentOrigin = (opts.parentOrigin ?? globalThis.location?.origin) as\r\n | string\r\n | undefined;\r\n if (!parentOrigin || typeof window === 'undefined') {\r\n throw new Error(\r\n 'loginWithXS requires a browser Window and explicit parentOrigin (or window.location).',\r\n );\r\n }\r\n\r\n const timeoutMs =\r\n opts.timeoutMs !== undefined ? opts.timeoutMs : 120_000;\r\n\r\n const state = randomBase64State();\r\n sessionStorage.setItem(STATE_KEY_STORAGE, state);\r\n\r\n const nonce = opts.useNonce ? randomBase64State() : undefined;\r\n const pair = await pkceVerifierAndChallenge();\r\n\r\n const params = new URLSearchParams({\r\n client_id: opts.clientId,\r\n state,\r\n code_challenge: pair.challenge,\r\n code_challenge_method: 'S256',\r\n parent_origin: parentOrigin,\r\n response_mode: 'popup',\r\n });\r\n if (opts.scope?.trim()) params.set('scope', opts.scope.trim());\r\n if (nonce) params.set('nonce', nonce);\r\n\r\n const authorizeUrl = `${normalizedBase.href.replace(/\\/?$/u, '')}/authorize?${params.toString()}`;\r\n\r\n const popupWidth = 480;\r\n const popupHeight = 640;\r\n const popup = window.open(\r\n authorizeUrl,\r\n `xs-auth-${crypto.randomUUID()}`,\r\n popupWindowFeatures(popupWidth, popupHeight),\r\n );\r\n if (!popup) throw new PopupBlockedError();\r\n\r\n return await new Promise<LoginWithXSResult>((resolve, reject) => {\r\n let cleaned = false;\r\n const teardown = (): void => {\r\n if (cleaned) return;\r\n cleaned = true;\r\n window.removeEventListener('message', handleMessage as EventListener);\r\n clearInterval(closePoll);\r\n clearTimeout(timer);\r\n try {\r\n popup.close();\r\n } catch {\r\n // ignore\r\n }\r\n };\r\n\r\n const fail = (e: unknown): void => {\r\n teardown();\r\n reject(e instanceof Error ? e : new Error(String(e)));\r\n };\r\n\r\n const timer = window.setTimeout(() => {\r\n fail(new LoginTimeoutError());\r\n }, timeoutMs);\r\n\r\n const handleMessage = (event: MessageEvent): void => {\r\n if (event.origin !== authOrigin) return;\r\n const d = event.data as Partial<Record<string, unknown>>;\r\n if (d?.source !== 'xs-auth') return;\r\n\r\n const st = typeof d.state === 'string' ? d.state : '';\r\n if (!st || st !== sessionStorage.getItem(STATE_KEY_STORAGE)) return;\r\n\r\n if (typeof d.error === 'string' && d.error.length > 0) {\r\n teardown();\r\n reject(\r\n new XSAuthDeniedError(\r\n String(d.error),\r\n typeof d.error_description === 'string'\r\n ? d.error_description\r\n : '',\r\n ),\r\n );\r\n return;\r\n }\r\n\r\n const idToken =\r\n typeof d.id_token === 'string'\r\n ? d.id_token\r\n : typeof d.idToken === 'string'\r\n ? d.idToken\r\n : '';\r\n const user = parseSdkUser(d.user);\r\n if (!idToken || !user) return;\r\n\r\n sessionStorage.removeItem(STATE_KEY_STORAGE);\r\n clearTimeout(timer);\r\n window.removeEventListener('message', handleMessage as EventListener);\r\n clearInterval(closePoll);\r\n cleaned = true;\r\n\r\n resolve({ user, idToken });\r\n\r\n try {\r\n popup.close();\r\n } catch {\r\n // ignore\r\n }\r\n };\r\n\r\n window.addEventListener('message', handleMessage as EventListener);\r\n\r\n const closePoll = window.setInterval(() => {\r\n if (!popup.closed) return;\r\n if (cleaned) return;\r\n const pending = sessionStorage.getItem(STATE_KEY_STORAGE);\r\n if (!pending) return;\r\n fail(new PopupClosedError());\r\n }, 280);\r\n });\r\n}\r\n\r\nfunction randomBase64State(): string {\r\n return base64UrlEncode(bytesBuffer(32));\r\n}\r\n\r\nfunction normalizeBaseUrl(s: string): URL {\r\n return new URL(s);\r\n}\r\n\r\n/** Center popup over the opener window (falls back to screen center). */\r\nfunction popupWindowFeatures(width: number, height: number): string {\r\n const win = window;\r\n const outerW = win.outerWidth > 0 ? win.outerWidth : win.innerWidth;\r\n const outerH = win.outerHeight > 0 ? win.outerHeight : win.innerHeight;\r\n const screenX = win.screenX ?? win.screenLeft ?? 0;\r\n const screenY = win.screenY ?? win.screenTop ?? 0;\r\n const left = Math.round(screenX + Math.max(0, (outerW - width) / 2));\r\n const top = Math.round(screenY + Math.max(0, (outerH - height) / 2));\r\n return `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;\r\n}\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@xinosolutions/auth-sdk",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Login with XS Auth — popup sign-in returning user profile and signed idToken.",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "require": {
18
+ "types": "./dist/index.d.cts",
19
+ "default": "./dist/index.cjs"
20
+ }
21
+ }
22
+ },
23
+ "files": ["dist", "README.md", "LICENSE"],
24
+ "keywords": [
25
+ "xs-auth",
26
+ "oauth",
27
+ "pkce",
28
+ "browser",
29
+ "authentication",
30
+ "popup",
31
+ "sso"
32
+ ],
33
+ "author": "Xino Solutions",
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "node scripts/build.mjs production",
43
+ "build:local": "node scripts/build.mjs local",
44
+ "typecheck": "tsc --noEmit",
45
+ "publint": "publint",
46
+ "attw": "attw --pack .",
47
+ "prerelease": "npm run build && npm run typecheck && npm run publint && npm run attw",
48
+ "prepublishOnly": "npm run build",
49
+ "prepack": "npm run build",
50
+ "deploy": "node scripts/deploy.mjs",
51
+ "link": "node scripts/link.mjs",
52
+ "unlink": "node scripts/unlink.mjs"
53
+ },
54
+ "devDependencies": {
55
+ "@arethetypeswrong/cli": "^0.17.4",
56
+ "publint": "^0.3.12",
57
+ "tsup": "^8.4.0",
58
+ "typescript": "^5.7.0"
59
+ }
60
+ }