@eva_solutions/auth-sdk 0.0.0-stage → 3.0.0-dev-20261107051125
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/CHANGELOG.md +290 -0
- package/LICENSE +21 -0
- package/README.md +481 -2
- package/dist/admin/index.cjs +4 -0
- package/dist/admin/index.d.cts +2 -0
- package/dist/admin/index.d.ts +2 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin-DjvuvuI8.js +408 -0
- package/dist/admin-DjvuvuI8.js.map +1 -0
- package/dist/admin-Q6lbWqLM.cjs +419 -0
- package/dist/admin-Q6lbWqLM.cjs.map +1 -0
- package/dist/api-path-2Ulruz7R.cjs +34 -0
- package/dist/api-path-2Ulruz7R.cjs.map +1 -0
- package/dist/api-path-B-vSkL2H.js +29 -0
- package/dist/api-path-B-vSkL2H.js.map +1 -0
- package/dist/auth-19r-Q6X2.js +84 -0
- package/dist/auth-19r-Q6X2.js.map +1 -0
- package/dist/auth-DX1N5JA4.cjs +131 -0
- package/dist/auth-DX1N5JA4.cjs.map +1 -0
- package/dist/auth-handlers-B3iJaPln.js +343 -0
- package/dist/auth-handlers-B3iJaPln.js.map +1 -0
- package/dist/auth-handlers-BTbXKJms.d.ts +15 -0
- package/dist/auth-handlers-BTbXKJms.d.ts.map +1 -0
- package/dist/auth-handlers-CDzGFDTu.cjs +404 -0
- package/dist/auth-handlers-CDzGFDTu.cjs.map +1 -0
- package/dist/auth-handlers-DRUUTEtW.d.cts +15 -0
- package/dist/auth-handlers-DRUUTEtW.d.cts.map +1 -0
- package/dist/catalog-CHPqldcj.cjs +172 -0
- package/dist/catalog-CHPqldcj.cjs.map +1 -0
- package/dist/catalog-CIqpzgsM.d.cts +75 -0
- package/dist/catalog-CIqpzgsM.d.cts.map +1 -0
- package/dist/catalog-CIqpzgsM.d.ts +75 -0
- package/dist/catalog-CIqpzgsM.d.ts.map +1 -0
- package/dist/catalog-DqPhtCKZ.js +95 -0
- package/dist/catalog-DqPhtCKZ.js.map +1 -0
- package/dist/config-D6Tr7ev6.d.cts +95 -0
- package/dist/config-D6Tr7ev6.d.cts.map +1 -0
- package/dist/config-rl4_-VAq.d.ts +95 -0
- package/dist/config-rl4_-VAq.d.ts.map +1 -0
- package/dist/constants-BE1_9Tod.cjs +52 -0
- package/dist/constants-BE1_9Tod.cjs.map +1 -0
- package/dist/constants-BY7QZokc.js +29 -0
- package/dist/constants-BY7QZokc.js.map +1 -0
- package/dist/constants-CcbhOaAP.cjs +153 -0
- package/dist/constants-CcbhOaAP.cjs.map +1 -0
- package/dist/constants-rNuD5qJn.js +94 -0
- package/dist/constants-rNuD5qJn.js.map +1 -0
- package/dist/contracts/index.cjs +1006 -0
- package/dist/contracts/index.cjs.map +1 -0
- package/dist/contracts/index.d.cts +488 -0
- package/dist/contracts/index.d.cts.map +1 -0
- package/dist/contracts/index.d.ts +488 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +817 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/cookies-DCIcr7uy.cjs +94 -0
- package/dist/cookies-DCIcr7uy.cjs.map +1 -0
- package/dist/cookies-DKEC7AWv.js +71 -0
- package/dist/cookies-DKEC7AWv.js.map +1 -0
- package/dist/delegated-token-CVNbMNfy.js +426 -0
- package/dist/delegated-token-CVNbMNfy.js.map +1 -0
- package/dist/delegated-token-DHSK4rHI.cjs +461 -0
- package/dist/delegated-token-DHSK4rHI.cjs.map +1 -0
- package/dist/error-helpers-BenWu-_u.d.ts +105 -0
- package/dist/error-helpers-BenWu-_u.d.ts.map +1 -0
- package/dist/error-helpers-KBHUbkOM.d.cts +105 -0
- package/dist/error-helpers-KBHUbkOM.d.cts.map +1 -0
- package/dist/error-helpers-XCh4ZJ-g.cjs +282 -0
- package/dist/error-helpers-XCh4ZJ-g.cjs.map +1 -0
- package/dist/error-helpers-lnwgNOw9.js +145 -0
- package/dist/error-helpers-lnwgNOw9.js.map +1 -0
- package/dist/error-messages-CGpW1fsg.d.cts +48 -0
- package/dist/error-messages-CGpW1fsg.d.cts.map +1 -0
- package/dist/error-messages-CGpW1fsg.d.ts +48 -0
- package/dist/error-messages-CGpW1fsg.d.ts.map +1 -0
- package/dist/event-codes-BBJHon-I.cjs +48 -0
- package/dist/event-codes-BBJHon-I.cjs.map +1 -0
- package/dist/event-codes-BgcJWsov.d.cts +42 -0
- package/dist/event-codes-BgcJWsov.d.cts.map +1 -0
- package/dist/event-codes-BgcJWsov.d.ts +42 -0
- package/dist/event-codes-BgcJWsov.d.ts.map +1 -0
- package/dist/event-codes-DR5_VlaR.js +43 -0
- package/dist/event-codes-DR5_VlaR.js.map +1 -0
- package/dist/generic/index.cjs +91 -0
- package/dist/generic/index.cjs.map +1 -0
- package/dist/generic/index.d.cts +27 -0
- package/dist/generic/index.d.cts.map +1 -0
- package/dist/generic/index.d.ts +27 -0
- package/dist/generic/index.d.ts.map +1 -0
- package/dist/generic/index.js +80 -0
- package/dist/generic/index.js.map +1 -0
- package/dist/hono/index.cjs +309 -0
- package/dist/hono/index.cjs.map +1 -0
- package/dist/hono/index.d.cts +272 -0
- package/dist/hono/index.d.cts.map +1 -0
- package/dist/hono/index.d.ts +272 -0
- package/dist/hono/index.d.ts.map +1 -0
- package/dist/hono/index.js +290 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/hono-openapi/index.cjs +326 -0
- package/dist/hono-openapi/index.cjs.map +1 -0
- package/dist/hono-openapi/index.d.cts +14 -0
- package/dist/hono-openapi/index.d.cts.map +1 -0
- package/dist/hono-openapi/index.d.ts +14 -0
- package/dist/hono-openapi/index.d.ts.map +1 -0
- package/dist/hono-openapi/index.js +307 -0
- package/dist/hono-openapi/index.js.map +1 -0
- package/dist/http-client-BVF3iktR.cjs +211 -0
- package/dist/http-client-BVF3iktR.cjs.map +1 -0
- package/dist/http-client-Da0-OMfc.js +206 -0
- package/dist/http-client-Da0-OMfc.js.map +1 -0
- package/dist/index-BYzDaMR9.d.ts +312 -0
- package/dist/index-BYzDaMR9.d.ts.map +1 -0
- package/dist/index-CNNMCrdl.d.ts +66 -0
- package/dist/index-CNNMCrdl.d.ts.map +1 -0
- package/dist/index-DBZoLU4E.d.ts +240 -0
- package/dist/index-DBZoLU4E.d.ts.map +1 -0
- package/dist/index-DcvQWByo.d.cts +66 -0
- package/dist/index-DcvQWByo.d.cts.map +1 -0
- package/dist/index-Dold-Ycb.d.cts +1711 -0
- package/dist/index-Dold-Ycb.d.cts.map +1 -0
- package/dist/index-Dold-Ycb.d.ts +1711 -0
- package/dist/index-Dold-Ycb.d.ts.map +1 -0
- package/dist/index-jAO62-OD.d.cts +240 -0
- package/dist/index-jAO62-OD.d.cts.map +1 -0
- package/dist/index-o7nXcl7z.d.cts +312 -0
- package/dist/index-o7nXcl7z.d.cts.map +1 -0
- package/dist/index.cjs +89 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +114 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt-BBNqk0TV.cjs +155 -0
- package/dist/jwt-BBNqk0TV.cjs.map +1 -0
- package/dist/jwt-DhqgH94B.js +150 -0
- package/dist/jwt-DhqgH94B.js.map +1 -0
- package/dist/key-resolver-CESqj4-P.cjs +45 -0
- package/dist/key-resolver-CESqj4-P.cjs.map +1 -0
- package/dist/key-resolver-D56I2Zpz.js +34 -0
- package/dist/key-resolver-D56I2Zpz.js.map +1 -0
- package/dist/parse-error-response-6qOQS5y9.cjs +316 -0
- package/dist/parse-error-response-6qOQS5y9.cjs.map +1 -0
- package/dist/parse-error-response-BM9-kjs0.js +239 -0
- package/dist/parse-error-response-BM9-kjs0.js.map +1 -0
- package/dist/path-segment-C25i6aaH.cjs +29 -0
- package/dist/path-segment-C25i6aaH.cjs.map +1 -0
- package/dist/path-segment-CyqnaFrP.js +24 -0
- package/dist/path-segment-CyqnaFrP.js.map +1 -0
- package/dist/react/index.cjs +336 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +151 -0
- package/dist/react/index.d.cts.map +1 -0
- package/dist/react/index.d.ts +151 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +300 -0
- package/dist/react/index.js.map +1 -0
- package/dist/refresh-dedup-CKaHHe7m.js +14 -0
- package/dist/refresh-dedup-CKaHHe7m.js.map +1 -0
- package/dist/refresh-dedup-CLdACQXR.cjs +19 -0
- package/dist/refresh-dedup-CLdACQXR.cjs.map +1 -0
- package/dist/rolldown-runtime-CmQyd0Gu.cjs +27 -0
- package/dist/routes-DpAzgY1V.cjs +424 -0
- package/dist/routes-DpAzgY1V.cjs.map +1 -0
- package/dist/routes-F1g5aVqP.js +347 -0
- package/dist/routes-F1g5aVqP.js.map +1 -0
- package/dist/s2s/index.cjs +24 -0
- package/dist/s2s/index.d.cts +5 -0
- package/dist/s2s/index.d.ts +5 -0
- package/dist/s2s/index.js +5 -0
- package/dist/s2s-6wbotr78.cjs +926 -0
- package/dist/s2s-6wbotr78.cjs.map +1 -0
- package/dist/s2s-B_0SIQzn.js +897 -0
- package/dist/s2s-B_0SIQzn.js.map +1 -0
- package/dist/sign-C7y6xdvT.cjs +324 -0
- package/dist/sign-C7y6xdvT.cjs.map +1 -0
- package/dist/sign-HFtcYwa-.js +259 -0
- package/dist/sign-HFtcYwa-.js.map +1 -0
- package/dist/tokens-C1nzkn21.cjs +232 -0
- package/dist/tokens-C1nzkn21.cjs.map +1 -0
- package/dist/tokens-C3Z40qd7.js +107 -0
- package/dist/tokens-C3Z40qd7.js.map +1 -0
- package/dist/types-B5sf7kKE.d.cts +52 -0
- package/dist/types-B5sf7kKE.d.cts.map +1 -0
- package/dist/types-CLOIHzRu.d.ts +96 -0
- package/dist/types-CLOIHzRu.d.ts.map +1 -0
- package/dist/types-CQTZdZIF.d.ts +52 -0
- package/dist/types-CQTZdZIF.d.ts.map +1 -0
- package/dist/types-DGeQ8j4k.d.cts +538 -0
- package/dist/types-DGeQ8j4k.d.cts.map +1 -0
- package/dist/types-DIUWza9-.d.ts +538 -0
- package/dist/types-DIUWza9-.d.ts.map +1 -0
- package/dist/types-qh6ANBCV.d.cts +96 -0
- package/dist/types-qh6ANBCV.d.cts.map +1 -0
- package/dist/webhooks/index.cjs +8 -0
- package/dist/webhooks/index.d.cts +5 -0
- package/dist/webhooks/index.d.ts +5 -0
- package/dist/webhooks/index.js +4 -0
- package/dist/webhooks-B5sAWC9W.js +74 -0
- package/dist/webhooks-B5sAWC9W.js.map +1 -0
- package/dist/webhooks-Cx11psg1.cjs +79 -0
- package/dist/webhooks-Cx11psg1.cjs.map +1 -0
- package/package.json +192 -3
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import { a as INTERNAL_AUTORIZACION_ROUTES, c as INTERNAL_TOKEN_EXCHANGE_ROUTES, i as DIRECTORIO_ROUTES, l as INTERNAL_TOKEN_ROUTES, o as INTERNAL_EMPRESA_ROUTES, p as WEBHOOK_ROUTES, s as INTERNAL_GRUPO_ROUTES, u as INTERNAL_USER_ROUTES } from "./routes-F1g5aVqP.js";
|
|
2
|
+
import { _ as TOKEN_EXCHANGE_GRANT_TYPE, f as SUBJECT_TOKEN_HEADER, p as SUBJECT_TOKEN_INVALID_CODE, t as ACCESS_TOKEN_TYPE } from "./tokens-C3Z40qd7.js";
|
|
3
|
+
import "./constants-rNuD5qJn.js";
|
|
4
|
+
import { t as apiPath } from "./api-path-B-vSkL2H.js";
|
|
5
|
+
import { t as parseErrorResponse, u as validateHttpsUrl } from "./parse-error-response-BM9-kjs0.js";
|
|
6
|
+
import { t as encodePathSegment } from "./path-segment-CyqnaFrP.js";
|
|
7
|
+
import { i as signS2SHeaders, s as AUTORIZACION_CACHE_TTL_MS } from "./sign-HFtcYwa-.js";
|
|
8
|
+
import { f as isScopeOfAudience, l as isDelegableAudience, p as isServiceAudience, u as isDelegableScope } from "./catalog-DqPhtCKZ.js";
|
|
9
|
+
// src/s2s/token-provider.ts
|
|
10
|
+
/**
|
|
11
|
+
* Proveedor de tokens de servicio (client credentials) para un servicio que llama a otro.
|
|
12
|
+
*
|
|
13
|
+
* Pide el token a `POST /api/v1/internal/token` autenticándose con la firma HMAC del service client
|
|
14
|
+
* (la única ruta servicio -> servicio que la conserva), lo guarda en memoria y lo entrega a cada
|
|
15
|
+
* llamada como `Authorization: Bearer`. El caché vive en la instancia: crea UN proveedor por
|
|
16
|
+
* (audiencia, scope) y compártelo.
|
|
17
|
+
*/
|
|
18
|
+
/** Cuánto antes del vencimiento se renueva el token, en milisegundos. */
|
|
19
|
+
const SERVICE_TOKEN_RENEW_MARGIN_MS = 3e4;
|
|
20
|
+
const REQUEST_TIMEOUT_MS = 1e4;
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
/** El Auth Service rechazó el pedido de token. `code` es el `error` de RFC 6749 (`invalid_client`, …). */
|
|
23
|
+
var ServiceTokenRequestError = class extends Error {
|
|
24
|
+
code;
|
|
25
|
+
status;
|
|
26
|
+
/** Segundos de espera que pidió Auth con `Retry-After` (429 / 503), si los mandó. */
|
|
27
|
+
retryAfterSeconds;
|
|
28
|
+
constructor(code, status, description, retryAfterSeconds) {
|
|
29
|
+
super(description ?? code);
|
|
30
|
+
this.name = "ServiceTokenRequestError";
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.status = status;
|
|
33
|
+
if (retryAfterSeconds !== void 0) this.retryAfterSeconds = retryAfterSeconds;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
async function requestToken(config, url, body) {
|
|
37
|
+
const requestedAtMs = Date.now();
|
|
38
|
+
const headers = await signS2SHeaders({
|
|
39
|
+
method: "POST",
|
|
40
|
+
path: url.pathname,
|
|
41
|
+
rawQuery: "",
|
|
42
|
+
bodyBytes: encoder.encode(body),
|
|
43
|
+
clientId: config.clientId,
|
|
44
|
+
secretHex: config.secretHex
|
|
45
|
+
});
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"content-type": "application/json",
|
|
50
|
+
...headers
|
|
51
|
+
},
|
|
52
|
+
body,
|
|
53
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
54
|
+
});
|
|
55
|
+
const json = await res.json().catch(() => null);
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const code = typeof json?.error === "string" ? json.error : res.status === 429 ? "temporarily_unavailable" : "server_error";
|
|
58
|
+
const retryAfterHeader = res.headers?.get("retry-after")?.trim();
|
|
59
|
+
const retryAfter = retryAfterHeader !== void 0 && /^\d+$/.test(retryAfterHeader) ? Number(retryAfterHeader) : void 0;
|
|
60
|
+
const description = typeof json?.error_description === "string" ? json.error_description : void 0;
|
|
61
|
+
throw new ServiceTokenRequestError(code, res.status, description, retryAfter);
|
|
62
|
+
}
|
|
63
|
+
const token = json?.access_token;
|
|
64
|
+
const expiresIn = json?.expires_in;
|
|
65
|
+
if (typeof token !== "string" || token === "" || typeof expiresIn !== "number" || !(expiresIn > 0)) throw new ServiceTokenRequestError("server_error", res.status, "Respuesta de token inválida");
|
|
66
|
+
return {
|
|
67
|
+
token,
|
|
68
|
+
renewAtMs: requestedAtMs + expiresIn * 1e3 - SERVICE_TOKEN_RENEW_MARGIN_MS
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Entries the exchange cache keeps per provider before it drops expired ones and then the oldest. */
|
|
72
|
+
const EXCHANGE_CACHE_MAX_ENTRIES = 500;
|
|
73
|
+
const malformed = (message) => ({
|
|
74
|
+
ok: false,
|
|
75
|
+
error: {
|
|
76
|
+
kind: "sdk",
|
|
77
|
+
reason: "malformed",
|
|
78
|
+
message,
|
|
79
|
+
status: 0
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
/** SHA-256 of the subject token as hex: the cache never keys on claims nobody verified here. */
|
|
83
|
+
async function fingerprint(value) {
|
|
84
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value));
|
|
85
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
86
|
+
}
|
|
87
|
+
/** Validates the exchange input locally (a bad audience or scope is a sure `invalid_target` / `invalid_scope`). */
|
|
88
|
+
function validateExchangeInput(input) {
|
|
89
|
+
if (typeof input.subjectToken !== "string" || input.subjectToken === "") return malformed("exchangeToken: subjectToken requerido");
|
|
90
|
+
if (!isDelegableAudience(input.audience)) return malformed(`exchangeToken: "${String(input.audience)}" no es una audiencia delegable del catálogo (auth-api nunca lo es)`);
|
|
91
|
+
for (const scope of input.scope?.split(" ").filter(Boolean) ?? []) if (!isDelegableScope(input.audience, scope)) return malformed(`exchangeToken: "${scope}" no es un permiso delegable de la audiencia "${input.audience}"`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function postExchange(url, bearer, input) {
|
|
95
|
+
return fetch(url, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
"content-type": "application/json",
|
|
99
|
+
authorization: `Bearer ${bearer}`
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
grant_type: TOKEN_EXCHANGE_GRANT_TYPE,
|
|
103
|
+
subject_token: input.subjectToken,
|
|
104
|
+
subject_token_type: ACCESS_TOKEN_TYPE,
|
|
105
|
+
audience: input.audience,
|
|
106
|
+
...input.scope !== void 0 && { scope: input.scope }
|
|
107
|
+
}),
|
|
108
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Crea un proveedor de tokens de servicio.
|
|
113
|
+
*
|
|
114
|
+
* Deduplica los pedidos simultáneos dentro del mismo proceso (isolate): N llamadas concurrentes a
|
|
115
|
+
* `getToken()` con el caché vacío o vencido hacen UN pedido al Auth Service. Un pedido fallido no
|
|
116
|
+
* se guarda: la siguiente llamada reintenta.
|
|
117
|
+
*
|
|
118
|
+
* Valida su configuración al crearse (audiencia del catálogo, permisos de esa audiencia, clave hex).
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const provider = createServiceTokenProvider({
|
|
123
|
+
* authUrl: env.AUTH_URL,
|
|
124
|
+
* clientId: env.AUTH_CLIENT_ID,
|
|
125
|
+
* secretHex: env.AUTH_CLIENT_SECRET_HEX,
|
|
126
|
+
* audience: 'auth-api',
|
|
127
|
+
* })
|
|
128
|
+
* const s2s = createS2SClient({ baseUrl: env.AUTH_URL, tokenProvider: provider })
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
function createServiceTokenProvider(config) {
|
|
132
|
+
validateHttpsUrl(config.authUrl, "ServiceTokenProviderConfig.authUrl");
|
|
133
|
+
if (!isServiceAudience(config.audience)) throw new TypeError(`[eva-auth-sdk] createServiceTokenProvider: audience "${config.audience}" no es una audiencia de servicio del catálogo`);
|
|
134
|
+
for (const scope of config.scope?.split(" ").filter(Boolean) ?? []) if (!isScopeOfAudience(config.audience, scope)) throw new TypeError(`[eva-auth-sdk] createServiceTokenProvider: "${scope}" no es un permiso de la audiencia "${config.audience}"`);
|
|
135
|
+
if (!/^(?:[0-9a-fA-F]{2})+$/.test(config.secretHex)) throw new TypeError("[eva-auth-sdk] createServiceTokenProvider: secretHex inválido (formato hex requerido)");
|
|
136
|
+
const url = new URL(`${config.authUrl.replace(/\/$/, "")}${apiPath(INTERNAL_TOKEN_ROUTES.issue)}`);
|
|
137
|
+
const body = JSON.stringify({
|
|
138
|
+
audience: config.audience,
|
|
139
|
+
...config.scope !== void 0 && { scope: config.scope }
|
|
140
|
+
});
|
|
141
|
+
let cached;
|
|
142
|
+
let inFlight;
|
|
143
|
+
const exchangeUrl = new URL(`${config.authUrl.replace(/\/$/, "")}${apiPath(INTERNAL_TOKEN_EXCHANGE_ROUTES.exchange)}`);
|
|
144
|
+
let exchangeBearer;
|
|
145
|
+
const exchangeCache = /* @__PURE__ */ new Map();
|
|
146
|
+
const rememberGrant = (key, entry) => {
|
|
147
|
+
if (exchangeCache.size >= EXCHANGE_CACHE_MAX_ENTRIES) {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
for (const [k, v] of exchangeCache) if (now >= v.renewAtMs) exchangeCache.delete(k);
|
|
150
|
+
if (exchangeCache.size >= EXCHANGE_CACHE_MAX_ENTRIES) {
|
|
151
|
+
const oldest = exchangeCache.keys().next().value;
|
|
152
|
+
if (oldest !== void 0) exchangeCache.delete(oldest);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exchangeCache.set(key, entry);
|
|
156
|
+
};
|
|
157
|
+
const exchangeInFlight = /* @__PURE__ */ new Map();
|
|
158
|
+
const requestExchange = async (input, key) => {
|
|
159
|
+
try {
|
|
160
|
+
exchangeBearer ??= createServiceTokenProvider({
|
|
161
|
+
authUrl: config.authUrl,
|
|
162
|
+
clientId: config.clientId,
|
|
163
|
+
secretHex: config.secretHex,
|
|
164
|
+
audience: "auth-api",
|
|
165
|
+
scope: "tokens:exchange"
|
|
166
|
+
});
|
|
167
|
+
const requestedAtMs = Date.now();
|
|
168
|
+
let bearer = await exchangeBearer.getToken();
|
|
169
|
+
let res = await postExchange(exchangeUrl, bearer, input);
|
|
170
|
+
if (res.status === 401) {
|
|
171
|
+
exchangeBearer.invalidate(bearer);
|
|
172
|
+
bearer = await exchangeBearer.getToken();
|
|
173
|
+
res = await postExchange(exchangeUrl, bearer, input);
|
|
174
|
+
}
|
|
175
|
+
const json = await res.json().catch(() => null);
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const code = typeof json?.error === "string" ? json.error : res.status === 429 ? "temporarily_unavailable" : "server_error";
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
error: {
|
|
181
|
+
kind: "api",
|
|
182
|
+
code,
|
|
183
|
+
message: typeof json?.error_description === "string" ? json.error_description : code,
|
|
184
|
+
status: res.status
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const accessToken = json?.access_token;
|
|
189
|
+
const expiresIn = json?.expires_in;
|
|
190
|
+
const scope = json?.scope;
|
|
191
|
+
if (typeof accessToken !== "string" || accessToken === "" || typeof expiresIn !== "number" || !(expiresIn > 0) || typeof scope !== "string" || json?.issued_token_type !== "urn:ietf:params:oauth:token-type:access_token" || json?.token_type !== "Bearer") return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: {
|
|
194
|
+
kind: "api",
|
|
195
|
+
code: "server_error",
|
|
196
|
+
message: "Respuesta de token exchange inválida",
|
|
197
|
+
status: res.status
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const grant = {
|
|
201
|
+
accessToken,
|
|
202
|
+
scope
|
|
203
|
+
};
|
|
204
|
+
rememberGrant(key, {
|
|
205
|
+
grant,
|
|
206
|
+
renewAtMs: requestedAtMs + expiresIn * 1e3 - SERVICE_TOKEN_RENEW_MARGIN_MS
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
data: grant
|
|
211
|
+
};
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (err instanceof ServiceTokenRequestError) return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: {
|
|
216
|
+
kind: "api",
|
|
217
|
+
code: err.code,
|
|
218
|
+
message: err.message,
|
|
219
|
+
status: err.status
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
error: {
|
|
225
|
+
kind: "sdk",
|
|
226
|
+
reason: "network",
|
|
227
|
+
message: err instanceof Error ? err.message : "Network error",
|
|
228
|
+
status: 0
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const exchangeToken = async (input) => {
|
|
234
|
+
const invalid = validateExchangeInput(input);
|
|
235
|
+
if (invalid !== null) return invalid;
|
|
236
|
+
const scopeKey = (input.scope ?? "").split(" ").filter(Boolean).sort().join(" ");
|
|
237
|
+
const key = `${await fingerprint(input.subjectToken)}|${input.audience}|${scopeKey}`;
|
|
238
|
+
const hit = exchangeCache.get(key);
|
|
239
|
+
if (hit !== void 0) {
|
|
240
|
+
if (Date.now() < hit.renewAtMs) return {
|
|
241
|
+
ok: true,
|
|
242
|
+
data: hit.grant
|
|
243
|
+
};
|
|
244
|
+
exchangeCache.delete(key);
|
|
245
|
+
}
|
|
246
|
+
const pending = exchangeInFlight.get(key);
|
|
247
|
+
if (pending !== void 0) return new Promise((resolve, reject) => {
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
if (exchangeInFlight.get(key) === pending) exchangeInFlight.delete(key);
|
|
250
|
+
exchangeToken(input).then(resolve, reject);
|
|
251
|
+
}, 11e3);
|
|
252
|
+
pending.then((result) => {
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
resolve(result);
|
|
255
|
+
}, (err) => {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
reject(err);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
const started = requestExchange(input, key).finally(() => {
|
|
261
|
+
if (exchangeInFlight.get(key) === started) exchangeInFlight.delete(key);
|
|
262
|
+
});
|
|
263
|
+
exchangeInFlight.set(key, started);
|
|
264
|
+
return started;
|
|
265
|
+
};
|
|
266
|
+
const joinWithDeadline = (pending, retry) => new Promise((resolve, reject) => {
|
|
267
|
+
const timer = setTimeout(() => {
|
|
268
|
+
if (inFlight === pending) inFlight = void 0;
|
|
269
|
+
retry().then(resolve, reject);
|
|
270
|
+
}, 11e3);
|
|
271
|
+
pending.then((token) => {
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
resolve(token);
|
|
274
|
+
}, (err) => {
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
reject(err);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
const provider = {
|
|
280
|
+
getToken() {
|
|
281
|
+
if (cached !== void 0 && Date.now() < cached.renewAtMs) return Promise.resolve(cached.token);
|
|
282
|
+
if (inFlight !== void 0) return joinWithDeadline(inFlight, () => provider.getToken());
|
|
283
|
+
const pending = requestToken(config, url, body).then((fresh) => {
|
|
284
|
+
cached = fresh;
|
|
285
|
+
return fresh.token;
|
|
286
|
+
}).finally(() => {
|
|
287
|
+
if (inFlight === pending) inFlight = void 0;
|
|
288
|
+
});
|
|
289
|
+
inFlight = pending;
|
|
290
|
+
return pending;
|
|
291
|
+
},
|
|
292
|
+
invalidate(rejectedToken) {
|
|
293
|
+
if (rejectedToken === void 0 || cached?.token === rejectedToken) cached = void 0;
|
|
294
|
+
},
|
|
295
|
+
exchangeToken
|
|
296
|
+
};
|
|
297
|
+
return provider;
|
|
298
|
+
}
|
|
299
|
+
// src/s2s/client.ts
|
|
300
|
+
/**
|
|
301
|
+
* Cliente S2S (service-to-service) para llamar a la API interna del Auth Service desde servicios
|
|
302
|
+
* backend. Autentica cada llamada con `Authorization: Bearer <token de servicio>` (audiencia
|
|
303
|
+
* `auth-api`) que entrega un `ServiceTokenProvider`; ante un 401 invalida el token y reintenta UNA vez.
|
|
304
|
+
*
|
|
305
|
+
* REQ-S2S-01, REQ-S2S-04, REQ-INT-01, REQ-INT-02 LOCKED.
|
|
306
|
+
*
|
|
307
|
+
* Endpoints disponibles:
|
|
308
|
+
* GET /internal/users/:id
|
|
309
|
+
* POST /internal/users/batch
|
|
310
|
+
* POST /internal/webhooks/subscriptions
|
|
311
|
+
* GET /internal/webhooks/subscriptions
|
|
312
|
+
* GET /internal/webhooks/subscriptions/:id
|
|
313
|
+
* PATCH /internal/webhooks/subscriptions/:id
|
|
314
|
+
* DELETE /internal/webhooks/subscriptions/:id
|
|
315
|
+
* POST /internal/webhooks/subscriptions/:id/rotate-secret
|
|
316
|
+
* GET /internal/webhooks/deliveries
|
|
317
|
+
* POST /internal/empresas
|
|
318
|
+
* GET /internal/empresas/:id
|
|
319
|
+
* POST /internal/empresas/:id/miembros
|
|
320
|
+
* DELETE /internal/empresas/:id/miembros/:userId
|
|
321
|
+
* POST /internal/empresas/:id/sucursales
|
|
322
|
+
* (fase 2) PATCH /internal/empresas/:id, GET /internal/empresas, POST .../baja|reactivar,
|
|
323
|
+
* PATCH .../miembros/:userId, PATCH|DELETE .../sucursales/:sucursalId, contratación
|
|
324
|
+
* (.../productos), grupos (/internal/grupos), directorio (/directorio) y autorizaciones.
|
|
325
|
+
*
|
|
326
|
+
* Los paths salen de `INTERNAL_USER_ROUTES`, `WEBHOOK_ROUTES` e `INTERNAL_EMPRESA_ROUTES`
|
|
327
|
+
* (contrato compartido) vía `apiPath`.
|
|
328
|
+
*/
|
|
329
|
+
/**
|
|
330
|
+
* Construye el query string a partir de un objeto de parámetros.
|
|
331
|
+
* Omite valores undefined.
|
|
332
|
+
*/
|
|
333
|
+
function buildQueryString(params) {
|
|
334
|
+
const parts = [];
|
|
335
|
+
for (const [key, value] of Object.entries(params)) if (value !== void 0) parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
336
|
+
return parts.join("&");
|
|
337
|
+
}
|
|
338
|
+
function invalidIdResult(method) {
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
error: {
|
|
342
|
+
kind: "sdk",
|
|
343
|
+
reason: "malformed",
|
|
344
|
+
message: `${method}: id inválido (no puede ser vacío, "." ni "..")`,
|
|
345
|
+
status: 0
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Codifica cada id como UN segmento del path (`encodePathSegment`). Devuelve los segmentos listos
|
|
351
|
+
* para `apiPath` o, si alguno es vacío, `.` o `..`, el `Result` de error (sin hacer la request).
|
|
352
|
+
*/
|
|
353
|
+
function encodeSegments(method, values) {
|
|
354
|
+
const out = {};
|
|
355
|
+
for (const key of Object.keys(values)) {
|
|
356
|
+
const segment = encodePathSegment(values[key]);
|
|
357
|
+
if (segment === null) return invalidIdResult(method);
|
|
358
|
+
out[key] = segment;
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
/** ¿El cuerpo de error es el fallo del token de la persona (`subject_token_invalid`)? */
|
|
363
|
+
const isSubjectTokenFailure = (body) => body?.error?.code === SUBJECT_TOKEN_INVALID_CODE;
|
|
364
|
+
const isInvalid = (value) => "ok" in value;
|
|
365
|
+
/** Entradas que guarda el caché de `checkAutorizacion` antes de soltar las vencidas y luego la más vieja. */
|
|
366
|
+
const AUTORIZACION_CACHE_MAX_ENTRIES = 1e3;
|
|
367
|
+
/**
|
|
368
|
+
* Realiza un fetch autenticado con el token de servicio del proveedor. Un 401 invalida el token y
|
|
369
|
+
* reintenta una sola vez con uno nuevo. Retorna `Result<T>` con la propiedad `data` del body.
|
|
370
|
+
*/
|
|
371
|
+
async function s2sFetch({ config, method, pathname, rawQuery = "", body, headers: extraHeaders }) {
|
|
372
|
+
try {
|
|
373
|
+
const bodyText = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
374
|
+
const url = rawQuery ? `${config.baseUrl}${pathname}?${rawQuery}` : `${config.baseUrl}${pathname}`;
|
|
375
|
+
let usedToken = "";
|
|
376
|
+
const send = async () => {
|
|
377
|
+
const token = await config.tokenProvider.getToken();
|
|
378
|
+
usedToken = token;
|
|
379
|
+
return fetch(url, {
|
|
380
|
+
method,
|
|
381
|
+
headers: {
|
|
382
|
+
"content-type": "application/json",
|
|
383
|
+
...extraHeaders,
|
|
384
|
+
authorization: `Bearer ${token}`
|
|
385
|
+
},
|
|
386
|
+
body: bodyText,
|
|
387
|
+
signal: AbortSignal.timeout(1e4)
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
let res = await send();
|
|
391
|
+
if (res.status === 401) {
|
|
392
|
+
const body = await res.json().catch(() => null);
|
|
393
|
+
if (isSubjectTokenFailure(body)) return {
|
|
394
|
+
ok: false,
|
|
395
|
+
error: parseErrorResponse(res.status, body)
|
|
396
|
+
};
|
|
397
|
+
config.tokenProvider.invalidate(usedToken);
|
|
398
|
+
res = await send();
|
|
399
|
+
}
|
|
400
|
+
if (!res.ok) {
|
|
401
|
+
const responseBody = await res.json().catch(() => null);
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
error: parseErrorResponse(res.status, responseBody)
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (res.status === 204) return {
|
|
408
|
+
ok: true,
|
|
409
|
+
data: void 0
|
|
410
|
+
};
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
data: (await res.json()).data
|
|
414
|
+
};
|
|
415
|
+
} catch (err) {
|
|
416
|
+
if (err instanceof ServiceTokenRequestError) return {
|
|
417
|
+
ok: false,
|
|
418
|
+
error: {
|
|
419
|
+
kind: "api",
|
|
420
|
+
code: err.code,
|
|
421
|
+
message: err.message,
|
|
422
|
+
status: err.status
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
error: {
|
|
428
|
+
kind: "sdk",
|
|
429
|
+
reason: "network",
|
|
430
|
+
message: err instanceof Error ? err.message : "Network error",
|
|
431
|
+
status: 0
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Crea un cliente S2S autenticado para comunicarse con la API interna del Auth Service.
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```ts
|
|
441
|
+
* const tokenProvider = createServiceTokenProvider({
|
|
442
|
+
* authUrl: 'https://auth.miapp.com',
|
|
443
|
+
* clientId: 'mi-servicio',
|
|
444
|
+
* secretHex: env.AUTH_CLIENT_SECRET_HEX,
|
|
445
|
+
* audience: 'auth-api',
|
|
446
|
+
* })
|
|
447
|
+
* const s2s = createS2SClient({ baseUrl: 'https://auth.miapp.com', tokenProvider })
|
|
448
|
+
* const result = await s2s.getUser(userId)
|
|
449
|
+
* if (!result.ok) return handleError(result.error)
|
|
450
|
+
* return result.data // perfil del usuario
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
function createS2SClient(config) {
|
|
454
|
+
validateHttpsUrl(config.baseUrl, "S2SClientConfig.baseUrl");
|
|
455
|
+
const autorizacionCache = /* @__PURE__ */ new Map();
|
|
456
|
+
return {
|
|
457
|
+
async getUser(userId) {
|
|
458
|
+
const segment = encodePathSegment(userId);
|
|
459
|
+
if (segment === null) return invalidIdResult("getUser");
|
|
460
|
+
return s2sFetch({
|
|
461
|
+
config,
|
|
462
|
+
method: "GET",
|
|
463
|
+
pathname: apiPath(INTERNAL_USER_ROUTES.get, { id: segment })
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
/**
|
|
467
|
+
* Consulta múltiples usuarios por ID en una sola llamada.
|
|
468
|
+
*
|
|
469
|
+
* @param input - Input con la lista de IDs a consultar.
|
|
470
|
+
* @param input.ids - Máximo `USERS_BATCH_MAX_IDS` (100, constante del contrato compartido)
|
|
471
|
+
* IDs por llamada. El schema del request no lleva ese tope: lo aplica este cliente.
|
|
472
|
+
* Si se superan, retorna error sin hacer fetch al API.
|
|
473
|
+
*/
|
|
474
|
+
async batchUsers(input) {
|
|
475
|
+
if (input.ids.length > 100) return {
|
|
476
|
+
ok: false,
|
|
477
|
+
error: {
|
|
478
|
+
kind: "sdk",
|
|
479
|
+
reason: "malformed",
|
|
480
|
+
message: `batchUsers acepta máximo 100 IDs por llamada (recibidos: ${input.ids.length})`,
|
|
481
|
+
status: 0
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
return s2sFetch({
|
|
485
|
+
config,
|
|
486
|
+
method: "POST",
|
|
487
|
+
pathname: apiPath(INTERNAL_USER_ROUTES.batch),
|
|
488
|
+
body: input
|
|
489
|
+
});
|
|
490
|
+
},
|
|
491
|
+
async createWebhookSubscription(input) {
|
|
492
|
+
return s2sFetch({
|
|
493
|
+
config,
|
|
494
|
+
method: "POST",
|
|
495
|
+
pathname: apiPath(WEBHOOK_ROUTES.createSubscription),
|
|
496
|
+
body: input
|
|
497
|
+
});
|
|
498
|
+
},
|
|
499
|
+
async listWebhookSubscriptions() {
|
|
500
|
+
return s2sFetch({
|
|
501
|
+
config,
|
|
502
|
+
method: "GET",
|
|
503
|
+
pathname: apiPath(WEBHOOK_ROUTES.listSubscriptions)
|
|
504
|
+
});
|
|
505
|
+
},
|
|
506
|
+
async getWebhookSubscription(id) {
|
|
507
|
+
const segment = encodePathSegment(id);
|
|
508
|
+
if (segment === null) return invalidIdResult("getWebhookSubscription");
|
|
509
|
+
return s2sFetch({
|
|
510
|
+
config,
|
|
511
|
+
method: "GET",
|
|
512
|
+
pathname: apiPath(WEBHOOK_ROUTES.getSubscription, { id: segment })
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
async updateWebhookSubscription(id, input) {
|
|
516
|
+
const segment = encodePathSegment(id);
|
|
517
|
+
if (segment === null) return invalidIdResult("updateWebhookSubscription");
|
|
518
|
+
return s2sFetch({
|
|
519
|
+
config,
|
|
520
|
+
method: "PATCH",
|
|
521
|
+
pathname: apiPath(WEBHOOK_ROUTES.updateSubscription, { id: segment }),
|
|
522
|
+
body: input
|
|
523
|
+
});
|
|
524
|
+
},
|
|
525
|
+
async deleteWebhookSubscription(id) {
|
|
526
|
+
const segment = encodePathSegment(id);
|
|
527
|
+
if (segment === null) return invalidIdResult("deleteWebhookSubscription");
|
|
528
|
+
return s2sFetch({
|
|
529
|
+
config,
|
|
530
|
+
method: "DELETE",
|
|
531
|
+
pathname: apiPath(WEBHOOK_ROUTES.deleteSubscription, { id: segment })
|
|
532
|
+
});
|
|
533
|
+
},
|
|
534
|
+
async rotateWebhookSubscriptionSecret(id) {
|
|
535
|
+
const segment = encodePathSegment(id);
|
|
536
|
+
if (segment === null) return invalidIdResult("rotateWebhookSubscriptionSecret");
|
|
537
|
+
return s2sFetch({
|
|
538
|
+
config,
|
|
539
|
+
method: "POST",
|
|
540
|
+
pathname: apiPath(WEBHOOK_ROUTES.rotateSubscriptionSecret, { id: segment })
|
|
541
|
+
});
|
|
542
|
+
},
|
|
543
|
+
async listWebhookDeliveries(query) {
|
|
544
|
+
if (query?.limit !== void 0 && (query.limit < 1 || query.limit > 100)) return {
|
|
545
|
+
ok: false,
|
|
546
|
+
error: {
|
|
547
|
+
kind: "sdk",
|
|
548
|
+
reason: "malformed",
|
|
549
|
+
message: `listWebhookDeliveries: limit debe estar entre 1 y 100 (recibido: ${query.limit})`,
|
|
550
|
+
status: 0
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
if (query?.offset !== void 0 && query.offset < 0) return {
|
|
554
|
+
ok: false,
|
|
555
|
+
error: {
|
|
556
|
+
kind: "sdk",
|
|
557
|
+
reason: "malformed",
|
|
558
|
+
message: `listWebhookDeliveries: offset no puede ser negativo (recibido: ${query.offset})`,
|
|
559
|
+
status: 0
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
const rawQuery = query ? buildQueryString(query) : "";
|
|
563
|
+
return s2sFetch({
|
|
564
|
+
config,
|
|
565
|
+
method: "GET",
|
|
566
|
+
pathname: apiPath(WEBHOOK_ROUTES.listDeliveries),
|
|
567
|
+
rawQuery
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
async createEmpresa(input) {
|
|
571
|
+
return s2sFetch({
|
|
572
|
+
config,
|
|
573
|
+
method: "POST",
|
|
574
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.create),
|
|
575
|
+
body: input
|
|
576
|
+
});
|
|
577
|
+
},
|
|
578
|
+
async updateEmpresa(empresaId, input) {
|
|
579
|
+
const seg = encodeSegments("updateEmpresa", { id: empresaId });
|
|
580
|
+
if (isInvalid(seg)) return seg;
|
|
581
|
+
return s2sFetch({
|
|
582
|
+
config,
|
|
583
|
+
method: "PATCH",
|
|
584
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.update, seg),
|
|
585
|
+
body: input
|
|
586
|
+
});
|
|
587
|
+
},
|
|
588
|
+
async listEmpresas(query) {
|
|
589
|
+
return s2sFetch({
|
|
590
|
+
config,
|
|
591
|
+
method: "GET",
|
|
592
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.list),
|
|
593
|
+
rawQuery: query ? buildQueryString({ ...query }) : ""
|
|
594
|
+
});
|
|
595
|
+
},
|
|
596
|
+
async getEmpresa(empresaId) {
|
|
597
|
+
const seg = encodeSegments("getEmpresa", { id: empresaId });
|
|
598
|
+
if (isInvalid(seg)) return seg;
|
|
599
|
+
return s2sFetch({
|
|
600
|
+
config,
|
|
601
|
+
method: "GET",
|
|
602
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.get, seg)
|
|
603
|
+
});
|
|
604
|
+
},
|
|
605
|
+
async bajaEmpresa(empresaId) {
|
|
606
|
+
const seg = encodeSegments("bajaEmpresa", { id: empresaId });
|
|
607
|
+
if (isInvalid(seg)) return seg;
|
|
608
|
+
return s2sFetch({
|
|
609
|
+
config,
|
|
610
|
+
method: "POST",
|
|
611
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.baja, seg)
|
|
612
|
+
});
|
|
613
|
+
},
|
|
614
|
+
async reactivarEmpresa(empresaId) {
|
|
615
|
+
const seg = encodeSegments("reactivarEmpresa", { id: empresaId });
|
|
616
|
+
if (isInvalid(seg)) return seg;
|
|
617
|
+
return s2sFetch({
|
|
618
|
+
config,
|
|
619
|
+
method: "POST",
|
|
620
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.reactivar, seg)
|
|
621
|
+
});
|
|
622
|
+
},
|
|
623
|
+
async addEmpresaMember(empresaId, input) {
|
|
624
|
+
const seg = encodeSegments("addEmpresaMember", { id: empresaId });
|
|
625
|
+
if (isInvalid(seg)) return seg;
|
|
626
|
+
return s2sFetch({
|
|
627
|
+
config,
|
|
628
|
+
method: "POST",
|
|
629
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.addMember, seg),
|
|
630
|
+
body: input
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
async setEmpresaMemberRole(empresaId, userId, input) {
|
|
634
|
+
const seg = encodeSegments("setEmpresaMemberRole", {
|
|
635
|
+
id: empresaId,
|
|
636
|
+
userId
|
|
637
|
+
});
|
|
638
|
+
if (isInvalid(seg)) return seg;
|
|
639
|
+
return s2sFetch({
|
|
640
|
+
config,
|
|
641
|
+
method: "PATCH",
|
|
642
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.updateMember, seg),
|
|
643
|
+
body: input
|
|
644
|
+
});
|
|
645
|
+
},
|
|
646
|
+
async removeEmpresaMember(empresaId, userId, input) {
|
|
647
|
+
const seg = encodeSegments("removeEmpresaMember", {
|
|
648
|
+
id: empresaId,
|
|
649
|
+
userId
|
|
650
|
+
});
|
|
651
|
+
if (isInvalid(seg)) return seg;
|
|
652
|
+
return s2sFetch({
|
|
653
|
+
config,
|
|
654
|
+
method: "DELETE",
|
|
655
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.removeMember, seg),
|
|
656
|
+
body: input
|
|
657
|
+
});
|
|
658
|
+
},
|
|
659
|
+
async createSucursal(empresaId, input) {
|
|
660
|
+
const seg = encodeSegments("createSucursal", { id: empresaId });
|
|
661
|
+
if (isInvalid(seg)) return seg;
|
|
662
|
+
return s2sFetch({
|
|
663
|
+
config,
|
|
664
|
+
method: "POST",
|
|
665
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.createBranch, seg),
|
|
666
|
+
body: input
|
|
667
|
+
});
|
|
668
|
+
},
|
|
669
|
+
async updateSucursal(empresaId, sucursalId, input) {
|
|
670
|
+
const seg = encodeSegments("updateSucursal", {
|
|
671
|
+
id: empresaId,
|
|
672
|
+
sucursalId
|
|
673
|
+
});
|
|
674
|
+
if (isInvalid(seg)) return seg;
|
|
675
|
+
return s2sFetch({
|
|
676
|
+
config,
|
|
677
|
+
method: "PATCH",
|
|
678
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.updateBranch, seg),
|
|
679
|
+
body: input
|
|
680
|
+
});
|
|
681
|
+
},
|
|
682
|
+
async deactivateSucursal(empresaId, sucursalId, input) {
|
|
683
|
+
const seg = encodeSegments("deactivateSucursal", {
|
|
684
|
+
id: empresaId,
|
|
685
|
+
sucursalId
|
|
686
|
+
});
|
|
687
|
+
if (isInvalid(seg)) return seg;
|
|
688
|
+
return s2sFetch({
|
|
689
|
+
config,
|
|
690
|
+
method: "DELETE",
|
|
691
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.deactivateBranch, seg),
|
|
692
|
+
body: input
|
|
693
|
+
});
|
|
694
|
+
},
|
|
695
|
+
async requestContratacion(empresaId, input) {
|
|
696
|
+
const seg = encodeSegments("requestContratacion", { id: empresaId });
|
|
697
|
+
if (isInvalid(seg)) return seg;
|
|
698
|
+
return s2sFetch({
|
|
699
|
+
config,
|
|
700
|
+
method: "POST",
|
|
701
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.requestContratacion, seg),
|
|
702
|
+
body: input
|
|
703
|
+
});
|
|
704
|
+
},
|
|
705
|
+
async cancelContratacion(empresaId, input) {
|
|
706
|
+
const seg = encodeSegments("cancelContratacion", { id: empresaId });
|
|
707
|
+
if (isInvalid(seg)) return seg;
|
|
708
|
+
return s2sFetch({
|
|
709
|
+
config,
|
|
710
|
+
method: "DELETE",
|
|
711
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.cancelContratacion, seg),
|
|
712
|
+
body: input
|
|
713
|
+
});
|
|
714
|
+
},
|
|
715
|
+
async getContratacion(empresaId, producto) {
|
|
716
|
+
const seg = encodeSegments("getContratacion", {
|
|
717
|
+
id: empresaId,
|
|
718
|
+
producto
|
|
719
|
+
});
|
|
720
|
+
if (isInvalid(seg)) return seg;
|
|
721
|
+
return s2sFetch({
|
|
722
|
+
config,
|
|
723
|
+
method: "GET",
|
|
724
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.getContratacion, seg)
|
|
725
|
+
});
|
|
726
|
+
},
|
|
727
|
+
async setContratacion(empresaId, producto, input) {
|
|
728
|
+
const seg = encodeSegments("setContratacion", {
|
|
729
|
+
id: empresaId,
|
|
730
|
+
producto
|
|
731
|
+
});
|
|
732
|
+
if (isInvalid(seg)) return seg;
|
|
733
|
+
return s2sFetch({
|
|
734
|
+
config,
|
|
735
|
+
method: "PUT",
|
|
736
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.setContratacion, seg),
|
|
737
|
+
body: input
|
|
738
|
+
});
|
|
739
|
+
},
|
|
740
|
+
async createGrupo(input) {
|
|
741
|
+
return s2sFetch({
|
|
742
|
+
config,
|
|
743
|
+
method: "POST",
|
|
744
|
+
pathname: apiPath(INTERNAL_GRUPO_ROUTES.create),
|
|
745
|
+
body: input
|
|
746
|
+
});
|
|
747
|
+
},
|
|
748
|
+
async renameGrupo(grupoId, input) {
|
|
749
|
+
const seg = encodeSegments("renameGrupo", { id: grupoId });
|
|
750
|
+
if (isInvalid(seg)) return seg;
|
|
751
|
+
return s2sFetch({
|
|
752
|
+
config,
|
|
753
|
+
method: "PATCH",
|
|
754
|
+
pathname: apiPath(INTERNAL_GRUPO_ROUTES.rename, seg),
|
|
755
|
+
body: input
|
|
756
|
+
});
|
|
757
|
+
},
|
|
758
|
+
async addGrupoEmpresa(grupoId, input) {
|
|
759
|
+
const seg = encodeSegments("addGrupoEmpresa", { id: grupoId });
|
|
760
|
+
if (isInvalid(seg)) return seg;
|
|
761
|
+
return s2sFetch({
|
|
762
|
+
config,
|
|
763
|
+
method: "POST",
|
|
764
|
+
pathname: apiPath(INTERNAL_GRUPO_ROUTES.addEmpresa, seg),
|
|
765
|
+
body: input
|
|
766
|
+
});
|
|
767
|
+
},
|
|
768
|
+
async removeGrupoEmpresa(grupoId, empresaId, input) {
|
|
769
|
+
const seg = encodeSegments("removeGrupoEmpresa", {
|
|
770
|
+
id: grupoId,
|
|
771
|
+
empresaId
|
|
772
|
+
});
|
|
773
|
+
if (isInvalid(seg)) return seg;
|
|
774
|
+
return s2sFetch({
|
|
775
|
+
config,
|
|
776
|
+
method: "DELETE",
|
|
777
|
+
pathname: apiPath(INTERNAL_GRUPO_ROUTES.removeEmpresa, seg),
|
|
778
|
+
body: input
|
|
779
|
+
});
|
|
780
|
+
},
|
|
781
|
+
async searchDirectorio(query) {
|
|
782
|
+
return s2sFetch({
|
|
783
|
+
config,
|
|
784
|
+
method: "GET",
|
|
785
|
+
pathname: apiPath(DIRECTORIO_ROUTES.search),
|
|
786
|
+
rawQuery: query ? buildQueryString({ ...query }) : ""
|
|
787
|
+
});
|
|
788
|
+
},
|
|
789
|
+
async createAutorizacion(otorganteId, input) {
|
|
790
|
+
const seg = encodeSegments("createAutorizacion", { id: otorganteId });
|
|
791
|
+
if (isInvalid(seg)) return seg;
|
|
792
|
+
return s2sFetch({
|
|
793
|
+
config,
|
|
794
|
+
method: "POST",
|
|
795
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.createAutorizacion, seg),
|
|
796
|
+
body: input
|
|
797
|
+
});
|
|
798
|
+
},
|
|
799
|
+
async deleteAutorizacion(empresaId, autorizacionId, input) {
|
|
800
|
+
const seg = encodeSegments("deleteAutorizacion", {
|
|
801
|
+
id: empresaId,
|
|
802
|
+
autorizacionId
|
|
803
|
+
});
|
|
804
|
+
if (isInvalid(seg)) return seg;
|
|
805
|
+
return s2sFetch({
|
|
806
|
+
config,
|
|
807
|
+
method: "DELETE",
|
|
808
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.deleteAutorizacion, seg),
|
|
809
|
+
body: input
|
|
810
|
+
});
|
|
811
|
+
},
|
|
812
|
+
async listAutorizaciones(empresaId, input) {
|
|
813
|
+
const seg = encodeSegments("listAutorizaciones", { id: empresaId });
|
|
814
|
+
if (isInvalid(seg)) return seg;
|
|
815
|
+
return s2sFetch({
|
|
816
|
+
config,
|
|
817
|
+
method: "GET",
|
|
818
|
+
pathname: apiPath(INTERNAL_EMPRESA_ROUTES.listAutorizaciones, seg),
|
|
819
|
+
rawQuery: buildQueryString({ direccion: input.direccion }),
|
|
820
|
+
headers: { [SUBJECT_TOKEN_HEADER]: input.subjectToken }
|
|
821
|
+
});
|
|
822
|
+
},
|
|
823
|
+
async checkAutorizacion(input) {
|
|
824
|
+
const key = `${input.otorgante}|${input.beneficiaria}|${input.permiso}`;
|
|
825
|
+
const hit = autorizacionCache.get(key);
|
|
826
|
+
if (hit !== void 0) {
|
|
827
|
+
if (Date.now() < hit.expiresAtMs) return {
|
|
828
|
+
ok: true,
|
|
829
|
+
data: { autorizado: hit.autorizado }
|
|
830
|
+
};
|
|
831
|
+
autorizacionCache.delete(key);
|
|
832
|
+
}
|
|
833
|
+
const result = await s2sFetch({
|
|
834
|
+
config,
|
|
835
|
+
method: "GET",
|
|
836
|
+
pathname: apiPath(INTERNAL_AUTORIZACION_ROUTES.verificar),
|
|
837
|
+
rawQuery: buildQueryString({
|
|
838
|
+
otorgante: input.otorgante,
|
|
839
|
+
beneficiaria: input.beneficiaria,
|
|
840
|
+
permiso: input.permiso
|
|
841
|
+
})
|
|
842
|
+
});
|
|
843
|
+
if (result.ok) {
|
|
844
|
+
if (autorizacionCache.size >= AUTORIZACION_CACHE_MAX_ENTRIES) {
|
|
845
|
+
const now = Date.now();
|
|
846
|
+
for (const [k, v] of autorizacionCache) if (now >= v.expiresAtMs) autorizacionCache.delete(k);
|
|
847
|
+
if (autorizacionCache.size >= AUTORIZACION_CACHE_MAX_ENTRIES) {
|
|
848
|
+
const oldest = autorizacionCache.keys().next().value;
|
|
849
|
+
if (oldest !== void 0) autorizacionCache.delete(oldest);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
autorizacionCache.set(key, {
|
|
853
|
+
autorizado: result.data.autorizado,
|
|
854
|
+
expiresAtMs: Date.now() + AUTORIZACION_CACHE_TTL_MS
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
return result;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
// src/s2s/error-codes.ts
|
|
862
|
+
/**
|
|
863
|
+
* Códigos de error específicos del módulo S2S.
|
|
864
|
+
*
|
|
865
|
+
* Complementan a `ERROR_CODES` (core) para los errores propios de los endpoints
|
|
866
|
+
* `/api/v1/internal/*` que consume `createS2SClient`: usuarios y subscriptions de webhooks.
|
|
867
|
+
*
|
|
868
|
+
* @example
|
|
869
|
+
* ```ts
|
|
870
|
+
* import { S2S_ERROR_CODES } from '@eva_solutions/auth-sdk/s2s'
|
|
871
|
+
*
|
|
872
|
+
* const r = await s2s.rotateWebhookSubscriptionSecret(id)
|
|
873
|
+
* if (!r.ok && r.error.kind === 'api' && r.error.code === S2S_ERROR_CODES.webhook_secret_rotation_conflict) {
|
|
874
|
+
* // otra rotación concurrente ganó; esta llamada no recibió clave. Reintenta.
|
|
875
|
+
* }
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
const S2S_ERROR_CODES = {
|
|
879
|
+
/** `GET /internal/users/{id}`: el usuario no existe o está soft-deleted (404). */
|
|
880
|
+
user_not_found: "user_not_found",
|
|
881
|
+
/** Ya existe una subscription del mismo service client con la misma URL y los mismos eventCodes (409). */
|
|
882
|
+
webhook_subscription_duplicate: "webhook_subscription_duplicate",
|
|
883
|
+
/** La subscription no existe o pertenece a otro service client (404). */
|
|
884
|
+
webhook_subscription_not_found: "webhook_subscription_not_found",
|
|
885
|
+
/**
|
|
886
|
+
* `rotate-secret` perdió contra otra rotación concurrente de la misma subscription (409).
|
|
887
|
+
* El perdedor no recibe clave; es reintentable.
|
|
888
|
+
*/
|
|
889
|
+
webhook_secret_rotation_conflict: "webhook_secret_rotation_conflict",
|
|
890
|
+
/** URL de webhook inválida: reglas estáticas, IP privada o host sin direcciones (400). */
|
|
891
|
+
webhook_url_invalid: "webhook_url_invalid",
|
|
892
|
+
/** `eventCodes` vacío al crear o actualizar (400). */
|
|
893
|
+
webhook_event_codes_empty: "webhook_event_codes_empty"
|
|
894
|
+
};
|
|
895
|
+
export { createServiceTokenProvider as a, ServiceTokenRequestError as i, createS2SClient as n, SERVICE_TOKEN_RENEW_MARGIN_MS as r, S2S_ERROR_CODES as t };
|
|
896
|
+
|
|
897
|
+
//# sourceMappingURL=s2s-B_0SIQzn.js.map
|