@utilia-os/sdk-js 1.4.0 → 1.7.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/README.md +22 -0
- package/dist/index.d.mts +378 -25
- package/dist/index.d.ts +378 -25
- package/dist/index.js +638 -43
- package/dist/index.mjs +628 -39
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,14 +31,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ErrorCode: () => ErrorCode,
|
|
34
|
+
FileTokenStorage: () => FileTokenStorage,
|
|
35
|
+
MemoryTokenStorage: () => MemoryTokenStorage,
|
|
36
|
+
OAuthService: () => OAuthService,
|
|
34
37
|
SDK_LIMITS: () => SDK_LIMITS,
|
|
35
38
|
UtiliaSDK: () => UtiliaSDK,
|
|
36
|
-
UtiliaSDKError: () => UtiliaSDKError
|
|
39
|
+
UtiliaSDKError: () => UtiliaSDKError,
|
|
40
|
+
base64UrlEncode: () => base64UrlEncode,
|
|
41
|
+
generateCodeChallenge: () => generateCodeChallenge,
|
|
42
|
+
generateCodeVerifier: () => generateCodeVerifier
|
|
37
43
|
});
|
|
38
44
|
module.exports = __toCommonJS(index_exports);
|
|
39
45
|
|
|
40
46
|
// src/client.ts
|
|
41
|
-
var
|
|
47
|
+
var import_axios2 = __toESM(require("axios"));
|
|
42
48
|
|
|
43
49
|
// src/errors/sdk-error.ts
|
|
44
50
|
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
@@ -48,6 +54,7 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
|
48
54
|
ErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
|
|
49
55
|
ErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
50
56
|
ErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
57
|
+
ErrorCode2["AUTH_EXPIRED"] = "AUTH_EXPIRED";
|
|
51
58
|
ErrorCode2["UNKNOWN"] = "UNKNOWN";
|
|
52
59
|
return ErrorCode2;
|
|
53
60
|
})(ErrorCode || {});
|
|
@@ -98,23 +105,469 @@ var UtiliaSDKError = class _UtiliaSDKError extends Error {
|
|
|
98
105
|
}
|
|
99
106
|
};
|
|
100
107
|
|
|
108
|
+
// src/auth/oauth.service.ts
|
|
109
|
+
var import_axios = __toESM(require("axios"));
|
|
110
|
+
|
|
111
|
+
// src/auth/pkce.ts
|
|
112
|
+
function base64UrlEncode(buffer) {
|
|
113
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
114
|
+
let binary = "";
|
|
115
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
116
|
+
binary += String.fromCharCode(bytes[i]);
|
|
117
|
+
}
|
|
118
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
119
|
+
}
|
|
120
|
+
function generateCodeVerifier(length = 64) {
|
|
121
|
+
const validLength = Math.max(43, Math.min(128, length));
|
|
122
|
+
const bytes = new Uint8Array(validLength);
|
|
123
|
+
crypto.getRandomValues(bytes);
|
|
124
|
+
return base64UrlEncode(bytes).slice(0, validLength);
|
|
125
|
+
}
|
|
126
|
+
async function generateCodeChallenge(verifier) {
|
|
127
|
+
const encoder = new TextEncoder();
|
|
128
|
+
const data = encoder.encode(verifier);
|
|
129
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
130
|
+
return base64UrlEncode(digest);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/auth/token-storage.ts
|
|
134
|
+
var MemoryTokenStorage = class {
|
|
135
|
+
constructor() {
|
|
136
|
+
this.tokens = null;
|
|
137
|
+
this.pendingState = null;
|
|
138
|
+
}
|
|
139
|
+
async getTokens() {
|
|
140
|
+
return this.tokens;
|
|
141
|
+
}
|
|
142
|
+
async setTokens(tokens) {
|
|
143
|
+
this.tokens = tokens;
|
|
144
|
+
}
|
|
145
|
+
async clearTokens() {
|
|
146
|
+
this.tokens = null;
|
|
147
|
+
}
|
|
148
|
+
async getPendingState() {
|
|
149
|
+
return this.pendingState;
|
|
150
|
+
}
|
|
151
|
+
async setPendingState(state) {
|
|
152
|
+
this.pendingState = state;
|
|
153
|
+
}
|
|
154
|
+
async clearPendingState() {
|
|
155
|
+
this.pendingState = null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var FileTokenStorage = class {
|
|
159
|
+
/**
|
|
160
|
+
* @param filePath - Ruta absoluta al archivo donde se guardarán los tokens
|
|
161
|
+
*/
|
|
162
|
+
constructor(filePath) {
|
|
163
|
+
this.filePath = filePath;
|
|
164
|
+
}
|
|
165
|
+
async getTokens() {
|
|
166
|
+
try {
|
|
167
|
+
const fs = await import("fs/promises");
|
|
168
|
+
const content = await fs.readFile(this.filePath, "utf-8");
|
|
169
|
+
const data = JSON.parse(content);
|
|
170
|
+
if (data._pendingState) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return data;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async setTokens(tokens) {
|
|
179
|
+
const fs = await import("fs/promises");
|
|
180
|
+
await fs.writeFile(this.filePath, JSON.stringify(tokens, null, 2), { encoding: "utf-8", mode: 384 });
|
|
181
|
+
}
|
|
182
|
+
async clearTokens() {
|
|
183
|
+
try {
|
|
184
|
+
const fs = await import("fs/promises");
|
|
185
|
+
await fs.unlink(this.filePath);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
get pendingStatePath() {
|
|
190
|
+
return this.filePath.replace(/\.json$/, "") + ".pending.json";
|
|
191
|
+
}
|
|
192
|
+
async getPendingState() {
|
|
193
|
+
try {
|
|
194
|
+
const fs = await import("fs/promises");
|
|
195
|
+
const content = await fs.readFile(this.pendingStatePath, "utf-8");
|
|
196
|
+
return JSON.parse(content);
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async setPendingState(state) {
|
|
202
|
+
const fs = await import("fs/promises");
|
|
203
|
+
await fs.writeFile(this.pendingStatePath, JSON.stringify(state, null, 2), { encoding: "utf-8", mode: 384 });
|
|
204
|
+
}
|
|
205
|
+
async clearPendingState() {
|
|
206
|
+
try {
|
|
207
|
+
const fs = await import("fs/promises");
|
|
208
|
+
await fs.unlink(this.pendingStatePath);
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/auth/oauth.service.ts
|
|
215
|
+
var OAuthService = class {
|
|
216
|
+
constructor(baseURL, config) {
|
|
217
|
+
/** Estado PKCE en memoria como fallback cuando el storage no soporta pendingState */
|
|
218
|
+
this._pendingState = null;
|
|
219
|
+
this.baseURL = baseURL.replace(/\/$/, "");
|
|
220
|
+
this.config = config;
|
|
221
|
+
this.storage = config.tokenStorage ?? new MemoryTokenStorage();
|
|
222
|
+
this.http = import_axios.default.create({
|
|
223
|
+
baseURL: this.baseURL,
|
|
224
|
+
timeout: 3e4,
|
|
225
|
+
headers: { "Content-Type": "application/json" }
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Genera la URL de autorización OAuth con PKCE y almacena el estado
|
|
230
|
+
* pendiente automáticamente. Usar con handleCallback(code) sin codeVerifier.
|
|
231
|
+
*
|
|
232
|
+
* Este es el método recomendado para la mayoría de los casos.
|
|
233
|
+
*
|
|
234
|
+
* @param options - Opciones adicionales (state personalizado, scopes)
|
|
235
|
+
* @returns URL de autorización lista para redirigir al usuario
|
|
236
|
+
*/
|
|
237
|
+
async getAuthorizationUrl(options) {
|
|
238
|
+
const result = await this.getLoginUrl(options);
|
|
239
|
+
const pending = {
|
|
240
|
+
codeVerifier: result.codeVerifier,
|
|
241
|
+
state: result.state
|
|
242
|
+
};
|
|
243
|
+
this._pendingState = pending;
|
|
244
|
+
if (this.storage.setPendingState) {
|
|
245
|
+
await this.storage.setPendingState(pending);
|
|
246
|
+
}
|
|
247
|
+
return result.url;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Genera la URL de inicio de sesión OAuth con PKCE.
|
|
251
|
+
*
|
|
252
|
+
* Método de control manual: devuelve codeVerifier y state que el desarrollador
|
|
253
|
+
* debe gestionar. Para un flujo automático, usar getAuthorizationUrl() en su lugar.
|
|
254
|
+
*
|
|
255
|
+
* @param options - Opciones adicionales (state personalizado, scopes)
|
|
256
|
+
* @returns URL de autorización, code_verifier y state
|
|
257
|
+
*/
|
|
258
|
+
async getLoginUrl(options) {
|
|
259
|
+
const codeVerifier = generateCodeVerifier();
|
|
260
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
261
|
+
const state = options?.state ?? Array.from(crypto.getRandomValues(new Uint8Array(24)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
262
|
+
const scopes = options?.scopes ?? this.config.scopes ?? ["openid", "profile", "email"];
|
|
263
|
+
const params = new URLSearchParams({
|
|
264
|
+
response_type: "code",
|
|
265
|
+
client_id: this.config.clientId,
|
|
266
|
+
redirect_uri: this.config.redirectUri,
|
|
267
|
+
scope: scopes.join(" "),
|
|
268
|
+
state,
|
|
269
|
+
code_challenge: codeChallenge,
|
|
270
|
+
code_challenge_method: "S256"
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
url: `${this.baseURL}/oauth/authorize?${params.toString()}`,
|
|
274
|
+
codeVerifier,
|
|
275
|
+
state
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Abre una ventana emergente para inicio de sesión OAuth y devuelve tokens al completarse.
|
|
280
|
+
*
|
|
281
|
+
* @param options - Opciones de configuración de la ventana emergente
|
|
282
|
+
* @returns Tokens OAuth
|
|
283
|
+
* @throws Error si la ventana es bloqueada, cerrada o excede el tiempo límite
|
|
284
|
+
*/
|
|
285
|
+
async loginWithPopup(options) {
|
|
286
|
+
if (typeof window === "undefined") {
|
|
287
|
+
throw new Error("loginWithPopup solo est\xE1 disponible en entornos de navegador.");
|
|
288
|
+
}
|
|
289
|
+
const { url, codeVerifier, state } = await this.getLoginUrl({
|
|
290
|
+
state: options?.state,
|
|
291
|
+
scopes: options?.scopes
|
|
292
|
+
});
|
|
293
|
+
const width = options?.width ?? 500;
|
|
294
|
+
const height = options?.height ?? 700;
|
|
295
|
+
const timeout = options?.timeout ?? 3e5;
|
|
296
|
+
const left = Math.max(0, Math.round(window.screenX + (window.outerWidth - width) / 2));
|
|
297
|
+
const top = Math.max(0, Math.round(window.screenY + (window.outerHeight - height) / 2));
|
|
298
|
+
const features = `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes,status=yes`;
|
|
299
|
+
const popup = window.open(url, "utilia-oauth-popup", features);
|
|
300
|
+
if (!popup || popup.closed) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
"No se pudo abrir la ventana de autorizaci\xF3n. Verifica que los popups no est\xE9n bloqueados."
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return new Promise((resolve, reject) => {
|
|
306
|
+
let timeoutId;
|
|
307
|
+
let pollId;
|
|
308
|
+
const cleanup = () => {
|
|
309
|
+
clearTimeout(timeoutId);
|
|
310
|
+
clearInterval(pollId);
|
|
311
|
+
window.removeEventListener("message", onMessage);
|
|
312
|
+
};
|
|
313
|
+
const onMessage = async (event) => {
|
|
314
|
+
if (event.origin !== window.location.origin) return;
|
|
315
|
+
const data = event.data;
|
|
316
|
+
if (!data || typeof data !== "object") return;
|
|
317
|
+
if (data.type === "oauth-mcp-success" && data.state === state) {
|
|
318
|
+
cleanup();
|
|
319
|
+
try {
|
|
320
|
+
if (popup && !popup.closed) popup.close();
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const tokens = await this.handleCallback(data.code, codeVerifier);
|
|
325
|
+
resolve(tokens);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
reject(err);
|
|
328
|
+
}
|
|
329
|
+
} else if (data.type === "oauth-mcp-error" && data.state === state) {
|
|
330
|
+
cleanup();
|
|
331
|
+
try {
|
|
332
|
+
if (popup && !popup.closed) popup.close();
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
reject(new Error(data.error_description || data.error || "Autorizaci\xF3n denegada"));
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
window.addEventListener("message", onMessage);
|
|
339
|
+
pollId = setInterval(() => {
|
|
340
|
+
if (popup.closed) {
|
|
341
|
+
cleanup();
|
|
342
|
+
reject(new Error("La ventana de autorizaci\xF3n fue cerrada antes de completar el proceso."));
|
|
343
|
+
}
|
|
344
|
+
}, 500);
|
|
345
|
+
timeoutId = setTimeout(() => {
|
|
346
|
+
cleanup();
|
|
347
|
+
try {
|
|
348
|
+
if (popup && !popup.closed) popup.close();
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
reject(new Error("El proceso de autorizaci\xF3n excedi\xF3 el tiempo l\xEDmite."));
|
|
352
|
+
}, timeout);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Intercambia el código de autorización por tokens.
|
|
357
|
+
*
|
|
358
|
+
* Si se llama sin codeVerifier, recupera automáticamente el estado PKCE
|
|
359
|
+
* almacenado por getAuthorizationUrl() y valida el state CSRF.
|
|
360
|
+
*
|
|
361
|
+
* @param code - Código de autorización recibido en el callback
|
|
362
|
+
* @param codeVerifier - Verificador PKCE (opcional si se usó getAuthorizationUrl)
|
|
363
|
+
* @param callbackState - Valor de state recibido en el callback (para validación CSRF automática)
|
|
364
|
+
* @returns Tokens OAuth
|
|
365
|
+
*/
|
|
366
|
+
async handleCallback(code, codeVerifier, callbackState) {
|
|
367
|
+
let verifier = codeVerifier;
|
|
368
|
+
if (!verifier) {
|
|
369
|
+
const pending = await this.getPendingState();
|
|
370
|
+
if (!pending) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
"No se encontr\xF3 el estado PKCE pendiente. Usa getAuthorizationUrl() antes de handleCallback(), o proporciona el codeVerifier manualmente."
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
verifier = pending.codeVerifier;
|
|
376
|
+
if (callbackState && callbackState !== pending.state) {
|
|
377
|
+
await this.clearPendingState();
|
|
378
|
+
throw new Error(
|
|
379
|
+
`El valor de state no coincide. Posible ataque CSRF. Esperado: ${pending.state}, recibido: ${callbackState}`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
await this.clearPendingState();
|
|
383
|
+
}
|
|
384
|
+
const body = {
|
|
385
|
+
grant_type: "authorization_code",
|
|
386
|
+
code,
|
|
387
|
+
redirect_uri: this.config.redirectUri,
|
|
388
|
+
code_verifier: verifier,
|
|
389
|
+
client_id: this.config.clientId
|
|
390
|
+
};
|
|
391
|
+
if (this.config.clientSecret) {
|
|
392
|
+
body.client_secret = this.config.clientSecret;
|
|
393
|
+
}
|
|
394
|
+
const response = await this.http.post("/oauth/token", body);
|
|
395
|
+
const data = response.data;
|
|
396
|
+
const tokens = {
|
|
397
|
+
accessToken: data.access_token,
|
|
398
|
+
refreshToken: data.refresh_token,
|
|
399
|
+
expiresIn: data.expires_in,
|
|
400
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
401
|
+
tokenType: data.token_type || "Bearer",
|
|
402
|
+
scope: data.scope,
|
|
403
|
+
idToken: data.id_token
|
|
404
|
+
};
|
|
405
|
+
await this.storage.setTokens(tokens);
|
|
406
|
+
return tokens;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Refresca el token de acceso usando el refresh_token
|
|
410
|
+
*
|
|
411
|
+
* @returns Nuevos tokens OAuth
|
|
412
|
+
* @throws Error si no hay refresh_token disponible
|
|
413
|
+
*/
|
|
414
|
+
async refreshToken() {
|
|
415
|
+
const current = await this.storage.getTokens();
|
|
416
|
+
if (!current?.refreshToken) {
|
|
417
|
+
throw new Error("No hay refresh token disponible. El usuario debe volver a autenticarse.");
|
|
418
|
+
}
|
|
419
|
+
const body = {
|
|
420
|
+
grant_type: "refresh_token",
|
|
421
|
+
refresh_token: current.refreshToken,
|
|
422
|
+
client_id: this.config.clientId
|
|
423
|
+
};
|
|
424
|
+
if (this.config.clientSecret) {
|
|
425
|
+
body.client_secret = this.config.clientSecret;
|
|
426
|
+
}
|
|
427
|
+
const response = await this.http.post("/oauth/token", body);
|
|
428
|
+
const data = response.data;
|
|
429
|
+
const tokens = {
|
|
430
|
+
accessToken: data.access_token,
|
|
431
|
+
refreshToken: data.refresh_token ?? current.refreshToken,
|
|
432
|
+
expiresIn: data.expires_in,
|
|
433
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
434
|
+
tokenType: data.token_type || "Bearer",
|
|
435
|
+
scope: data.scope,
|
|
436
|
+
idToken: data.id_token
|
|
437
|
+
};
|
|
438
|
+
await this.storage.setTokens(tokens);
|
|
439
|
+
return tokens;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Obtiene información del usuario autenticado
|
|
443
|
+
*
|
|
444
|
+
* @returns Datos del usuario desde /oauth/userinfo
|
|
445
|
+
*/
|
|
446
|
+
async getUserInfo() {
|
|
447
|
+
const accessToken = await this.getAccessToken();
|
|
448
|
+
const response = await this.http.get("/oauth/userinfo", {
|
|
449
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
450
|
+
});
|
|
451
|
+
return response.data;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Revoca el token actual
|
|
455
|
+
*
|
|
456
|
+
* @param tokenType - Tipo de token a revocar ('access_token' | 'refresh_token').
|
|
457
|
+
* Por defecto revoca el access_token.
|
|
458
|
+
*/
|
|
459
|
+
async revokeToken(tokenType) {
|
|
460
|
+
const current = await this.storage.getTokens();
|
|
461
|
+
if (!current) return;
|
|
462
|
+
const token = tokenType === "refresh_token" ? current.refreshToken : current.accessToken;
|
|
463
|
+
if (!token) return;
|
|
464
|
+
const body = {
|
|
465
|
+
token,
|
|
466
|
+
client_id: this.config.clientId
|
|
467
|
+
};
|
|
468
|
+
if (tokenType) {
|
|
469
|
+
body.token_type_hint = tokenType;
|
|
470
|
+
}
|
|
471
|
+
if (this.config.clientSecret) {
|
|
472
|
+
body.client_secret = this.config.clientSecret;
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
await this.http.post("/oauth/revoke", body);
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
await this.storage.clearTokens();
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Obtiene un access token válido, refrescándolo automáticamente si ha expirado
|
|
482
|
+
*
|
|
483
|
+
* @returns Access token válido
|
|
484
|
+
* @throws Error si no hay tokens disponibles
|
|
485
|
+
*/
|
|
486
|
+
async getAccessToken() {
|
|
487
|
+
const tokens = await this.storage.getTokens();
|
|
488
|
+
if (!tokens) {
|
|
489
|
+
throw new Error("No hay tokens OAuth. El usuario debe iniciar sesi\xF3n primero.");
|
|
490
|
+
}
|
|
491
|
+
if (tokens.expiresAt < Date.now() + 3e4) {
|
|
492
|
+
const refreshed = await this.refreshToken();
|
|
493
|
+
return refreshed.accessToken;
|
|
494
|
+
}
|
|
495
|
+
return tokens.accessToken;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Verifica si hay una sesión OAuth activa con tokens válidos.
|
|
499
|
+
*
|
|
500
|
+
* @returns true si hay tokens almacenados y el access token no ha expirado
|
|
501
|
+
* (o si hay un refresh token disponible para renovarlo)
|
|
502
|
+
*/
|
|
503
|
+
async isAuthenticated() {
|
|
504
|
+
const tokens = await this.storage.getTokens();
|
|
505
|
+
if (!tokens) return false;
|
|
506
|
+
if (tokens.expiresAt > Date.now() + 3e4) return true;
|
|
507
|
+
return !!tokens.refreshToken;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Cierra la sesión OAuth: revoca los tokens y limpia el almacenamiento.
|
|
511
|
+
*/
|
|
512
|
+
async logout() {
|
|
513
|
+
await this.revokeToken();
|
|
514
|
+
}
|
|
515
|
+
// -- Helpers internos para estado PKCE pendiente --
|
|
516
|
+
async getPendingState() {
|
|
517
|
+
if (this.storage.getPendingState) {
|
|
518
|
+
const stored = await this.storage.getPendingState();
|
|
519
|
+
if (stored) return stored;
|
|
520
|
+
}
|
|
521
|
+
return this._pendingState;
|
|
522
|
+
}
|
|
523
|
+
async clearPendingState() {
|
|
524
|
+
this._pendingState = null;
|
|
525
|
+
if (this.storage.clearPendingState) {
|
|
526
|
+
await this.storage.clearPendingState();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
101
531
|
// src/client.ts
|
|
102
532
|
var UtiliaClient = class {
|
|
103
533
|
constructor(config) {
|
|
534
|
+
/** Promesa compartida para evitar refreshes concurrentes */
|
|
535
|
+
this._refreshPromise = null;
|
|
536
|
+
if (!config.apiKey && !config.oauth) {
|
|
537
|
+
throw new UtiliaSDKError(
|
|
538
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
539
|
+
"Debes proporcionar apiKey o configuraci\xF3n oauth"
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (config.apiKey && config.oauth) {
|
|
543
|
+
throw new UtiliaSDKError(
|
|
544
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
545
|
+
"No puedes proporcionar apiKey y oauth a la vez. Usa uno u otro."
|
|
546
|
+
);
|
|
547
|
+
}
|
|
104
548
|
this.config = {
|
|
105
549
|
timeout: 3e4,
|
|
106
550
|
retryAttempts: 3,
|
|
107
551
|
debug: false,
|
|
108
552
|
...config
|
|
109
553
|
};
|
|
110
|
-
|
|
554
|
+
const headers = {
|
|
555
|
+
"Content-Type": "application/json"
|
|
556
|
+
};
|
|
557
|
+
if (this.config.apiKey) {
|
|
558
|
+
headers["X-Api-Key"] = this.config.apiKey;
|
|
559
|
+
}
|
|
560
|
+
this.axios = import_axios2.default.create({
|
|
111
561
|
baseURL: this.config.baseURL,
|
|
112
562
|
timeout: this.config.timeout,
|
|
113
|
-
headers
|
|
114
|
-
"Content-Type": "application/json",
|
|
115
|
-
"X-Api-Key": this.config.apiKey
|
|
116
|
-
}
|
|
563
|
+
headers
|
|
117
564
|
});
|
|
565
|
+
if (this.config.oauth) {
|
|
566
|
+
this._oauthService = new OAuthService(this.config.baseURL, this.config.oauth);
|
|
567
|
+
if (this.config.oauth.tokenStorage) {
|
|
568
|
+
}
|
|
569
|
+
this.setupOAuthInterceptors();
|
|
570
|
+
}
|
|
118
571
|
this.axios.interceptors.request.use((config2) => {
|
|
119
572
|
const requestId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : this.generateSimpleId();
|
|
120
573
|
config2.headers["x-request-id"] = requestId;
|
|
@@ -123,20 +576,71 @@ var UtiliaClient = class {
|
|
|
123
576
|
});
|
|
124
577
|
this.setupInterceptors();
|
|
125
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Acceso al servicio OAuth (solo disponible en modo OAuth)
|
|
581
|
+
*/
|
|
582
|
+
get oauth() {
|
|
583
|
+
if (!this._oauthService) {
|
|
584
|
+
throw new UtiliaSDKError(
|
|
585
|
+
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
|
|
586
|
+
"El servicio OAuth no est\xE1 disponible. Configura oauth en el constructor del SDK."
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return this._oauthService;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Configura interceptores para autenticación OAuth
|
|
593
|
+
*/
|
|
594
|
+
setupOAuthInterceptors() {
|
|
595
|
+
this.axios.interceptors.request.use(async (config) => {
|
|
596
|
+
if (this._oauthService) {
|
|
597
|
+
try {
|
|
598
|
+
const accessToken = await this._oauthService.getAccessToken();
|
|
599
|
+
config.headers["Authorization"] = `Bearer ${accessToken}`;
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return config;
|
|
604
|
+
});
|
|
605
|
+
this.axios.interceptors.response.use(
|
|
606
|
+
(response) => response,
|
|
607
|
+
async (error) => {
|
|
608
|
+
const originalRequest = error.config;
|
|
609
|
+
if (error.response?.status === 401 && this._oauthService && originalRequest && !originalRequest._retried) {
|
|
610
|
+
originalRequest._retried = true;
|
|
611
|
+
if (!this._refreshPromise) {
|
|
612
|
+
this._refreshPromise = this._oauthService.refreshToken().then(() => {
|
|
613
|
+
}).finally(() => {
|
|
614
|
+
this._refreshPromise = null;
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
await this._refreshPromise;
|
|
619
|
+
const newToken = await this._oauthService.getAccessToken();
|
|
620
|
+
originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
|
|
621
|
+
return this.axios(originalRequest);
|
|
622
|
+
} catch {
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
}
|
|
126
630
|
/**
|
|
127
631
|
* Configura los interceptores de request y response
|
|
128
632
|
*/
|
|
129
633
|
setupInterceptors() {
|
|
130
634
|
if (this.config.debug) {
|
|
131
635
|
this.axios.interceptors.request.use((request) => {
|
|
132
|
-
console.log("[UtiliaSDK]
|
|
636
|
+
console.log("[UtiliaSDK] Petici\xF3n:", request.method?.toUpperCase(), request.url);
|
|
133
637
|
return request;
|
|
134
638
|
});
|
|
135
639
|
}
|
|
136
640
|
if (this.config.debug) {
|
|
137
641
|
this.axios.interceptors.response.use(
|
|
138
642
|
(response) => {
|
|
139
|
-
console.log("[UtiliaSDK]
|
|
643
|
+
console.log("[UtiliaSDK] Respuesta:", response.status, response.config.url);
|
|
140
644
|
return response;
|
|
141
645
|
}
|
|
142
646
|
);
|
|
@@ -153,15 +657,15 @@ var UtiliaClient = class {
|
|
|
153
657
|
const errorOptions = { requestId, statusCode, errorCode };
|
|
154
658
|
if (!error.response) {
|
|
155
659
|
if (error.code === "ECONNABORTED") {
|
|
156
|
-
return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Timeout: la solicitud
|
|
660
|
+
return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Timeout: la solicitud excedi\xF3 el tiempo l\xEDmite", errorOptions);
|
|
157
661
|
}
|
|
158
|
-
return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Error de
|
|
662
|
+
return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Error de conexi\xF3n: no se pudo conectar con el servidor", errorOptions);
|
|
159
663
|
}
|
|
160
664
|
const status = error.response.status;
|
|
161
665
|
const message = data?.message || data?.error || error.message;
|
|
162
666
|
switch (status) {
|
|
163
667
|
case 401:
|
|
164
|
-
return new UtiliaSDKError("UNAUTHORIZED" /* UNAUTHORIZED */, message || "API Key
|
|
668
|
+
return new UtiliaSDKError("UNAUTHORIZED" /* UNAUTHORIZED */, message || "API Key inv\xE1lida o faltante", errorOptions);
|
|
165
669
|
case 403:
|
|
166
670
|
if (message?.toLowerCase().includes("rate limit")) {
|
|
167
671
|
return new UtiliaSDKError("RATE_LIMITED" /* RATE_LIMITED */, "Rate limit excedido. Intenta de nuevo mas tarde.", errorOptions);
|
|
@@ -174,7 +678,7 @@ var UtiliaClient = class {
|
|
|
174
678
|
return new UtiliaSDKError("NOT_FOUND" /* NOT_FOUND */, message || "Recurso no encontrado", errorOptions);
|
|
175
679
|
case 400:
|
|
176
680
|
case 422:
|
|
177
|
-
return new UtiliaSDKError("VALIDATION_ERROR" /* VALIDATION_ERROR */, message || "Error de
|
|
681
|
+
return new UtiliaSDKError("VALIDATION_ERROR" /* VALIDATION_ERROR */, message || "Error de validaci\xF3n en los datos enviados", errorOptions);
|
|
178
682
|
case 429:
|
|
179
683
|
return new UtiliaSDKError("RATE_LIMITED" /* RATE_LIMITED */, message || "Rate limit excedido. Intenta de nuevo mas tarde.", errorOptions);
|
|
180
684
|
case 500:
|
|
@@ -187,10 +691,10 @@ var UtiliaClient = class {
|
|
|
187
691
|
}
|
|
188
692
|
}
|
|
189
693
|
/**
|
|
190
|
-
* Determina si un error es recuperable y se debe reintentar
|
|
694
|
+
* Determina si un error es recuperable y se debe reintentar.
|
|
191
695
|
*/
|
|
192
696
|
isRetryableError(error) {
|
|
193
|
-
if (error instanceof
|
|
697
|
+
if (error instanceof import_axios2.AxiosError) {
|
|
194
698
|
if (!error.response) {
|
|
195
699
|
const retryCodes = ["ECONNABORTED", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "ENETUNREACH", "ERR_NETWORK"];
|
|
196
700
|
return retryCodes.includes(error.code || "");
|
|
@@ -201,7 +705,7 @@ var UtiliaClient = class {
|
|
|
201
705
|
return false;
|
|
202
706
|
}
|
|
203
707
|
/**
|
|
204
|
-
* Ejecuta una
|
|
708
|
+
* Ejecuta una función con reintentos y backoff exponencial
|
|
205
709
|
*/
|
|
206
710
|
async executeWithRetry(fn) {
|
|
207
711
|
let lastError;
|
|
@@ -211,7 +715,7 @@ var UtiliaClient = class {
|
|
|
211
715
|
} catch (error) {
|
|
212
716
|
lastError = error;
|
|
213
717
|
if (!this.isRetryableError(error)) {
|
|
214
|
-
if (error instanceof
|
|
718
|
+
if (error instanceof import_axios2.AxiosError) {
|
|
215
719
|
throw this.toSDKError(error);
|
|
216
720
|
}
|
|
217
721
|
throw error;
|
|
@@ -219,19 +723,19 @@ var UtiliaClient = class {
|
|
|
219
723
|
if (attempt < this.config.retryAttempts - 1) {
|
|
220
724
|
const delay = 500 * Math.pow(2, attempt);
|
|
221
725
|
if (this.config.debug) {
|
|
222
|
-
console.log(`[UtiliaSDK]
|
|
726
|
+
console.log(`[UtiliaSDK] Reintento ${attempt + 1}/${this.config.retryAttempts - 1}, esperando ${delay}ms`);
|
|
223
727
|
}
|
|
224
728
|
await this.sleep(delay);
|
|
225
729
|
}
|
|
226
730
|
}
|
|
227
731
|
}
|
|
228
|
-
if (lastError instanceof
|
|
732
|
+
if (lastError instanceof import_axios2.AxiosError) {
|
|
229
733
|
throw this.toSDKError(lastError);
|
|
230
734
|
}
|
|
231
735
|
throw lastError;
|
|
232
736
|
}
|
|
233
737
|
/**
|
|
234
|
-
* Genera un ID simple como fallback cuando crypto.randomUUID no
|
|
738
|
+
* Genera un ID simple como fallback cuando crypto.randomUUID no está disponible
|
|
235
739
|
*/
|
|
236
740
|
generateSimpleId() {
|
|
237
741
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
@@ -243,19 +747,44 @@ var UtiliaClient = class {
|
|
|
243
747
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
244
748
|
}
|
|
245
749
|
/**
|
|
246
|
-
* Construye una URL completa para conexiones SSE con
|
|
247
|
-
*
|
|
750
|
+
* Construye una URL completa para conexiones SSE con credenciales como query param.
|
|
751
|
+
* Necesario porque EventSource no soporta headers personalizados.
|
|
248
752
|
*
|
|
249
753
|
* @param path - Ruta relativa de la API (ej: /external/v1/tickets/ai-suggest/123/stream)
|
|
250
|
-
* @returns URL completa con
|
|
754
|
+
* @returns URL completa con credenciales como parametro de consulta
|
|
251
755
|
*/
|
|
252
756
|
buildSseUrl(path) {
|
|
253
757
|
const base = this.config.baseURL.replace(/\/$/, "");
|
|
254
758
|
const separator = path.includes("?") ? "&" : "?";
|
|
255
|
-
|
|
759
|
+
if (this.config.apiKey) {
|
|
760
|
+
return `${base}${path}${separator}apiKey=${this.config.apiKey}`;
|
|
761
|
+
}
|
|
762
|
+
return `${base}${path}`;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Construye una URL completa para conexiones SSE, incluyendo token OAuth si está disponible.
|
|
766
|
+
* Versión asíncrona que obtiene el access token actual.
|
|
767
|
+
*
|
|
768
|
+
* @param path - Ruta relativa de la API
|
|
769
|
+
* @returns URL completa con credenciales como parametro de consulta
|
|
770
|
+
*/
|
|
771
|
+
async buildSseUrlAsync(path) {
|
|
772
|
+
const base = this.config.baseURL.replace(/\/$/, "");
|
|
773
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
774
|
+
if (this.config.apiKey) {
|
|
775
|
+
return `${base}${path}${separator}apiKey=${this.config.apiKey}`;
|
|
776
|
+
}
|
|
777
|
+
if (this._oauthService) {
|
|
778
|
+
try {
|
|
779
|
+
const accessToken = await this._oauthService.getAccessToken();
|
|
780
|
+
return `${base}${path}${separator}access_token=${accessToken}`;
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return `${base}${path}`;
|
|
256
785
|
}
|
|
257
786
|
/**
|
|
258
|
-
* Realiza una
|
|
787
|
+
* Realiza una petición GET
|
|
259
788
|
*/
|
|
260
789
|
async get(url, config) {
|
|
261
790
|
return this.executeWithRetry(async () => {
|
|
@@ -635,6 +1164,54 @@ var TicketsService = class {
|
|
|
635
1164
|
{ params: { userId } }
|
|
636
1165
|
);
|
|
637
1166
|
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Suscribirse a actualizaciones de tickets en tiempo real via SSE.
|
|
1169
|
+
* Recibe eventos cuando un agente responde, cambia el estado, resuelve o cierra un ticket.
|
|
1170
|
+
*
|
|
1171
|
+
* Requiere un entorno con EventSource nativo (navegador) o un polyfill como 'eventsource'.
|
|
1172
|
+
*
|
|
1173
|
+
* @param userId - ID externo del usuario (opcional, filtra eventos por usuario)
|
|
1174
|
+
* @param options - Callbacks para eventos y errores
|
|
1175
|
+
* @returns Objeto con metodo close() para cerrar la conexion
|
|
1176
|
+
*
|
|
1177
|
+
* @example
|
|
1178
|
+
* ```typescript
|
|
1179
|
+
* const stream = sdk.tickets.streamUpdates('user-123', {
|
|
1180
|
+
* onTicketUpdated: (event) => {
|
|
1181
|
+
* console.log(`Ticket ${event.ticketKey} actualizado: ${event.type}`);
|
|
1182
|
+
* // Refrescar la UI del ticket
|
|
1183
|
+
* },
|
|
1184
|
+
* });
|
|
1185
|
+
*
|
|
1186
|
+
* // Para cerrar la conexion:
|
|
1187
|
+
* stream.close();
|
|
1188
|
+
* ```
|
|
1189
|
+
*/
|
|
1190
|
+
streamUpdates(userId, options) {
|
|
1191
|
+
if (typeof EventSource === "undefined") {
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
'EventSource no esta disponible en este entorno. Para Node.js, instala un polyfill como "eventsource" y asignalo a globalThis.EventSource.'
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
const queryParams = userId ? `?userId=${encodeURIComponent(userId)}` : "";
|
|
1197
|
+
const sseUrl = this.client.buildSseUrl(`${this.basePath}/events${queryParams}`);
|
|
1198
|
+
const eventSource = new EventSource(sseUrl);
|
|
1199
|
+
eventSource.onmessage = (event) => {
|
|
1200
|
+
try {
|
|
1201
|
+
const data = JSON.parse(event.data);
|
|
1202
|
+
if (data.event === "ticket-updated" && options?.onTicketUpdated) {
|
|
1203
|
+
options.onTicketUpdated(data);
|
|
1204
|
+
}
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
eventSource.onerror = () => {
|
|
1209
|
+
options?.onError?.(new Error("Error en la conexion SSE"));
|
|
1210
|
+
};
|
|
1211
|
+
return {
|
|
1212
|
+
close: () => eventSource.close()
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
638
1215
|
/**
|
|
639
1216
|
* Cerrar un ticket
|
|
640
1217
|
* Solo el usuario que creo el ticket puede cerrarlo
|
|
@@ -1140,40 +1717,58 @@ var UtiliaSDK = class {
|
|
|
1140
1717
|
/**
|
|
1141
1718
|
* Crea una nueva instancia del SDK
|
|
1142
1719
|
*
|
|
1143
|
-
* @param config -
|
|
1720
|
+
* @param config - Configuración del SDK
|
|
1144
1721
|
*
|
|
1145
|
-
* @example
|
|
1722
|
+
* @example Autenticación con API Key
|
|
1146
1723
|
* ```typescript
|
|
1147
1724
|
* const sdk = new UtiliaSDK({
|
|
1148
1725
|
* baseURL: 'https://os.utilia.ai/api',
|
|
1149
1726
|
* apiKey: 'tu-api-key',
|
|
1150
|
-
* timeout: 30000, // opcional
|
|
1151
|
-
* debug: true, // opcional, habilita logs
|
|
1152
1727
|
* });
|
|
1728
|
+
* ```
|
|
1153
1729
|
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
* const
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1730
|
+
* @example Autenticación con OAuth 2.1 + PKCE
|
|
1731
|
+
* ```typescript
|
|
1732
|
+
* const sdk = new UtiliaSDK({
|
|
1733
|
+
* baseURL: 'https://os.utilia.ai/api',
|
|
1734
|
+
* oauth: {
|
|
1735
|
+
* clientId: 'tu-client-id',
|
|
1736
|
+
* redirectUri: 'https://tu-app.com/callback',
|
|
1737
|
+
* },
|
|
1161
1738
|
* });
|
|
1739
|
+
*
|
|
1740
|
+
* // Obtener URL de autorización (gestiona PKCE automáticamente)
|
|
1741
|
+
* const authUrl = await sdk.oauth.getAuthorizationUrl();
|
|
1742
|
+
*
|
|
1743
|
+
* // Tras el callback (recupera PKCE automáticamente)
|
|
1744
|
+
* const tokens = await sdk.oauth.handleCallback(code);
|
|
1162
1745
|
* ```
|
|
1163
1746
|
*/
|
|
1164
1747
|
constructor(config) {
|
|
1165
|
-
|
|
1166
|
-
this.errors = new ErrorsService(
|
|
1167
|
-
this.files = new FilesService(
|
|
1168
|
-
this.tickets = new TicketsService(
|
|
1169
|
-
this.users = new UsersService(
|
|
1170
|
-
this.ai = new AiService(
|
|
1748
|
+
this._client = new UtiliaClient(config);
|
|
1749
|
+
this.errors = new ErrorsService(this._client);
|
|
1750
|
+
this.files = new FilesService(this._client);
|
|
1751
|
+
this.tickets = new TicketsService(this._client);
|
|
1752
|
+
this.users = new UsersService(this._client);
|
|
1753
|
+
this.ai = new AiService(this._client);
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Servicio OAuth (solo disponible si se configuro oauth en el constructor)
|
|
1757
|
+
*/
|
|
1758
|
+
get oauth() {
|
|
1759
|
+
return this._client.oauth;
|
|
1171
1760
|
}
|
|
1172
1761
|
};
|
|
1173
1762
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1174
1763
|
0 && (module.exports = {
|
|
1175
1764
|
ErrorCode,
|
|
1765
|
+
FileTokenStorage,
|
|
1766
|
+
MemoryTokenStorage,
|
|
1767
|
+
OAuthService,
|
|
1176
1768
|
SDK_LIMITS,
|
|
1177
1769
|
UtiliaSDK,
|
|
1178
|
-
UtiliaSDKError
|
|
1770
|
+
UtiliaSDKError,
|
|
1771
|
+
base64UrlEncode,
|
|
1772
|
+
generateCodeChallenge,
|
|
1773
|
+
generateCodeVerifier
|
|
1179
1774
|
});
|