@miguelmorales13/nestkit 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +250 -6
  2. package/dist/auth/index.cjs +12 -70
  3. package/dist/auth/index.js +11 -69
  4. package/dist/auth/oauth/http.d.ts +38 -0
  5. package/dist/auth/oauth/index.cjs +610 -0
  6. package/dist/auth/oauth/index.d.ts +12 -0
  7. package/dist/auth/oauth/index.js +610 -0
  8. package/dist/auth/oauth/oauth-auth.module.d.ts +28 -0
  9. package/dist/auth/oauth/oauth-auth.service.d.ts +101 -0
  10. package/dist/auth/oauth/oauth-callback.filter.d.ts +20 -0
  11. package/dist/auth/oauth/oauth-provider.d.ts +73 -0
  12. package/dist/auth/oauth/oauth.controller.d.ts +38 -0
  13. package/dist/auth/oauth/oauth.options.d.ts +89 -0
  14. package/dist/auth/oauth/oauth.ports.d.ts +86 -0
  15. package/dist/bootstrap/index.cjs +3 -3
  16. package/dist/bootstrap/index.js +2 -2
  17. package/dist/chunk-A3B2EY4V.js +75 -0
  18. package/dist/chunk-NVCI3CQI.cjs +75 -0
  19. package/dist/database/typeorm/index.cjs +79 -0
  20. package/dist/database/typeorm/index.d.ts +3 -0
  21. package/dist/database/typeorm/index.js +79 -0
  22. package/dist/database/typeorm/tenant-scope.d.ts +18 -0
  23. package/dist/database/typeorm/typeorm.module.d.ts +35 -0
  24. package/dist/index.cjs +17 -17
  25. package/dist/index.js +24 -24
  26. package/dist/umami/index.cjs +109 -0
  27. package/dist/umami/index.d.ts +4 -0
  28. package/dist/umami/index.js +109 -0
  29. package/dist/umami/umami.module.d.ts +6 -0
  30. package/dist/umami/umami.options.d.ts +20 -0
  31. package/dist/umami/umami.service.d.ts +40 -0
  32. package/dist/umami/umami.types.d.ts +24 -0
  33. package/package.json +22 -2
  34. package/dist/{chunk-AOCF5QCZ.js → chunk-ANQ3YPDI.js} +3 -3
  35. package/dist/{chunk-54ZXIB5T.cjs → chunk-YARLPYG5.cjs} +2 -2
package/README.md CHANGED
@@ -1,10 +1,19 @@
1
1
  # @miguelmorales13/nestkit
2
2
 
3
- Bloques de infraestructura reutilizables para servicios NestJS: i18n, manejo de errores,
4
- respuestas estandarizadas, tracking de requests, entidades base, CRUD genérico, y conexión a
5
- Postgres/Supabase (con el contrato listo para Mongo a futuro). Pensado para eliminar el boilerplate
3
+ Bloques de infraestructura reutilizables para servicios NestJS. Pensado para eliminar el boilerplate
6
4
  que se repite al arrancar cada backend nuevo.
7
5
 
6
+ **El esqueleto**: entidades base, `Repository<T>` y CRUD genérico, respuestas estandarizadas, manejo
7
+ de errores, tracking de requests, i18n, y un `applyNestKitDefaults` que lo cablea todo en una línea.
8
+
9
+ **Las conexiones**: Postgres (con aislamiento por tenant vía RLS) y Supabase — el contrato de Mongo
10
+ está listo, la implementación no.
11
+
12
+ **Las integraciones**, cada una en su subpath y sin arrastrar dependencias a quien no la use: acceso
13
+ con Google o Facebook (`auth/oauth`) o con email y contraseña (`auth`), Stripe, Telegram, WhatsApp,
14
+ Instagram y Facebook Pages (`meta`), subida de archivos (`storage`), email (Resend o Nodemailer) y
15
+ lectura de analítica (`umami`).
16
+
8
17
  ## ¿Para qué proyectos es esto?
9
18
 
10
19
  **Sí**: cualquier proyecto NestJS nuevo que arranque con el patrón hexagonal/ports-and-adapters
@@ -31,16 +40,30 @@ tu proyecto realmente use:
31
40
  |---------------------|------------------|
32
41
  | `@nestjs/common`, `@nestjs/core`, `reflect-metadata`, `rxjs` | `pg` → `./database/postgres` |
33
42
  | | `@supabase/supabase-js` → `./database/supabase` |
34
- | | `helmet`, `@nestjs/swagger` → `./bootstrap` (`applyNestKitDefaults`) |
43
+ | | `helmet`, `@nestjs/swagger` → `./bootstrap` |
44
+ | | `telegraf` → `./telegram` |
45
+ | | `stripe` → `./stripe` |
46
+ | | `resend` → `./email/resend` |
47
+ | | `nodemailer` → `./email/nodemailer` |
48
+ | | `@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, `@aws-sdk/s3-request-presigner` → `./storage` (solo el adaptador S3) |
35
49
 
36
50
  ```bash
