planvortex 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1594 @@
1
+ 'use strict';
2
+
3
+ var path = require('path');
4
+
5
+ // src/version.ts
6
+ var PLANVORTEX_API_URL = "https://api.planvortex.com/v1.0.0";
7
+ var VERSION = "0.0.1";
8
+
9
+ // src/core/errors.ts
10
+ var PLANVORTEX_ERROR_RANGES = [
11
+ { from: 500, to: 541, family: "auth" },
12
+ { from: 601, to: 612, family: "user" },
13
+ { from: 700, to: 715, family: "account" },
14
+ { from: 800, to: 810, family: "file" },
15
+ { from: 900, to: 960, family: "publication" },
16
+ { from: 1e3, to: 1003, family: "general" },
17
+ { from: 1100, to: 1111, family: "organization" },
18
+ { from: 1200, to: 1207, family: "role" },
19
+ { from: 1300, to: 1307, family: "plan_limit" },
20
+ { from: 1400, to: 1408, family: "plan_limit" },
21
+ { from: 1500, to: 1512, family: "messaging" },
22
+ { from: 1600, to: 1601, family: "contact" },
23
+ { from: 1900, to: 1906, family: "payment" },
24
+ { from: 2e3, to: 2099, family: "product" },
25
+ { from: 2100, to: 2199, family: "ai_plan" },
26
+ { from: 2200, to: 2299, family: "integration" }
27
+ ];
28
+ var NO_ERROR_CODE = 0;
29
+ var TOKEN_ERROR_CODES = [501, 522];
30
+ var PlanVortexError = class extends Error {
31
+ /** El `code` del cuerpo, tal cual. {@link NO_ERROR_CODE} si el error no viene del catálogo. */
32
+ code;
33
+ /** La familia del rango: `auth`, `publication`, `plan_limit`... Ver {@link PLANVORTEX_ERROR_RANGES}. */
34
+ family;
35
+ /** El `data` del cuerpo. */
36
+ data;
37
+ /** Status HTTP, o `undefined` si nunca hubo respuesta. */
38
+ status;
39
+ /** `x-request-id`, si el despliegue lo pone. Hoy el servidor no lo emite; un proxy delante sí. */
40
+ requestId;
41
+ /** Segundos que pidió esperar la cabecera `Retry-After`, si llegó. */
42
+ retryAfter;
43
+ constructor(code, message, options = {}) {
44
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
45
+ this.name = new.target.name;
46
+ this.code = code;
47
+ this.family = options.family ?? errorFamilyForCode(code) ?? "unknown";
48
+ this.data = options.data ?? {};
49
+ this.status = options.status;
50
+ this.requestId = options.requestId;
51
+ this.retryAfter = options.retryAfter;
52
+ }
53
+ };
54
+ var AuthError = class extends PlanVortexError {
55
+ };
56
+ var UserError = class extends PlanVortexError {
57
+ };
58
+ var AccountError = class extends PlanVortexError {
59
+ };
60
+ var FileError = class extends PlanVortexError {
61
+ };
62
+ var PublicationError = class extends PlanVortexError {
63
+ };
64
+ var OrganizationError = class extends PlanVortexError {
65
+ };
66
+ var PlanLimitError = class extends PlanVortexError {
67
+ };
68
+ var MessagingError = class extends PlanVortexError {
69
+ };
70
+ var ContactError = class extends PlanVortexError {
71
+ };
72
+ var ProductError = class extends PlanVortexError {
73
+ };
74
+ var AiPlanError = class extends PlanVortexError {
75
+ };
76
+ var IntegrationError = class extends PlanVortexError {
77
+ };
78
+ var PlanVortexConnectionError = class extends PlanVortexError {
79
+ /** `true` si lo que se agotó fue nuestro propio timeout, no la red. */
80
+ timeout;
81
+ constructor(message, options = {}) {
82
+ super(NO_ERROR_CODE, message, { ...options, family: "connection" });
83
+ this.timeout = options.timeout ?? false;
84
+ }
85
+ };
86
+ var PlanVortexAuthenticationError = class extends PlanVortexError {
87
+ /** `invalid_client`, `invalid_request`, `unsupported_grant_type`, `slow_down` o `server_error`. */
88
+ oauthError;
89
+ constructor(oauthError, description, options = {}) {
90
+ super(NO_ERROR_CODE, description, { ...options, family: "oauth" });
91
+ this.oauthError = oauthError;
92
+ }
93
+ };
94
+ var PlanVortexConfigError = class extends PlanVortexError {
95
+ constructor(message) {
96
+ super(NO_ERROR_CODE, message, { family: "config" });
97
+ }
98
+ };
99
+ function errorFamilyForCode(code) {
100
+ return PLANVORTEX_ERROR_RANGES.find((range) => code >= range.from && code <= range.to)?.family;
101
+ }
102
+ var FAMILY_CLASSES = {
103
+ auth: AuthError,
104
+ user: UserError,
105
+ account: AccountError,
106
+ file: FileError,
107
+ publication: PublicationError,
108
+ organization: OrganizationError,
109
+ plan_limit: PlanLimitError,
110
+ messaging: MessagingError,
111
+ contact: ContactError,
112
+ product: ProductError,
113
+ ai_plan: AiPlanError,
114
+ integration: IntegrationError
115
+ };
116
+ function isApiErrorBody(body) {
117
+ return typeof body === "object" && body !== null && typeof body.code === "number";
118
+ }
119
+ function createErrorFromResponse(input) {
120
+ const options = {
121
+ status: input.status,
122
+ ...input.requestId === void 0 ? {} : { requestId: input.requestId },
123
+ ...input.retryAfter === void 0 ? {} : { retryAfter: input.retryAfter }
124
+ };
125
+ if (isApiErrorBody(input.body)) {
126
+ const family = errorFamilyForCode(input.body.code);
127
+ const ErrorClass = (family === void 0 ? void 0 : FAMILY_CLASSES[family]) ?? PlanVortexError;
128
+ return new ErrorClass(input.body.code, input.body.message ?? "PlanVortex error", {
129
+ ...options,
130
+ data: input.body.data ?? {}
131
+ });
132
+ }
133
+ return new PlanVortexError(NO_ERROR_CODE, `HTTP ${input.status}`, {
134
+ ...options,
135
+ family: "http",
136
+ data: typeof input.body === "object" && input.body !== null ? input.body : { body: input.body }
137
+ });
138
+ }
139
+ function isPlanVortexError(error) {
140
+ return error instanceof PlanVortexError;
141
+ }
142
+ function isTokenError(error) {
143
+ return isPlanVortexError(error) && TOKEN_ERROR_CODES.includes(error.code);
144
+ }
145
+
146
+ // src/core/auth.ts
147
+ var TOKEN_REFRESH_MARGIN_MS = 6e4;
148
+ var TOKEN_PATH = "/oauth/token";
149
+ var StaticTokenAuth = class {
150
+ constructor(token) {
151
+ this.token = token;
152
+ }
153
+ token;
154
+ getToken() {
155
+ return Promise.resolve(this.token);
156
+ }
157
+ invalidate() {
158
+ }
159
+ };
160
+ var ClientCredentialsAuth = class {
161
+ constructor(http, options) {
162
+ this.http = http;
163
+ this.options = options;
164
+ this.now = options.now ?? Date.now;
165
+ }
166
+ http;
167
+ options;
168
+ now;
169
+ cached;
170
+ /** La petición de token en vuelo. Es el cerrojo: mientras exista, todos esperan a ésta. */
171
+ pending;
172
+ async getToken() {
173
+ const cached = this.cached;
174
+ if (cached && cached.expiresAt - this.now() > TOKEN_REFRESH_MARGIN_MS) {
175
+ return cached.token;
176
+ }
177
+ this.pending ??= this.fetchToken().finally(() => {
178
+ this.pending = void 0;
179
+ });
180
+ return this.pending;
181
+ }
182
+ invalidate() {
183
+ this.cached = void 0;
184
+ }
185
+ async fetchToken() {
186
+ const body = new URLSearchParams({
187
+ grant_type: "client_credentials",
188
+ client_id: this.options.clientId,
189
+ client_secret: this.options.clientSecret
190
+ });
191
+ if (this.options.scope) {
192
+ body.set("scope", this.options.scope);
193
+ }
194
+ let token;
195
+ try {
196
+ const response = await this.http.request({
197
+ method: "POST",
198
+ path: TOKEN_PATH,
199
+ body,
200
+ //Es el único POST reintentable de la librería: pedir un token no crea nada.
201
+ idempotent: true
202
+ });
203
+ token = response.data;
204
+ } catch (error) {
205
+ throw toAuthenticationError(error);
206
+ }
207
+ if (!token?.access_token) {
208
+ throw new PlanVortexAuthenticationError(
209
+ "server_error",
210
+ "El endpoint de token respondi\xF3 sin access_token"
211
+ );
212
+ }
213
+ const lifetimeMs = (Number(token.expires_in) > 0 ? Number(token.expires_in) : 3600) * 1e3;
214
+ this.cached = { token: token.access_token, expiresAt: this.now() + lifetimeMs };
215
+ return token.access_token;
216
+ }
217
+ };
218
+ function toAuthenticationError(error) {
219
+ if (!isPlanVortexError(error) || error.status === void 0) {
220
+ return error;
221
+ }
222
+ const body = error.data;
223
+ const oauthError = typeof body.error === "string" ? body.error : "invalid_client";
224
+ const description = typeof body.error_description === "string" ? body.error_description : error.message;
225
+ return new PlanVortexAuthenticationError(oauthError, description, {
226
+ status: error.status,
227
+ ...error.requestId === void 0 ? {} : { requestId: error.requestId },
228
+ ...error.retryAfter === void 0 ? {} : { retryAfter: error.retryAfter },
229
+ cause: error
230
+ });
231
+ }
232
+
233
+ // src/core/http.ts
234
+ var DEFAULT_RETRY = {
235
+ maxRetries: 2,
236
+ baseDelayMs: 500,
237
+ maxDelayMs: 8e3
238
+ };
239
+ var DEFAULT_TIMEOUT_MS = 12e4;
240
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 502, 503, 504]);
241
+ var PRE_FLIGHT_NETWORK_CODES = /* @__PURE__ */ new Set([
242
+ "ECONNREFUSED",
243
+ "ENOTFOUND",
244
+ "EAI_AGAIN",
245
+ "EHOSTUNREACH",
246
+ "ENETUNREACH"
247
+ ]);
248
+ function isIdempotent(request) {
249
+ return request.idempotent ?? request.method !== "POST";
250
+ }
251
+ function networkErrorCode(error) {
252
+ const cause = error?.cause;
253
+ const code = cause?.code ?? error?.code;
254
+ return typeof code === "string" ? code : void 0;
255
+ }
256
+ function parseRetryAfter(header, now = Date.now()) {
257
+ if (!header) {
258
+ return void 0;
259
+ }
260
+ const seconds = Number(header);
261
+ if (Number.isFinite(seconds) && seconds >= 0) {
262
+ return seconds;
263
+ }
264
+ const date = Date.parse(header);
265
+ if (Number.isNaN(date)) {
266
+ return void 0;
267
+ }
268
+ return Math.max(0, Math.round((date - now) / 1e3));
269
+ }
270
+ function buildQuery(query) {
271
+ if (!query) {
272
+ return "";
273
+ }
274
+ const params = new URLSearchParams();
275
+ for (const [key, value] of Object.entries(query)) {
276
+ if (value === void 0 || value === null) {
277
+ continue;
278
+ }
279
+ if (Array.isArray(value)) {
280
+ for (const item of value) {
281
+ params.append(key, String(item));
282
+ }
283
+ continue;
284
+ }
285
+ params.append(key, value instanceof Date ? value.toISOString() : String(value));
286
+ }
287
+ const serialized = params.toString();
288
+ return serialized ? `?${serialized}` : "";
289
+ }
290
+ function sleep(ms) {
291
+ return new Promise((resolve) => {
292
+ setTimeout(resolve, ms);
293
+ });
294
+ }
295
+ var HttpClient = class {
296
+ baseUrl;
297
+ timeoutMs;
298
+ retry;
299
+ hooks;
300
+ fetchImpl;
301
+ baseHeaders;
302
+ constructor(config) {
303
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
304
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
305
+ this.retry = { ...DEFAULT_RETRY, ...config.retry };
306
+ this.hooks = config.hooks ?? {};
307
+ this.fetchImpl = config.fetch ?? ((input, init) => globalThis.fetch(input, init));
308
+ this.baseHeaders = Object.fromEntries(
309
+ Object.entries(config.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value])
310
+ );
311
+ }
312
+ async request(request) {
313
+ const url = `${this.baseUrl}${request.path}${buildQuery(request.query)}`;
314
+ const { body, headers } = this.prepareBody(request);
315
+ const timeoutMs = request.timeoutMs ?? this.timeoutMs;
316
+ const retryable = isIdempotent(request);
317
+ let attempt = 0;
318
+ for (; ; ) {
319
+ attempt++;
320
+ this.hooks.onRequest?.({ method: request.method, url, attempt });
321
+ const startedAt = Date.now();
322
+ let response;
323
+ try {
324
+ response = await this.send(url, request, body, headers, timeoutMs);
325
+ } catch (error) {
326
+ const failure = this.toConnectionError(error, request, timeoutMs);
327
+ const canRetry = retryable || PRE_FLIGHT_NETWORK_CODES.has(networkErrorCode(error) ?? "");
328
+ if (!canRetry || attempt > this.retry.maxRetries) {
329
+ throw failure;
330
+ }
331
+ const delayMs = this.backoff(attempt);
332
+ this.hooks.onRetry?.({
333
+ method: request.method,
334
+ url,
335
+ attempt,
336
+ delayMs,
337
+ status: void 0,
338
+ error
339
+ });
340
+ await sleep(delayMs);
341
+ continue;
342
+ }
343
+ this.hooks.onResponse?.({
344
+ method: request.method,
345
+ url,
346
+ attempt,
347
+ status: response.status,
348
+ durationMs: Date.now() - startedAt
349
+ });
350
+ const requestId = response.headers.get("x-request-id") ?? void 0;
351
+ if (response.ok) {
352
+ return {
353
+ data: await this.parseBody(response, request),
354
+ status: response.status,
355
+ headers: response.headers,
356
+ requestId
357
+ };
358
+ }
359
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
360
+ const shouldRetry = retryable && RETRYABLE_STATUS.has(response.status) && attempt <= this.retry.maxRetries;
361
+ const waitMs = retryAfter === void 0 ? this.backoff(attempt) : retryAfter * 1e3;
362
+ if (!shouldRetry || waitMs > this.retry.maxDelayMs) {
363
+ throw await this.toApiError(response, requestId, retryAfter);
364
+ }
365
+ await response.arrayBuffer().catch(() => void 0);
366
+ this.hooks.onRetry?.({
367
+ method: request.method,
368
+ url,
369
+ attempt,
370
+ delayMs: waitMs,
371
+ status: response.status,
372
+ error: void 0
373
+ });
374
+ await sleep(waitMs);
375
+ }
376
+ }
377
+ /** Backoff exponencial con jitter completo: aleatorio entre 0 y el tope de ese intento. */
378
+ backoff(attempt) {
379
+ const ceiling = Math.min(this.retry.maxDelayMs, this.retry.baseDelayMs * 2 ** (attempt - 1));
380
+ return Math.round(Math.random() * ceiling);
381
+ }
382
+ prepareBody(request) {
383
+ const headers = { accept: "application/json", ...this.baseHeaders };
384
+ for (const [key, value] of Object.entries(request.headers ?? {})) {
385
+ headers[key.toLowerCase()] = value;
386
+ }
387
+ if (request.body === void 0) {
388
+ return { body: void 0, headers };
389
+ }
390
+ if (request.body instanceof FormData || request.body instanceof URLSearchParams) {
391
+ return { body: request.body, headers };
392
+ }
393
+ if (typeof request.body === "string" || request.body instanceof Uint8Array) {
394
+ return { body: request.body, headers };
395
+ }
396
+ headers["content-type"] = "application/json";
397
+ return { body: JSON.stringify(request.body), headers };
398
+ }
399
+ async send(url, request, body, headers, timeoutMs) {
400
+ const controller = new AbortController();
401
+ const timer = setTimeout(() => controller.abort(new PlanVortexTimeout()), timeoutMs);
402
+ const external = request.signal;
403
+ const forward = () => controller.abort(external?.reason);
404
+ if (external) {
405
+ if (external.aborted) {
406
+ clearTimeout(timer);
407
+ throw external.reason;
408
+ }
409
+ external.addEventListener("abort", forward, { once: true });
410
+ }
411
+ try {
412
+ return await this.fetchImpl(url, {
413
+ method: request.method,
414
+ ...body === void 0 ? {} : { body },
415
+ headers,
416
+ signal: controller.signal
417
+ });
418
+ } catch (error) {
419
+ if (controller.signal.reason instanceof PlanVortexTimeout && !external?.aborted) {
420
+ throw controller.signal.reason;
421
+ }
422
+ throw error;
423
+ } finally {
424
+ clearTimeout(timer);
425
+ external?.removeEventListener("abort", forward);
426
+ }
427
+ }
428
+ /**
429
+ * Un abort tiene dos orígenes y sólo uno es nuestro: si canceló el integrador, su error sale tal
430
+ * cual —envolverlo le rompería el `if (e.name === "AbortError")` que tenga escrito.
431
+ */
432
+ toConnectionError(error, request, timeoutMs) {
433
+ if (error instanceof PlanVortexTimeout || error?.name === "TimeoutError") {
434
+ return new PlanVortexConnectionError(
435
+ `${request.method} ${request.path} agot\xF3 el timeout de ${timeoutMs} ms`,
436
+ { timeout: true, cause: error }
437
+ );
438
+ }
439
+ if (request.signal?.aborted) {
440
+ return error;
441
+ }
442
+ const code = networkErrorCode(error);
443
+ return new PlanVortexConnectionError(
444
+ `${request.method} ${request.path} no pudo conectar${code ? ` (${code})` : ""}`,
445
+ { timeout: false, cause: error }
446
+ );
447
+ }
448
+ async toApiError(response, requestId, retryAfter) {
449
+ const body = await this.readBody(response);
450
+ return createErrorFromResponse({ body, status: response.status, requestId, retryAfter });
451
+ }
452
+ async parseBody(response, request) {
453
+ if (request.parse === "none" || response.status === 204) {
454
+ return void 0;
455
+ }
456
+ return this.readBody(response);
457
+ }
458
+ /** JSON si se puede; si no, el texto crudo. Un cuerpo vacío es `undefined`, nunca un throw. */
459
+ async readBody(response) {
460
+ const text = await response.text().catch(() => "");
461
+ if (!text) {
462
+ return void 0;
463
+ }
464
+ try {
465
+ return JSON.parse(text);
466
+ } catch {
467
+ return text;
468
+ }
469
+ }
470
+ };
471
+ var PlanVortexTimeout = class extends Error {
472
+ constructor() {
473
+ super("PlanVortex request timeout");
474
+ this.name = "TimeoutError";
475
+ }
476
+ };
477
+
478
+ // src/core/pagination.ts
479
+ var DEFAULT_PAGE_SIZE = 50;
480
+ var MAX_PAGES = 1e4;
481
+ function unwrapList(body, key) {
482
+ const envelope = body;
483
+ const data = envelope?.[key];
484
+ if (!Array.isArray(data)) {
485
+ throw new PlanVortexError(
486
+ NO_ERROR_CODE,
487
+ `La respuesta no trae "${key}": el sobre de la lista no es el esperado.`,
488
+ {
489
+ family: "http",
490
+ data: { expected: key, received: envelope ? Object.keys(envelope) : envelope }
491
+ }
492
+ );
493
+ }
494
+ const total = envelope?.total;
495
+ return {
496
+ data,
497
+ //`total` viaja siempre, pero si un despliegue viejo no lo mandara, la longitud de la
498
+ //página es mejor respuesta que un `undefined` colándose como número.
499
+ total: typeof total === "number" ? total : data.length
500
+ };
501
+ }
502
+ function unwrapOne(body, key) {
503
+ const envelope = body;
504
+ const value = envelope?.[key];
505
+ if (value === void 0 || value === null) {
506
+ throw new PlanVortexError(NO_ERROR_CODE, `La respuesta no trae "${key}".`, {
507
+ family: "http",
508
+ data: { expected: key, received: envelope ? Object.keys(envelope) : envelope }
509
+ });
510
+ }
511
+ return value;
512
+ }
513
+ async function* iteratePages(fetchPage, options = {}) {
514
+ const limit = options.limit ?? DEFAULT_PAGE_SIZE;
515
+ let offset = options.offset ?? 0;
516
+ for (let page = 0; page < MAX_PAGES; page++) {
517
+ const { data } = await fetchPage({ limit, offset });
518
+ if (!data.length) {
519
+ return;
520
+ }
521
+ for (const item of data) {
522
+ yield item;
523
+ }
524
+ if (data.length < limit) {
525
+ return;
526
+ }
527
+ offset += data.length;
528
+ }
529
+ throw new PlanVortexError(
530
+ NO_ERROR_CODE,
531
+ `La paginaci\xF3n pas\xF3 de ${MAX_PAGES} p\xE1ginas: el servidor no est\xE1 avanzando con el offset.`,
532
+ { family: "http" }
533
+ );
534
+ }
535
+
536
+ // src/resources/base.ts
537
+ var Resource = class {
538
+ constructor(client) {
539
+ this.client = client;
540
+ }
541
+ client;
542
+ async send(request, options = {}) {
543
+ const response = await this.client.request({
544
+ ...request,
545
+ ...options.signal === void 0 ? {} : { signal: options.signal },
546
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
547
+ });
548
+ return response.data;
549
+ }
550
+ /*
551
+ * Los cuatro verbos se llaman `httpGet`/`httpPost`/... y no `get`/`post`/... porque los
552
+ * recursos que heredan de aquí SÍ tienen métodos públicos con esos nombres —`uploads.get(org,
553
+ * id)` es lo que un integrador escribe— y en TypeScript una subclase no puede declarar un
554
+ * miembro con la firma de uno heredado. Sin el prefijo, `this.get(...)` dentro de un recurso
555
+ * resolvía a su propio método público y compilaba llamando a otra cosa.
556
+ */
557
+ httpGet(path, query, options) {
558
+ return this.send({ method: "GET", path, ...query ? { query } : {} }, options);
559
+ }
560
+ httpPost(path, body, options, query) {
561
+ return this.send(
562
+ { method: "POST", path, ...body === void 0 ? {} : { body }, ...query ? { query } : {} },
563
+ options
564
+ );
565
+ }
566
+ httpPut(path, body, options) {
567
+ return this.send({ method: "PUT", path, ...body === void 0 ? {} : { body } }, options);
568
+ }
569
+ httpDelete(path, query, options) {
570
+ return this.send({ method: "DELETE", path, ...query ? { query } : {} }, options);
571
+ }
572
+ /** `GET` de una lista, ya desenvuelta a `{data, total}`. */
573
+ async getList(path, key, query, options) {
574
+ return unwrapList(await this.httpGet(path, query, options), key);
575
+ }
576
+ /** `GET` de un recurso suelto, ya sacado de su sobre. */
577
+ async getOne(path, key, query, options) {
578
+ return unwrapOne(await this.httpGet(path, query, options), key);
579
+ }
580
+ /** `POST` que devuelve un recurso envuelto. */
581
+ async postOne(path, key, body, options, query) {
582
+ return unwrapOne(await this.httpPost(path, body, options, query), key);
583
+ }
584
+ /** `PUT` que devuelve un recurso envuelto. */
585
+ async putOne(path, key, body, options) {
586
+ return unwrapOne(await this.httpPut(path, body, options), key);
587
+ }
588
+ };
589
+ function requireId(value, name) {
590
+ if (typeof value !== "string" || !value.trim()) {
591
+ throw new TypeError(`Falta ${name}.`);
592
+ }
593
+ return value;
594
+ }
595
+
596
+ // src/resources/accounts.ts
597
+ var AccountsResource = class extends Resource {
598
+ /**
599
+ * Los enlaces de autorización de cada red conectable, para mandar a la persona a la suya.
600
+ *
601
+ * **Con credenciales de app contesta 519.** Se llama con un cliente autenticado con el token
602
+ * temporal: `pv.asTemporalToken(token).accounts.connectLinks(orgId)`.
603
+ *
604
+ * **Una red que no puede dar enlace simplemente no aparece**, y eso es una respuesta legítima y
605
+ * no un fallo: es lo que pasa con Discord en una organización que todavía no ha guardado sus
606
+ * propias credenciales de bot.
607
+ *
608
+ * OJO: la red devuelve al usuario a un front de PlanVortex, no a una URL tuya — ver
609
+ * `redirect_uri` en {@link ConnectLinksOptions}.
610
+ */
611
+ async connectLinks(idOrganization, options = {}) {
612
+ return unwrapOne(
613
+ await this.httpGet(
614
+ `/organizations/${requireId(idOrganization, "idOrganization")}/connect_links`,
615
+ { social_network: options.social_network, redirect_uri: options.redirect_uri },
616
+ options
617
+ ),
618
+ "links"
619
+ );
620
+ }
621
+ /**
622
+ * Completa la conexión con lo que la red social pegó a la URL de vuelta.
623
+ *
624
+ * **Esta llamada no la necesita la mayoría.** La URL de vuelta la construye la red a partir del
625
+ * enlace de {@link connectLinks}, y apunta a un front de PlanVortex: es ese front el que llama
626
+ * aquí. El método existe para quien sirve su propia interfaz en uno de los dominios registrados
627
+ * en el servidor. En la integración normal —la del ejemplo `connect-flow`— basta con mandar al
628
+ * usuario a la `url` del token temporal y esperarlo de vuelta.
629
+ *
630
+ * **El endpoint contesta 200 aunque haya fallado**, con el error dentro del cuerpo, porque el
631
+ * navegador aterriza aquí desde una redirección y un 400 crudo sería una página rota. La
632
+ * librería deshace ese apaño: si viene `errorCode`, **lanza** el error que le toca, igual que
633
+ * cualquier otro método. Lo que devuelve son sólo cuentas buenas.
634
+ *
635
+ * **Y vuelven SIN habilitar**: no ocupan plaza del plan ni publican hasta que se llama a
636
+ * {@link enable}. Una sola autorización puede dejar varias — un usuario de Facebook con cuatro
637
+ * páginas son cuatro—, y por eso hay un paso de elección en medio.
638
+ */
639
+ async connect(idOrganization, socialNetwork, params = {}, options = {}) {
640
+ const path = `/organizations/${requireId(idOrganization, "idOrganization")}/account-connect/${requireId(socialNetwork, "socialNetwork")}`;
641
+ const body = await this.httpGet(path, params, options);
642
+ if (body.errorCode) {
643
+ const code = Number(body.errorCode);
644
+ throw createErrorFromResponse({
645
+ body: {
646
+ code: Number.isFinite(code) ? code : NO_ERROR_CODE,
647
+ message: body.errorMsg || `La conexi\xF3n no se complet\xF3 (${body.errorCode}).`
648
+ },
649
+ status: 200
650
+ });
651
+ }
652
+ return {
653
+ accounts: body.accounts ?? [],
654
+ ...body.redirect_uri === void 0 ? {} : { redirect_uri: body.redirect_uri }
655
+ };
656
+ }
657
+ /**
658
+ * Da de alta una de las cuentas que dejó {@link connect}, o recupera una que se desconectó
659
+ * mientras su token guardado siga sirviendo (si no, error 700 y hay que autorizar otra vez).
660
+ *
661
+ * **Es el paso que ocupa plaza del plan**: con el cupo lleno contesta 706, así que se llama una
662
+ * a una y se mira el hueco antes (`organizations.limits`). Y es también el que enciende los
663
+ * webhooks de la red, en cualquier plan que no sea el gratuito.
664
+ */
665
+ async enable(idOrganization, idAccount, options = {}) {
666
+ const body = await this.httpPost(
667
+ `${this.path(idOrganization, idAccount)}/enable`,
668
+ void 0,
669
+ options
670
+ );
671
+ return body.redirect_uri === void 0 ? {} : { redirect_uri: body.redirect_uri };
672
+ }
673
+ /** Las cuentas de una organización. */
674
+ async list(idOrganization, options = {}) {
675
+ return this.getList(
676
+ `/organizations/${requireId(idOrganization, "idOrganization")}/accounts`,
677
+ "accounts",
678
+ {
679
+ offset: options.offset,
680
+ limit: options.limit,
681
+ name: options.name,
682
+ social_network: options.social_network,
683
+ accounts: options.accounts,
684
+ capability: options.capability
685
+ },
686
+ options
687
+ );
688
+ }
689
+ /** Las cuentas de una organización, encadenando páginas. */
690
+ iterate(idOrganization, options = {}) {
691
+ return iteratePages((page) => this.list(idOrganization, { ...options, ...page }), options);
692
+ }
693
+ /** La ficha de una cuenta. */
694
+ async get(idOrganization, idAccount, options = {}) {
695
+ return this.getOne(this.path(idOrganization, idAccount), "account", void 0, options);
696
+ }
697
+ /** Cambia el nombre con el que la cuenta se ve en PlanVortex. Es lo único editable. */
698
+ async update(idOrganization, idAccount, body, options = {}) {
699
+ return this.putOne(this.path(idOrganization, idAccount), "account", body, options);
700
+ }
701
+ /**
702
+ * Desconecta la cuenta y **borra sus publicaciones**. Lo ya publicado en la red se queda donde
703
+ * está: esto no la toca.
704
+ */
705
+ async remove(idOrganization, idAccount, options = {}) {
706
+ await this.httpDelete(this.path(idOrganization, idAccount), void 0, options);
707
+ }
708
+ /**
709
+ * La serie de métricas ya medidas de una cuenta.
710
+ *
711
+ * Es lectura de lo guardado, no una llamada a la red: mirar la gráfica no cuesta créditos. El
712
+ * agrupado lo decide el rango — hasta 31 días por día, hasta 720 por mes, y de ahí por año— y
713
+ * viene dicho en `group`.
714
+ */
715
+ async metrics(idOrganization, idAccount, options = {}) {
716
+ return this.httpGet(
717
+ `${this.path(idOrganization, idAccount)}/metrics`,
718
+ {
719
+ from_date: options.from_date,
720
+ to_date: options.to_date,
721
+ names: options.names
722
+ },
723
+ options
724
+ );
725
+ }
726
+ /**
727
+ * Los nombres CRUDOS de las métricas que publica la red de esta cuenta.
728
+ *
729
+ * Son los que se pasan a {@link metrics} y los que vuelven en cada fila. No es el vocabulario
730
+ * común —eso es `metrics` de una publicación—: aquí cada red habla su idioma
731
+ * (`page_impressions`, `total_interactions`, `allPageViews`).
732
+ */
733
+ async metricList(idOrganization, idAccount, options = {}) {
734
+ return this.httpGet(
735
+ `${this.path(idOrganization, idAccount)}/metric_list`,
736
+ void 0,
737
+ options
738
+ );
739
+ }
740
+ /**
741
+ * El menú fijo del chat, una entrada por idioma.
742
+ *
743
+ * Sólo las redes con mensajería lo tienen: en las demás la llamada devuelve el error 710. Se
744
+ * comprueba con `persistent_menu` de `catalog.socialCapabilities()`.
745
+ */
746
+ async getPersistentMenu(idOrganization, idAccount, options = {}) {
747
+ return unwrapOne(
748
+ await this.httpGet(
749
+ `${this.path(idOrganization, idAccount)}/persistent_menu`,
750
+ void 0,
751
+ options
752
+ ),
753
+ "persistent_menu"
754
+ );
755
+ }
756
+ /**
757
+ * Reemplaza el menú fijo del chat. Es un REEMPLAZO: lo que no vaya en el array desaparece.
758
+ *
759
+ * La entrada con `locale: "default"` es obligatoria — es la que se enseña cuando ninguna otra
760
+ * encaja.
761
+ */
762
+ async setPersistentMenu(idOrganization, idAccount, menu, options = {}) {
763
+ return unwrapOne(
764
+ await this.httpPost(
765
+ `${this.path(idOrganization, idAccount)}/persistent_menu`,
766
+ { persistent_menu: menu },
767
+ options
768
+ ),
769
+ "persistent_menu"
770
+ );
771
+ }
772
+ path(idOrganization, idAccount) {
773
+ return `/organizations/${requireId(idOrganization, "idOrganization")}/accounts/${requireId(idAccount, "idAccount")}`;
774
+ }
775
+ };
776
+
777
+ // src/resources/catalog.ts
778
+ var CatalogResource = class extends Resource {
779
+ cache = /* @__PURE__ */ new Map();
780
+ /**
781
+ * Las redes soportadas.
782
+ *
783
+ * **La lista crece varias veces al año.** No la copies a una constante tuya: pídela.
784
+ */
785
+ async socialNetworks(options) {
786
+ return this.cached(
787
+ "/social_networks",
788
+ () => this.httpGet("/social_networks", void 0, options)
789
+ );
790
+ }
791
+ /** Las redes que aceptan publicaciones. Ni WhatsApp ni Google Business están. */
792
+ async allowedSocialPublications(options) {
793
+ return this.cached(
794
+ "/allowed_social_publications",
795
+ () => this.httpGet("/allowed_social_publications", void 0, options)
796
+ );
797
+ }
798
+ /**
799
+ * Las redes con conversaciones.
800
+ *
801
+ * Va en `POST` y no en `GET`, que es raro y es así: es la ruta que hay. No manda cuerpo.
802
+ */
803
+ async allowedSocialMessages(options) {
804
+ return this.cached(
805
+ "/allowed_social_messages",
806
+ () => this.httpPost("/allowed_social_messages", void 0, options)
807
+ );
808
+ }
809
+ /**
810
+ * La matriz red → qué sabe hacer: publicar, mensajes, productos, webhooks, menú persistente y
811
+ * comentarios.
812
+ *
813
+ * Es lo que evita ofrecer una cuenta en una pantalla que su red no soporta — WhatsApp en el
814
+ * compositor, LinkedIn en el chat.
815
+ */
816
+ async socialCapabilities(options) {
817
+ return this.cached(
818
+ "/social_capabilities",
819
+ () => this.httpGet("/social_capabilities", void 0, options)
820
+ );
821
+ }
822
+ /**
823
+ * La matriz red → qué se puede hacer con un comentario: responder, ocultar, borrar el propio y
824
+ * borrar el de otro.
825
+ *
826
+ * Va **aparte** de {@link socialCapabilities} porque aquélla es `{[capacidad]: boolean}` y esto
827
+ * es un objeto por red: meterlo dentro rompería su forma. Que la red tenga comentarios no dice
828
+ * lo suficiente — Instagram, X y Bluesky no dejan borrar el de otro, LinkedIn no tiene
829
+ * "ocultar", y Google Business sólo deja borrar **nuestra propia respuesta**.
830
+ */
831
+ async socialCommentActions(options) {
832
+ return this.cached(
833
+ "/social_comment_actions",
834
+ () => this.httpGet("/social_comment_actions", void 0, options)
835
+ );
836
+ }
837
+ /**
838
+ * Los topes de cada red, por los que el servidor valida.
839
+ *
840
+ * Bluesky lleva **dos** cuentas del mismo texto y en unidades distintas: 300 grafemas en
841
+ * `characters` y 3.000 bytes en `max_post_bytes`. `.length` miente en las dos direcciones —un
842
+ * emoji de familia es UN grafema y 25 bytes—, así que un contador que use `.length` da por
843
+ * bueno lo que la API rechaza y al revés.
844
+ */
845
+ async socialLimits(options) {
846
+ return this.cached(
847
+ "/social_limits",
848
+ () => this.httpGet("/social_limits", void 0, options)
849
+ );
850
+ }
851
+ /** Los topes de una publicación que no dependen de la red: hoy, cuántos reintentos manuales admite. */
852
+ async publicationLimits(options) {
853
+ return this.cached(
854
+ "/publication_limits",
855
+ () => this.httpGet("/publication_limits", void 0, options)
856
+ );
857
+ }
858
+ /**
859
+ * Los recortes que acepta cada red.
860
+ *
861
+ * Se indexa por red **y por formato** (`facebook`, `facebook_reels`, `facebook_stories`), así
862
+ * que no todas las claves son una red. `values` y `text` son arrays paralelos: mismo índice,
863
+ * mismo recorte.
864
+ */
865
+ async allowedAspectRatios(options) {
866
+ return this.cached(
867
+ "/allowed_aspect_ratios",
868
+ () => this.httpGet("/allowed_aspect_ratios", void 0, options)
869
+ );
870
+ }
871
+ /** Tira la caché. Para un proceso largo que quiera enterarse de una red nueva sin reiniciar. */
872
+ clearCache() {
873
+ this.cache.clear();
874
+ }
875
+ /**
876
+ * Guarda la PROMESA, no el resultado: dos llamadas a la vez comparten una petición en vez de
877
+ * lanzar dos. Si falla, la entrada se retira para que el siguiente intento vuelva a pedirla —
878
+ * cachear un fallo de red deja la instancia rota para siempre.
879
+ */
880
+ cached(key, fetch) {
881
+ const hit = this.cache.get(key);
882
+ if (hit) {
883
+ return hit;
884
+ }
885
+ const pending = fetch().catch((error) => {
886
+ this.cache.delete(key);
887
+ throw error;
888
+ });
889
+ this.cache.set(key, pending);
890
+ return pending;
891
+ }
892
+ };
893
+
894
+ // src/resources/clients.ts
895
+ var ClientsResource = class extends Resource {
896
+ /** Los clientes que puede ver quien llama. Con credenciales de app, el suyo. */
897
+ async list(options = {}) {
898
+ return this.getList("/clients", "clients", listQuery(options), options);
899
+ }
900
+ /** Los clientes, página a página, sin tener que llevar el `offset` a mano. */
901
+ iterate(options = {}) {
902
+ return iteratePages((page) => this.list({ ...options, ...page }), options);
903
+ }
904
+ /** La ficha de un cliente. */
905
+ async get(idClient, options = {}) {
906
+ return this.getOne(
907
+ `/clients/${requireId(idClient, "idClient")}`,
908
+ "client",
909
+ options.getUse ? { getUse: true } : void 0,
910
+ options
911
+ );
912
+ }
913
+ /** Cambia lo poco que de un cliente se puede cambiar: hoy, su nombre. */
914
+ async update(idClient, body, options = {}) {
915
+ return this.putOne(`/clients/${requireId(idClient, "idClient")}`, "client", body, options);
916
+ }
917
+ /** Las organizaciones RAÍZ de un cliente. Las hijas cuelgan de cada una. */
918
+ async organizations(idClient, options = {}) {
919
+ return this.getList(
920
+ `/clients/${requireId(idClient, "idClient")}/organizations`,
921
+ "organizations",
922
+ { ...listQuery(options), name: options.name },
923
+ options
924
+ );
925
+ }
926
+ /** Las organizaciones raíz de un cliente, encadenando páginas. */
927
+ iterateOrganizations(idClient, options = {}) {
928
+ return iteratePages(
929
+ (page) => this.organizations(idClient, { ...options, ...page }),
930
+ options
931
+ );
932
+ }
933
+ /** Crea una organización raíz. Lo que se le asigne se descuenta de lo que el cliente tiene. */
934
+ async createOrganization(idClient, body, options = {}) {
935
+ return this.postOne(
936
+ `/clients/${requireId(idClient, "idClient")}/organizations`,
937
+ "organization",
938
+ body,
939
+ options
940
+ );
941
+ }
942
+ /** Cambia una organización raíz: su nombre o el cupo que tiene asignado. */
943
+ async updateOrganization(idClient, idOrganization, body, options = {}) {
944
+ return this.putOne(
945
+ `/clients/${requireId(idClient, "idClient")}/organizations/${requireId(idOrganization, "idOrganization")}`,
946
+ "organization",
947
+ body,
948
+ options
949
+ );
950
+ }
951
+ /**
952
+ * Borra una organización raíz **con todo lo que tiene dentro**: sus organizaciones hijas, sus
953
+ * cuentas, sus publicaciones, sus ficheros y sus comentarios. No se deshace.
954
+ */
955
+ async deleteOrganization(idClient, idOrganization, options = {}) {
956
+ await this.httpDelete(
957
+ `/clients/${requireId(idClient, "idClient")}/organizations/${requireId(idOrganization, "idOrganization")}`,
958
+ void 0,
959
+ options
960
+ );
961
+ }
962
+ };
963
+ function listQuery(options) {
964
+ return {
965
+ offset: options.offset,
966
+ limit: options.limit,
967
+ //`getUse` sólo se manda cuando se pide: un `getUse=false` en la query es ruido en el log.
968
+ getUse: options.getUse ? true : void 0
969
+ };
970
+ }
971
+
972
+ // src/resources/organizations.ts
973
+ var OrganizationsResource = class extends Resource {
974
+ /** La ficha de una organización. */
975
+ async get(idOrganization, options = {}) {
976
+ return this.getOne(
977
+ `/organizations/${requireId(idOrganization, "idOrganization")}`,
978
+ "organization",
979
+ options.getUse ? { getUse: true } : void 0,
980
+ options
981
+ );
982
+ }
983
+ /** Cambia el nombre de una organización o el cupo que tiene asignado. */
984
+ async update(idOrganization, body, options = {}) {
985
+ return this.putOne(
986
+ `/organizations/${requireId(idOrganization, "idOrganization")}`,
987
+ "organization",
988
+ body,
989
+ options
990
+ );
991
+ }
992
+ /**
993
+ * Borra una organización **con todo lo que tiene dentro**: sus hijas, sus cuentas, sus
994
+ * publicaciones, sus ficheros y sus comentarios. No se deshace.
995
+ */
996
+ async remove(idOrganization, options = {}) {
997
+ await this.httpDelete(
998
+ `/organizations/${requireId(idOrganization, "idOrganization")}`,
999
+ void 0,
1000
+ options
1001
+ );
1002
+ }
1003
+ /** Las organizaciones que cuelgan de ésta. */
1004
+ async children(idOrganization, options = {}) {
1005
+ return this.getList(
1006
+ `/organizations/${requireId(idOrganization, "idOrganization")}/organizations`,
1007
+ "organizations",
1008
+ {
1009
+ offset: options.offset,
1010
+ limit: options.limit,
1011
+ name: options.name,
1012
+ getUse: options.getUse ? true : void 0
1013
+ },
1014
+ options
1015
+ );
1016
+ }
1017
+ /** Las organizaciones hijas, encadenando páginas. */
1018
+ iterateChildren(idOrganization, options = {}) {
1019
+ return iteratePages(
1020
+ (page) => this.children(idOrganization, { ...options, ...page }),
1021
+ options
1022
+ );
1023
+ }
1024
+ /** Crea una organización hija con el cupo que se le reparta del plan de ésta. */
1025
+ async createChild(idOrganization, body, options = {}) {
1026
+ return this.postOne(
1027
+ `/organizations/${requireId(idOrganization, "idOrganization")}/organizations`,
1028
+ "organization",
1029
+ body,
1030
+ options
1031
+ );
1032
+ }
1033
+ /**
1034
+ * Lo que esta organización puede usar de verdad, con la cascada ya resuelta: su plan propio, o
1035
+ * el del primer padre que tenga uno, o el resto sin repartir del cliente.
1036
+ *
1037
+ * Es lo que hay que mirar antes de conectar una cuenta o programar una publicación, no
1038
+ * `organization.actual_plan`.
1039
+ */
1040
+ async limits(idOrganization, options = {}) {
1041
+ return this.httpGet(
1042
+ `/organizations/${requireId(idOrganization, "idOrganization")}/limits`,
1043
+ void 0,
1044
+ options
1045
+ );
1046
+ }
1047
+ /**
1048
+ * El consumo de esta organización y lo que ya tiene repartido a sus hijas.
1049
+ *
1050
+ * Es un atajo de `get(id, {getUse: true})` que devuelve sólo las dos cifras, que es lo que se
1051
+ * quiere cuando se está pintando una barra de "3 de 5 cuentas".
1052
+ */
1053
+ async use(idOrganization, options = {}) {
1054
+ const organization = await this.get(idOrganization, { ...options, getUse: true });
1055
+ return { actual_use: organization.actual_use, actual_asigned: organization.actual_asigned };
1056
+ }
1057
+ /**
1058
+ * Emite el token temporal con el que **una persona** conecta una cuenta social a esta
1059
+ * organización. Es la única forma que tiene una app de que se le conecte una cuenta.
1060
+ *
1061
+ * Y es el reverso exacto del resto del flujo: éste es el endpoint que **exige credenciales de
1062
+ * app** —con un token de usuario contesta 514—, mientras que los tres que vienen después
1063
+ * (`accounts.connectLinks`, `accounts.connect`, `accounts.enable`) las rechazan con un 519.
1064
+ *
1065
+ * Vuelven las dos formas del mismo credencial, y las dos sirven:
1066
+ *
1067
+ * - **`url`** — el camino alojado. Se redirige al usuario ahí y PlanVortex se encarga de la
1068
+ * elección de red, del OAuth y de la pantalla donde elige qué cuentas dar de alta. Es lo que
1069
+ * hace el ejemplo `examples/connect-flow`, y lo que casi todo el mundo quiere.
1070
+ * - **`token`** — el credencial suelto, para `pv.asTemporalToken(token)` cuando la interfaz la
1071
+ * pone el integrador.
1072
+ *
1073
+ * Caduca en una hora y **sólo vale para esta organización**: usarlo contra otra contesta 1101.
1074
+ */
1075
+ async createConnectToken(idOrganization, options = {}) {
1076
+ return this.httpGet(
1077
+ `/organizations/${requireId(idOrganization, "idOrganization")}/temporal_connect_token`,
1078
+ { social_network: options.social_network, redirect_uri: options.redirect_uri },
1079
+ options
1080
+ );
1081
+ }
1082
+ };
1083
+
1084
+ // src/resources/publications.ts
1085
+ var PublicationsResource = class extends Resource {
1086
+ /**
1087
+ * Crea una publicación en una cuenta.
1088
+ *
1089
+ * Sin `publish_date` se envía en esta misma petición y la respuesta ya dice si salió
1090
+ * (`state: "sended"`) o si falló y por qué. Con fecha futura queda `ready`.
1091
+ *
1092
+ * ```ts
1093
+ * const publication = await pv.publications.create(orgId, accountId, {
1094
+ * social_network: "instagram",
1095
+ * text: "Nuevo horno, nuevas hogazas",
1096
+ * files: [upload._id],
1097
+ * publish_date: new Date("2026-09-01T10:00:00Z"),
1098
+ * });
1099
+ * ```
1100
+ */
1101
+ async create(idOrganization, idAccount, body, options = {}) {
1102
+ return this.postOne(
1103
+ `/organizations/${requireId(idOrganization, "idOrganization")}/accounts/${requireId(idAccount, "idAccount")}/publish`,
1104
+ "publication",
1105
+ serializeInput(body),
1106
+ options
1107
+ );
1108
+ }
1109
+ /** Una publicación, con sus ficheros y su cuenta ya resueltos. */
1110
+ async get(idOrganization, idPublication, options = {}) {
1111
+ return this.getOne(
1112
+ this.path(idOrganization, idPublication),
1113
+ "publication",
1114
+ void 0,
1115
+ options
1116
+ );
1117
+ }
1118
+ /** Las publicaciones de una organización. */
1119
+ async list(idOrganization, options = {}) {
1120
+ return this.getList(
1121
+ `/organizations/${requireId(idOrganization, "idOrganization")}/publish`,
1122
+ "publications",
1123
+ listQuery2(options),
1124
+ options
1125
+ );
1126
+ }
1127
+ /** Las publicaciones de una organización, encadenando páginas. */
1128
+ iterate(idOrganization, options = {}) {
1129
+ return iteratePages(
1130
+ (page) => this.list(idOrganization, { ...options, ...page }),
1131
+ options
1132
+ );
1133
+ }
1134
+ /** Las publicaciones de UNA cuenta. Mismos filtros que {@link list}. */
1135
+ async listByAccount(idOrganization, idAccount, options = {}) {
1136
+ return this.getList(
1137
+ `/organizations/${requireId(idOrganization, "idOrganization")}/accounts/${requireId(idAccount, "idAccount")}/publish`,
1138
+ "publications",
1139
+ listQuery2(options),
1140
+ options
1141
+ );
1142
+ }
1143
+ /**
1144
+ * Cambia una publicación que todavía no ha salido. Una `sended` devuelve el error 921.
1145
+ *
1146
+ * Editar PONE EL CONTADOR DE REINTENTOS A CERO: el contador cuenta intentos de publicar *ese*
1147
+ * contenido, y acabas de cambiarlo. Es además la salida cuando se agotan los tres.
1148
+ */
1149
+ async update(idOrganization, idPublication, body, options = {}) {
1150
+ return this.putOne(
1151
+ this.path(idOrganization, idPublication),
1152
+ "publication",
1153
+ serializeInput(body),
1154
+ options
1155
+ );
1156
+ }
1157
+ /**
1158
+ * Borra una publicación **y también el post en la red social**.
1159
+ *
1160
+ * En X borrar cuesta créditos: sin ellos devuelve un 940 en vez de un error genérico.
1161
+ */
1162
+ async remove(idOrganization, idPublication, options = {}) {
1163
+ await this.httpDelete(this.path(idOrganization, idPublication), void 0, options);
1164
+ }
1165
+ /**
1166
+ * Vuelve a intentar una publicación que falló, sin tocar su contenido.
1167
+ *
1168
+ * Se reintenta EN LA PETICIÓN, así que la respuesta ya dice si esta vez salió. Cada llamada
1169
+ * gasta un reintento aunque vuelva a fallar por el contenido; sólo los créditos de X cortan
1170
+ * antes de gastarlo. Una publicación que no está en `withErrors` devuelve un 949, y agotar el
1171
+ * tope, un 950.
1172
+ */
1173
+ async retry(idOrganization, idPublication, options = {}) {
1174
+ return this.httpPost(
1175
+ `${this.path(idOrganization, idPublication)}/retry`,
1176
+ void 0,
1177
+ options
1178
+ );
1179
+ }
1180
+ /**
1181
+ * Pide las métricas A LA RED, en vivo, y devuelve su desglose crudo.
1182
+ *
1183
+ * **En X esto cuesta un crédito por lectura.** Para pintar una gráfica usa {@link stats}, que
1184
+ * lee lo ya medido y no cuesta nada.
1185
+ */
1186
+ async metrics(idOrganization, idPublication, options = {}) {
1187
+ return this.httpGet(
1188
+ `${this.path(idOrganization, idPublication)}/metrics`,
1189
+ void 0,
1190
+ options
1191
+ );
1192
+ }
1193
+ /**
1194
+ * La evolución medida de una publicación: una fila por día, más su última medición.
1195
+ *
1196
+ * Es lectura pura de lo guardado: mirar la gráfica no llama a la red y no cuesta créditos. Una
1197
+ * `series` vacía es una respuesta válida —recién enviada, o una red sin estadísticas—, no un
1198
+ * error. Cada `metrics` es el ACUMULADO a esa fecha, no el incremento del día.
1199
+ */
1200
+ async stats(idOrganization, idPublication, options = {}) {
1201
+ return this.httpGet(
1202
+ `${this.path(idOrganization, idPublication)}/stats`,
1203
+ void 0,
1204
+ options
1205
+ );
1206
+ }
1207
+ /**
1208
+ * Lo que hay publicado en el muro de la cuenta **según la red**, no según PlanVortex: incluye
1209
+ * lo que se publicó por fuera.
1210
+ *
1211
+ * **En X cuesta un crédito por elemento leído**, así que el `limit` es dinero.
1212
+ */
1213
+ async listOnNetwork(idOrganization, idAccount, options = {}) {
1214
+ return this.getList(
1215
+ `/organizations/${requireId(idOrganization, "idOrganization")}/accounts/${requireId(idAccount, "idAccount")}/social_publications`,
1216
+ "publications",
1217
+ { offset: options.offset, limit: options.limit },
1218
+ options
1219
+ );
1220
+ }
1221
+ path(idOrganization, idPublication) {
1222
+ return `/organizations/${requireId(idOrganization, "idOrganization")}/publish/${requireId(idPublication, "idPublication")}`;
1223
+ }
1224
+ };
1225
+ function listQuery2(options) {
1226
+ return {
1227
+ offset: options.offset,
1228
+ limit: options.limit,
1229
+ from_date: options.from_date,
1230
+ to_date: options.to_date,
1231
+ search: options.search,
1232
+ //El servidor lo lee como el literal "true": un `false` no cambia nada, así que se omite.
1233
+ orderByPublish: options.orderByPublish ? true : void 0,
1234
+ state: options.state,
1235
+ accounts: options.accounts,
1236
+ social_network: options.social_network
1237
+ };
1238
+ }
1239
+ function serializeInput(body) {
1240
+ const { publish_date, ...rest } = body;
1241
+ return {
1242
+ ...rest,
1243
+ ...publish_date === void 0 ? {} : { publish_date: typeof publish_date === "string" ? publish_date : publish_date.toISOString() }
1244
+ };
1245
+ }
1246
+ var MIME_BY_EXTENSION = {
1247
+ jpg: "image/jpeg",
1248
+ jpeg: "image/jpeg",
1249
+ png: "image/png",
1250
+ gif: "image/gif",
1251
+ heic: "image/heic",
1252
+ heif: "image/heif",
1253
+ mp4: "video/mp4"
1254
+ };
1255
+ function guessContentType(filename) {
1256
+ const extension = filename.split(".").pop()?.toLowerCase();
1257
+ return extension ? MIME_BY_EXTENSION[extension] : void 0;
1258
+ }
1259
+ function isBinary(value) {
1260
+ return value instanceof Uint8Array;
1261
+ }
1262
+ async function toFilePart(input) {
1263
+ const { file } = input;
1264
+ if (typeof file === "string") {
1265
+ const filename = input.filename ?? path.basename(file);
1266
+ const type = input.contentType ?? guessContentType(filename);
1267
+ if (!type) {
1268
+ throw new PlanVortexConfigError(
1269
+ `No se puede deducir el tipo de "${filename}": pasa contentType. El servidor decide si es imagen o v\xEDdeo por el tipo que mandes, no por el contenido.`
1270
+ );
1271
+ }
1272
+ return { blob: await openFileAsBlob(file, type), filename };
1273
+ }
1274
+ if (isBinary(file)) {
1275
+ const filename = input.filename;
1276
+ if (!filename) {
1277
+ throw new PlanVortexConfigError("Un Buffer no lleva nombre: pasa filename junto al fichero.");
1278
+ }
1279
+ const type = input.contentType ?? guessContentType(filename);
1280
+ if (!type) {
1281
+ throw new PlanVortexConfigError(
1282
+ `No se puede deducir el tipo de "${filename}": pasa contentType.`
1283
+ );
1284
+ }
1285
+ return { blob: new Blob([file.slice()], { type }), filename };
1286
+ }
1287
+ if (file instanceof Blob) {
1288
+ const filename = input.filename ?? (file instanceof File ? file.name : void 0);
1289
+ if (!filename) {
1290
+ throw new PlanVortexConfigError("Un Blob sin nombre: pasa filename junto al fichero.");
1291
+ }
1292
+ const type = input.contentType ?? (file.type || guessContentType(filename));
1293
+ if (!type) {
1294
+ throw new PlanVortexConfigError(
1295
+ `No se puede deducir el tipo de "${filename}": pasa contentType.`
1296
+ );
1297
+ }
1298
+ return { blob: file.type === type ? file : file.slice(0, file.size, type), filename };
1299
+ }
1300
+ throw new PlanVortexConfigError("El fichero tiene que ser una ruta, un Buffer o un Blob.");
1301
+ }
1302
+ async function buildUploadForm(input) {
1303
+ const { blob, filename } = await toFilePart(input);
1304
+ const form = new FormData();
1305
+ form.append("file", blob, filename);
1306
+ return form;
1307
+ }
1308
+ async function openFileAsBlob(path, type) {
1309
+ const fs = await import('fs');
1310
+ const openAsBlob = fs.openAsBlob;
1311
+ if (typeof openAsBlob === "function") {
1312
+ return openAsBlob(path, { type });
1313
+ }
1314
+ const { readFile } = await import('fs/promises');
1315
+ return new Blob([await readFile(path)], { type });
1316
+ }
1317
+
1318
+ // src/resources/uploads.ts
1319
+ var UploadsResource = class extends Resource {
1320
+ /**
1321
+ * Sube un fichero. Admite una ruta en disco, un `Buffer` o un `Blob`.
1322
+ *
1323
+ * La ruta es la buena para lo grande: es la única forma que no pasa el fichero por memoria.
1324
+ *
1325
+ * ```ts
1326
+ * await pv.uploads.create(orgId, { file: "./hogaza.jpg" });
1327
+ * await pv.uploads.create(orgId, { file: bytes, filename: "hogaza.jpg" });
1328
+ * ```
1329
+ */
1330
+ async create(idOrganization, input, options = {}) {
1331
+ const form = await buildUploadForm(input);
1332
+ return this.postOne(
1333
+ `/organizations/${requireId(idOrganization, "idOrganization")}/uploads`,
1334
+ "upload",
1335
+ form,
1336
+ options
1337
+ );
1338
+ }
1339
+ /**
1340
+ * Los ficheros de la biblioteca.
1341
+ *
1342
+ * No salen aquí los recortes que la plataforma se hace para sí misma (`is_temporal`) ni las
1343
+ * portadas de vídeo, que son otro upload y viajan dentro del suyo.
1344
+ */
1345
+ async list(idOrganization, options = {}) {
1346
+ return this.getList(
1347
+ `/organizations/${requireId(idOrganization, "idOrganization")}/uploads`,
1348
+ "uploads",
1349
+ { offset: options.offset, limit: options.limit },
1350
+ options
1351
+ );
1352
+ }
1353
+ /** Los ficheros de la biblioteca, encadenando páginas. */
1354
+ iterate(idOrganization, options = {}) {
1355
+ return iteratePages((page) => this.list(idOrganization, { ...options, ...page }), options);
1356
+ }
1357
+ /** Un fichero. Pídelo de nuevo cuando necesites un `public_path` vigente. */
1358
+ async get(idOrganization, idUpload, options = {}) {
1359
+ return this.getOne(this.path(idOrganization, idUpload), "upload", void 0, options);
1360
+ }
1361
+ /** Cambia la portada de un vídeo. Ojo con `cover_offset`: se escribe siempre (ver arriba). */
1362
+ async update(idOrganization, idUpload, body, options = {}) {
1363
+ return this.putOne(this.path(idOrganization, idUpload), "upload", body, options);
1364
+ }
1365
+ /**
1366
+ * Borra un fichero.
1367
+ *
1368
+ * Con `force` se borra aunque una publicación siga apuntándolo; sin él, un fichero en uso se
1369
+ * conserva y sólo se saca de la biblioteca.
1370
+ */
1371
+ async remove(idOrganization, idUpload, options = {}) {
1372
+ await this.httpDelete(
1373
+ this.path(idOrganization, idUpload),
1374
+ options.force ? { forceDelete: true } : void 0,
1375
+ options
1376
+ );
1377
+ }
1378
+ /**
1379
+ * Trae a la biblioteca ficheros elegidos en una integración (el selector de Drive).
1380
+ *
1381
+ * La respuesta es PARCIAL a propósito: mira `errors` aunque `uploads` traiga algo — de seis
1382
+ * ficheros pueden entrar cuatro. Un `2204` significa que el proveedor no da los bytes: un
1383
+ * documento nativo de Google no tiene fichero que descargar.
1384
+ */
1385
+ async import(idOrganization, idIntegration, files, options = {}) {
1386
+ return this.httpPost(
1387
+ `/organizations/${requireId(idOrganization, "idOrganization")}/uploads/import`,
1388
+ { id_integration: requireId(idIntegration, "idIntegration"), files },
1389
+ options
1390
+ );
1391
+ }
1392
+ path(idOrganization, idUpload) {
1393
+ return `/organizations/${requireId(idOrganization, "idOrganization")}/uploads/${requireId(idUpload, "idUpload")}`;
1394
+ }
1395
+ };
1396
+
1397
+ // src/client.ts
1398
+ function buildUserAgent() {
1399
+ const runtime = typeof process !== "undefined" && process.versions?.node ? ` node/${process.versions.node}` : "";
1400
+ return `planvortex-node/${VERSION}${runtime}`;
1401
+ }
1402
+ function readEnv(name) {
1403
+ return typeof process !== "undefined" ? process.env?.[name] : void 0;
1404
+ }
1405
+ function looksLikeBrowser() {
1406
+ const maybeWindow = globalThis.window;
1407
+ return maybeWindow !== void 0 && maybeWindow.document !== void 0;
1408
+ }
1409
+ var PlanVortex = class _PlanVortex {
1410
+ /** Sin barra final. Útil para componer una URL a mano cuando haga falta. */
1411
+ baseUrl;
1412
+ /** Metadatos estáticos de las redes: qué hay, qué sabe hacer cada una y sus límites. Cacheado. */
1413
+ catalog;
1414
+ /** Clientes: el plan contratado y sus organizaciones raíz. */
1415
+ clients;
1416
+ /** Organizaciones: la ficha, las hijas, y el cupo que tienen y gastan. */
1417
+ organizations;
1418
+ /** Cuentas sociales conectadas. Conectar una es otra cosa: hace falta una persona (§ fase 9). */
1419
+ accounts;
1420
+ /** La biblioteca de ficheros de una organización. */
1421
+ uploads;
1422
+ /** Publicaciones: crear, programar, reintentar y medir. */
1423
+ publications;
1424
+ http;
1425
+ auth;
1426
+ options;
1427
+ constructor(options = {}) {
1428
+ if (looksLikeBrowser() && options.dangerouslyAllowBrowser !== true) {
1429
+ throw new PlanVortexConfigError(
1430
+ "planvortex es un paquete de servidor: el client_secret no puede vivir en un navegador. Para conectar una cuenta desde el front usa el temporal_connect_token."
1431
+ );
1432
+ }
1433
+ this.options = options;
1434
+ this.baseUrl = (options.baseUrl ?? readEnv("PLANVORTEX_BASE_URL") ?? PLANVORTEX_API_URL).replace(
1435
+ /\/+$/,
1436
+ ""
1437
+ );
1438
+ this.http = new HttpClient({
1439
+ baseUrl: this.baseUrl,
1440
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
1441
+ ...options.retry === void 0 ? {} : { retry: options.retry },
1442
+ ...options.hooks === void 0 ? {} : { hooks: options.hooks },
1443
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
1444
+ headers: { "user-agent": buildUserAgent() }
1445
+ });
1446
+ const clientId = options.clientId ?? readEnv("PLANVORTEX_CLIENT_ID");
1447
+ const clientSecret = options.clientSecret ?? readEnv("PLANVORTEX_CLIENT_SECRET");
1448
+ if (options.accessToken) {
1449
+ this.auth = new StaticTokenAuth(options.accessToken);
1450
+ } else if (clientId && clientSecret) {
1451
+ this.auth = new ClientCredentialsAuth(this.http, {
1452
+ clientId,
1453
+ clientSecret,
1454
+ scope: options.scope
1455
+ });
1456
+ } else {
1457
+ throw new PlanVortexConfigError(
1458
+ "Faltan credenciales: pasa clientId y clientSecret (o PLANVORTEX_CLIENT_ID y PLANVORTEX_CLIENT_SECRET en el entorno), o un accessToken."
1459
+ );
1460
+ }
1461
+ this.catalog = new CatalogResource(this);
1462
+ this.clients = new ClientsResource(this);
1463
+ this.organizations = new OrganizationsResource(this);
1464
+ this.accounts = new AccountsResource(this);
1465
+ this.uploads = new UploadsResource(this);
1466
+ this.publications = new PublicationsResource(this);
1467
+ }
1468
+ /**
1469
+ * Una petición autenticada. Es lo que usarán los recursos de las fases 6 y 7; un integrador no
1470
+ * debería necesitarla, pero está expuesta para no dejar a nadie tirado ante un endpoint que la
1471
+ * librería aún no cubra.
1472
+ *
1473
+ * Reintenta **una sola vez** ante los códigos de token (501 y 522), que llegan dentro de un 400.
1474
+ * Un token puede morir antes de su `expires_in` —un despliegue de Keycloak, la app revocada— y
1475
+ * ese caso se arregla pidiendo otro; si el segundo también falla, el error sale.
1476
+ */
1477
+ async request(request) {
1478
+ try {
1479
+ return await this.send(request);
1480
+ } catch (error) {
1481
+ if (!isTokenError(error)) {
1482
+ throw error;
1483
+ }
1484
+ this.auth.invalidate();
1485
+ return this.send(request);
1486
+ }
1487
+ }
1488
+ /**
1489
+ * El mismo cliente, con la misma forma, autenticado con un token temporal de conexión.
1490
+ *
1491
+ * Es la pieza del flujo de conexión de cuentas (§ trampa 2): una app **no** puede conectar una
1492
+ * cuenta de Instagram —eso es un OAuth con una persona delante—, así que emite un token
1493
+ * temporal, se lo pasa a su usuario y con él se piden los `connect_links`. El token va atado a
1494
+ * una sola organización: usarlo contra otra devuelve el error 1101.
1495
+ */
1496
+ asTemporalToken(token) {
1497
+ return new _PlanVortex({
1498
+ ...this.options,
1499
+ clientId: void 0,
1500
+ clientSecret: void 0,
1501
+ accessToken: token,
1502
+ baseUrl: this.baseUrl
1503
+ });
1504
+ }
1505
+ async send(request) {
1506
+ const token = await this.auth.getToken();
1507
+ return this.http.request({
1508
+ ...request,
1509
+ headers: { ...request.headers, authorization: `Bearer ${token}` }
1510
+ });
1511
+ }
1512
+ };
1513
+
1514
+ // src/types.ts
1515
+ function accountId(publication) {
1516
+ const value = publication.id_account;
1517
+ return typeof value === "string" ? value : value._id;
1518
+ }
1519
+ function account(publication) {
1520
+ const value = publication.id_account;
1521
+ return typeof value === "string" ? void 0 : value;
1522
+ }
1523
+ function messageDirection(message) {
1524
+ if (message.from_contact_id) {
1525
+ return "incoming";
1526
+ }
1527
+ return message.contact_id ? "outgoing" : "unknown";
1528
+ }
1529
+ function messageContactId(message) {
1530
+ const value = message.from_contact_id ?? message.contact_id;
1531
+ if (value === void 0) {
1532
+ return void 0;
1533
+ }
1534
+ return typeof value === "string" ? value : value._id;
1535
+ }
1536
+ function messageContact(message) {
1537
+ const value = message.from_contact_id ?? message.contact_id;
1538
+ return value === void 0 || typeof value === "string" ? void 0 : value;
1539
+ }
1540
+ function messageFiles(message) {
1541
+ const files = message.message_options?.files ?? [];
1542
+ return files.filter((file) => typeof file !== "string");
1543
+ }
1544
+ function messageFileIds(message) {
1545
+ const files = message.message_options?.files ?? [];
1546
+ return files.map((file) => typeof file === "string" ? file : file._id);
1547
+ }
1548
+
1549
+ exports.AccountError = AccountError;
1550
+ exports.AccountsResource = AccountsResource;
1551
+ exports.AiPlanError = AiPlanError;
1552
+ exports.AuthError = AuthError;
1553
+ exports.CatalogResource = CatalogResource;
1554
+ exports.ClientsResource = ClientsResource;
1555
+ exports.ContactError = ContactError;
1556
+ exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
1557
+ exports.DEFAULT_RETRY = DEFAULT_RETRY;
1558
+ exports.DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
1559
+ exports.FileError = FileError;
1560
+ exports.IntegrationError = IntegrationError;
1561
+ exports.MAX_PAGES = MAX_PAGES;
1562
+ exports.MessagingError = MessagingError;
1563
+ exports.NO_ERROR_CODE = NO_ERROR_CODE;
1564
+ exports.OrganizationError = OrganizationError;
1565
+ exports.OrganizationsResource = OrganizationsResource;
1566
+ exports.PLANVORTEX_API_URL = PLANVORTEX_API_URL;
1567
+ exports.PLANVORTEX_ERROR_RANGES = PLANVORTEX_ERROR_RANGES;
1568
+ exports.PlanLimitError = PlanLimitError;
1569
+ exports.PlanVortex = PlanVortex;
1570
+ exports.PlanVortexAuthenticationError = PlanVortexAuthenticationError;
1571
+ exports.PlanVortexConfigError = PlanVortexConfigError;
1572
+ exports.PlanVortexConnectionError = PlanVortexConnectionError;
1573
+ exports.PlanVortexError = PlanVortexError;
1574
+ exports.ProductError = ProductError;
1575
+ exports.PublicationError = PublicationError;
1576
+ exports.PublicationsResource = PublicationsResource;
1577
+ exports.TOKEN_ERROR_CODES = TOKEN_ERROR_CODES;
1578
+ exports.TOKEN_REFRESH_MARGIN_MS = TOKEN_REFRESH_MARGIN_MS;
1579
+ exports.UploadsResource = UploadsResource;
1580
+ exports.UserError = UserError;
1581
+ exports.VERSION = VERSION;
1582
+ exports.account = account;
1583
+ exports.accountId = accountId;
1584
+ exports.errorFamilyForCode = errorFamilyForCode;
1585
+ exports.guessContentType = guessContentType;
1586
+ exports.isPlanVortexError = isPlanVortexError;
1587
+ exports.isTokenError = isTokenError;
1588
+ exports.messageContact = messageContact;
1589
+ exports.messageContactId = messageContactId;
1590
+ exports.messageDirection = messageDirection;
1591
+ exports.messageFileIds = messageFileIds;
1592
+ exports.messageFiles = messageFiles;
1593
+ //# sourceMappingURL=index.cjs.map
1594
+ //# sourceMappingURL=index.cjs.map