@miguelmorales13/nestkit 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -535,6 +535,87 @@ contenedor esté listo — no hace falta encadenarlo a mano.
535
535
  `forRoot()` sin argumentos toma las credenciales del entorno; pasándoselas explícitas se puede tener
536
536
  un proceso publicando en varias cuentas.
537
537
 
538
+ ### `pinterest` — publicar pines, y por qué no es una red social más
539
+
540
+ Pinterest es un **buscador visual**, y eso cambia dos cosas frente a Instagram
541
+ y Facebook.
542
+
543
+ **Cada pin lleva su propio enlace.** Hay un camino directo del contenido al
544
+ sitio, cosa que en Instagram no existe porque el enlace solo vive en la
545
+ biografía.
546
+
547
+ **El contenido no caduca.** Un pin sigue apareciendo en búsquedas meses después
548
+ de publicarse, mientras que una historia dura 24 horas. Por eso el título y la
549
+ descripción pesan mucho más que un pie de foto en cualquier otro destino: son
550
+ lo que hace que alguien lo encuentre medio año después.
551
+
552
+ ```ts
553
+ import { PinterestModule, PinterestService } from '@miguelmorales13/nestkit/pinterest';
554
+
555
+ @Module({ imports: [PinterestModule.forRoot()] }) // lee PINTEREST_ACCESS_TOKEN
556
+ export class AppModule {}
557
+ ```
558
+
559
+ ```ts
560
+ await pinterest.listBoards(); // para sacar los ids
561
+ await pinterest.publish({
562
+ boardId, title, description,
563
+ link: 'https://tusitio.com/receta', // esto es lo que importa
564
+ imageUrl, // Pinterest la descarga él
565
+ });
566
+ ```
567
+
568
+ Igual que Meta, **no acepta el binario**: se le pasa una URL pública y va a
569
+ buscarla. `media` sirve justo para eso.
570
+
571
+ El servicio se puede usar **suelto, fuera de Nest**: recibe credenciales
572
+ planas y solo usa `fetch`, así que un script de CLI hace
573
+ `new PinterestService({ accessToken })` sin nada de inyección de dependencias.
574
+ Es lo que hace nutrimx, donde quien publica es un script con Playwright y no el
575
+ backend.
576
+
577
+ El token se saca en developers.pinterest.com con los permisos `boards:read`,
578
+ `pins:read` y `pins:write`. Los títulos se recortan a 100 caracteres y las
579
+ descripciones a 800, que son los límites del API — pasarse devuelve un 400.
580
+
581
+ ### `tiktok` — publicar videos, y el modo que evita la auditoría
582
+
583
+ Tres llamadas, ninguna opcional: preguntar por la cuenta, inicializar la
584
+ subida y sondear hasta que TikTok termine de procesar. La primera es un
585
+ requisito de sus guías, no una cortesía: la API rechaza publicar si no se
586
+ consultó antes qué admite la cuenta.
587
+
588
+ ```ts
589
+ import { TikTokModule, TikTokService } from '@miguelmorales13/nestkit/tiktok';
590
+
591
+ @Module({ imports: [TikTokModule.forRoot()] }) // lee TIKTOK_ACCESS_TOKEN
592
+ export class AppModule {}
593
+ ```
594
+
595
+ ```ts
596
+ await tiktok.creatorInfo(); // obligatorio antes de publicar
597
+ const id = await tiktok.publish({ data, title }); // sin privacyLevel: a borradores
598
+ await tiktok.status(id); // hasta PUBLISH_COMPLETE
599
+ ```
600
+
601
+ **La diferencia entre los dos modos importa mucho al principio.** En *Direct
602
+ Post* el video sale en vivo, pero exige el permiso `video.publish`, que solo se
603
+ consigue pasando una auditoría de TikTok de varias semanas —y hasta aprobarla
604
+ todo lo publicado queda en `SELF_ONLY`, o sea que no lo ve nadie—. En modo
605
+ **borrador**, que es el de por defecto aquí, el video cae en la bandeja de la
606
+ cuenta y una persona le da publicar desde el teléfono: llega con todo su
607
+ alcance y solo necesita `video.upload`.
608
+
609
+ Dicho de otro modo: publicar en directo antes de la auditoría es peor que no
610
+ publicar, porque gastas la pieza sin que la vea nadie.
611
+
612
+ Sube el binario en vez de pasar una URL a propósito. La otra vía,
613
+ `PULL_FROM_URL`, obliga a verificar el dominio en el portal de TikTok, y
614
+ cambiar un trámite por otro no compensa cuando el archivo ya está en disco.
615
+
616
+ El video tiene que ir en **MP4 H.264**. Límite de **6 peticiones por minuto**
617
+ por token, así que el sondeo del estado va espaciado.
618
+
538
619
  ### `stripe` — cliente de la SDK oficial
539
620
 