37
51
  bun add @nestjs/common @nestjs/core reflect-metadata rxjs
38
52
  bun add pg # si usas ./database/postgres
39
53
  bun add @supabase/supabase-js # si usas ./database/supabase
40
54
  bun add helmet @nestjs/swagger # si usas ./bootstrap
55
+ bun add telegraf # si usas ./telegram
56
+ bun add stripe # si usas ./stripe
41
57
  bun add class-validator class-transformer # requeridos en runtime por ValidationPipe
42
58
  ```
43
59
 
60
+ `meta`, `media`, `umami` y `auth/oauth` no piden nada extra: hablan con sus APIs por el `fetch` del
61
+ runtime. `auth/oauth` sí necesita cookies en el adaptador HTTP de tu app —`@fastify/cookie` o
62
+ `cookie-parser`—, que es tuyo, no del paquete.
63
+
64
+ `bcryptjs` y `jsonwebtoken` (los usan `auth` y `auth/oauth`) van como dependencias normales, ocultas:
65
+ no los instalas ni los importas vos.
66
+
44
67
  ## Quickstart (`main.ts`)
45
68
 
46
69
  ```ts
@@ -209,6 +232,31 @@ hace que Row-Level-Security de Postgres vea el usuario correcto: abre una transa
209
232
  commit/rollback — **cualquier query que dependa de una policy RLS basada en `auth.uid()` debe pasar
210
233
  por aquí**, `pool.query()` directo no aplica el `set_config` y verá (o no verá) filas incorrectamente.
211
234
 
235
+ ### `database/typeorm` — DataSource + RLS por tenant, para el que quiere ORM
236
+
237
+ Mismo modelo que `database/postgres`, pero con **TypeORM** para quien prefiere entidades,
238
+ repositorios y migraciones del ORM. `TypeOrmDbModule.forRoot({ entities, migrations })` provee un
239
+ `DataSource` inicializado bajo el token `DATA_SOURCE` (`type: 'postgres'` fijo, `synchronize`
240
+ siempre apagado — los cambios de esquema van por migraciones, jamás por auto-sync). Funciona con
241
+ cualquier Postgres, incluido **Neon**.
242
+
243
+ Lo importante para multi-tenant: el pool del ORM no tiene sesión por-request, así que la RLS no
244
+ vería el tenant. Por eso este subpath trae su propio **`withTenantScope(dataSource, session, fn)`**,
245
+ que abre un QueryRunner, corre `set_config(name, value, true)` por cada variable de sesión, y te pasa
246
+ el `EntityManager` **atado a esa conexión y transacción** — dentro, `manager.getRepository(X).find()`
247
+ corre con la RLS aplicada; fuera, no. Es el equivalente en TypeORM del `withTenantScope` de `pg`, y
248
+ está probado: con una policy sobre un GUC, cada tenant ve solo lo suyo y sin scope no ve nada.
249
+
250
+ ```ts
251
+ await withTenantScope(dataSource, {
252
+ 'request.jwt.claim.sub': userId,
253
+ 'request.jwt.claim.tenant': tenantId,
254
+ }, (manager) => manager.getRepository(Order).find());
255
+ ```
256
+
257
+ Peer dep opcional: `typeorm` (solo si se usa este módulo). Elegí ESTE o `database/postgres`, no los
258
+ dos a la vez — son dos formas de lo mismo.
259
+
212
260
  ### `database/supabase` — clientes anon y service-role
213
261
 
214
262
  ```ts
@@ -449,6 +497,44 @@ razonable para la Cloud API, que de todos modos es un REST plano, así que `What
449
497
  `fetch` global de Node (18+) directo. Solo manda mensajes de texto (`sendTextMessage`) — sin
450
498
  templates, sin media, sin manejo de mensajes entrantes.
451
499
 
