@bigso/auth-sdk 0.3.1 → 0.4.3

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @bigso/auth-sdk v0.3.0
1
+ # @bigso/auth-sdk
2
2
 
3
3
  SDK oficial de autenticación para Bigso SSO, compatible con el estándar SSO v2.3. Este paquete permite integrar aplicaciones web con el flujo de autenticación basado en iframe seguro, utilizando PKCE, JWS firmados y comunicación mediante postMessage.
4
4
 
@@ -162,7 +162,7 @@ npm run lint
162
162
 
163
163
  ## 📝 Changelog
164
164
 
165
- ### v0.3.0 (2026-03-21)
165
+ ### v0.4.0 (2026-03-23)
166
166
  Protocolo actualizado a SSO v2.3
167
167
  - Mensaje `sso-init` con `v: '2.3'`.
168
168
  - Timeout reactivo (se inicia tras `sso-ready`).
package/dist/index.cjs ADDED
@@ -0,0 +1,371 @@
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
+ BigsoAuth: () => BigsoAuth
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/utils/crypto.ts
28
+ async function sha256Base64Url(input) {
29
+ const encoder = new TextEncoder();
30
+ const data = encoder.encode(input);
31
+ const digest = await crypto.subtle.digest("SHA-256", data);
32
+ return base64Url(new Uint8Array(digest));
33
+ }
34
+ function generateVerifier(length = 32) {
35
+ const array = new Uint8Array(length);
36
+ crypto.getRandomValues(array);
37
+ return base64Url(array);
38
+ }
39
+ function base64Url(bytes) {
40
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
41
+ }
42
+ function generateRandomId() {
43
+ return crypto.randomUUID();
44
+ }
45
+
46
+ // src/utils/events.ts
47
+ var EventEmitter = class {
48
+ constructor() {
49
+ this.events = {};
50
+ }
51
+ on(event, handler) {
52
+ if (!this.events[event]) this.events[event] = [];
53
+ this.events[event].push(handler);
54
+ }
55
+ off(event, handler) {
56
+ if (!this.events[event]) return;
57
+ this.events[event] = this.events[event].filter((h) => h !== handler);
58
+ }
59
+ emit(event, data) {
60
+ this.events[event]?.forEach((fn) => fn(data));
61
+ }
62
+ };
63
+
64
+ // src/utils/jws.ts
65
+ var import_jose = require("jose");
66
+ async function verifySignedPayload(token, jwksUrl, expectedAudience) {
67
+ const JWKS = (0, import_jose.createRemoteJWKSet)(new URL(jwksUrl));
68
+ const { payload } = await (0, import_jose.jwtVerify)(token, JWKS, {
69
+ audience: expectedAudience
70
+ });
71
+ return payload;
72
+ }
73
+
74
+ // src/core/auth.ts
75
+ var BigsoAuth = class extends EventEmitter {
76
+ constructor(options) {
77
+ super();
78
+ this.authCompleted = false;
79
+ this.requestId = generateRandomId();
80
+ this.loginInProgress = false;
81
+ this.options = {
82
+ timeout: 5e3,
83
+ // por defecto 5s (estándar v2.3)
84
+ debug: false,
85
+ redirectUri: "",
86
+ tenantHint: "",
87
+ theme: "light",
88
+ ...options
89
+ };
90
+ }
91
+ /**
92
+ * Inicia el flujo de autenticación.
93
+ * @returns Promise que resuelve con el payload decodificado del JWS (solo para información; el backend debe validar)
94
+ */
95
+ async login() {
96
+ if (this.loginInProgress) {
97
+ this.debug("login() ya en curso, ignorando llamada duplicada");
98
+ return Promise.reject(new Error("Login already in progress"));
99
+ }
100
+ this.loginInProgress = true;
101
+ this.authCompleted = false;
102
+ const state = generateRandomId();
103
+ const nonce = generateRandomId();
104
+ const verifier = generateVerifier();
105
+ const requestId = this.requestId;
106
+ sessionStorage.setItem("sso_ctx", JSON.stringify({ state, nonce, verifier, requestId }));
107
+ this.createUI();
108
+ return new Promise((resolve, reject) => {
109
+ this.abortController = new AbortController();
110
+ const { signal } = this.abortController;
111
+ const cleanup = () => {
112
+ if (this.timeoutId) clearTimeout(this.timeoutId);
113
+ if (this.messageListener) window.removeEventListener("message", this.messageListener);
114
+ this.iframe?.remove();
115
+ this.iframe = void 0;
116
+ this.authCompleted = true;
117
+ this.loginInProgress = false;
118
+ };
119
+ this.messageListener = async (event) => {
120
+ if (event.origin !== this.options.ssoOrigin) {
121
+ this.debug("Ignorado mensaje de origen no autorizado:", event.origin);
122
+ return;
123
+ }
124
+ const msg = event.data;
125
+ this.debug("Mensaje recibido:", msg);
126
+ if (msg.requestId && msg.requestId !== requestId) {
127
+ this.debug("requestId no coincide, ignorado");
128
+ return;
129
+ }
130
+ if (msg.type === "sso-ready") {
131
+ this.debug("sso-ready recibido, iniciando timeout y enviando sso-init");
132
+ this.timeoutId = window.setTimeout(() => {
133
+ if (!this.authCompleted) {
134
+ this.debug("Timeout alcanzado, activando fallback");
135
+ this.closeUI();
136
+ cleanup();
137
+ this.emit("fallback");
138
+ window.location.href = this.buildFallbackUrl();
139
+ reject(new Error("Timeout"));
140
+ }
141
+ }, this.options.timeout);
142
+ const codeChallenge = await sha256Base64Url(verifier);
143
+ const initPayload = {
144
+ state,
145
+ nonce,
146
+ code_challenge: codeChallenge,
147
+ code_challenge_method: "S256",
148
+ origin: window.location.origin,
149
+ ...this.options.redirectUri && { redirect_uri: this.options.redirectUri },
150
+ ...this.options.tenantHint && { tenant_hint: this.options.tenantHint },
151
+ timeout_ms: this.options.timeout
152
+ // pasar el timeout configurado (opcional)
153
+ };
154
+ this.iframe?.contentWindow?.postMessage({
155
+ v: "2.3",
156
+ // versión del protocolo (estándar v2.3)
157
+ source: "@app/widget",
158
+ type: "sso-init",
159
+ requestId: this.requestId,
160
+ payload: initPayload
161
+ }, this.options.ssoOrigin);
162
+ this.emit("ready");
163
+ return;
164
+ }
165
+ if (msg.type === "sso-success") {
166
+ this.debug("sso-success recibido");
167
+ clearTimeout(this.timeoutId);
168
+ try {
169
+ const payload = msg.payload;
170
+ const ctx = JSON.parse(sessionStorage.getItem("sso_ctx") || "{}");
171
+ if (payload.state !== ctx.state) {
172
+ throw new Error("Invalid state");
173
+ }
174
+ const decoded = await verifySignedPayload(
175
+ payload.signed_payload,
176
+ this.options.jwksUrl,
177
+ window.location.origin
178
+ // aud esperado
179
+ );
180
+ if (decoded.nonce !== ctx.nonce) {
181
+ throw new Error("Invalid nonce");
182
+ }
183
+ this.debug("JWS v\xE1lido, payload:", decoded);
184
+ this.closeUI();
185
+ cleanup();
186
+ this.emit("success", decoded);
187
+ resolve(decoded);
188
+ } catch (err) {
189
+ this.debug("Error en sso-success:", err);
190
+ this.closeUI();
191
+ cleanup();
192
+ this.emit("error", err);
193
+ reject(err);
194
+ }
195
+ return;
196
+ }
197
+ if (msg.type === "sso-error") {
198
+ const errorPayload = msg.payload;
199
+ this.debug("sso-error recibido:", errorPayload);
200
+ clearTimeout(this.timeoutId);
201
+ this.closeUI();
202
+ cleanup();
203
+ if (errorPayload.code === "version_mismatch") {
204
+ this.emit("error", errorPayload);
205
+ window.location.href = this.buildFallbackUrl();
206
+ reject(new Error(`Version mismatch: expected ${errorPayload.expected_version}`));
207
+ } else {
208
+ this.emit("error", errorPayload);
209
+ reject(errorPayload);
210
+ }
211
+ }
212
+ if (msg.type === "sso-close") {
213
+ this.debug("sso-close recibido");
214
+ this.closeUI();
215
+ cleanup();
216
+ reject(new Error("Login cancelled by user"));
217
+ }
218
+ };
219
+ window.addEventListener("message", this.messageListener);
220
+ signal.addEventListener("abort", () => {
221
+ this.debug("Operaci\xF3n abortada");
222
+ this.closeUI();
223
+ cleanup();
224
+ reject(new Error("Login aborted"));
225
+ });
226
+ });
227
+ }
228
+ /** Cancela el flujo de autenticación en curso */
229
+ abort() {
230
+ this.abortController?.abort();
231
+ }
232
+ // ─── UI Management ───────────────────────────────────────────────
233
+ /**
234
+ * Crea (o reutiliza) el overlay con Shadow DOM y el iframe visible.
235
+ * Patrón tomado del CDN widget v1: Shadow DOM para aislar estilos.
236
+ */
237
+ createUI() {
238
+ if (!this.hostEl) {
239
+ this.hostEl = document.createElement("div");
240
+ this.hostEl.id = "bigso-auth-host";
241
+ this.shadowRoot = this.hostEl.attachShadow({ mode: "open" });
242
+ const style = document.createElement("style");
243
+ style.textContent = this.getOverlayStyles();
244
+ this.shadowRoot.appendChild(style);
245
+ this.overlayEl = document.createElement("div");
246
+ this.overlayEl.className = "sso-overlay";
247
+ const closeBtn = document.createElement("button");
248
+ closeBtn.className = "sso-close-btn";
249
+ closeBtn.innerHTML = "×";
250
+ closeBtn.setAttribute("aria-label", "Cerrar modal");
251
+ closeBtn.addEventListener("click", () => this.abort());
252
+ this.overlayEl.appendChild(closeBtn);
253
+ this.overlayEl.addEventListener("click", (event) => {
254
+ if (event.target === this.overlayEl) {
255
+ this.abort();
256
+ }
257
+ });
258
+ this.shadowRoot.appendChild(this.overlayEl);
259
+ document.body.appendChild(this.hostEl);
260
+ }
261
+ this.iframe = document.createElement("iframe");
262
+ this.iframe.className = "sso-frame";
263
+ this.iframe.src = `${this.options.ssoOrigin}/auth/sign-in?v=2.3&client_id=${this.options.clientId}`;
264
+ this.iframe.setAttribute("title", "SSO Login");
265
+ this.overlayEl.appendChild(this.iframe);
266
+ this.debug("Iframe creado", this.iframe.src);
267
+ this.overlayEl.classList.remove("sso-closing");
268
+ this.overlayEl.style.display = "flex";
269
+ }
270
+ /**
271
+ * Cierra el overlay con animación suave (fadeOut + slideDown).
272
+ * El overlay persiste en el DOM (solo se oculta).
273
+ */
274
+ closeUI() {
275
+ if (!this.overlayEl || this.overlayEl.style.display === "none") return;
276
+ this.overlayEl.classList.add("sso-closing");
277
+ setTimeout(() => {
278
+ if (this.overlayEl) {
279
+ this.overlayEl.style.display = "none";
280
+ this.overlayEl.classList.remove("sso-closing");
281
+ }
282
+ }, 200);
283
+ }
284
+ /**
285
+ * Estilos CSS encapsulados dentro del Shadow DOM.
286
+ * Migrados del widget CDN v1 con las mismas animaciones y responsive.
287
+ */
288
+ getOverlayStyles() {
289
+ return `
290
+ .sso-overlay {
291
+ position: fixed;
292
+ inset: 0;
293
+ display: none;
294
+ justify-content: center;
295
+ align-items: center;
296
+ background: rgba(0, 0, 0, 0.6);
297
+ z-index: 999999;
298
+ backdrop-filter: blur(4px);
299
+ -webkit-backdrop-filter: blur(4px);
300
+ animation: fadeIn 0.2s ease;
301
+ }
302
+ .sso-frame {
303
+ width: 370px;
304
+ height: 350px;
305
+ border: none;
306
+ border-radius: 16px;
307
+ background: var(--card-bg, #fff);
308
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
309
+ animation: slideUp 0.3s ease;
310
+ }
311
+ @media (max-width: 480px), (max-height: 480px) {
312
+ .sso-frame {
313
+ width: 100%;
314
+ height: 100%;
315
+ border-radius: 0;
316
+ }
317
+ }
318
+ .sso-close-btn {
319
+ position: absolute;
320
+ top: 12px;
321
+ right: 12px;
322
+ width: 32px;
323
+ height: 32px;
324
+ background: rgba(0, 0, 0, 0.4);
325
+ color: white;
326
+ border: none;
327
+ border-radius: 50%;
328
+ font-size: 24px;
329
+ line-height: 1;
330
+ cursor: pointer;
331
+ display: flex;
332
+ align-items: center;
333
+ justify-content: center;
334
+ z-index: 1000000;
335
+ transition: background 0.2s;
336
+ }
337
+ .sso-close-btn:hover {
338
+ background: rgba(0, 0, 0, 0.8);
339
+ }
340
+ .sso-overlay.sso-closing {
341
+ animation: fadeOut 0.2s ease forwards;
342
+ }
343
+ .sso-overlay.sso-closing .sso-frame {
344
+ animation: slideDown 0.2s ease forwards;
345
+ }
346
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
347
+ @keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
348
+ @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
349
+ @keyframes slideDown { from { transform: translateY(0); opacity: 1; } to { transform: translateY(20px); opacity: 0; } }
350
+ `;
351
+ }
352
+ // ─── Helpers ──────────────────────────────────────────────────────
353
+ buildFallbackUrl() {
354
+ const url = new URL(`${this.options.ssoOrigin}/authorize`);
355
+ url.searchParams.set("client_id", this.options.clientId);
356
+ url.searchParams.set("response_type", "code");
357
+ url.searchParams.set("redirect_uri", this.options.redirectUri || window.location.origin);
358
+ url.searchParams.set("state", generateRandomId());
359
+ url.searchParams.set("code_challenge_method", "S256");
360
+ return url.toString();
361
+ }
362
+ debug(...args) {
363
+ if (this.options.debug) {
364
+ console.log("[BigsoAuth]", ...args);
365
+ }
366
+ }
367
+ };
368
+ // Annotate the CommonJS export names for ESM import in node:
369
+ 0 && (module.exports = {
370
+ BigsoAuth
371
+ });
@@ -0,0 +1,66 @@
1
+ declare class EventEmitter {
2
+ private events;
3
+ on(event: string, handler: Function): void;
4
+ off(event: string, handler: Function): void;
5
+ emit(event: string, data?: any): void;
6
+ }
7
+
8
+ interface BigsoAuthOptions {
9
+ /** Client ID registrado en el SSO */
10
+ clientId: string;
11
+ /** Origen del SSO (ej: https://sso.bigso.co) */
12
+ ssoOrigin: string;
13
+ /** URL del JWKS para verificar firmas (ej: https://sso.bigso.co/.well-known/jwks.json) */
14
+ jwksUrl: string;
15
+ /** Timeout en milisegundos (por defecto 5000) */
16
+ timeout?: number;
17
+ /** Activar logs de depuración */
18
+ debug?: boolean;
19
+ /** URI de redirección registrada (opcional, si se requiere validación exacta) */
20
+ redirectUri?: string;
21
+ /** Sugerencia de tenant (opcional) */
22
+ tenantHint?: string;
23
+ /** Tema visual del iframe ('light' | 'dark', por defecto 'light') */
24
+ theme?: 'light' | 'dark';
25
+ }
26
+
27
+ declare class BigsoAuth extends EventEmitter {
28
+ private options;
29
+ private iframe?;
30
+ private authCompleted;
31
+ private requestId;
32
+ private timeoutId?;
33
+ private messageListener?;
34
+ private abortController?;
35
+ private hostEl?;
36
+ private shadowRoot?;
37
+ private overlayEl?;
38
+ private loginInProgress;
39
+ constructor(options: BigsoAuthOptions);
40
+ /**
41
+ * Inicia el flujo de autenticación.
42
+ * @returns Promise que resuelve con el payload decodificado del JWS (solo para información; el backend debe validar)
43
+ */
44
+ login(): Promise<any>;
45
+ /** Cancela el flujo de autenticación en curso */
46
+ abort(): void;
47
+ /**
48
+ * Crea (o reutiliza) el overlay con Shadow DOM y el iframe visible.
49
+ * Patrón tomado del CDN widget v1: Shadow DOM para aislar estilos.
50
+ */
51
+ private createUI;
52
+ /**
53
+ * Cierra el overlay con animación suave (fadeOut + slideDown).
54
+ * El overlay persiste en el DOM (solo se oculta).
55
+ */
56
+ private closeUI;
57
+ /**
58
+ * Estilos CSS encapsulados dentro del Shadow DOM.
59
+ * Migrados del widget CDN v1 con las mismas animaciones y responsive.
60
+ */
61
+ private getOverlayStyles;
62
+ private buildFallbackUrl;
63
+ private debug;
64
+ }
65
+
66
+ export { BigsoAuth, type BigsoAuthOptions };
@@ -0,0 +1,66 @@
1
+ declare class EventEmitter {
2
+ private events;
3
+ on(event: string, handler: Function): void;
4
+ off(event: string, handler: Function): void;
5
+ emit(event: string, data?: any): void;
6
+ }
7
+
8
+ interface BigsoAuthOptions {
9
+ /** Client ID registrado en el SSO */
10
+ clientId: string;
11
+ /** Origen del SSO (ej: https://sso.bigso.co) */
12
+ ssoOrigin: string;
13
+ /** URL del JWKS para verificar firmas (ej: https://sso.bigso.co/.well-known/jwks.json) */
14
+ jwksUrl: string;
15
+ /** Timeout en milisegundos (por defecto 5000) */
16
+ timeout?: number;
17
+ /** Activar logs de depuración */
18
+ debug?: boolean;
19
+ /** URI de redirección registrada (opcional, si se requiere validación exacta) */
20
+ redirectUri?: string;
21
+ /** Sugerencia de tenant (opcional) */
22
+ tenantHint?: string;
23
+ /** Tema visual del iframe ('light' | 'dark', por defecto 'light') */
24
+ theme?: 'light' | 'dark';
25
+ }
26
+
27
+ declare class BigsoAuth extends EventEmitter {
28
+ private options;
29
+ private iframe?;
30
+ private authCompleted;
31
+ private requestId;
32
+ private timeoutId?;
33
+ private messageListener?;
34
+ private abortController?;
35
+ private hostEl?;
36
+ private shadowRoot?;
37
+ private overlayEl?;
38
+ private loginInProgress;
39
+ constructor(options: BigsoAuthOptions);
40
+ /**
41
+ * Inicia el flujo de autenticación.
42
+ * @returns Promise que resuelve con el payload decodificado del JWS (solo para información; el backend debe validar)
43
+ */
44
+ login(): Promise<any>;
45
+ /** Cancela el flujo de autenticación en curso */
46
+ abort(): void;
47
+ /**
48
+ * Crea (o reutiliza) el overlay con Shadow DOM y el iframe visible.
49
+ * Patrón tomado del CDN widget v1: Shadow DOM para aislar estilos.
50
+ */
51
+ private createUI;
52
+ /**
53
+ * Cierra el overlay con animación suave (fadeOut + slideDown).
54
+ * El overlay persiste en el DOM (solo se oculta).
55
+ */
56
+ private closeUI;
57
+ /**
58
+ * Estilos CSS encapsulados dentro del Shadow DOM.
59
+ * Migrados del widget CDN v1 con las mismas animaciones y responsive.
60
+ */
61
+ private getOverlayStyles;
62
+ private buildFallbackUrl;
63
+ private debug;
64
+ }
65
+
66
+ export { BigsoAuth, type BigsoAuthOptions };
package/dist/index.js ADDED
@@ -0,0 +1,344 @@
1
+ // src/utils/crypto.ts
2
+ async function sha256Base64Url(input) {
3
+ const encoder = new TextEncoder();
4
+ const data = encoder.encode(input);
5
+ const digest = await crypto.subtle.digest("SHA-256", data);
6
+ return base64Url(new Uint8Array(digest));
7
+ }
8
+ function generateVerifier(length = 32) {
9
+ const array = new Uint8Array(length);
10
+ crypto.getRandomValues(array);
11
+ return base64Url(array);
12
+ }
13
+ function base64Url(bytes) {
14
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
15
+ }
16
+ function generateRandomId() {
17
+ return crypto.randomUUID();
18
+ }
19
+
20
+ // src/utils/events.ts
21
+ var EventEmitter = class {
22
+ constructor() {
23
+ this.events = {};
24
+ }
25
+ on(event, handler) {
26
+ if (!this.events[event]) this.events[event] = [];
27
+ this.events[event].push(handler);
28
+ }
29
+ off(event, handler) {
30
+ if (!this.events[event]) return;
31
+ this.events[event] = this.events[event].filter((h) => h !== handler);
32
+ }
33
+ emit(event, data) {
34
+ this.events[event]?.forEach((fn) => fn(data));
35
+ }
36
+ };
37
+
38
+ // src/utils/jws.ts
39
+ import { jwtVerify, createRemoteJWKSet } from "jose";
40
+ async function verifySignedPayload(token, jwksUrl, expectedAudience) {
41
+ const JWKS = createRemoteJWKSet(new URL(jwksUrl));
42
+ const { payload } = await jwtVerify(token, JWKS, {
43
+ audience: expectedAudience
44
+ });
45
+ return payload;
46
+ }
47
+
48
+ // src/core/auth.ts
49
+ var BigsoAuth = class extends EventEmitter {
50
+ constructor(options) {
51
+ super();
52
+ this.authCompleted = false;
53
+ this.requestId = generateRandomId();
54
+ this.loginInProgress = false;
55
+ this.options = {
56
+ timeout: 5e3,
57
+ // por defecto 5s (estándar v2.3)
58
+ debug: false,
59
+ redirectUri: "",
60
+ tenantHint: "",
61
+ theme: "light",
62
+ ...options
63
+ };
64
+ }
65
+ /**
66
+ * Inicia el flujo de autenticación.
67
+ * @returns Promise que resuelve con el payload decodificado del JWS (solo para información; el backend debe validar)
68
+ */
69
+ async login() {
70
+ if (this.loginInProgress) {
71
+ this.debug("login() ya en curso, ignorando llamada duplicada");
72
+ return Promise.reject(new Error("Login already in progress"));
73
+ }
74
+ this.loginInProgress = true;
75
+ this.authCompleted = false;
76
+ const state = generateRandomId();
77
+ const nonce = generateRandomId();
78
+ const verifier = generateVerifier();
79
+ const requestId = this.requestId;
80
+ sessionStorage.setItem("sso_ctx", JSON.stringify({ state, nonce, verifier, requestId }));
81
+ this.createUI();
82
+ return new Promise((resolve, reject) => {
83
+ this.abortController = new AbortController();
84
+ const { signal } = this.abortController;
85
+ const cleanup = () => {
86
+ if (this.timeoutId) clearTimeout(this.timeoutId);
87
+ if (this.messageListener) window.removeEventListener("message", this.messageListener);
88
+ this.iframe?.remove();
89
+ this.iframe = void 0;
90
+ this.authCompleted = true;
91
+ this.loginInProgress = false;
92
+ };
93
+ this.messageListener = async (event) => {
94
+ if (event.origin !== this.options.ssoOrigin) {
95
+ this.debug("Ignorado mensaje de origen no autorizado:", event.origin);
96
+ return;
97
+ }
98
+ const msg = event.data;
99
+ this.debug("Mensaje recibido:", msg);
100
+ if (msg.requestId && msg.requestId !== requestId) {
101
+ this.debug("requestId no coincide, ignorado");
102
+ return;
103
+ }
104
+ if (msg.type === "sso-ready") {
105
+ this.debug("sso-ready recibido, iniciando timeout y enviando sso-init");
106
+ this.timeoutId = window.setTimeout(() => {
107
+ if (!this.authCompleted) {
108
+ this.debug("Timeout alcanzado, activando fallback");
109
+ this.closeUI();
110
+ cleanup();
111
+ this.emit("fallback");
112
+ window.location.href = this.buildFallbackUrl();
113
+ reject(new Error("Timeout"));
114
+ }
115
+ }, this.options.timeout);
116
+ const codeChallenge = await sha256Base64Url(verifier);
117
+ const initPayload = {
118
+ state,
119
+ nonce,
120
+ code_challenge: codeChallenge,
121
+ code_challenge_method: "S256",
122
+ origin: window.location.origin,
123
+ ...this.options.redirectUri && { redirect_uri: this.options.redirectUri },
124
+ ...this.options.tenantHint && { tenant_hint: this.options.tenantHint },
125
+ timeout_ms: this.options.timeout
126
+ // pasar el timeout configurado (opcional)
127
+ };
128
+ this.iframe?.contentWindow?.postMessage({
129
+ v: "2.3",
130
+ // versión del protocolo (estándar v2.3)
131
+ source: "@app/widget",
132
+ type: "sso-init",
133
+ requestId: this.requestId,
134
+ payload: initPayload
135
+ }, this.options.ssoOrigin);
136
+ this.emit("ready");
137
+ return;
138
+ }
139
+ if (msg.type === "sso-success") {
140
+ this.debug("sso-success recibido");
141
+ clearTimeout(this.timeoutId);
142
+ try {
143
+ const payload = msg.payload;
144
+ const ctx = JSON.parse(sessionStorage.getItem("sso_ctx") || "{}");
145
+ if (payload.state !== ctx.state) {
146
+ throw new Error("Invalid state");
147
+ }
148
+ const decoded = await verifySignedPayload(
149
+ payload.signed_payload,
150
+ this.options.jwksUrl,
151
+ window.location.origin
152
+ // aud esperado
153
+ );
154
+ if (decoded.nonce !== ctx.nonce) {
155
+ throw new Error("Invalid nonce");
156
+ }
157
+ this.debug("JWS v\xE1lido, payload:", decoded);
158
+ this.closeUI();
159
+ cleanup();
160
+ this.emit("success", decoded);
161
+ resolve(decoded);
162
+ } catch (err) {
163
+ this.debug("Error en sso-success:", err);
164
+ this.closeUI();
165
+ cleanup();
166
+ this.emit("error", err);
167
+ reject(err);
168
+ }
169
+ return;
170
+ }
171
+ if (msg.type === "sso-error") {
172
+ const errorPayload = msg.payload;
173
+ this.debug("sso-error recibido:", errorPayload);
174
+ clearTimeout(this.timeoutId);
175
+ this.closeUI();
176
+ cleanup();
177
+ if (errorPayload.code === "version_mismatch") {
178
+ this.emit("error", errorPayload);
179
+ window.location.href = this.buildFallbackUrl();
180
+ reject(new Error(`Version mismatch: expected ${errorPayload.expected_version}`));
181
+ } else {
182
+ this.emit("error", errorPayload);
183
+ reject(errorPayload);
184
+ }
185
+ }
186
+ if (msg.type === "sso-close") {
187
+ this.debug("sso-close recibido");
188
+ this.closeUI();
189
+ cleanup();
190
+ reject(new Error("Login cancelled by user"));
191
+ }
192
+ };
193
+ window.addEventListener("message", this.messageListener);
194
+ signal.addEventListener("abort", () => {
195
+ this.debug("Operaci\xF3n abortada");
196
+ this.closeUI();
197
+ cleanup();
198
+ reject(new Error("Login aborted"));
199
+ });
200
+ });
201
+ }
202
+ /** Cancela el flujo de autenticación en curso */
203
+ abort() {
204
+ this.abortController?.abort();
205
+ }
206
+ // ─── UI Management ───────────────────────────────────────────────
207
+ /**
208
+ * Crea (o reutiliza) el overlay con Shadow DOM y el iframe visible.
209
+ * Patrón tomado del CDN widget v1: Shadow DOM para aislar estilos.
210
+ */
211
+ createUI() {
212
+ if (!this.hostEl) {
213
+ this.hostEl = document.createElement("div");
214
+ this.hostEl.id = "bigso-auth-host";
215
+ this.shadowRoot = this.hostEl.attachShadow({ mode: "open" });
216
+ const style = document.createElement("style");
217
+ style.textContent = this.getOverlayStyles();
218
+ this.shadowRoot.appendChild(style);
219
+ this.overlayEl = document.createElement("div");
220
+ this.overlayEl.className = "sso-overlay";
221
+ const closeBtn = document.createElement("button");
222
+ closeBtn.className = "sso-close-btn";
223
+ closeBtn.innerHTML = "&times;";
224
+ closeBtn.setAttribute("aria-label", "Cerrar modal");
225
+ closeBtn.addEventListener("click", () => this.abort());
226
+ this.overlayEl.appendChild(closeBtn);
227
+ this.overlayEl.addEventListener("click", (event) => {
228
+ if (event.target === this.overlayEl) {
229
+ this.abort();
230
+ }
231
+ });
232
+ this.shadowRoot.appendChild(this.overlayEl);
233
+ document.body.appendChild(this.hostEl);
234
+ }
235
+ this.iframe = document.createElement("iframe");
236
+ this.iframe.className = "sso-frame";
237
+ this.iframe.src = `${this.options.ssoOrigin}/auth/sign-in?v=2.3&client_id=${this.options.clientId}`;
238
+ this.iframe.setAttribute("title", "SSO Login");
239
+ this.overlayEl.appendChild(this.iframe);
240
+ this.debug("Iframe creado", this.iframe.src);
241
+ this.overlayEl.classList.remove("sso-closing");
242
+ this.overlayEl.style.display = "flex";
243
+ }
244
+ /**
245
+ * Cierra el overlay con animación suave (fadeOut + slideDown).
246
+ * El overlay persiste en el DOM (solo se oculta).
247
+ */
248
+ closeUI() {
249
+ if (!this.overlayEl || this.overlayEl.style.display === "none") return;
250
+ this.overlayEl.classList.add("sso-closing");
251
+ setTimeout(() => {
252
+ if (this.overlayEl) {
253
+ this.overlayEl.style.display = "none";
254
+ this.overlayEl.classList.remove("sso-closing");
255
+ }
256
+ }, 200);
257
+ }
258
+ /**
259
+ * Estilos CSS encapsulados dentro del Shadow DOM.
260
+ * Migrados del widget CDN v1 con las mismas animaciones y responsive.
261
+ */
262
+ getOverlayStyles() {
263
+ return `
264
+ .sso-overlay {
265
+ position: fixed;
266
+ inset: 0;
267
+ display: none;
268
+ justify-content: center;
269
+ align-items: center;
270
+ background: rgba(0, 0, 0, 0.6);
271
+ z-index: 999999;
272
+ backdrop-filter: blur(4px);
273
+ -webkit-backdrop-filter: blur(4px);
274
+ animation: fadeIn 0.2s ease;
275
+ }
276
+ .sso-frame {
277
+ width: 370px;
278
+ height: 350px;
279
+ border: none;
280
+ border-radius: 16px;
281
+ background: var(--card-bg, #fff);
282
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
283
+ animation: slideUp 0.3s ease;
284
+ }
285
+ @media (max-width: 480px), (max-height: 480px) {
286
+ .sso-frame {
287
+ width: 100%;
288
+ height: 100%;
289
+ border-radius: 0;
290
+ }
291
+ }
292
+ .sso-close-btn {
293
+ position: absolute;
294
+ top: 12px;
295
+ right: 12px;
296
+ width: 32px;
297
+ height: 32px;
298
+ background: rgba(0, 0, 0, 0.4);
299
+ color: white;
300
+ border: none;
301
+ border-radius: 50%;
302
+ font-size: 24px;
303
+ line-height: 1;
304
+ cursor: pointer;
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: center;
308
+ z-index: 1000000;
309
+ transition: background 0.2s;
310
+ }
311
+ .sso-close-btn:hover {
312
+ background: rgba(0, 0, 0, 0.8);
313
+ }
314
+ .sso-overlay.sso-closing {
315
+ animation: fadeOut 0.2s ease forwards;
316
+ }
317
+ .sso-overlay.sso-closing .sso-frame {
318
+ animation: slideDown 0.2s ease forwards;
319
+ }
320
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
321
+ @keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
322
+ @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
323
+ @keyframes slideDown { from { transform: translateY(0); opacity: 1; } to { transform: translateY(20px); opacity: 0; } }
324
+ `;
325
+ }
326
+ // ─── Helpers ──────────────────────────────────────────────────────
327
+ buildFallbackUrl() {
328
+ const url = new URL(`${this.options.ssoOrigin}/authorize`);
329
+ url.searchParams.set("client_id", this.options.clientId);
330
+ url.searchParams.set("response_type", "code");
331
+ url.searchParams.set("redirect_uri", this.options.redirectUri || window.location.origin);
332
+ url.searchParams.set("state", generateRandomId());
333
+ url.searchParams.set("code_challenge_method", "S256");
334
+ return url.toString();
335
+ }
336
+ debug(...args) {
337
+ if (this.options.debug) {
338
+ console.log("[BigsoAuth]", ...args);
339
+ }
340
+ }
341
+ };
342
+ export {
343
+ BigsoAuth
344
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigso/auth-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.4.3",
4
4
  "description": "SDK de autenticación para SSO v2.3 con iframe seguro",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -16,7 +16,8 @@
16
16
  "build": "tsup src/index.ts --format esm,cjs --dts",
17
17
  "dev": "tsup src/index.ts --watch",
18
18
  "lint": "eslint .",
19
- "test": "vitest"
19
+ "test": "vitest",
20
+ "release": "git tag v$npm_package_version && git push origin v$npm_package_version"
20
21
  },
21
22
  "dependencies": {
22
23
  "jose": "^5.0.0"