540
621
  ```ts
@@ -979,6 +1060,8 @@ trae los datos.
979
1060
  | `@miguelmorales13/nestkit/whatsapp` | `WhatsAppModule`, `WHATSAPP_CLIENT`, `WhatsAppClient` |
980
1061
  | `@miguelmorales13/nestkit/meta` | `MetaModule`, `MetaService` — publicar en Instagram y Facebook Pages |
981
1062
  | `@miguelmorales13/nestkit/media` | `MediaModule`, `MediaService` — hosting efímero en memoria |
1063
+ | `@miguelmorales13/nestkit/pinterest` | `PinterestModule`, `PinterestService` — publicar pines |
1064
+ | `@miguelmorales13/nestkit/tiktok` | `TikTokModule`, `TikTokService` — publicar videos |
982
1065
  | `@miguelmorales13/nestkit/stripe` | `StripeModule`, `STRIPE_CLIENT`, `createStripeWebhookController` |
983
1066
  | `@miguelmorales13/nestkit/auth` | `AuthUserPort`, `BaseAuthService`, `createAuthController`, `TokenService`, `JwtAuthGuard`, `RolesGuard`, `RequireTenantGuard`, `CurrentUser`, `CurrentTenant`, `Roles` |
984
1067
  | `@miguelmorales13/nestkit/auth/oauth` | `OAuthAuthModule`, `OAuthAuthService`, `createOAuthController`, `googleProvider`, `facebookProvider`, `OAuthStorePort`, `RefreshTokenStorePort` |
@@ -991,7 +1074,7 @@ trae los datos.
991
1074
 
992
1075
  ## Estado del paquete
993
1076
 
994
- `0.7.0`. Sin adaptador Mongo real. Sin tests unitarios propios.
1077
+ `0.9.0`. Sin adaptador Mongo real. Sin tests unitarios propios.
995
1078
 
996
1079
  `auth/oauth` y `umami` sí tienen un consumidor real: **nutrimx** corre su acceso con Google y
997
1080
  Facebook sobre este módulo en producción. El resto sigue sin ejercitarse de verdad.
@@ -1,11 +1,11 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunkYARLPYG5cjs = require('../chunk-YARLPYG5.cjs');
4
- require('../chunk-J7TURDAL.cjs');
3
+ var _chunk54ZXIB5Tcjs = require('../chunk-54ZXIB5T.cjs');
5
4
  require('../chunk-TJHRABML.cjs');
5
+ require('../chunk-J7TURDAL.cjs');
6
6
  require('../chunk-ZA56XBCK.cjs');
7
7
  require('../chunk-R7BVS6CI.cjs');
8
8
  require('../chunk-2REOCMUD.cjs');
9
9
 
10
10
 
11
- exports.applyNestKitDefaults = _chunkYARLPYG5cjs.applyNestKitDefaults;
11
+ exports.applyNestKitDefaults = _chunk54ZXIB5Tcjs.applyNestKitDefaults;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  applyNestKitDefaults
3
- } from "../chunk-ANQ3YPDI.js";
4
- import "../chunk-7I2Y7V52.js";
3
+ } from "../chunk-AOCF5QCZ.js";
5
4
  import "../chunk-KDAA6GFF.js";
5
+ import "../chunk-7I2Y7V52.js";
6
6
  import "../chunk-EBO6UKHL.js";
7
7
  import "../chunk-YFYHLYHN.js";
8
8
  import "../chunk-4MGIQFAJ.js";
package/dist/index.cjs CHANGED
@@ -3,13 +3,12 @@
3
3
 
4
4
 
5
5
 
6
- var _chunkSPTDXUEDcjs = require('./chunk-SPTDXUED.cjs');
6
+ var _chunkRF75KC63cjs = require('./chunk-RF75KC63.cjs');
7
7
 
8
8
 
9
9
 
10
10
 
11
- var _chunkRF75KC63cjs = require('./chunk-RF75KC63.cjs');
12
- require('./chunk-7SOM7EZP.cjs');
11
+ var _chunkSPTDXUEDcjs = require('./chunk-SPTDXUED.cjs');
13
12
  require('./chunk-MR2IFCZE.cjs');
14
13
 
15
14
 
@@ -18,30 +17,31 @@ require('./chunk-MR2IFCZE.cjs');
18
17
  var _chunkV2M75FN3cjs = require('./chunk-V2M75FN3.cjs');
19
18
 
20
19
 
20
+ var _chunk54ZXIB5Tcjs = require('./chunk-54ZXIB5T.cjs');
21
21
 
22
22
 
23
+ var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
23
24
 
24
25
 
25
- var _chunkFDNGAYTZcjs = require('./chunk-FDNGAYTZ.cjs');
26
26
 
27
+ var _chunk3CLYZC3Tcjs = require('./chunk-3CLYZC3T.cjs');
28
+ require('./chunk-7SOM7EZP.cjs');
27
29
 
28
- var _chunkYARLPYG5cjs = require('./chunk-YARLPYG5.cjs');
29
30
 
30
31
 
31
- var _chunkJ7TURDALcjs = require('./chunk-J7TURDAL.cjs');
32
32
 
33
33
 
34
- var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
35
34
 
35
+ var _chunkFDNGAYTZcjs = require('./chunk-FDNGAYTZ.cjs');
36
36
 
37
- var _chunkZA56XBCKcjs = require('./chunk-ZA56XBCK.cjs');
38
37
 
38
+ var _chunkJ7TURDALcjs = require('./chunk-J7TURDAL.cjs');
39
39
 
40
- var _chunkR7BVS6CIcjs = require('./chunk-R7BVS6CI.cjs');
41
40
 
41
+ var _chunkZA56XBCKcjs = require('./chunk-ZA56XBCK.cjs');
42
42
 
43
43
 
44
- var _chunk3CLYZC3Tcjs = require('./chunk-3CLYZC3T.cjs');
44
+ var _chunkR7BVS6CIcjs = require('./chunk-R7BVS6CI.cjs');
45
45
 
46
46
 
47
47
  var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
@@ -75,4 +75,4 @@ require('./chunk-2REOCMUD.cjs');
75
75
 
76
76
 
77
77
 
78
- exports.AppException = _chunkR7BVS6CIcjs.AppException; exports.BaseCrudService = _chunk3CLYZC3Tcjs.BaseCrudService; exports.BaseResponseDto = _chunkQS2W5XCQcjs.BaseResponseDto; exports.ConflictAppException = _chunkFDNGAYTZcjs.ConflictAppException; exports.ForbiddenAppException = _chunkFDNGAYTZcjs.ForbiddenAppException; exports.GlobalExceptionFilter = _chunkJ7TURDALcjs.GlobalExceptionFilter; exports.I18nModule = _chunkM3EL5O6Tcjs.I18nModule; exports.NotFoundAppException = _chunkFDNGAYTZcjs.NotFoundAppException; exports.PG_POOL = _chunkRF75KC63cjs.PG_POOL; exports.PgModule = _chunkRF75KC63cjs.PgModule; exports.REQUEST_ID_HEADER = _chunkV2M75FN3cjs.REQUEST_ID_HEADER; exports.RequestContext = _chunkZA56XBCKcjs.RequestContext; exports.RequestIdMiddleware = _chunkV2M75FN3cjs.RequestIdMiddleware; exports.ResponseInterceptor = _chunkTJHRABMLcjs.ResponseInterceptor; exports.SUPABASE_ANON_CLIENT = _chunkSPTDXUEDcjs.SUPABASE_ANON_CLIENT; exports.SUPABASE_SERVICE_ROLE_CLIENT = _chunkSPTDXUEDcjs.SUPABASE_SERVICE_ROLE_CLIENT; exports.SupabaseModule = _chunkSPTDXUEDcjs.SupabaseModule; exports.TrackingModule = _chunkV2M75FN3cjs.TrackingModule; exports.UnauthorizedAppException = _chunkFDNGAYTZcjs.UnauthorizedAppException; exports.ValidationAppException = _chunkFDNGAYTZcjs.ValidationAppException; exports.applyNestKitDefaults = _chunkYARLPYG5cjs.applyNestKitDefaults; exports.createCrudController = _chunk3CLYZC3Tcjs.createCrudController; exports.translateOr = _chunkM3EL5O6Tcjs.translateOr; exports.withTenantScope = _chunkRF75KC63cjs.withTenantScope;
78
+ exports.AppException = _chunkR7BVS6CIcjs.AppException; exports.BaseCrudService = _chunk3CLYZC3Tcjs.BaseCrudService; exports.BaseResponseDto = _chunkQS2W5XCQcjs.BaseResponseDto; exports.ConflictAppException = _chunkFDNGAYTZcjs.ConflictAppException; exports.ForbiddenAppException = _chunkFDNGAYTZcjs.ForbiddenAppException; exports.GlobalExceptionFilter = _chunkJ7TURDALcjs.GlobalExceptionFilter; exports.I18nModule = _chunkM3EL5O6Tcjs.I18nModule; exports.NotFoundAppException = _chunkFDNGAYTZcjs.NotFoundAppException; exports.PG_POOL = _chunkRF75KC63cjs.PG_POOL; exports.PgModule = _chunkRF75KC63cjs.PgModule; exports.REQUEST_ID_HEADER = _chunkV2M75FN3cjs.REQUEST_ID_HEADER; exports.RequestContext = _chunkZA56XBCKcjs.RequestContext; exports.RequestIdMiddleware = _chunkV2M75FN3cjs.RequestIdMiddleware; exports.ResponseInterceptor = _chunkTJHRABMLcjs.ResponseInterceptor; exports.SUPABASE_ANON_CLIENT = _chunkSPTDXUEDcjs.SUPABASE_ANON_CLIENT; exports.SUPABASE_SERVICE_ROLE_CLIENT = _chunkSPTDXUEDcjs.SUPABASE_SERVICE_ROLE_CLIENT; exports.SupabaseModule = _chunkSPTDXUEDcjs.SupabaseModule; exports.TrackingModule = _chunkV2M75FN3cjs.TrackingModule; exports.UnauthorizedAppException = _chunkFDNGAYTZcjs.UnauthorizedAppException; exports.ValidationAppException = _chunkFDNGAYTZcjs.ValidationAppException; exports.applyNestKitDefaults = _chunk54ZXIB5Tcjs.applyNestKitDefaults; exports.createCrudController = _chunk3CLYZC3Tcjs.createCrudController; exports.translateOr = _chunkM3EL5O6Tcjs.translateOr; exports.withTenantScope = _chunkRF75KC63cjs.withTenantScope;
package/dist/index.js CHANGED
@@ -1,21 +1,31 @@
1
1
  import "./chunk-EPVKCBPT.js";
2
- import {
3
- SUPABASE_ANON_CLIENT,
4
- SUPABASE_SERVICE_ROLE_CLIENT,
5
- SupabaseModule
6
- } from "./chunk-PA24P76K.js";
7
2
  import {
8
3
  PG_POOL,
9
4
  PgModule,
10
5
  withTenantScope
11
6
  } from "./chunk-VKOPDDCC.js";
12
- import "./chunk-DQYAIQQ5.js";
7
+ import {
8
+ SUPABASE_ANON_CLIENT,
9
+ SUPABASE_SERVICE_ROLE_CLIENT,
10
+ SupabaseModule
11
+ } from "./chunk-PA24P76K.js";
13
12
  import "./chunk-NAK4WDKS.js";
14
13
  import {
15
14
  REQUEST_ID_HEADER,
16
15
  RequestIdMiddleware,
17
16
  TrackingModule
18
17
  } from "./chunk-EYURGACO.js";
18
+ import {
19
+ applyNestKitDefaults
20
+ } from "./chunk-AOCF5QCZ.js";
21
+ import {
22
+ ResponseInterceptor
23
+ } from "./chunk-KDAA6GFF.js";
24
+ import {
25
+ BaseCrudService,
26
+ createCrudController
27
+ } from "./chunk-JOVBJDJ2.js";
28
+ import "./chunk-DQYAIQQ5.js";
19
29
  import {
20
30
  ConflictAppException,
21
31
  ForbiddenAppException,
@@ -23,25 +33,15 @@ import {
23
33
  UnauthorizedAppException,
24
34
  ValidationAppException
25
35
  } from "./chunk-ORWJ7LES.js";
26
- import {
27
- applyNestKitDefaults
28
- } from "./chunk-ANQ3YPDI.js";
29
36
  import {
30
37
  GlobalExceptionFilter
31
38
  } from "./chunk-7I2Y7V52.js";
32
- import {
33
- ResponseInterceptor
34
- } from "./chunk-KDAA6GFF.js";
35
39
  import {
36
40
  RequestContext
37
41
  } from "./chunk-EBO6UKHL.js";
38
42
  import {
39
43
  AppException
40
44
  } from "./chunk-YFYHLYHN.js";
41
- import {
42
- BaseCrudService,
43
- createCrudController
44
- } from "./chunk-JOVBJDJ2.js";
45
45
  import {
46
46
  BaseResponseDto
47
47
  } from "./chunk-XX2HPTRU.js";
@@ -0,0 +1,101 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
+
3
+ var _chunk2REOCMUDcjs = require('../chunk-2REOCMUD.cjs');
4
+
5
+ // src/pinterest/pinterest.service.ts
6
+ var _common = require('@nestjs/common');
7
+ var API = "https://api.pinterest.com/v5";
8
+ var MAX_TITLE = 100;
9
+ var MAX_DESCRIPTION = 800;
10
+ var PinterestService = class {
11
+ constructor(credentials) {
12
+ this.credentials = credentials;
13
+ }
14
+ /** The account's boards. Used to resolve the ids that configuration needs. */
15
+ async listBoards() {
16
+ const data = await this.request("/boards?page_size=50");
17
+ return _nullishCoalesce(data.items, () => ( []));
18
+ }
19
+ /** Creates a pin and returns its id. */
20
+ async publish(pin) {
21
+ const created = await this.request("/pins", {
22
+ method: "POST",
23
+ body: {
24
+ board_id: pin.boardId,
25
+ title: pin.title.slice(0, MAX_TITLE),
26
+ description: pin.description.slice(0, MAX_DESCRIPTION),
27
+ link: pin.link,
28
+ media_source: { source_type: "image_url", url: pin.imageUrl }
29
+ }
30
+ });
31
+ return created.id;
32
+ }
33
+ /** Whether there is a token at all. */
34
+ get configured() {
35
+ return Boolean(this.credentials.accessToken);
36
+ }
37
+ async request(path, options = {}) {
38
+ if (!this.credentials.accessToken) {
39
+ throw new Error("PinterestService: no access token configured");
40
+ }
41
+ const response = await fetch(`${API}${path}`, {
42
+ method: _nullishCoalesce(options.method, () => ( "GET")),
43
+ headers: {
44
+ Authorization: `Bearer ${this.credentials.accessToken}`,
45
+ "Content-Type": "application/json"
46
+ },
47
+ ...options.body ? { body: JSON.stringify(options.body) } : {}
48
+ });
49
+ const text = await response.text();
50
+ let data;
51
+ try {
52
+ data = JSON.parse(text);
53
+ } catch (e) {
54
+ throw new Error(`Pinterest answered ${response.status} with non-JSON: ${text.slice(0, 200)}`);
55
+ }
56
+ if (!response.ok) {
57
+ const error = data;
58
+ throw new Error(
59
+ `Pinterest answered ${response.status}: ${_nullishCoalesce(error.message, () => ( text.slice(0, 200)))}`
60
+ );
61
+ }
62
+ return data;
63
+ }
64
+ };
65
+ PinterestService = exports.PinterestService = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
66
+ _common.Injectable.call(void 0, )
67
+ ], PinterestService);
68
+
69
+ // src/pinterest/pinterest.module.ts
70
+
71
+ var PinterestModule = class {
72
+ static forRoot(credentials) {
73
+ return {
74
+ module: PinterestModule,
75
+ providers: [
76
+ {
77
+ provide: PinterestService,
78
+ useFactory: () => {
79
+ const resolved = _nullishCoalesce(credentials, () => ( {
80
+ accessToken: _nullishCoalesce(process.env.PINTEREST_ACCESS_TOKEN, () => ( ""))
81
+ }));
82
+ if (!resolved.accessToken) {
83
+ throw new Error(
84
+ "PinterestModule: no access token. Set PINTEREST_ACCESS_TOKEN or pass credentials to forRoot()."
85
+ );
86
+ }
87
+ return new PinterestService(resolved);
88
+ }
89
+ }
90
+ ],
91
+ exports: [PinterestService]
92
+ };
93
+ }
94
+ };
95
+ PinterestModule = exports.PinterestModule = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
96
+ _common.Module.call(void 0, {})
97
+ ], PinterestModule);
98
+
99
+
100
+
101
+ exports.PinterestModule = PinterestModule; exports.PinterestService = PinterestService;
@@ -0,0 +1,3 @@
1
+ export { PinterestService } from './pinterest.service.js';
2
+ export type { PinterestCredentials, PinterestBoard, NewPin, } from './pinterest.service.js';
3
+ export { PinterestModule } from './pinterest.module.js';
@@ -0,0 +1,101 @@
1
+ import {
2
+ __decorateClass
3
+ } from "../chunk-4MGIQFAJ.js";
4
+
5
+ // src/pinterest/pinterest.service.ts
6
+ import { Injectable } from "@nestjs/common";
7
+ var API = "https://api.pinterest.com/v5";
8
+ var MAX_TITLE = 100;
9
+ var MAX_DESCRIPTION = 800;
10
+ var PinterestService = class {
11
+ constructor(credentials) {
12
+ this.credentials = credentials;
13
+ }
14
+ /** The account's boards. Used to resolve the ids that configuration needs. */
15
+ async listBoards() {
16
+ const data = await this.request("/boards?page_size=50");
17
+ return data.items ?? [];
18
+ }
19
+ /** Creates a pin and returns its id. */
20
+ async publish(pin) {
21
+ const created = await this.request("/pins", {
22
+ method: "POST",
23
+ body: {
24
+ board_id: pin.boardId,
25
+ title: pin.title.slice(0, MAX_TITLE),
26
+ description: pin.description.slice(0, MAX_DESCRIPTION),
27
+ link: pin.link,
28
+ media_source: { source_type: "image_url", url: pin.imageUrl }
29
+ }
30
+ });
31
+ return created.id;
32
+ }
33
+ /** Whether there is a token at all. */
34
+ get configured() {
35
+ return Boolean(this.credentials.accessToken);
36
+ }
37
+ async request(path, options = {}) {
38
+ if (!this.credentials.accessToken) {
39
+ throw new Error("PinterestService: no access token configured");
40
+ }
41
+ const response = await fetch(`${API}${path}`, {
42
+ method: options.method ?? "GET",
43
+ headers: {
44
+ Authorization: `Bearer ${this.credentials.accessToken}`,
45
+ "Content-Type": "application/json"
46
+ },
47
+ ...options.body ? { body: JSON.stringify(options.body) } : {}
48
+ });
49
+ const text = await response.text();
50
+ let data;
51
+ try {
52
+ data = JSON.parse(text);
53
+ } catch {
54
+ throw new Error(`Pinterest answered ${response.status} with non-JSON: ${text.slice(0, 200)}`);
55
+ }
56
+ if (!response.ok) {
57
+ const error = data;
58
+ throw new Error(
59
+ `Pinterest answered ${response.status}: ${error.message ?? text.slice(0, 200)}`
60
+ );
61
+ }
62
+ return data;
63
+ }
64
+ };
65
+ PinterestService = __decorateClass([
66
+ Injectable()
67
+ ], PinterestService);
68
+
69
+ // src/pinterest/pinterest.module.ts
70
+ import { Module } from "@nestjs/common";
71
+ var PinterestModule = class {
72
+ static forRoot(credentials) {
73
+ return {
74
+ module: PinterestModule,
75
+ providers: [
76
+ {
77
+ provide: PinterestService,
78
+ useFactory: () => {
79
+ const resolved = credentials ?? {
80
+ accessToken: process.env.PINTEREST_ACCESS_TOKEN ?? ""
81
+ };
82
+ if (!resolved.accessToken) {
83
+ throw new Error(
84
+ "PinterestModule: no access token. Set PINTEREST_ACCESS_TOKEN or pass credentials to forRoot()."
85
+ );
86
+ }
87
+ return new PinterestService(resolved);
88
+ }
89
+ }
90
+ ],
91
+ exports: [PinterestService]
92
+ };
93
+ }
94
+ };
95
+ PinterestModule = __decorateClass([
96
+ Module({})
97
+ ], PinterestModule);
98
+ export {
99
+ PinterestModule,
100
+ PinterestService
101
+ };
@@ -0,0 +1,16 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import { type PinterestCredentials } from './pinterest.service.js';
3
+ /**
4
+ * Outbound client for Pinterest. See `PinterestService`.
5
+ *
6
+ * Credentials come from `PINTEREST_ACCESS_TOKEN` by default, or are passed
7
+ * explicitly to `forRoot` — which is what a process publishing to several
8
+ * accounts needs.
9
+ *
10
+ * Unlike most modules here, the service is also useful on its own: it takes
11
+ * plain credentials and uses nothing but `fetch`, so a script outside Nest can
12
+ * do `new PinterestService({ accessToken })` without any of the DI machinery.
13
+ */
14
+ export declare class PinterestModule {
15
+ static forRoot(credentials?: PinterestCredentials): DynamicModule;
16
+ }
@@ -0,0 +1,51 @@
1
+ export interface PinterestCredentials {
2
+ /** Token from developers.pinterest.com with boards:read, pins:read, pins:write. */
3
+ accessToken: string;
4
+ }
5
+ export interface PinterestBoard {
6
+ id: string;
7
+ name: string;
8
+ }
9
+ export interface NewPin {
10
+ boardId: string;
11
+ /** What people read in search results. Trimmed to 100 characters. */
12
+ title: string;
13
+ /** The long text carrying the words you want to be found by. */
14
+ description: string;
15
+ /** Where the pin leads when tapped. This is the whole point of Pinterest. */
16
+ link: string;
17
+ /** Publicly reachable image URL — Pinterest downloads it when creating the pin. */
18
+ imageUrl: string;
19
+ }
20
+ /**
21
+ * Publishes pins through the Pinterest API v5.
22
+ *
23
+ * Pinterest is not one more social network: it is a visual search engine, and
24
+ * that changes two things about how content behaves there.
25
+ *
26
+ * **Every pin carries its own link.** So there is a direct path from a piece of
27
+ * content to a site — unlike Instagram, where a link only lives in the bio.
28
+ *
29
+ * **The content does not expire.** A pin keeps surfacing in searches months
30
+ * after it was published, while a story lasts a day. That makes the title and
31
+ * description matter far more than a caption elsewhere: they are what makes
32
+ * someone find the pin half a year later.
33
+ *
34
+ * Like Meta's API, Pinterest does not take the binary — it is given a public
35
+ * URL and fetches the image itself, so the caller needs somewhere to serve it
36
+ * from during publication. `MediaModule` in this same package exists for that.
37
+ *
38
+ * Send-only, like `TelegramModule` and `MetaModule`: it publishes and lists
39
+ * boards, it does not read analytics or manage comments.
40
+ */
41
+ export declare class PinterestService {
42
+ private readonly credentials;
43
+ constructor(credentials: PinterestCredentials);
44
+ /** The account's boards. Used to resolve the ids that configuration needs. */
45
+ listBoards(): Promise<PinterestBoard[]>;
46
+ /** Creates a pin and returns its id. */
47
+ publish(pin: NewPin): Promise<string>;
48
+ /** Whether there is a token at all. */
49
+ get configured(): boolean;
50
+ private request;
51
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
+
3
+ var _chunk2REOCMUDcjs = require('../chunk-2REOCMUD.cjs');
4
+
5
+ // src/tiktok/tiktok.service.ts
6
+ var _common = require('@nestjs/common');
7
+ var API = "https://open.tiktokapis.com/v2";
8
+ var TikTokService = class {
9
+ constructor(credentials) {
10
+ this.credentials = credentials;
11
+ }
12
+ get configured() {
13
+ return Boolean(this.credentials.accessToken);
14
+ }
15
+ /**
16
+ * Datos de la cuenta. Hay que llamarlo antes de publicar: TikTok lo exige
17
+ * y además dice qué niveles de privacidad admite, que varían según si la
18
+ * cuenta es privada, de empresa o de menor de edad.
19
+ */
20
+ async creatorInfo() {
21
+ return this.request("/post/publish/creator_info/query/", {});
22
+ }
23
+ /**
24
+ * Sube un vídeo y devuelve el `publish_id` con el que seguir su estado.
25
+ *
26
+ * Sin `privacyLevel` va a borradores; con él, se publica directo —y para eso
27
+ * hace falta que la aplicación haya pasado la auditoría.
28
+ */
29
+ async publish(video) {
30
+ const directo = Boolean(video.privacyLevel);
31
+ const ruta = directo ? "/post/publish/video/init/" : "/post/publish/inbox/video/init/";
32
+ const cuerpo = {
33
+ source_info: {
34
+ source: "FILE_UPLOAD",
35
+ video_size: video.data.byteLength,
36
+ // Una sola parte: estos clips son de unos pocos megas y trocearlos
37
+ // solo añadiría puntos donde fallar.
38
+ chunk_size: video.data.byteLength,
39
+ total_chunk_count: 1
40
+ }
41
+ };
42
+ if (directo) {
43
+ cuerpo.post_info = {
44
+ title: video.title.slice(0, 2200),
45
+ privacy_level: video.privacyLevel
46
+ };
47
+ }
48
+ const inicio = await this.request(
49
+ ruta,
50
+ cuerpo
51
+ );
52
+ const subida = await fetch(inicio.upload_url, {
53
+ method: "PUT",
54
+ headers: {
55
+ "Content-Type": "video/mp4",
56
+ // TikTok exige el rango incluso subiendo el archivo entero de una vez.
57
+ "Content-Range": `bytes 0-${video.data.byteLength - 1}/${video.data.byteLength}`
58
+ },
59
+ body: new Uint8Array(video.data)
60
+ });
61
+ if (!subida.ok) {
62
+ throw new Error(
63
+ `TikTok rechaz\xF3 el archivo (HTTP ${subida.status}): ${(await subida.text()).slice(0, 200)}`
64
+ );
65
+ }
66
+ return inicio.publish_id;
67
+ }
68
+ /** En qué va el procesado. Termina en `PUBLISH_COMPLETE` o en un fallo. */
69
+ async status(publishId) {
70
+ return this.request(
71
+ "/post/publish/status/fetch/",
72
+ { publish_id: publishId }
73
+ );
74
+ }
75
+ async request(path, body) {
76
+ if (!this.credentials.accessToken) {
77
+ throw new Error("TikTokService: no access token configured");
78
+ }
79
+ const response = await fetch(`${API}${path}`, {
80
+ method: "POST",
81
+ headers: {
82
+ Authorization: `Bearer ${this.credentials.accessToken}`,
83
+ "Content-Type": "application/json; charset=UTF-8"
84
+ },
85
+ body: JSON.stringify(body)
86
+ });
87
+ const text = await response.text();
88
+ let json;
89
+ try {
90
+ json = JSON.parse(text);
91
+ } catch (e) {
92
+ throw new Error(`TikTok respondi\xF3 ${response.status} y no era JSON: ${text.slice(0, 200)}`);
93
+ }
94
+ if (json.error && json.error.code && json.error.code !== "ok") {
95
+ throw new Error(`TikTok: ${json.error.code} \u2014 ${_nullishCoalesce(json.error.message, () => ( ""))}`.trim());
96
+ }
97
+ if (!response.ok) {
98
+ throw new Error(`TikTok respondi\xF3 HTTP ${response.status}: ${text.slice(0, 200)}`);
99
+ }
100
+ if (!json.data) {
101
+ throw new Error(`TikTok devolvi\xF3 una respuesta sin datos: ${text.slice(0, 200)}`);
102
+ }
103
+ return json.data;
104
+ }
105
+ };
106
+ TikTokService = exports.TikTokService = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
107
+ _common.Injectable.call(void 0, )
108
+ ], TikTokService);
109
+
110
+ // src/tiktok/tiktok.module.ts
111
+
112
+ var TikTokModule = class {
113
+ static forRoot(credentials) {
114
+ return {
115
+ module: TikTokModule,
116
+ providers: [
117
+ {
118
+ provide: TikTokService,
119
+ useFactory: () => {
120
+ const resolved = _nullishCoalesce(credentials, () => ( {
121
+ accessToken: _nullishCoalesce(process.env.TIKTOK_ACCESS_TOKEN, () => ( ""))
122
+ }));
123
+ if (!resolved.accessToken) {
124
+ throw new Error(
125
+ "TikTokModule: no access token. Set TIKTOK_ACCESS_TOKEN or pass credentials to forRoot()."
126
+ );
127
+ }
128
+ return new TikTokService(resolved);
129
+ }
130
+ }
131
+ ],
132
+ exports: [TikTokService]
133
+ };
134
+ }
135
+ };
136
+ TikTokModule = exports.TikTokModule = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
137
+ _common.Module.call(void 0, {})
138
+ ], TikTokModule);
139
+
140
+
141
+
142
+ exports.TikTokModule = TikTokModule; exports.TikTokService = TikTokService;
@@ -0,0 +1,3 @@
1
+ export { TikTokService } from './tiktok.service.js';
2
+ export type { TikTokCredentials, TikTokCreatorInfo, TikTokVideo, } from './tiktok.service.js';
3
+ export { TikTokModule } from './tiktok.module.js';
@@ -0,0 +1,142 @@
1
+ import {
2
+ __decorateClass
3
+ } from "../chunk-4MGIQFAJ.js";
4
+
5
+ // src/tiktok/tiktok.service.ts
6
+ import { Injectable } from "@nestjs/common";
7
+ var API = "https://open.tiktokapis.com/v2";
8
+ var TikTokService = class {
9
+ constructor(credentials) {
10
+ this.credentials = credentials;
11
+ }
12
+ get configured() {
13
+ return Boolean(this.credentials.accessToken);
14
+ }
15
+ /**
16
+ * Datos de la cuenta. Hay que llamarlo antes de publicar: TikTok lo exige
17
+ * y además dice qué niveles de privacidad admite, que varían según si la
18
+ * cuenta es privada, de empresa o de menor de edad.
19
+ */
20
+ async creatorInfo() {
21
+ return this.request("/post/publish/creator_info/query/", {});
22
+ }
23
+ /**
24
+ * Sube un vídeo y devuelve el `publish_id` con el que seguir su estado.
25
+ *
26
+ * Sin `privacyLevel` va a borradores; con él, se publica directo —y para eso
27
+ * hace falta que la aplicación haya pasado la auditoría.
28
+ */
29
+ async publish(video) {
30
+ const directo = Boolean(video.privacyLevel);
31
+ const ruta = directo ? "/post/publish/video/init/" : "/post/publish/inbox/video/init/";
32
+ const cuerpo = {
33
+ source_info: {
34
+ source: "FILE_UPLOAD",
35
+ video_size: video.data.byteLength,
36
+ // Una sola parte: estos clips son de unos pocos megas y trocearlos
37
+ // solo añadiría puntos donde fallar.
38
+ chunk_size: video.data.byteLength,
39
+ total_chunk_count: 1
40
+ }
41
+ };
42
+ if (directo) {
43
+ cuerpo.post_info = {
44
+ title: video.title.slice(0, 2200),
45
+ privacy_level: video.privacyLevel
46
+ };
47
+ }
48
+ const inicio = await this.request(
49
+ ruta,
50
+ cuerpo
51
+ );
52
+ const subida = await fetch(inicio.upload_url, {
53
+ method: "PUT",
54
+ headers: {
55
+ "Content-Type": "video/mp4",
56
+ // TikTok exige el rango incluso subiendo el archivo entero de una vez.
57
+ "Content-Range": `bytes 0-${video.data.byteLength - 1}/${video.data.byteLength}`
58
+ },
59
+ body: new Uint8Array(video.data)
60
+ });
61
+ if (!subida.ok) {
62
+ throw new Error(
63
+ `TikTok rechaz\xF3 el archivo (HTTP ${subida.status}): ${(await subida.text()).slice(0, 200)}`
64
+ );
65
+ }
66
+ return inicio.publish_id;
67
+ }
68
+ /** En qué va el procesado. Termina en `PUBLISH_COMPLETE` o en un fallo. */
69
+ async status(publishId) {
70
+ return this.request(
71
+ "/post/publish/status/fetch/",
72
+ { publish_id: publishId }
73
+ );
74
+ }
75
+ async request(path, body) {
76
+ if (!this.credentials.accessToken) {
77
+ throw new Error("TikTokService: no access token configured");
78
+ }
79
+ const response = await fetch(`${API}${path}`, {
80
+ method: "POST",
81
+ headers: {
82
+ Authorization: `Bearer ${this.credentials.accessToken}`,
83
+ "Content-Type": "application/json; charset=UTF-8"
84
+ },
85
+ body: JSON.stringify(body)
86
+ });
87
+ const text = await response.text();
88
+ let json;
89
+ try {
90
+ json = JSON.parse(text);
91
+ } catch {
92
+ throw new Error(`TikTok respondi\xF3 ${response.status} y no era JSON: ${text.slice(0, 200)}`);
93
+ }
94
+ if (json.error && json.error.code && json.error.code !== "ok") {
95
+ throw new Error(`TikTok: ${json.error.code} \u2014 ${json.error.message ?? ""}`.trim());
96
+ }
97
+ if (!response.ok) {
98
+ throw new Error(`TikTok respondi\xF3 HTTP ${response.status}: ${text.slice(0, 200)}`);
99
+ }
100
+ if (!json.data) {
101
+ throw new Error(`TikTok devolvi\xF3 una respuesta sin datos: ${text.slice(0, 200)}`);
102
+ }
103
+ return json.data;
104
+ }
105
+ };
106
+ TikTokService = __decorateClass([
107
+ Injectable()
108
+ ], TikTokService);
109
+
110
+ // src/tiktok/tiktok.module.ts
111
+ import { Module } from "@nestjs/common";
112
+ var TikTokModule = class {
113
+ static forRoot(credentials) {
114
+ return {
115
+ module: TikTokModule,
116
+ providers: [
117
+ {
118
+ provide: TikTokService,
119
+ useFactory: () => {
120
+ const resolved = credentials ?? {
121
+ accessToken: process.env.TIKTOK_ACCESS_TOKEN ?? ""
122
+ };
123
+ if (!resolved.accessToken) {
124
+ throw new Error(
125
+ "TikTokModule: no access token. Set TIKTOK_ACCESS_TOKEN or pass credentials to forRoot()."
126
+ );
127
+ }
128
+ return new TikTokService(resolved);
129
+ }
130
+ }
131
+ ],
132
+ exports: [TikTokService]
133
+ };
134
+ }
135
+ };
136
+ TikTokModule = __decorateClass([
137
+ Module({})
138
+ ], TikTokModule);
139
+ export {
140
+ TikTokModule,
141
+ TikTokService
142
+ };
@@ -0,0 +1,13 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import { type TikTokCredentials } from './tiktok.service.js';
3
+ /**
4
+ * Outbound client for TikTok's Content Posting API. See `TikTokService`.
5
+ *
6
+ * Reads `TIKTOK_ACCESS_TOKEN` by default. Like `PinterestService`, the service
7
+ * takes plain credentials and uses nothing but `fetch`, so a script outside
8
+ * Nest can do `new TikTokService({ accessToken })` — which is what the caller
9
+ * holding the rendered video usually is.
10
+ */
11
+ export declare class TikTokModule {
12
+ static forRoot(credentials?: TikTokCredentials): DynamicModule;
13
+ }
@@ -0,0 +1,75 @@
1
+ export interface TikTokCredentials {
2
+ /** Token de usuario con `video.upload` o `video.publish`. */
3
+ accessToken: string;
4
+ }
5
+ /** Lo que TikTok cuenta de la cuenta antes de dejar publicar. */
6
+ export interface TikTokCreatorInfo {
7
+ creator_username: string;
8
+ creator_nickname: string;
9
+ /** Niveles de privacidad que la cuenta admite. Ver `publish`. */
10
+ privacy_level_options: string[];
11
+ max_video_post_duration_sec: number;
12
+ }
13
+ export interface TikTokVideo {
14
+ /** El vídeo, ya en MP4 H.264. */
15
+ data: Buffer;
16
+ /** El texto del post. Hasta 2200 caracteres. */
17
+ title: string;
18
+ /**
19
+ * Cómo queda publicado. Si se omite, va a **borradores**: aparece en la
20
+ * bandeja de TikTok de la cuenta y una persona le da publicar.
21
+ *
22
+ * Ese es el modo que conviene mientras la aplicación no haya pasado la
23
+ * auditoría de TikTok, porque el modo directo publica en `SELF_ONLY` hasta
24
+ * entonces —es decir, no lo ve nadie— mientras que el borrador llega igual
25
+ * y se publica a mano sin perder alcance.
26
+ */
27
+ privacyLevel?: string;
28
+ }
29
+ /**
30
+ * Publica vídeos con la Content Posting API de TikTok.
31
+ *
32
+ * El flujo son tres llamadas y ninguna es opcional: preguntar por la cuenta,
33
+ * inicializar la subida, y sondear hasta que TikTok termine de procesar. La
34
+ * primera es un requisito de sus guías —no una cortesía—: la API rechaza
35
+ * publicar sin haber consultado antes qué admite la cuenta.
36
+ *
37
+ * **Dos modos, y la diferencia importa mucho al principio.** En *Direct Post*
38
+ * el vídeo sale en vivo, pero exige el permiso `video.publish`, que solo se
39
+ * consigue pasando una auditoría de TikTok de varias semanas; hasta aprobarla
40
+ * todo lo publicado queda en `SELF_ONLY` y no lo ve nadie. En modo *borrador*
41
+ * —el de por defecto aquí— el vídeo cae en la bandeja de la cuenta y una
42
+ * persona le da publicar desde el teléfono: llega con todo su alcance y solo
43
+ * necesita `video.upload`.
44
+ *
45
+ * Se sube el binario en vez de pasar una URL a propósito. La otra vía,
46
+ * `PULL_FROM_URL`, obliga a verificar el dominio en el portal de TikTok, y
47
+ * cambiar un trámite por otro no compensa cuando el archivo ya está en disco.
48
+ *
49
+ * El límite es de 6 peticiones por minuto y por token, así que el sondeo del
50
+ * estado va espaciado.
51
+ */
52
+ export declare class TikTokService {
53
+ private readonly credentials;
54
+ constructor(credentials: TikTokCredentials);
55
+ get configured(): boolean;
56
+ /**
57
+ * Datos de la cuenta. Hay que llamarlo antes de publicar: TikTok lo exige
58
+ * y además dice qué niveles de privacidad admite, que varían según si la
59
+ * cuenta es privada, de empresa o de menor de edad.
60
+ */
61
+ creatorInfo(): Promise<TikTokCreatorInfo>;
62
+ /**
63
+ * Sube un vídeo y devuelve el `publish_id` con el que seguir su estado.
64
+ *
65
+ * Sin `privacyLevel` va a borradores; con él, se publica directo —y para eso
66
+ * hace falta que la aplicación haya pasado la auditoría.
67
+ */
68
+ publish(video: TikTokVideo): Promise<string>;
69
+ /** En qué va el procesado. Termina en `PUBLISH_COMPLETE` o en un fallo. */
70
+ status(publishId: string): Promise<{
71
+ status: string;
72
+ fail_reason?: string;
73
+ }>;
74
+ private request;
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miguelmorales13/nestkit",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -116,6 +116,16 @@
116
116
  "import": "./dist/meta/index.js",
117
117
  "require": "./dist/meta/index.cjs"
118
118
  },
119
+ "./pinterest": {
120
+ "types": "./dist/pinterest/index.d.ts",
121
+ "import": "./dist/pinterest/index.js",
122
+ "require": "./dist/pinterest/index.cjs"
123
+ },
124
+ "./tiktok": {
125
+ "types": "./dist/tiktok/index.d.ts",
126
+ "import": "./dist/tiktok/index.js",
127
+ "require": "./dist/tiktok/index.cjs"
128
+ },
119
129
  "./media": {
120
130
  "types": "./dist/media/index.d.ts",
121
131
  "import": "./dist/media/index.js",
@@ -1,9 +1,9 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkJ7TURDALcjs = require('./chunk-J7TURDAL.cjs');
3
+ var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
4
4
 
5
5
 
6
- var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
6
+ var _chunkJ7TURDALcjs = require('./chunk-J7TURDAL.cjs');
7
7
 
8
8
  // src/bootstrap/apply-defaults.ts
9
9
 
@@ -1,9 +1,9 @@
1
- import {
2
- GlobalExceptionFilter
3
- } from "./chunk-7I2Y7V52.js";
4
1
  import {
5
2
  ResponseInterceptor
6
3
  } from "./chunk-KDAA6GFF.js";
4
+ import {
5
+ GlobalExceptionFilter
6
+ } from "./chunk-7I2Y7V52.js";
7
7
 
8
8
  // src/bootstrap/apply-defaults.ts
9
9
  import {