500
+ ### `meta` — publicar en Instagram y Facebook Pages
501
+
502
+ Cliente de salida sobre la Graph API, solo para publicar: no lee insights, ni maneja webhooks, ni
503
+ comentarios. Mismo alcance que `telegram`.
504
+
505
+ ```ts
506
+ import { MetaModule } from '@miguelmorales13/nestkit/meta';
507
+
508
+ @Module({ imports: [MetaModule.forRoot()] }) // lee META_ACCESS_TOKEN, META_PAGE_ID, META_IG_USER_ID
509
+ export class AppModule {}
510
+ ```
511
+
512
+ ```ts
513
+ await meta.publishToInstagram(url, 'Texto del pie', 'feed'); // 'feed' | 'stories' | 'reels'
514
+ await meta.publishToPage(url, 'Texto');
515
+ await meta.publishStoryToPage(url);
516
+ await meta.describe(); // a qué cuenta apunta el token, para diagnosticar
517
+ ```
518
+
519
+ Dos cosas de esta API mandan sobre todo lo demás, y conviene saberlas antes:
520
+
521
+ **Las imágenes nunca se suben.** Se pasa una URL pública y Meta la descarga. Solo la necesita
522
+ durante la publicación —después guarda su propia copia y sirve el archivo desde su CDN—, así que
523
+ alcanza con un hosting efímero. Para eso está `media`, en este mismo paquete.
524
+
525
+ **Publicar en una Página exige un Page token, no un user token.** Con un token de usuario Meta
526
+ asume que estás posteando como persona y pide `publish_actions`, un permiso que eliminó en 2018, así
527
+ que el error que devuelve no apunta ni de cerca a la causa real.
528
+
529
+ Permisos: `instagram_basic` e `instagram_content_publish` para Instagram, `pages_manage_posts` para
530
+ Facebook. Publicar en tus propias Páginas no necesita App Review; hacerlo en nombre de terceros sí.
531
+
532
+ Instagram publica en dos pasos (crear contenedor, publicar) y el servicio espera solo a que el
533
+ contenedor esté listo — no hace falta encadenarlo a mano.
534
+
535
+ `forRoot()` sin argumentos toma las credenciales del entorno; pasándoselas explícitas se puede tener
536
+ un proceso publicando en varias cuentas.
537
+
452
538
  ### `stripe` — cliente de la SDK oficial
453
539
 
454
540
  ```ts
@@ -628,6 +714,81 @@ necesitás otra lógica de roles al alta (ej. el primer usuario de un tenant nue
628
714
  `BaseAuthService` y sobreescribí `register()`, o llamá `users.create()` directo con los roles que
629
715
  quieras desde tu propio controller.
630
716
 
717
+ ### `auth/oauth` — entrar con Google o Facebook, sin Passport
718
+
719
+ Lo de arriba es email + contraseña. Esto es lo otro: alta y acceso delegados a un proveedor,
720
+ vinculación de cuentas, y sesiones con refresh rotativo de un solo uso guardado hasheado. Son
721
+ módulos separados y se pueden usar los dos, uno, o ninguno.
722
+
723
+ **No usa Passport, a propósito.** Su paso de redirección llama a `res.setHeader()`, que no existe
724
+ en el reply de Fastify, así que el inicio del flujo hay que armarlo a mano igual; y su mapeo de
725
+ perfiles es una tabla de traducción no documentada que reenvía tal cual los nombres de campo que no
726
+ reconoce —así es como `photos.type(large)` llega a la Graph API de Facebook pidiendo los álbumes
727
+ subidos por la persona, que necesitan un permiso que la app no tiene, y el callback muere con un
728
+ 500. El flujo entero son unas cuarenta líneas de `fetch` y cada campo que se manda se ve en
729
+ `oauth-provider.ts`.
730
+
731
+ Un proveedor es un objeto, no una clase que heredar: agregar Apple o GitHub es declarar cuatro URLs
732
+ y una función que mapee su JSON a `OAuthProfile`.
733
+
734
+ ```ts
735
+ import {
736
+ OAuthAuthModule, OAUTH_STORE, REFRESH_TOKEN_STORE,
737
+ googleProvider, facebookProvider, createOAuthController,
738
+ } from '@miguelmorales13/nestkit/auth/oauth';
739
+
740
+ @Module({
741
+ imports: [
742
+ MiStoreModule, // exporta la clase que implementa los puertos
743
+ OAuthAuthModule.forRoot({
744
+ imports: [MiStoreModule],
745
+ providers: [
746
+ { provide: OAUTH_STORE, useExisting: MiStore },
747
+ { provide: REFRESH_TOKEN_STORE, useExisting: MiStore },
748
+ ],
749
+ options: {
750
+ providers: [googleProvider(), facebookProvider()],
751
+ webUrl: process.env.WEB_URL!,
752
+ cookie: { path: '/api/auth' }, // acotá la cookie a las rutas que la usan
753
+ mapUser: (u) => ({ id: u.id, email: u.email, name: u.name }),
754
+ },
755
+ }),
756
+ ],
757
+ controllers: [class AuthController extends createOAuthController() {}],
758
+ })
759
+ export class AuthModule {}
760
+ ```
761
+
762
+ Eso da `GET /auth/:provider`, `GET /auth/:provider/callback`, `POST /auth/refresh`,
763
+ `POST /auth/logout`, `GET /auth/me`, `GET /auth/linked-accounts` y `POST /auth/link-intent`. El
764
+ proveedor va como parámetro de ruta y no una pareja de rutas por proveedor, así que sumar Apple
765
+ después toca solo configuración.
766
+
767
+ **Los dos puertos van separados** (`OAuthStorePort` para identidades, `RefreshTokenStorePort` para
768
+ sesiones) porque responden preguntas distintas y un proyecto puede querer las sesiones en Redis. Si
769
+ van a la misma base, una sola clase implementa los dos y se atan con `useExisting`, como arriba.
770
+
771
+ `revokeIfActive` **tiene que ser atómico** —un UPDATE condicional, no un leer-y-escribir—: esa
772
+ fila es lo único que impide que un refresh token robado se use dos veces.
773
+
774
+ Personalizable sin tocar la librería: nombre y path de la cookie, TTLs, `sameSite`, a dónde vuelve
775
+ el navegador tras entrar / fallar / vincular, si un login cierra las demás sesiones
776
+ (`singleSession`, por defecto sí), qué campos del usuario salen por la API (`mapUser`), el guard de
777
+ las rutas con sesión (`guard`, si la app ya tiene el suyo) y todos los textos que ve el usuario.
778
+
779
+ `syncProfile` es la opción que casi siempre hay que pasar en cuanto la app deja elegir foto de
780
+ perfil: por defecto resincroniza nombre y foto del proveedor en cada login, lo que deshace en
781
+ silencio la elección de la persona la próxima vez que entre.
782
+
783
+ Sin email no hay alta. Facebook puede legítimamente no darlo —cuenta registrada con teléfono, o
784
+ permiso retirado en la pantalla de consentimiento—, y en ese caso se corta con un mensaje que la
785
+ persona puede accionar, no con un fallo interno. `OAuthCallbackFilter` se encarga de que cualquier
786
+ error del callback devuelva al navegador a la pantalla de acceso: sin él la persona se queda
787
+ mirando un JSON en una página en blanco, porque no llegó ahí por una petición de la app sino
788
+ redirigida desde el proveedor.
789
+
790
+ Requiere cookies en el adaptador HTTP: `@fastify/cookie` o `cookie-parser`.
791
+
631
792
  ### `email/resend` y `email/nodemailer` — dos transportes, elegís uno por proyecto
632
793
 
633
794
  Mismo patrón que `database/postgres` vs `database/supabase`: dos módulos independientes, no una
@@ -724,6 +885,82 @@ Bunny y local no requieren nada extra. Variables de entorno reconocidas: `STORAG
724
885
  Storage (no hay firma que oculte la contraseña de la zona); en esos casos se sube por la API, o se
