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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Talia Softworks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/taliasoftworks/PlanVortexNode/main/assets/logo-horizontal-dark.png">
3
+ <img src="https://raw.githubusercontent.com/taliasoftworks/PlanVortexNode/main/assets/logo-horizontal.png" alt="PlanVortex" width="300">
4
+ </picture>
5
+
6
+ # planvortex
7
+
8
+ Official Node.js client for the [PlanVortex](https://planvortex.com) API: connect social accounts,
9
+ schedule and publish posts, read comments and messages, and verify webhooks.
10
+
11
+ > **This is a server-side package. Do not use it in a browser.**
12
+ > Authenticating uses the `client_credentials` flow, which needs your `client_secret`. A secret
13
+ > inside a front-end bundle is your whole account handed over. To let _an end user_ connect their
14
+ > own social account from a browser, issue a **temporal connect token** instead — it lasts an hour,
15
+ > is tied to a single organization, and can only create accounts.
16
+
17
+ ```bash
18
+ npm install planvortex
19
+ ```
20
+
21
+ Requires **Node 20 or newer** (`fetch`, `FormData`, `Blob` and `fs.openAsBlob` are globals there)
22
+ and has **zero runtime dependencies**.
23
+
24
+ ## Status
25
+
26
+ **Early. Connecting accounts, the publishing path and the webhooks work; the inbox does not, yet.**
27
+
28
+ | Phase | What it adds | State |
29
+ | ----- | -------------------------------------------------------- | ------- |
30
+ | 3 | Package skeleton: dual ESM/CJS build, tests, CI, release | done |
31
+ | 4 | Transport, authentication and errors | done |
32
+ | 5 | Types generated from the OpenAPI specification | done |
33
+ | 6 | Resources: the publishing path | done |
34
+ | 7 | Resources: inbox and the rest | pending |
35
+ | 8 | Webhooks | done |
36
+ | 9 | The account connection flow | done |
37
+
38
+ ## Publishing
39
+
40
+ ```ts
41
+ import { PlanVortex } from "planvortex";
42
+
43
+ const pv = new PlanVortex({
44
+ clientId: process.env.PLANVORTEX_CLIENT_ID,
45
+ clientSecret: process.env.PLANVORTEX_CLIENT_SECRET,
46
+ });
47
+
48
+ // The accounts you can actually publish with. The server applies the same capability matrix it
49
+ // publishes at GET /social_capabilities, so you never keep your own table of what each network does.
50
+ const { data: accounts } = await pv.accounts.list(orgId, { capability: "publications" });
51
+
52
+ // A path is the form that does not read the file into memory. A Buffer or a Blob also work.
53
+ const upload = await pv.uploads.create(orgId, { file: "./hogaza.jpg" });
54
+
55
+ const publication = await pv.publications.create(orgId, accounts[0]._id, {
56
+ social_network: accounts[0].social_network,
57
+ text: "Nuevo horno, nuevas hogazas",
58
+ files: [upload._id],
59
+ publish_date: new Date("2026-09-01T10:00:00Z"),
60
+ });
61
+
62
+ // A publication whose content does not fit the network is NOT an error: it is saved in state
63
+ // `withErrors` with the reason inside. A try/catch alone reports it as published.
64
+ if (publication.state === "withErrors") {
65
+ console.error(publication.publication_errors.map((failure) => failure.message));
66
+ }
67
+ ```
68
+
69
+ A complete, runnable version is in [examples/publish.ts](examples/publish.ts).
70
+
71
+ Every listing pages the same way — `{data, total}` — and every one has an iterator that chains the
72
+ pages for you:
73
+
74
+ ```ts
75
+ const { data, total } = await pv.publications.list(orgId, { state: ["ready"], limit: 50 });
76
+
77
+ for await (const publication of pv.publications.iterate(orgId, { state: ["ready"] })) {
78
+ // ...
79
+ }
80
+ ```
81
+
82
+ ### What is available
83
+
84
+ | Resource | Methods |
85
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
86
+ | `pv.catalog` | `socialNetworks`, `socialLimits`, `socialCapabilities`, `socialCommentActions`, `allowedAspectRatios`, `publicationLimits`, `allowedSocialPublications`, `allowedSocialMessages` — all cached in memory per client |
87
+ | `pv.clients` | `list`, `iterate`, `get`, `update`, `organizations`, `iterateOrganizations`, `createOrganization`, `updateOrganization`, `deleteOrganization` |
88
+ | `pv.organizations` | `get`, `update`, `remove`, `children`, `iterateChildren`, `createChild`, `limits`, `use`, `createConnectToken` |
89
+ | `pv.accounts` | `list`, `iterate`, `get`, `update`, `remove`, `metrics`, `metricList`, `getPersistentMenu`, `setPersistentMenu`, `connectLinks`, `connect`, `enable` |
90
+ | `pv.uploads` | `create`, `list`, `iterate`, `get`, `update`, `remove`, `import` |
91
+ | `pv.publications` | `create`, `get`, `list`, `iterate`, `listByAccount`, `update`, `remove`, `retry`, `metrics`, `stats`, `listOnNetwork` |
92
+
93
+ Anything not covered yet is reachable through the generic `pv.request(...)`, and the response types
94
+ are already published, so you can annotate what comes back:
95
+
96
+ ```ts
97
+ import type { Comment } from "planvortex";
98
+
99
+ const { data } = await pv.request<{ comments: Comment[]; total: number }>({
100
+ method: "GET",
101
+ path: "/organizations/ORG_ID/comments",
102
+ });
103
+ ```
104
+
105
+ These types are generated from the same OpenAPI specification the
106
+ [documentation](https://planvortex.com/documentation) is rendered from, so they cannot describe an
107
+ endpoint that does not exist. One deliberate exception to "generated": every enumeration that grows
108
+ with the product — `SocialNetwork`, `PublicationState`, `FileFormat` — is **open**. The known values
109
+ autocomplete, and a network added to PlanVortex next month does not break your build.
110
+
111
+ ## Connecting a social account
112
+
113
+ **An app cannot connect one.** Authorizing Instagram is an OAuth flow with a person in front of it,
114
+ so app credentials are refused (error 519) by every endpoint of this flow. What an app does instead
115
+ is mint a one-hour token, hand it to its user, and wait for them to come back. Your `client_secret`
116
+ never leaves your server.
117
+
118
+ ```ts
119
+ // On your server, when your user asks to connect a social account:
120
+ const connect = await pv.organizations.createConnectToken(orgId, {
121
+ // Must be one of the app's registered redirect_urls, or you get error 532.
122
+ redirect_uri: "https://your-app.example/done",
123
+ });
124
+
125
+ response.redirect(connect.url); // PlanVortex takes over, and returns them to your redirect_uri
126
+ ```
127
+
128
+ `connect.url` is the hosted path, and the one you want: PlanVortex asks which network, runs the
129
+ OAuth, and shows the person which accounts to enable. `connect.token` is the same credential on its
130
+ own, for when you would rather render the picker yourself:
131
+
132
+ ```ts
133
+ const guest = pv.asTemporalToken(connect.token);
134
+ const links = await guest.accounts.connectLinks(orgId); // one authorization URL per network
135
+ ```
136
+
137
+ Three things that surprise everyone:
138
+
139
+ - **A network that cannot be connected right now simply does not appear** in `connectLinks()`. That
140
+ is an answer, not a failure — Discord in an organization that has not saved its own bot
141
+ credentials, for instance.
142
+ - **The network sends the user back to PlanVortex, not to you.** Its `redirect_uri` has to be
143
+ registered in the network's own app settings, so it can never be a URL of yours. Where _your_ user
144
+ ends up afterwards is the `redirect_uri` you passed to `createConnectToken`.
145
+ - **A connected account is not an enabled account.** One authorization can produce several — a
146
+ Facebook user with four pages — and none of them takes a plan slot or publishes until
147
+ `accounts.enable()` is called on it. That is also the call that answers 706 when the plan is full.
148
+
149
+ A complete, runnable version is in [examples/connect-flow.ts](examples/connect-flow.ts).
150
+
151
+ ## Webhooks
152
+
153
+ PlanVortex `POST`s to your app's `webhook_url` when something happens. **The body is an array of
154
+ changes**, and each one carries `field` telling you what it is.
155
+
156
+ ```ts
157
+ import express from "express";
158
+ import { planvortexWebhooks, isCommentChange, isMessageChange } from "planvortex/webhooks";
159
+
160
+ const app = express();
161
+
162
+ app.post(
163
+ "/webhooks/planvortex",
164
+ planvortexWebhooks({
165
+ secret: process.env.PLANVORTEX_CLIENT_SECRET!,
166
+ onChanges: async (changes) => {
167
+ for (const change of changes) {
168
+ if (isCommentChange(change)) await moderate(change.commentObj);
169
+ if (isMessageChange(change)) await reply(change.messageObj);
170
+ }
171
+ },
172
+ }),
173
+ );
174
+ ```
175
+
176
+ No `express.raw()` is needed in front: if nothing has parsed the body yet, the middleware reads the
177
+ stream itself. It answers 200 when your handler returns, 401 when the signature does not match, 400
178
+ when the body is not what it should be, and 500 when your handler throws.
179
+
180
+ Outside Express — Hono, Fastify, a Next route handler — use the framework-agnostic function:
181
+
182
+ ```ts
183
+ import { handleWebhookRequest } from "planvortex/webhooks";
184
+
185
+ const changes = handleWebhookRequest({
186
+ body: await request.text(), // the RAW body
187
+ headers: request.headers, // a Headers or a plain object
188
+ secret: process.env.PLANVORTEX_CLIENT_SECRET!,
189
+ });
190
+ ```
191
+
192
+ Both throw `WebhookSignatureError` when the signature is missing or wrong, and `WebhookBodyError`
193
+ when the body is not raw bytes, not JSON, or not an array. If you only want the check,
194
+ `verifyWebhookSignature({payload, signature, secret})` returns a boolean and never throws on a
195
+ malformed signature.
196
+
197
+ ### The events
198
+
199
+ | `field` | What happened | Where the payload is |
200
+ |---|---|---|
201
+ | `new_account` | An account was connected | — |
202
+ | `change_state_account` | An account changed state: broke, refreshed, disconnected | — |
203
+ | `messages` | A message came in | `messageObj` |
204
+ | `messaging_postbacks` | The contact pressed a button or a quick reply | `messageObj` |
205
+ | `messaging_seen` | The contact read the conversation | `messageObj`, when we have it |
206
+ | `messaging_error` | The network refused a message you sent | `messageObj.message_errors` |
207
+ | `comments` | A comment came in | `commentObj` |
208
+ | `integration_error` | An integration stopped working | `provider`, `error_code` |
209
+
210
+ `isAccountStateChange`, `isMessageChange`, `isCommentChange` and `isIntegrationErrorChange` narrow
211
+ a change to its own type. Use them rather than a `switch`: the union carries a member for the
212
+ `field`s this version does not know yet — the list grows — and TypeScript cannot rule that one out
213
+ of a `case`.
214
+
215
+ Two things about the payload that are easy to get wrong. An **integration** change carries neither
216
+ `id_account` nor `social_network`, because an integration hangs off the organization. And
217
+ `messageObj` arrives **populated**: `contact_id`, `from_contact_id` and `message_options.files`
218
+ carry whole objects rather than identifiers, which is what `messageContact`, `messageContactId`,
219
+ `messageDirection` and `messageFiles` are for.
220
+
221
+ **Meta repeats deliveries**, and PlanVortex does not retry a failed one. Deduplicate on
222
+ `commentObj.external_id`, and if your work is slow, queue it and return.
223
+
224
+ ## Errors
225
+
226
+ ```ts
227
+ import { PlanVortex, PlanLimitError, PlanVortexError } from "planvortex";
228
+
229
+ try {
230
+ await pv.publications.create(orgId, accountId, { social_network: "instagram", text: "..." });
231
+ } catch (error) {
232
+ if (error instanceof PlanLimitError) {
233
+ // Plan quota exhausted (codes 1300-1408). Retrying will not help.
234
+ } else if (error instanceof PlanVortexError) {
235
+ console.error(error.code, error.message, error.data, error.status);
236
+ }
237
+ }
238
+ ```
239
+
240
+ What the core does on your behalf: exchanges your credentials at `POST /oauth/token` and caches the
241
+ token, refreshes it a minute before it expires, collapses concurrent calls into a **single** token
242
+ request, retries 429/502/503/504 and network failures with exponential backoff and jitter, honours
243
+ `Retry-After`, and turns every error body into a typed class. It never retries a domain error, and
244
+ it never repeats a `POST` that reached the server.
245
+
246
+ ## Five things to know before you write any code
247
+
248
+ **Classify errors by `code`, never by the HTTP status.** Every domain error travels with HTTP 400 —
249
+ an expired token, a disconnected account, an exhausted plan quota and a text that is too long are
250
+ all 400. Only error 520 (permissions) answers 401. An `if (res.status === 401) refresh()` is a silent
251
+ bug: the token errors, 501 and 522, arrive inside a 400.
252
+
253
+ **An app cannot connect social accounts.** Authorizing Instagram is an OAuth flow with a person in
254
+ front of it, so those endpoints refuse app credentials. Your server issues a temporal connect token,
255
+ your end user completes the flow with it, and the account lands in your organization.
256
+
257
+ **A publication that does not fit the network is not an error.** It is stored with
258
+ `state: "withErrors"` and the reason in `publication_errors` — which is an **array**. Check the
259
+ state; a `try/catch` will not tell you.
260
+
261
+ **`upload.public_path` expires.** It is a signed URL, not a permanent link: identical within the
262
+ same hour, gone afterwards. Do not store it in your database — ask for the upload again.
263
+
264
+ **A webhook signature is computed over the raw body.** Not over a re-serialized copy of the parsed
265
+ JSON: the bytes differ and the signature never matches. Either let `planvortexWebhooks()` read the
266
+ stream, or put `express.raw({ type: "application/json" })` in front of that one route. A global
267
+ `express.json()` is what breaks it, and it breaks it silently.
268
+
269
+ ## Development
270
+
271
+ ```bash
272
+ npm install
273
+ npm run build # dual ESM + CJS with both sets of types
274
+ npm test # no network, no credentials
275
+ npm run typecheck
276
+ npm run lint
277
+ npm run generate # regenerate the OpenAPI bundle and the types
278
+ npm run check:exports # publint + arethetypeswrong
279
+ ```
280
+
281
+ ## License
282
+
283
+ MIT
@@ -0,0 +1,140 @@
1
+ // src/core/errors.ts
2
+ var PLANVORTEX_ERROR_RANGES = [
3
+ { from: 500, to: 541, family: "auth" },
4
+ { from: 601, to: 612, family: "user" },
5
+ { from: 700, to: 715, family: "account" },
6
+ { from: 800, to: 810, family: "file" },
7
+ { from: 900, to: 960, family: "publication" },
8
+ { from: 1e3, to: 1003, family: "general" },
9
+ { from: 1100, to: 1111, family: "organization" },
10
+ { from: 1200, to: 1207, family: "role" },
11
+ { from: 1300, to: 1307, family: "plan_limit" },
12
+ { from: 1400, to: 1408, family: "plan_limit" },
13
+ { from: 1500, to: 1512, family: "messaging" },
14
+ { from: 1600, to: 1601, family: "contact" },
15
+ { from: 1900, to: 1906, family: "payment" },
16
+ { from: 2e3, to: 2099, family: "product" },
17
+ { from: 2100, to: 2199, family: "ai_plan" },
18
+ { from: 2200, to: 2299, family: "integration" }
19
+ ];
20
+ var NO_ERROR_CODE = 0;
21
+ var TOKEN_ERROR_CODES = [501, 522];
22
+ var PlanVortexError = class extends Error {
23
+ /** El `code` del cuerpo, tal cual. {@link NO_ERROR_CODE} si el error no viene del catálogo. */
24
+ code;
25
+ /** La familia del rango: `auth`, `publication`, `plan_limit`... Ver {@link PLANVORTEX_ERROR_RANGES}. */
26
+ family;
27
+ /** El `data` del cuerpo. */
28
+ data;
29
+ /** Status HTTP, o `undefined` si nunca hubo respuesta. */
30
+ status;
31
+ /** `x-request-id`, si el despliegue lo pone. Hoy el servidor no lo emite; un proxy delante sí. */
32
+ requestId;
33
+ /** Segundos que pidió esperar la cabecera `Retry-After`, si llegó. */
34
+ retryAfter;
35
+ constructor(code, message, options = {}) {
36
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
37
+ this.name = new.target.name;
38
+ this.code = code;
39
+ this.family = options.family ?? errorFamilyForCode(code) ?? "unknown";
40
+ this.data = options.data ?? {};
41
+ this.status = options.status;
42
+ this.requestId = options.requestId;
43
+ this.retryAfter = options.retryAfter;
44
+ }
45
+ };
46
+ var AuthError = class extends PlanVortexError {
47
+ };
48
+ var UserError = class extends PlanVortexError {
49
+ };
50
+ var AccountError = class extends PlanVortexError {
51
+ };
52
+ var FileError = class extends PlanVortexError {
53
+ };
54
+ var PublicationError = class extends PlanVortexError {
55
+ };
56
+ var OrganizationError = class extends PlanVortexError {
57
+ };
58
+ var PlanLimitError = class extends PlanVortexError {
59
+ };
60
+ var MessagingError = class extends PlanVortexError {
61
+ };
62
+ var ContactError = class extends PlanVortexError {
63
+ };
64
+ var ProductError = class extends PlanVortexError {
65
+ };
66
+ var AiPlanError = class extends PlanVortexError {
67
+ };
68
+ var IntegrationError = class extends PlanVortexError {
69
+ };
70
+ var PlanVortexConnectionError = class extends PlanVortexError {
71
+ /** `true` si lo que se agotó fue nuestro propio timeout, no la red. */
72
+ timeout;
73
+ constructor(message, options = {}) {
74
+ super(NO_ERROR_CODE, message, { ...options, family: "connection" });
75
+ this.timeout = options.timeout ?? false;
76
+ }
77
+ };
78
+ var PlanVortexAuthenticationError = class extends PlanVortexError {
79
+ /** `invalid_client`, `invalid_request`, `unsupported_grant_type`, `slow_down` o `server_error`. */
80
+ oauthError;
81
+ constructor(oauthError, description, options = {}) {
82
+ super(NO_ERROR_CODE, description, { ...options, family: "oauth" });
83
+ this.oauthError = oauthError;
84
+ }
85
+ };
86
+ var PlanVortexConfigError = class extends PlanVortexError {
87
+ constructor(message) {
88
+ super(NO_ERROR_CODE, message, { family: "config" });
89
+ }
90
+ };
91
+ function errorFamilyForCode(code) {
92
+ return PLANVORTEX_ERROR_RANGES.find((range) => code >= range.from && code <= range.to)?.family;
93
+ }
94
+ var FAMILY_CLASSES = {
95
+ auth: AuthError,
96
+ user: UserError,
97
+ account: AccountError,
98
+ file: FileError,
99
+ publication: PublicationError,
100
+ organization: OrganizationError,
101
+ plan_limit: PlanLimitError,
102
+ messaging: MessagingError,
103
+ contact: ContactError,
104
+ product: ProductError,
105
+ ai_plan: AiPlanError,
106
+ integration: IntegrationError
107
+ };
108
+ function isApiErrorBody(body) {
109
+ return typeof body === "object" && body !== null && typeof body.code === "number";
110
+ }
111
+ function createErrorFromResponse(input) {
112
+ const options = {
113
+ status: input.status,
114
+ ...input.requestId === void 0 ? {} : { requestId: input.requestId },
115
+ ...input.retryAfter === void 0 ? {} : { retryAfter: input.retryAfter }
116
+ };
117
+ if (isApiErrorBody(input.body)) {
118
+ const family = errorFamilyForCode(input.body.code);
119
+ const ErrorClass = (family === void 0 ? void 0 : FAMILY_CLASSES[family]) ?? PlanVortexError;
120
+ return new ErrorClass(input.body.code, input.body.message ?? "PlanVortex error", {
121
+ ...options,
122
+ data: input.body.data ?? {}
123
+ });
124
+ }
125
+ return new PlanVortexError(NO_ERROR_CODE, `HTTP ${input.status}`, {
126
+ ...options,
127
+ family: "http",
128
+ data: typeof input.body === "object" && input.body !== null ? input.body : { body: input.body }
129
+ });
130
+ }
131
+ function isPlanVortexError(error) {
132
+ return error instanceof PlanVortexError;
133
+ }
134
+ function isTokenError(error) {
135
+ return isPlanVortexError(error) && TOKEN_ERROR_CODES.includes(error.code);
136
+ }
137
+
138
+ export { AccountError, AiPlanError, AuthError, ContactError, FileError, IntegrationError, MessagingError, NO_ERROR_CODE, OrganizationError, PLANVORTEX_ERROR_RANGES, PlanLimitError, PlanVortexAuthenticationError, PlanVortexConfigError, PlanVortexConnectionError, PlanVortexError, ProductError, PublicationError, TOKEN_ERROR_CODES, UserError, createErrorFromResponse, errorFamilyForCode, isPlanVortexError, isTokenError };
139
+ //# sourceMappingURL=chunk-B4DEHU6Q.js.map
140
+ //# sourceMappingURL=chunk-B4DEHU6Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/errors.ts"],"names":[],"mappings":"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","file":"chunk-B4DEHU6Q.js","sourcesContent":["/**\n * El catálogo de errores de PlanVortex, por rangos, y las clases que salen de él.\n *\n * LA REGLA, y no es negociable: **los errores se clasifican por `body.code`, nunca por el status\n * HTTP.** Todo error de dominio viaja con un 400 — un token caducado, una cuenta desconectada, el\n * cupo del plan agotado y un texto demasiado largo son los cuatro un 400. Sólo el 520 (permisos)\n * sale 401 y un fallo inesperado sale 500. Un `if (response.status === 401) refreshToken()` sería\n * un bug silencioso: los códigos de token, 501 y 522, viajan dentro de un 400.\n *\n * El catálogo del servidor crece cada mes, así que un código fuera de estos rangos NO es un error\n * del cliente: cae en la clase base con su `code` y su `message` intactos. Nunca se traga y nunca\n * se renombra.\n */\n\nexport type PlanVortexErrorRange = {\n /** Primer código del rango, incluido */\n from: number;\n /** Último código del rango, incluido */\n to: number;\n /** Qué familia de problemas es */\n family: string;\n};\n\nexport const PLANVORTEX_ERROR_RANGES: readonly PlanVortexErrorRange[] = [\n { from: 500, to: 541, family: \"auth\" },\n { from: 601, to: 612, family: \"user\" },\n { from: 700, to: 715, family: \"account\" },\n { from: 800, to: 810, family: \"file\" },\n { from: 900, to: 960, family: \"publication\" },\n { from: 1000, to: 1003, family: \"general\" },\n { from: 1100, to: 1111, family: \"organization\" },\n { from: 1200, to: 1207, family: \"role\" },\n { from: 1300, to: 1307, family: \"plan_limit\" },\n { from: 1400, to: 1408, family: \"plan_limit\" },\n { from: 1500, to: 1512, family: \"messaging\" },\n { from: 1600, to: 1601, family: \"contact\" },\n { from: 1900, to: 1906, family: \"payment\" },\n { from: 2000, to: 2099, family: \"product\" },\n { from: 2100, to: 2199, family: \"ai_plan\" },\n { from: 2200, to: 2299, family: \"integration\" },\n] as const;\n\n/**\n * El `code` que lleva un error que **no** trae código del servidor: un fallo de red, un timeout, un\n * 502 de un proxy con cuerpo HTML, o el propio constructor quejándose de la configuración.\n *\n * El servidor no emite nunca el 0, así que sirve de centinela sin pisar el catálogo. Cuál de esos\n * casos es se distingue por `family`: `connection`, `http`, `oauth`, `config` o `webhook`.\n */\nexport const NO_ERROR_CODE = 0;\n\n/**\n * Los dos códigos que significan \"tu token ya no sirve\". Los dos llegan **dentro de un 400**, que es\n * justo por lo que existe esta constante: quien mire el status no los va a encontrar.\n *\n * El 520 (permisos) NO está aquí a propósito: sale 401, pero pedir un token nuevo no lo arregla — a\n * la app le faltan permisos, y con un token recién emitido le seguirán faltando.\n */\nexport const TOKEN_ERROR_CODES: readonly number[] = [501, 522];\n\n/** Opciones de construcción de un error. Todas opcionales: un error siempre se puede construir. */\nexport interface PlanVortexErrorOptions {\n /** El `data` del cuerpo — lo que el servidor adjuntó con `.withData({...})`. `{}` si no vino nada. */\n data?: Record<string, unknown>;\n /** Status HTTP. `undefined` cuando la petición no llegó a tener respuesta. */\n status?: number;\n /** `x-request-id` de la respuesta, si el despliegue lo pone delante. */\n requestId?: string;\n /** Segundos de la cabecera `Retry-After`, cuando la hay. */\n retryAfter?: number;\n /** El error original (un `TypeError` de `fetch`, por ejemplo). */\n cause?: unknown;\n /**\n * Familia, sólo para los errores que NO salen del catálogo: `connection`, `http`, `oauth`,\n * `config` y `webhook`. Los de dominio la deducen de su `code` y no la pasan nunca.\n */\n family?: string;\n}\n\n/**\n * La base de todo lo que lanza esta librería.\n *\n * Un `catch (e) { if (e instanceof PlanVortexError) }` los coge todos: los de dominio, los de red y\n * los de configuración. Para afinar están las subclases y el `code`.\n */\nexport class PlanVortexError extends Error {\n /** El `code` del cuerpo, tal cual. {@link NO_ERROR_CODE} si el error no viene del catálogo. */\n readonly code: number;\n /** La familia del rango: `auth`, `publication`, `plan_limit`... Ver {@link PLANVORTEX_ERROR_RANGES}. */\n readonly family: string;\n /** El `data` del cuerpo. */\n readonly data: Record<string, unknown>;\n /** Status HTTP, o `undefined` si nunca hubo respuesta. */\n readonly status: number | undefined;\n /** `x-request-id`, si el despliegue lo pone. Hoy el servidor no lo emite; un proxy delante sí. */\n readonly requestId: string | undefined;\n /** Segundos que pidió esperar la cabecera `Retry-After`, si llegó. */\n readonly retryAfter: number | undefined;\n\n constructor(code: number, message: string, options: PlanVortexErrorOptions = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = new.target.name;\n this.code = code;\n this.family = options.family ?? errorFamilyForCode(code) ?? \"unknown\";\n this.data = options.data ?? {};\n this.status = options.status;\n this.requestId = options.requestId;\n this.retryAfter = options.retryAfter;\n }\n}\n\n/** 500-541 — tokens, apps de cliente, permisos. Incluye el 501 y el 522, los de token caducado. */\nexport class AuthError extends PlanVortexError {}\n/** 601-612 — el usuario final. */\nexport class UserError extends PlanVortexError {}\n/** 700-715 — cuentas sociales: desconectada, sin permisos en la red, sin refrescar. */\nexport class AccountError extends PlanVortexError {}\n/** 800-810 — ficheros: formato no admitido, demasiado grande, conversión fallida. */\nexport class FileError extends PlanVortexError {}\n/** 900-960 — publicaciones, incluidos los límites por red (caracteres, imágenes, duración). */\nexport class PublicationError extends PlanVortexError {}\n/** 1100-1111 — organizaciones, y el token temporal atado a una sola de ellas (1101). */\nexport class OrganizationError extends PlanVortexError {}\n/**\n * 1300-1307 y 1400-1408 — el cupo del plan, del cliente o de la organización.\n *\n * Es el error que un integrador **sí** quiere distinguir: no se arregla reintentando, se arregla\n * cambiando de plan. Por eso los dos rangos comparten clase.\n */\nexport class PlanLimitError extends PlanVortexError {}\n/** 1500-1512 — conversaciones, mensajes y plantillas. Exige plan de pago. */\nexport class MessagingError extends PlanVortexError {}\n/** 1600-1601 — contactos. */\nexport class ContactError extends PlanVortexError {}\n/** 2000-2099 — catálogos y productos (sólo Facebook e Instagram). */\nexport class ProductError extends PlanVortexError {}\n/** 2100-2199 — planes de publicaciones generados con IA. */\nexport class AiPlanError extends PlanVortexError {}\n/** 2200-2299 — integraciones: Google Drive, RSS. */\nexport class IntegrationError extends PlanVortexError {}\n\n/**\n * La petición no llegó a tener respuesta: DNS, conexión rechazada, socket cortado o timeout.\n *\n * No lleva código del catálogo porque el servidor nunca llegó a opinar.\n */\nexport class PlanVortexConnectionError extends PlanVortexError {\n /** `true` si lo que se agotó fue nuestro propio timeout, no la red. */\n readonly timeout: boolean;\n\n constructor(message: string, options: PlanVortexErrorOptions & { timeout?: boolean } = {}) {\n super(NO_ERROR_CODE, message, { ...options, family: \"connection\" });\n this.timeout = options.timeout ?? false;\n }\n}\n\n/**\n * `POST /oauth/token` rechazó las credenciales.\n *\n * Es el ÚNICO sitio del API con forma de error distinta: `{error, error_description}` de OAuth2, no\n * el `{code, message, data}` de todo lo demás. Y por eso el `code` es {@link NO_ERROR_CODE}: el\n * servidor tiene códigos para esto (538-541) pero **no los manda en el cuerpo**, así que ponerlos\n * aquí sería inventarse algo que nadie dijo. Lo que sí viaja es `oauthError`.\n */\nexport class PlanVortexAuthenticationError extends PlanVortexError {\n /** `invalid_client`, `invalid_request`, `unsupported_grant_type`, `slow_down` o `server_error`. */\n readonly oauthError: string;\n\n constructor(oauthError: string, description: string, options: PlanVortexErrorOptions = {}) {\n super(NO_ERROR_CODE, description, { ...options, family: \"oauth\" });\n this.oauthError = oauthError;\n }\n}\n\n/**\n * La librería está mal configurada y no ha llegado a salir de casa: sin credenciales, o instanciada\n * en un navegador (§ trampa 9 del roadmap).\n */\nexport class PlanVortexConfigError extends PlanVortexError {\n constructor(message: string) {\n super(NO_ERROR_CODE, message, { family: \"config\" });\n }\n}\n\n/** La familia a la que pertenece un código, o `undefined` si cae fuera del catálogo conocido. */\nexport function errorFamilyForCode(code: number): string | undefined {\n return PLANVORTEX_ERROR_RANGES.find((range) => code >= range.from && code <= range.to)?.family;\n}\n\n/**\n * Familia -> clase. Las que faltan —`general`, `role`, `payment` y cualquier rango nuevo— caen a\n * propósito en la clase base: existen en el servidor pero no son superficie de integración, y darles\n * clase propia sería prometer un `instanceof` que luego habría que mantener.\n */\nconst FAMILY_CLASSES: Record<string, typeof PlanVortexError> = {\n auth: AuthError,\n user: UserError,\n account: AccountError,\n file: FileError,\n publication: PublicationError,\n organization: OrganizationError,\n plan_limit: PlanLimitError,\n messaging: MessagingError,\n contact: ContactError,\n product: ProductError,\n ai_plan: AiPlanError,\n integration: IntegrationError,\n};\n\n/** El cuerpo de error del API: `{code, message, data}`. */\nexport interface ApiErrorBody {\n code: number;\n message?: string;\n data?: Record<string, unknown>;\n}\n\nfunction isApiErrorBody(body: unknown): body is ApiErrorBody {\n return typeof body === \"object\" && body !== null && typeof (body as ApiErrorBody).code === \"number\";\n}\n\n/**\n * Convierte una respuesta de error en la clase que le toca.\n *\n * Un cuerpo sin `code` —un 502 de un proxy, la página HTML de un balanceador— no es un error de\n * dominio: sale como clase base con `family: \"http\"` y el cuerpo entero en `data`, que es además lo\n * que necesita `auth.ts` para reconocer el `{error, error_description}` de OAuth2.\n */\nexport function createErrorFromResponse(input: {\n body: unknown;\n status: number;\n requestId?: string | undefined;\n retryAfter?: number | undefined;\n}): PlanVortexError {\n const options: PlanVortexErrorOptions = {\n status: input.status,\n ...(input.requestId === undefined ? {} : { requestId: input.requestId }),\n ...(input.retryAfter === undefined ? {} : { retryAfter: input.retryAfter }),\n };\n\n if (isApiErrorBody(input.body)) {\n const family = errorFamilyForCode(input.body.code);\n const ErrorClass = (family === undefined ? undefined : FAMILY_CLASSES[family]) ?? PlanVortexError;\n return new ErrorClass(input.body.code, input.body.message ?? \"PlanVortex error\", {\n ...options,\n data: input.body.data ?? {},\n });\n }\n\n return new PlanVortexError(NO_ERROR_CODE, `HTTP ${input.status}`, {\n ...options,\n family: \"http\",\n data:\n typeof input.body === \"object\" && input.body !== null\n ? (input.body as Record<string, unknown>)\n : { body: input.body },\n });\n}\n\n/** ¿Es un error de esta librería? Útil en un `catch` donde no apetece importar la clase. */\nexport function isPlanVortexError(error: unknown): error is PlanVortexError {\n return error instanceof PlanVortexError;\n}\n\n/**\n * ¿Dice este error que el token ya no sirve? (los códigos 501 y 522, los dos dentro de un 400).\n *\n * Es lo que dispara el único reintento con token nuevo del cliente.\n */\nexport function isTokenError(error: unknown): boolean {\n return isPlanVortexError(error) && TOKEN_ERROR_CODES.includes(error.code);\n}\n"]}