725
886
  usa Bunny Stream/TUS para video grande.
726
887
 
888
+ ### `media` — hosting efímero para APIs que descargan por URL
889
+
890
+ No es almacenamiento. Es el complemento de `meta`: existe porque hay APIs —la de Meta es el caso
891
+ típico— que **rechazan el binario** y exigen bajarse el archivo ellas mismas, pero solo lo necesitan
892
+ durante la llamada. Sin algo así, el apaño habitual es commitear las imágenes al repositorio, que
893
+ las conserva para siempre por una URL que importó un minuto.
894
+
895
+ ```ts
896
+ import { MediaModule } from '@miguelmorales13/nestkit/media';
897
+
898
+ MediaModule.forRoot({
899
+ publicUrl: 'https://api.tuapp.com/api', // ¡con el prefijo global!
900
+ uploadSecret: process.env.MEDIA_SECRET,
901
+ ttlMs: 60 * 60 * 1000, // por defecto una hora
902
+ })
903
+ ```
904
+
905
+ Expone `POST /media` (devuelve `{ url }`) y `GET /media/:id`. La subida va protegida con un secreto
906
+ compartido en la cabecera `x-media-secret`, no con un JWT, porque quien llama es un job de CI y no
907
+ una persona con sesión; sin él cualquiera podría alojar contenido arbitrario en tu dominio. La
908
+ descarga es pública a propósito: los servicios que consumen estas URLs las piden sin credenciales.
909
+
910
+ **Vive en memoria, a propósito.** Entre guardar y publicar pasan segundos, así que un reinicio en el
911
+ medio solo significa reintentar, y no queda nada en disco. Si necesitás que los archivos sobrevivan
912
+ a un reinicio, esta es la herramienta equivocada: usá `storage`.
913
+
914
+ Dos errores que cuestan una tarde:
915
+
916
+ - `publicUrl` **tiene que incluir el prefijo global** de la app. Sin él la URL cae en lo que sirva el
917
+ frontend, que responde 200 con una página HTML, y el consumidor se descarga eso en vez del archivo.
918
+ - Fastify solo parsea JSON de fábrica. Sin registrar un parser para los tipos que vas a aceptar, la
919
+ subida responde 415:
920
+
921
+ ```ts
922
+ app.getHttpAdapter().getInstance().addContentTypeParser(
923
+ /^image\//, { parseAs: 'buffer' }, (_req, body, done) => done(null, body),
924
+ )
925
+ ```
926
+
927
+ ### `umami` — leer la API de analítica
928
+
929
+ Solo lee. Los eventos los manda el script del navegador; un backend escribiéndolos estaría
930
+ reportando tráfico que nunca ocurrió.
931
+
932
+ ```ts
933
+ import { UmamiModule, UmamiService } from '@miguelmorales13/nestkit/umami';
934
+
935
+ UmamiModule.forRoot({
936
+ apiKey: process.env.UMAMI_API_KEY, // Umami Cloud: Settings → API Keys
937
+ websiteId: process.env.UMAMI_WEBSITE_ID,
938
+ timezone: 'America/Mexico_City',
939
+ // baseUrl: 'https://tu-host/api', // solo si es self-hosted
940
+ })
941
+ ```
942
+
943
+ ```ts
944
+ const rango = UmamiService.lastDays(7);
945
+ await umami.stats(rango); // visitantes, pageviews, visitas, rebotes
946
+ await umami.metrics('referrer', rango, 5); // top 5 de dónde llegan
947
+ await umami.eventCount('registro', rango); // cuántas veces disparó un evento
948
+ ```
949
+
950
+ **Todos los métodos devuelven `null` en vez de lanzar** cuando la llamada falla. Un resumen suele
951
+ pedir varias cosas a la vez, y que un endpoint esté caído no debería llevarse el reporte entero; quien
952
+ llama decide qué significa un bloque ausente. Los fallos se loguean, así que uno persistente sigue
953
+ siendo visible.
954
+
955
+ `eventCount` distingue `0` de `null` a propósito: uno es «nadie lo hizo», el otro es «no sabemos».
956
+
957
+ `timezone` no es opcional en la práctica. Umami agrupa por día en esa zona, y dejarlo sin poner
958
+ reporta días UTC en silencio — que fuera de UTC significa un «día» que empieza a media tarde.
959
+
960
+ Deliberadamente no opina sobre qué significan los números: qué eventos forman un embudo, cómo
961
+ llamarlos y cómo redactar un reporte son decisiones de producto que cambian por proyecto. Esto solo
962
+ trae los datos.
963
+
727
964
  ## Subpaths disponibles
728
965
 
729
966
  | Subpath | Qué trae |
@@ -735,12 +972,17 @@ usa Bunny Stream/TUS para video grande.
735
972
  | `@miguelmorales13/nestkit/tracking` | `RequestContext`, `RequestIdMiddleware`, `TrackingModule` |
736
973
  | `@miguelmorales13/nestkit/crud` | `Repository<T>`, `BaseCrudService`, `createCrudController` |
737
974
  | `@miguelmorales13/nestkit/database/postgres` | `PgModule`, `PG_POOL`, `withTenantScope` |
975
+ | `@miguelmorales13/nestkit/database/typeorm` | `TypeOrmDbModule`, `DATA_SOURCE`, `withTenantScope` |
738
976
  | `@miguelmorales13/nestkit/database/supabase` | `SupabaseModule`, `SUPABASE_ANON_CLIENT`, `SUPABASE_SERVICE_ROLE_CLIENT` |
739
977
  | `@miguelmorales13/nestkit/database/mongo` | Solo el contrato `MongoRepositoryPort` — sin implementación |
740
978
  | `@miguelmorales13/nestkit/telegram` | `TelegramModule`, `TELEGRAM_BOT` |
741
979
  | `@miguelmorales13/nestkit/whatsapp` | `WhatsAppModule`, `WHATSAPP_CLIENT`, `WhatsAppClient` |
980
+ | `@miguelmorales13/nestkit/meta` | `MetaModule`, `MetaService` — publicar en Instagram y Facebook Pages |
981
+ | `@miguelmorales13/nestkit/media` | `MediaModule`, `MediaService` — hosting efímero en memoria |
742
982
  | `@miguelmorales13/nestkit/stripe` | `StripeModule`, `STRIPE_CLIENT`, `createStripeWebhookController` |
743
983
  | `@miguelmorales13/nestkit/auth` | `AuthUserPort`, `BaseAuthService`, `createAuthController`, `TokenService`, `JwtAuthGuard`, `RolesGuard`, `RequireTenantGuard`, `CurrentUser`, `CurrentTenant`, `Roles` |
984
+ | `@miguelmorales13/nestkit/auth/oauth` | `OAuthAuthModule`, `OAuthAuthService`, `createOAuthController`, `googleProvider`, `facebookProvider`, `OAuthStorePort`, `RefreshTokenStorePort` |
985
+ | `@miguelmorales13/nestkit/umami` | `UmamiModule`, `UmamiService` — lectura de la API de analítica |
744
986
  | `@miguelmorales13/nestkit/storage` | `StorageModule`, `STORAGE`, `StoragePort`, adaptadores `LocalStorage`/`S3Storage`/`BunnyStorage` |
745
987
  | `@miguelmorales13/nestkit/email/resend` | `ResendModule`, `RESEND_CLIENT` |
746
988
  | `@miguelmorales13/nestkit/email/nodemailer` | `NodemailerModule`, `NODEMAILER_TRANSPORT` |
@@ -749,5 +991,7 @@ usa Bunny Stream/TUS para video grande.
749
991
 
750
992
  ## Estado del paquete
751
993
 
752
- `0.5.1`. Sin adaptador Mongo real. Sin tests unitarios propios — es un paquete nuevo; la primera
753
- integración real (ej. en `hr-pymes-saas` o `cuota`) es la que ejercita el código de verdad.
994
+ `0.7.0`. Sin adaptador Mongo real. Sin tests unitarios propios.
995
+
996
+ `auth/oauth` y `umami` sí tienen un consumidor real: **nutrimx** corre su acceso con Google y
997
+ Facebook sobre este módulo en producción. El resto sigue sin ejercitarse de verdad.
@@ -1,4 +1,9 @@
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; }
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); 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
+
3
+
4
+
5
+ var _chunkNVCI3CQIcjs = require('../chunk-NVCI3CQI.cjs');
6
+
2
7
 
3
8
 
4
9
 
@@ -19,65 +24,8 @@ function verifyPassword(plain, passwordHash) {
19
24
  return _bcryptjs.compare.call(void 0, plain, passwordHash);
20
25
  }
21
26
 
22
- // src/auth/token.service.ts
23
- var _jsonwebtoken = require('jsonwebtoken'); var _jsonwebtoken2 = _interopRequireDefault(_jsonwebtoken);
24
- function requireEnv(name) {
25
- const value = process.env[name];
26
- if (!value) {
27
- throw new Error(`TokenService: ${name} environment variable is not set`);
28
- }
29
- return value;
30
- }
31
- var TokenService = class {
32
- signAccessToken(payload) {
33
- return _jsonwebtoken2.default.sign(payload, requireEnv("JWT_ACCESS_SECRET"), {
34
- expiresIn: _nullishCoalesce(process.env.JWT_ACCESS_EXPIRES_IN, () => ( "15m"))
35
- });
36
- }
37
- signRefreshToken(payload) {
38
- return _jsonwebtoken2.default.sign(payload, requireEnv("JWT_REFRESH_SECRET"), {
39
- expiresIn: _nullishCoalesce(process.env.JWT_REFRESH_EXPIRES_IN, () => ( "30d"))
40
- });
41
- }
42
- verifyAccessToken(token) {
43
- try {
44
- return _jsonwebtoken2.default.verify(token, requireEnv("JWT_ACCESS_SECRET"));
45
- } catch (e) {
46
- throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Invalid or expired access token");
47
- }
48
- }
49
- verifyRefreshToken(token) {
50
- try {
51
- return _jsonwebtoken2.default.verify(token, requireEnv("JWT_REFRESH_SECRET"));
52
- } catch (e2) {
53
- throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Invalid or expired refresh token");
54
- }
55
- }
56
- };
57
-
58
- // src/auth/jwt-auth.guard.ts
59
- var _common = require('@nestjs/common');
60
- var JwtAuthGuard = class {
61
- constructor() {
62
- this.tokens = new TokenService();
63
- }
64
- canActivate(context) {
65
- const req = context.switchToHttp().getRequest();
66
- const header = req.headers.authorization;
67
- const token = _optionalChain([header, 'optionalAccess', _2 => _2.startsWith, 'call', _3 => _3("Bearer ")]) ? header.slice("Bearer ".length) : void 0;
68
- if (!token) {
69
- throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Missing Authorization bearer token");
70
- }
71
- req.user = this.tokens.verifyAccessToken(token);
72
- return true;
73
- }
74
- };
75
- JwtAuthGuard = exports.JwtAuthGuard = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
76
- _common.Injectable.call(void 0, )
77
- ], JwtAuthGuard);
78
-
79
27
  // src/auth/roles.decorator.ts
80
-
28
+ var _common = require('@nestjs/common');
81
29
  var ROLES_KEY = "nestkit:roles";
82
30
  var Roles = (...roles) => _common.SetMetadata.call(void 0, ROLES_KEY, roles);
83
31
 
@@ -95,7 +43,7 @@ var RolesGuard = class {
95
43
  ]);
96
44
  if (!required || required.length === 0) return true;
97
45
  const req = context.switchToHttp().getRequest();
98
- const has = _nullishCoalesce(_optionalChain([req, 'access', _4 => _4.user, 'optionalAccess', _5 => _5.roles, 'access', _6 => _6.some, 'call', _7 => _7((role) => required.includes(role))]), () => ( false));
46
+ const has = _nullishCoalesce(_optionalChain([req, 'access', _2 => _2.user, 'optionalAccess', _3 => _3.roles, 'access', _4 => _4.some, 'call', _5 => _5((role) => required.includes(role))]), () => ( false));
99
47
  if (!has) {
100
48
  throw new (0, _chunkFDNGAYTZcjs.ForbiddenAppException)("Insufficient role");
101
49
  }
@@ -106,16 +54,10 @@ RolesGuard = exports.RolesGuard = _chunk2REOCMUDcjs.__decorateClass.call(void 0,
106
54
  _common.Injectable.call(void 0, )
107
55
  ], RolesGuard);
108
56
 
109
- // src/auth/current-user.decorator.ts
110
-
111
- var CurrentUser = _common.createParamDecorator.call(void 0,
112
- (_, ctx) => ctx.switchToHttp().getRequest().user
113
- );
114
-
115
57
  // src/auth/current-tenant.decorator.ts
116
58
 
117
59
  var CurrentTenant = _common.createParamDecorator.call(void 0,
118
- (_, ctx) => _optionalChain([ctx, 'access', _8 => _8.switchToHttp, 'call', _9 => _9(), 'access', _10 => _10.getRequest, 'call', _11 => _11(), 'access', _12 => _12.user, 'optionalAccess', _13 => _13.tenantId])
60
+ (_, ctx) => _optionalChain([ctx, 'access', _6 => _6.switchToHttp, 'call', _7 => _7(), 'access', _8 => _8.getRequest, 'call', _9 => _9(), 'access', _10 => _10.user, 'optionalAccess', _11 => _11.tenantId])
119
61
  );
120
62
 
121
63
  // src/auth/require-tenant.guard.ts
@@ -123,7 +65,7 @@ var CurrentTenant = _common.createParamDecorator.call(void 0,
123
65
  var RequireTenantGuard = class {
124
66
  canActivate(context) {
125
67
  const req = context.switchToHttp().getRequest();
126
- if (!_optionalChain([req, 'access', _14 => _14.user, 'optionalAccess', _15 => _15.tenantId])) {
68
+ if (!_optionalChain([req, 'access', _12 => _12.user, 'optionalAccess', _13 => _13.tenantId])) {
127
69
  throw new (0, _chunkFDNGAYTZcjs.ForbiddenAppException)("This action requires a company account");
128
70
  }
129
71
  return true;
@@ -164,7 +106,7 @@ _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
164
106
 
165
107
  // src/auth/auth.service.ts
166
108
  var BaseAuthService = class {
167
- constructor(users, tokens = new TokenService()) {
109
+ constructor(users, tokens = new (0, _chunkNVCI3CQIcjs.TokenService)()) {
168
110
  this.users = users;
169
111
  this.tokens = tokens;
170
112
  }
@@ -262,4 +204,4 @@ function createAuthController(serviceToken, options = {}) {
262
204
 
263
205
 
264
206
 
265
- exports.BaseAuthService = BaseAuthService; exports.CurrentTenant = CurrentTenant; exports.CurrentUser = CurrentUser; exports.JwtAuthGuard = JwtAuthGuard; exports.LoginDto = LoginDto; exports.ROLES_KEY = ROLES_KEY; exports.RefreshDto = RefreshDto; exports.RegisterDto = RegisterDto; exports.RequireTenantGuard = RequireTenantGuard; exports.Roles = Roles; exports.RolesGuard = RolesGuard; exports.TokenService = TokenService; exports.createAuthController = createAuthController; exports.hashPassword = hashPassword; exports.verifyPassword = verifyPassword;
207
+ exports.BaseAuthService = BaseAuthService; exports.CurrentTenant = CurrentTenant; exports.CurrentUser = _chunkNVCI3CQIcjs.CurrentUser; exports.JwtAuthGuard = _chunkNVCI3CQIcjs.JwtAuthGuard; exports.LoginDto = LoginDto; exports.ROLES_KEY = ROLES_KEY; exports.RefreshDto = RefreshDto; exports.RegisterDto = RegisterDto; exports.RequireTenantGuard = RequireTenantGuard; exports.Roles = Roles; exports.RolesGuard = RolesGuard; exports.TokenService = _chunkNVCI3CQIcjs.TokenService; exports.createAuthController = createAuthController; exports.hashPassword = hashPassword; exports.verifyPassword = verifyPassword;
@@ -1,3 +1,8 @@
1
+ import {
2
+ CurrentUser,
3
+ JwtAuthGuard,
4
+ TokenService
5
+ } from "../chunk-A3B2EY4V.js";
1
6
  import {
2
7
  ConflictAppException,
3
8
  ForbiddenAppException,
@@ -19,70 +24,13 @@ function verifyPassword(plain, passwordHash) {
19
24
  return compare(plain, passwordHash);
20
25
  }
21
26
 
22
- // src/auth/token.service.ts
23
- import jwt from "jsonwebtoken";
24
- function requireEnv(name) {
25
- const value = process.env[name];
26
- if (!value) {
27
- throw new Error(`TokenService: ${name} environment variable is not set`);
28
- }
29
- return value;
30
- }
31
- var TokenService = class {
32
- signAccessToken(payload) {
33
- return jwt.sign(payload, requireEnv("JWT_ACCESS_SECRET"), {
34
- expiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? "15m"
35
- });
36
- }
37
- signRefreshToken(payload) {
38
- return jwt.sign(payload, requireEnv("JWT_REFRESH_SECRET"), {
39
- expiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? "30d"
40
- });
41
- }
42
- verifyAccessToken(token) {
43
- try {
44
- return jwt.verify(token, requireEnv("JWT_ACCESS_SECRET"));
45
- } catch {
46
- throw new UnauthorizedAppException("Invalid or expired access token");
47
- }
48
- }
49
- verifyRefreshToken(token) {
50
- try {
51
- return jwt.verify(token, requireEnv("JWT_REFRESH_SECRET"));
52
- } catch {
53
- throw new UnauthorizedAppException("Invalid or expired refresh token");
54
- }
55
- }
56
- };
57
-
58
- // src/auth/jwt-auth.guard.ts
59
- import { Injectable } from "@nestjs/common";
60
- var JwtAuthGuard = class {
61
- constructor() {
62
- this.tokens = new TokenService();
63
- }
64
- canActivate(context) {
65
- const req = context.switchToHttp().getRequest();
66
- const header = req.headers.authorization;
67
- const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : void 0;
68
- if (!token) {
69
- throw new UnauthorizedAppException("Missing Authorization bearer token");
70
- }
71
- req.user = this.tokens.verifyAccessToken(token);
72
- return true;
73
- }
74
- };
75
- JwtAuthGuard = __decorateClass([
76
- Injectable()
77
- ], JwtAuthGuard);
78
-
79
27
  // src/auth/roles.decorator.ts
80
28
  import { SetMetadata } from "@nestjs/common";
81
29
  var ROLES_KEY = "nestkit:roles";
82
30
  var Roles = (...roles) => SetMetadata(ROLES_KEY, roles);
83
31
 
84
32
  // src/auth/roles.guard.ts
85
- import { Injectable as Injectable2 } from "@nestjs/common";
33
+ import { Injectable } from "@nestjs/common";
86
34
  import "@nestjs/core";
87
35
  var RolesGuard = class {
88
36
  constructor(reflector) {
@@ -103,23 +51,17 @@ var RolesGuard = class {
103
51
  }
104
52
  };
105
53
  RolesGuard = __decorateClass([
106
- Injectable2()
54
+ Injectable()
107
55
  ], RolesGuard);
108
56
 
109
- // src/auth/current-user.decorator.ts
110
- import { createParamDecorator } from "@nestjs/common";
111
- var CurrentUser = createParamDecorator(
112
- (_, ctx) => ctx.switchToHttp().getRequest().user
113
- );
114
-
115
57
  // src/auth/current-tenant.decorator.ts
116
- import { createParamDecorator as createParamDecorator2 } from "@nestjs/common";
117
- var CurrentTenant = createParamDecorator2(
58
+ import { createParamDecorator } from "@nestjs/common";
59
+ var CurrentTenant = createParamDecorator(
118
60
  (_, ctx) => ctx.switchToHttp().getRequest().user?.tenantId
119
61
  );
120
62
 
121
63
  // src/auth/require-tenant.guard.ts
122
- import { Injectable as Injectable3 } from "@nestjs/common";
64
+ import { Injectable as Injectable2 } from "@nestjs/common";
123
65
  var RequireTenantGuard = class {
124
66
  canActivate(context) {
125
67
  const req = context.switchToHttp().getRequest();
@@ -130,7 +72,7 @@ var RequireTenantGuard = class {
130
72
  }
131
73
  };
132
74
  RequireTenantGuard = __decorateClass([
133
- Injectable3()
75
+ Injectable2()
134
76
  ], RequireTenantGuard);
135
77
 
136
78
  // src/auth/auth.dto.ts
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The little the OAuth controller needs from the HTTP layer, described
3
+ * structurally so the module works on Fastify and Express without depending
4
+ * on either.
5
+ *
6
+ * Cookie support is not built in on either adapter — the app has to register
7
+ * `@fastify/cookie` or `cookie-parser`. Fastify names the methods
8
+ * `setCookie`/`clearCookie`, Express names them `cookie`/`clearCookie`; the
9
+ * helpers below paper over exactly that difference and nothing else.
10
+ */
11
+ export interface CookieOptions {
12
+ httpOnly?: boolean;
13
+ secure?: boolean;
14
+ sameSite?: 'lax' | 'strict' | 'none';
15
+ path?: string;
16
+ expires?: Date;
17
+ }
18
+ export interface ReplyLike {
19
+ redirect: ((url: string, statusCode?: number) => unknown) & ((...args: never[]) => unknown);
20
+ setCookie?(name: string, value: string, options?: CookieOptions): unknown;
21
+ cookie?(name: string, value: string, options?: CookieOptions): unknown;
22
+ clearCookie?(name: string, options?: CookieOptions): unknown;
23
+ }
24
+ export interface RequestLike {
25
+ cookies?: Record<string, string | undefined>;
26
+ query?: Record<string, unknown>;
27
+ }
28
+ export declare function setCookie(reply: ReplyLike, name: string, value: string, options: CookieOptions): void;
29
+ export declare function clearCookie(reply: ReplyLike, name: string, options: CookieOptions): void;
30
+ /**
31
+ * Redirects with an explicit 302.
32
+ *
33
+ * Not optional: Nest's Fastify adapter answers `reply.redirect(url)` with a
34
+ * 200 carrying a Location header, which no browser follows — the sign-in
35
+ * simply stops on a blank page. Express ignores the second argument when the
36
+ * first is a URL, so the same call is correct on both.
37
+ */
38
+ export declare function redirect(reply: ReplyLike, url: string): void;