@hed-hog/core 0.0.373 → 0.0.374

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 (107) hide show
  1. package/README.md +83 -1
  2. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  3. package/dist/integration-profile/integration-profile.secrets.js +4 -0
  4. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  5. package/dist/integration-profile/integration-profile.utils.d.ts +6 -0
  6. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  7. package/dist/integration-profile/integration-profile.utils.js +1 -1
  8. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  9. package/dist/oauth/oauth.controller.d.ts +11 -3
  10. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  11. package/dist/oauth/oauth.controller.js +52 -14
  12. package/dist/oauth/oauth.controller.js.map +1 -1
  13. package/dist/oauth/oauth.module.d.ts.map +1 -1
  14. package/dist/oauth/oauth.module.js +4 -0
  15. package/dist/oauth/oauth.module.js.map +1 -1
  16. package/dist/oauth/oauth.service.d.ts +38 -2
  17. package/dist/oauth/oauth.service.d.ts.map +1 -1
  18. package/dist/oauth/oauth.service.js +122 -5
  19. package/dist/oauth/oauth.service.js.map +1 -1
  20. package/dist/oauth/providers/apple.provider.d.ts +34 -0
  21. package/dist/oauth/providers/apple.provider.d.ts.map +1 -0
  22. package/dist/oauth/providers/apple.provider.js +197 -0
  23. package/dist/oauth/providers/apple.provider.js.map +1 -0
  24. package/dist/oauth/providers/facebook.provider.d.ts +1 -1
  25. package/dist/oauth/providers/facebook.provider.d.ts.map +1 -1
  26. package/dist/oauth/providers/facebook.provider.js +3 -2
  27. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  28. package/dist/oauth/providers/google.provider.d.ts +1 -1
  29. package/dist/oauth/providers/google.provider.d.ts.map +1 -1
  30. package/dist/oauth/providers/google.provider.js +3 -2
  31. package/dist/oauth/providers/google.provider.js.map +1 -1
  32. package/dist/oauth/providers/linkedin.provider.d.ts +25 -0
  33. package/dist/oauth/providers/linkedin.provider.d.ts.map +1 -0
  34. package/dist/oauth/providers/linkedin.provider.js +99 -0
  35. package/dist/oauth/providers/linkedin.provider.js.map +1 -0
  36. package/dist/oauth/providers/microsoft-entra-id.provider.js +1 -1
  37. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  38. package/dist/oauth/providers/microsoft.provider.d.ts +1 -1
  39. package/dist/oauth/providers/microsoft.provider.d.ts.map +1 -1
  40. package/dist/oauth/providers/microsoft.provider.js +3 -2
  41. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  42. package/dist/pdf/pdf.service.d.ts.map +1 -1
  43. package/dist/pdf/pdf.service.js +19 -2
  44. package/dist/pdf/pdf.service.js.map +1 -1
  45. package/dist/setting/setting.service.d.ts.map +1 -1
  46. package/dist/setting/setting.service.js +4 -1
  47. package/dist/setting/setting.service.js.map +1 -1
  48. package/hedhog/data/integration_provider.yaml +16 -0
  49. package/hedhog/data/setting_group.yaml +98 -0
  50. package/hedhog/data/setting_subgroup.yaml +16 -0
  51. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +2 -0
  52. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +11 -11
  53. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +42 -0
  54. package/hedhog/frontend/app/menu/page.tsx.ejs +1 -1
  55. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +8 -0
  56. package/hedhog/frontend/messages/en.json +35 -0
  57. package/hedhog/frontend/messages/pt.json +35 -0
  58. package/hedhog/table/user_account.yaml +1 -0
  59. package/package.json +3 -3
  60. package/src/access-log/access-log.service.spec.ts +235 -0
  61. package/src/ai-instruction/ai-prompt-builder.service.spec.ts +250 -0
  62. package/src/auth/auth.service.spec.ts +891 -0
  63. package/src/auth/guards/auth.guard.spec.ts +177 -0
  64. package/src/challenge/challenge.service.spec.ts +334 -0
  65. package/src/dashboard/dashboard-core/dashboard-core.service.spec.ts +193 -0
  66. package/src/file/image-optimization.service.spec.ts +283 -0
  67. package/src/file/provider/provider.factory.spec.ts +111 -0
  68. package/src/install/install.service.spec.ts +139 -0
  69. package/src/integration/services/event-subscriber.registry.spec.ts +143 -0
  70. package/src/integration/services/inbox.service.spec.ts +183 -0
  71. package/src/integration/services/integration-link.service.spec.ts +217 -0
  72. package/src/integration/services/outbox.service.spec.ts +290 -0
  73. package/src/integration-profile/integration-credential-crypto.service.spec.ts +289 -0
  74. package/src/integration-profile/integration-profile.secrets.ts +4 -0
  75. package/src/integration-profile/integration-profile.utils.spec.ts +332 -0
  76. package/src/integration-profile/integration-profile.utils.ts +14 -1
  77. package/src/locale/locale.service.spec.ts +78 -0
  78. package/src/mail/mail.service.spec.ts +257 -0
  79. package/src/mcp/guards/mcp-auth.guard.spec.ts +87 -0
  80. package/src/mcp/guards/mcp-permission.guard.spec.ts +64 -0
  81. package/src/mcp/mcp-api-key.service.spec.ts +125 -0
  82. package/src/mcp/mcp-registry.service.spec.ts +96 -0
  83. package/src/mcp/mcp-server.service.spec.ts +187 -0
  84. package/src/menu/menu.service.spec.ts +76 -0
  85. package/src/notification/notification.service.spec.ts +297 -0
  86. package/src/oauth/oauth.controller.ts +53 -10
  87. package/src/oauth/oauth.module.ts +4 -0
  88. package/src/oauth/oauth.service.spec.ts +332 -0
  89. package/src/oauth/oauth.service.ts +156 -1
  90. package/src/oauth/providers/apple.provider.ts +175 -0
  91. package/src/oauth/providers/facebook.provider.ts +3 -2
  92. package/src/oauth/providers/google.provider.ts +3 -2
  93. package/src/oauth/providers/linkedin.provider.ts +96 -0
  94. package/src/oauth/providers/microsoft-entra-id.provider.ts +1 -1
  95. package/src/oauth/providers/microsoft.provider.ts +3 -2
  96. package/src/pdf/pdf.service.ts +20 -2
  97. package/src/role/guards/role.guard.spec.ts +157 -0
  98. package/src/role/role.service.spec.ts +179 -0
  99. package/src/session/session.service.spec.ts +376 -0
  100. package/src/setting/setting.service.spec.ts +316 -0
  101. package/src/setting/setting.service.ts +4 -1
  102. package/src/task/task.service.spec.ts +47 -0
  103. package/src/token/token.service.spec.ts +351 -0
  104. package/src/user/user.service.spec.ts +256 -0
  105. package/src/utils/locale-context.spec.ts +22 -0
  106. package/src/utils/webauthn-origin.spec.ts +73 -0
  107. package/src/webhook-integration/integration-event-catalog.service.spec.ts +149 -0
@@ -981,6 +981,104 @@
981
981
  subgroup_id:
982
982
  where:
983
983
  slug: microsoft-entra-id
984
+ - slug: oauth-apple-profile-id
985
+ type: string
986
+ component: entity-picker
987
+ name:
988
+ en: Apple OAuth Profile
989
+ pt: Perfil OAuth Apple
990
+ description:
991
+ en: Integration profile (type OAuth / Apple) used for Sign in with Apple
992
+ pt: Perfil de integração (tipo OAuth / Apple) utilizado para autenticação com a Apple
993
+ value: ""
994
+ user_override: false
995
+ subgroup_id:
996
+ where:
997
+ slug: apple
998
+ - slug: oauth-apple-enabled
999
+ type: boolean
1000
+ component: switch
1001
+ name:
1002
+ en: Enable Apple
1003
+ pt: Habilitar Apple
1004
+ description:
1005
+ en: Enable or disable Sign in with Apple without removing the configured profile
1006
+ pt: Habilitar ou desabilitar login com Apple sem remover o perfil configurado
1007
+ value: 'false'
1008
+ user_override: false
1009
+ subgroup_id:
1010
+ where:
1011
+ slug: apple
1012
+ - slug: oauth-apple-scopes
1013
+ type: array
1014
+ component: checkbox
1015
+ name:
1016
+ en: OAuth Scopes
1017
+ pt: Escopos OAuth
1018
+ description:
1019
+ en: Permissions requested from the user during Sign in with Apple
1020
+ pt: Permissões solicitadas ao usuário durante o fluxo de login com a Apple
1021
+ value: '["name","email"]'
1022
+ user_override: false
1023
+ relations:
1024
+ setting_list:
1025
+ - value: name
1026
+ order: 0
1027
+ - value: email
1028
+ order: 1
1029
+ subgroup_id:
1030
+ where:
1031
+ slug: apple
1032
+ - slug: oauth-linkedin-profile-id
1033
+ type: string
1034
+ component: entity-picker
1035
+ name:
1036
+ en: LinkedIn OAuth Profile
1037
+ pt: Perfil OAuth LinkedIn
1038
+ description:
1039
+ en: Integration profile (type OAuth / LinkedIn) used for LinkedIn authentication
1040
+ pt: Perfil de integração (tipo OAuth / LinkedIn) utilizado para autenticação com o LinkedIn
1041
+ value: ""
1042
+ user_override: false
1043
+ subgroup_id:
1044
+ where:
1045
+ slug: linkedin
1046
+ - slug: oauth-linkedin-enabled
1047
+ type: boolean
1048
+ component: switch
1049
+ name:
1050
+ en: Enable LinkedIn
1051
+ pt: Habilitar LinkedIn
1052
+ description:
1053
+ en: Enable or disable LinkedIn OAuth without removing the configured profile
1054
+ pt: Habilitar ou desabilitar OAuth do LinkedIn sem remover o perfil configurado
1055
+ value: 'false'
1056
+ user_override: false
1057
+ subgroup_id:
1058
+ where:
1059
+ slug: linkedin
1060
+ - slug: oauth-linkedin-scopes
1061
+ type: array
1062
+ component: checkbox
1063
+ name:
1064
+ en: OAuth Scopes
1065
+ pt: Escopos OAuth
1066
+ description:
1067
+ en: Permissions requested from the user during LinkedIn OAuth flow
1068
+ pt: Permissões solicitadas ao usuário durante o fluxo OAuth do LinkedIn
1069
+ value: '["openid","profile","email"]'
1070
+ user_override: false
1071
+ relations:
1072
+ setting_list:
1073
+ - value: openid
1074
+ order: 0
1075
+ - value: profile
1076
+ order: 1
1077
+ - value: email
1078
+ order: 2
1079
+ subgroup_id:
1080
+ where:
1081
+ slug: linkedin
984
1082
  - slug: appearance
985
1083
  icon: palette
986
1084
  name:
@@ -182,6 +182,22 @@
182
182
  en: Microsoft Entra ID (Azure AD) OAuth Provider
183
183
  pt: Provedor OAuth Microsoft Entra ID (Azure AD)
184
184
 
185
+ - slug: apple
186
+ name:
187
+ en: Apple
188
+ pt: Apple
189
+ description:
190
+ en: Apple OAuth Provider (Sign in with Apple)
191
+ pt: Provedor OAuth da Apple (Sign in with Apple)
192
+
193
+ - slug: linkedin
194
+ name:
195
+ en: LinkedIn
196
+ pt: LinkedIn
197
+ description:
198
+ en: LinkedIn OAuth Provider
199
+ pt: Provedor OAuth do LinkedIn
200
+
185
201
  - slug: theme-selector
186
202
  name:
187
203
  en: Theme Selector
@@ -109,6 +109,8 @@ const OAUTH_PROFILE_SLUG_TO_PROVIDER: Record<string, string> = {
109
109
  'oauth-github-profile-id': 'github-oauth',
110
110
  'oauth-microsoft-profile-id': 'microsoft-oauth',
111
111
  'oauth-microsoft-entra-id-profile-id': 'microsoft-entra-id-oauth',
112
+ 'oauth-apple-profile-id': 'apple-oauth',
113
+ 'oauth-linkedin-profile-id': 'linkedin-oauth',
112
114
  };
113
115
  // Settings cujo valor é o SLUG de um integration_profile. O picker continua sendo
114
116
  // selecionado/exibido pelo nome; apenas o valor persistido é o slug.
@@ -12,27 +12,27 @@
12
12
 
13
13
  .dashboard-tabs-list .dashboard-tab-trigger.dashboard-tab-trigger-inactive {
14
14
  background: transparent !important;
15
- color: hsl(var(--muted-foreground) / 0.6) !important;
15
+ color: color-mix(in oklab, var(--muted-foreground) 60%, transparent) !important;
16
16
  }
17
17
 
18
18
  .dashboard-tabs-list
19
19
  .dashboard-tab-trigger.dashboard-tab-trigger-inactive:hover {
20
- background: hsl(var(--background) / 0.5) !important;
21
- color: hsl(var(--foreground) / 0.8) !important;
20
+ background: color-mix(in oklab, var(--background) 50%, transparent) !important;
21
+ color: color-mix(in oklab, var(--foreground) 80%, transparent) !important;
22
22
  }
23
23
 
24
24
  .dashboard-tabs-list .dashboard-tab-trigger.dashboard-tab-trigger-active,
25
25
  .dashboard-tabs-list
26
26
  .dashboard-tab-trigger.dashboard-tab-trigger-active[data-state='active'] {
27
27
  z-index: 10;
28
- background: hsl(var(--background)) !important;
29
- color: hsl(var(--primary)) !important;
28
+ background: var(--background) !important;
29
+ color: var(--primary) !important;
30
30
  border: none !important;
31
31
  box-shadow: none !important;
32
32
  }
33
33
 
34
34
  .dashboard-tabs-list .dashboard-tab-trigger.dashboard-tab-trigger-active svg {
35
- color: hsl(var(--primary)) !important;
35
+ color: var(--primary) !important;
36
36
  }
37
37
 
38
38
  .dashboard-grid.react-grid-layout {
@@ -66,8 +66,8 @@
66
66
  bottom: 3px;
67
67
  width: 6px;
68
68
  height: 6px;
69
- border-right: 2px solid hsl(var(--muted-foreground));
70
- border-bottom: 2px solid hsl(var(--muted-foreground));
69
+ border-right: 2px solid var(--muted-foreground);
70
+ border-bottom: 2px solid var(--muted-foreground);
71
71
  opacity: 0.45;
72
72
  }
73
73
 
@@ -125,16 +125,16 @@
125
125
 
126
126
  /* Placeholder styling */
127
127
  .dashboard-grid .react-grid-placeholder {
128
- background: hsl(var(--primary) / 0.2);
128
+ background: color-mix(in oklab, var(--primary) 20%, transparent);
129
129
  opacity: 0.5;
130
130
  transition-duration: 100ms;
131
131
  z-index: 2;
132
132
  border-radius: 0.5rem;
133
- border: 2px dashed hsl(var(--primary));
133
+ border: 2px dashed var(--primary);
134
134
  }
135
135
 
136
136
  .dashboard-grid .react-grid-item:hover {
137
- outline: 1px solid hsl(var(--primary) / 0.12);
137
+ outline: 1px solid color-mix(in oklab, var(--primary) 12%, transparent);
138
138
  outline-offset: -1px;
139
139
  }
140
140
 
@@ -542,6 +542,48 @@ const PROVIDER_FIELDS: Record<string, FieldDef[]> = {
542
542
  required: true,
543
543
  },
544
544
  ],
545
+ 'apple-oauth': [
546
+ {
547
+ key: 'client_id',
548
+ labelKey: 'fieldClientId',
549
+ type: 'text',
550
+ required: true,
551
+ placeholder: 'com.yourdomain.service',
552
+ },
553
+ {
554
+ key: 'team_id',
555
+ labelKey: 'fieldTeamId',
556
+ type: 'text',
557
+ required: true,
558
+ },
559
+ {
560
+ key: 'key_id',
561
+ labelKey: 'fieldKeyId',
562
+ type: 'text',
563
+ required: true,
564
+ },
565
+ {
566
+ key: 'private_key',
567
+ labelKey: 'fieldPrivateKey',
568
+ type: 'password',
569
+ required: true,
570
+ placeholder: '-----BEGIN PRIVATE KEY-----...',
571
+ },
572
+ ],
573
+ 'linkedin-oauth': [
574
+ {
575
+ key: 'client_id',
576
+ labelKey: 'fieldClientId',
577
+ type: 'text',
578
+ required: true,
579
+ },
580
+ {
581
+ key: 'client_secret',
582
+ labelKey: 'fieldClientSecret',
583
+ type: 'password',
584
+ required: true,
585
+ },
586
+ ],
545
587
  stripe: [
546
588
  {
547
589
  key: 'publishable_key',
@@ -992,7 +992,7 @@ function MenuTreeWorkspace({
992
992
  'group relative mr-1 flex cursor-pointer items-center gap-2 rounded-xl border px-2 py-1.5 transition-all duration-150 select-none active:scale-[0.99]',
993
993
  isDraggingThis ? 'opacity-40' : 'opacity-100',
994
994
  selectedId === node.id
995
- ? 'border-primary/70 bg-linear-to-r from-primary/10 via-primary/5 to-background shadow-[0_10px_24px_-18px_hsl(var(--primary))] ring-1 ring-primary/15'
995
+ ? 'border-primary/70 bg-linear-to-r from-primary/10 via-primary/5 to-background shadow-[0_10px_24px_-18px_var(--primary)] ring-1 ring-primary/15'
996
996
  : 'border-transparent hover:border-primary/30 hover:bg-muted/60 hover:shadow-sm',
997
997
  isInside ? 'ring-2 ring-primary bg-primary/10' : ''
998
998
  )}
@@ -147,6 +147,14 @@ const OAUTH_PROVIDER_TOGGLE_MAP: Array<{
147
147
  name: 'Microsoft Entra ID',
148
148
  },
149
149
  },
150
+ {
151
+ settingSlug: 'oauth-apple-enabled',
152
+ provider: { id: 'apple', slug: 'apple-oauth', name: 'Apple' },
153
+ },
154
+ {
155
+ settingSlug: 'oauth-linkedin-enabled',
156
+ provider: { id: 'linkedin', slug: 'linkedin-oauth', name: 'LinkedIn' },
157
+ },
150
158
  ];
151
159
 
152
160
  function generateRandomPassword(length = 16) {
@@ -870,6 +870,38 @@
870
870
  "loading": "Loading...",
871
871
  "errorOccurred": "An error occurred.",
872
872
  "alreadyProcessed": "This authentication callback has already been processed. Start the flow again."
873
+ },
874
+ "apple": {
875
+ "loginTitle": "Completing login...",
876
+ "loginDescription": "Please wait while we process your Apple authentication",
877
+ "loginSuccess": "Login successful!",
878
+ "registerTitle": "Completing registration...",
879
+ "registerDescription": "Please wait while we process your Apple authentication",
880
+ "registerSuccess": "Account created successfully!",
881
+ "connectTitle": "Completing account connection...",
882
+ "connectDescription": "Please wait while we process your Apple authentication",
883
+ "connectSuccess": "Account connected successfully!",
884
+ "errorTitle": "Unable to authenticate",
885
+ "buttonRetry": "Try again",
886
+ "loading": "Loading...",
887
+ "errorOccurred": "An error occurred.",
888
+ "alreadyProcessed": "This authentication callback has already been processed. Start the flow again."
889
+ },
890
+ "linkedin": {
891
+ "loginTitle": "Completing login...",
892
+ "loginDescription": "Please wait while we process your LinkedIn authentication",
893
+ "loginSuccess": "Login successful!",
894
+ "registerTitle": "Completing registration...",
895
+ "registerDescription": "Please wait while we process your LinkedIn authentication",
896
+ "registerSuccess": "Account created successfully!",
897
+ "connectTitle": "Completing account connection...",
898
+ "connectDescription": "Please wait while we process your LinkedIn authentication",
899
+ "connectSuccess": "Account connected successfully!",
900
+ "errorTitle": "Unable to authenticate",
901
+ "buttonRetry": "Try again",
902
+ "loading": "Loading...",
903
+ "errorOccurred": "An error occurred.",
904
+ "alreadyProcessed": "This authentication callback has already been processed. Start the flow again."
873
905
  }
874
906
  },
875
907
  "ConnectedAccounts": {
@@ -2408,6 +2440,9 @@
2408
2440
  "fieldOrganization": "Organization ID",
2409
2441
  "fieldRedirectUri": "Redirect URI",
2410
2442
  "fieldTenantId": "Tenant ID",
2443
+ "fieldTeamId": "Team ID",
2444
+ "fieldKeyId": "Key ID",
2445
+ "fieldPrivateKey": "Private Key (.p8)",
2411
2446
  "fieldPublishableKey": "Publishable Key",
2412
2447
  "fieldSecretKey": "Secret Key",
2413
2448
  "fieldWebhookSecret": "Webhook Secret",
@@ -870,6 +870,38 @@
870
870
  "loading": "Carregando...",
871
871
  "errorOccurred": "Ocorreu um erro.",
872
872
  "alreadyProcessed": "Esta autenticacao ja foi processada. Inicie o fluxo novamente."
873
+ },
874
+ "apple": {
875
+ "loginTitle": "Finalizando login...",
876
+ "loginDescription": "Aguarde enquanto processamos sua autenticação com a Apple",
877
+ "loginSuccess": "Login realizado com sucesso!",
878
+ "registerTitle": "Finalizando cadastro...",
879
+ "registerDescription": "Aguarde enquanto processamos sua autenticação com a Apple",
880
+ "registerSuccess": "Conta criada com sucesso!",
881
+ "connectTitle": "Finalizando conexão da conta...",
882
+ "connectDescription": "Aguarde enquanto processamos sua autenticação com a Apple",
883
+ "connectSuccess": "Conta conectada com sucesso!",
884
+ "errorTitle": "Não foi possível autenticar",
885
+ "buttonRetry": "Tentar novamente",
886
+ "loading": "Carregando...",
887
+ "errorOccurred": "Ocorreu um erro.",
888
+ "alreadyProcessed": "Esta autenticacao ja foi processada. Inicie o fluxo novamente."
889
+ },
890
+ "linkedin": {
891
+ "loginTitle": "Finalizando login...",
892
+ "loginDescription": "Aguarde enquanto processamos sua autenticação com o LinkedIn",
893
+ "loginSuccess": "Login realizado com sucesso!",
894
+ "registerTitle": "Finalizando cadastro...",
895
+ "registerDescription": "Aguarde enquanto processamos sua autenticação com o LinkedIn",
896
+ "registerSuccess": "Conta criada com sucesso!",
897
+ "connectTitle": "Finalizando conexão da conta...",
898
+ "connectDescription": "Aguarde enquanto processamos sua autenticação com o LinkedIn",
899
+ "connectSuccess": "Conta conectada com sucesso!",
900
+ "errorTitle": "Não foi possível autenticar",
901
+ "buttonRetry": "Tentar novamente",
902
+ "loading": "Carregando...",
903
+ "errorOccurred": "Ocorreu um erro.",
904
+ "alreadyProcessed": "Esta autenticacao ja foi processada. Inicie o fluxo novamente."
873
905
  }
874
906
  },
875
907
  "ConnectedAccounts": {
@@ -2463,6 +2495,9 @@
2463
2495
  "fieldOrganization": "ID da Organização",
2464
2496
  "fieldRedirectUri": "URI de Redirecionamento",
2465
2497
  "fieldTenantId": "Tenant ID",
2498
+ "fieldTeamId": "Team ID",
2499
+ "fieldKeyId": "Key ID",
2500
+ "fieldPrivateKey": "Chave Privada (.p8)",
2466
2501
  "fieldPublishableKey": "Chave Publicável",
2467
2502
  "fieldSecretKey": "Chave Secreta",
2468
2503
  "fieldWebhookSecret": "Segredo do Webhook",
@@ -15,6 +15,7 @@ columns:
15
15
  - apple
16
16
  - microsoft
17
17
  - microsoft-entra-id
18
+ - linkedin
18
19
  - name: provider_user_id
19
20
  - name: email
20
21
  isNullable: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.373",
3
+ "version": "0.0.374",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -41,10 +41,10 @@
41
41
  "speakeasy": "^2.0.0",
42
42
  "ua-parser-js": "^2.0.10",
43
43
  "uuid": "^11.1.0",
44
- "@hed-hog/api": "0.0.8",
45
44
  "@hed-hog/api-locale": "0.0.14",
46
- "@hed-hog/api-mail": "0.0.9",
47
45
  "@hed-hog/api-pagination": "0.0.7",
46
+ "@hed-hog/api": "0.0.8",
47
+ "@hed-hog/api-mail": "0.0.9",
48
48
  "@hed-hog/api-types": "0.0.1",
49
49
  "@hed-hog/api-prisma": "0.0.6"
50
50
  },
@@ -0,0 +1,235 @@
1
+ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
2
+
3
+ jest.mock('@hed-hog/api-prisma', () => ({
4
+ __esModule: true,
5
+ PrismaService: class {},
6
+ Prisma: {
7
+ sql: (...args: any[]) => ({ __sql: args }),
8
+ raw: (v: string) => ({ __raw: v }),
9
+ },
10
+ }));
11
+
12
+ jest.mock('@hed-hog/api-pagination', () => ({
13
+ __esModule: true,
14
+ PaginationService: class {},
15
+ }));
16
+
17
+ jest.mock('geoip-lite', () => ({
18
+ __esModule: true,
19
+ lookup: jest.fn(),
20
+ }));
21
+
22
+ jest.mock('ua-parser-js', () => ({
23
+ __esModule: true,
24
+ UAParser: jest.fn(),
25
+ }));
26
+
27
+ import * as geoip from 'geoip-lite';
28
+ import { UAParser } from 'ua-parser-js';
29
+ import { AccessLogService } from './access-log.service';
30
+
31
+ const mockLookup = geoip.lookup as unknown as jest.Mock;
32
+ const mockUAParser = UAParser as unknown as jest.Mock;
33
+
34
+ const makeService = () => {
35
+ const prisma = {
36
+ access_log: { create: jest.fn() },
37
+ $executeRaw: jest.fn(),
38
+ };
39
+ const pagination = {
40
+ paginate: jest.fn(),
41
+ };
42
+ const service = new AccessLogService(prisma as any, pagination as any);
43
+ return { service, prisma, pagination };
44
+ };
45
+
46
+ describe('AccessLogService', () => {
47
+ beforeEach(() => {
48
+ mockUAParser.mockImplementation(() => ({
49
+ getResult: () => ({
50
+ browser: { name: 'Chrome', version: '120' },
51
+ os: { name: 'Windows', version: '11' },
52
+ }),
53
+ }));
54
+ mockLookup.mockReturnValue({ country: 'BR', city: 'Sao Paulo' });
55
+ });
56
+
57
+ afterEach(() => {
58
+ jest.clearAllMocks();
59
+ });
60
+
61
+ describe('recordHttp', () => {
62
+ it('persiste o log HTTP com os campos mapeados', () => {
63
+ const { service, prisma } = makeService();
64
+ prisma.access_log.create.mockResolvedValue({});
65
+
66
+ service.recordHttp({
67
+ userId: 3,
68
+ ip: '1.2.3.4',
69
+ userAgent: 'UA',
70
+ method: 'GET',
71
+ path: '/api/x',
72
+ });
73
+
74
+ expect(prisma.access_log.create).toHaveBeenCalledWith({
75
+ data: {
76
+ user_id: 3,
77
+ ip: '1.2.3.4',
78
+ user_agent: 'UA',
79
+ method: 'GET',
80
+ path: '/api/x',
81
+ },
82
+ });
83
+ });
84
+
85
+ it('usa null para campos opcionais ausentes', () => {
86
+ const { service, prisma } = makeService();
87
+ prisma.access_log.create.mockResolvedValue({});
88
+
89
+ service.recordHttp({ method: 'POST', path: '/x' });
90
+
91
+ expect(prisma.access_log.create).toHaveBeenCalledWith({
92
+ data: { user_id: null, ip: null, user_agent: null, method: 'POST', path: '/x' },
93
+ });
94
+ });
95
+
96
+ it('não propaga erro de persistência (loga warn)', async () => {
97
+ const { service, prisma } = makeService();
98
+ prisma.access_log.create.mockRejectedValue(new Error('db down'));
99
+ const warnSpy = jest
100
+ .spyOn((service as any).logger, 'warn')
101
+ .mockImplementation(() => undefined);
102
+
103
+ expect(() => service.recordHttp({ method: 'GET', path: '/x' })).not.toThrow();
104
+ await new Promise((r) => process.nextTick(r));
105
+
106
+ expect(warnSpy).toHaveBeenCalled();
107
+ });
108
+ });
109
+
110
+ describe('recordMcp', () => {
111
+ it('persiste o log MCP com tool e prompt', () => {
112
+ const { service, prisma } = makeService();
113
+ prisma.access_log.create.mockResolvedValue({});
114
+
115
+ service.recordMcp({ userId: 9, mcpTool: 'search', mcpPrompt: 'q' });
116
+
117
+ expect(prisma.access_log.create).toHaveBeenCalledWith({
118
+ data: { user_id: 9, mcp_tool: 'search', mcp_prompt: 'q' },
119
+ });
120
+ });
121
+ });
122
+
123
+ describe('list', () => {
124
+ const paginateResult = (data: any[]) => ({ data, total: data.length, page: 1 });
125
+
126
+ it('sem filtros usa where vazio e mantém orderBy/include', async () => {
127
+ const { service, pagination } = makeService();
128
+ pagination.paginate.mockResolvedValue(paginateResult([]));
129
+
130
+ await service.list({});
131
+
132
+ const [, , opts] = pagination.paginate.mock.calls[0];
133
+ expect(opts.where).toEqual({});
134
+ expect(opts.orderBy).toEqual({ created_at: 'desc' });
135
+ expect(opts.include.user.select).toMatchObject({ id: true, name: true });
136
+ });
137
+
138
+ it('filtra por userId', async () => {
139
+ const { service, pagination } = makeService();
140
+ pagination.paginate.mockResolvedValue(paginateResult([]));
141
+
142
+ await service.list({ userId: 5 });
143
+
144
+ expect(pagination.paginate.mock.calls[0][2].where).toMatchObject({ user_id: 5 });
145
+ });
146
+
147
+ it('filtra por userSearch com contains insensitive', async () => {
148
+ const { service, pagination } = makeService();
149
+ pagination.paginate.mockResolvedValue(paginateResult([]));
150
+
151
+ await service.list({ userSearch: 'ana' });
152
+
153
+ expect(pagination.paginate.mock.calls[0][2].where.user).toEqual({
154
+ OR: [{ name: { contains: 'ana', mode: 'insensitive' } }],
155
+ });
156
+ });
157
+
158
+ it('monta o range de datas com fim de dia em createdAtTo', async () => {
159
+ const { service, pagination } = makeService();
160
+ pagination.paginate.mockResolvedValue(paginateResult([]));
161
+
162
+ await service.list({ createdAtFrom: '2026-01-01', createdAtTo: '2026-01-31' });
163
+
164
+ const where = pagination.paginate.mock.calls[0][2].where;
165
+ expect(where.created_at.gte).toEqual(new Date('2026-01-01'));
166
+ const lte: Date = where.created_at.lte;
167
+ expect(lte.getHours()).toBe(23);
168
+ expect(lte.getMinutes()).toBe(59);
169
+ expect(lte.getSeconds()).toBe(59);
170
+ expect(lte.getMilliseconds()).toBe(999);
171
+ });
172
+
173
+ it('type=http filtra method not null; type=mcp filtra mcp_tool not null', async () => {
174
+ const { service, pagination } = makeService();
175
+ pagination.paginate.mockResolvedValue(paginateResult([]));
176
+
177
+ await service.list({ type: 'http' });
178
+ expect(pagination.paginate.mock.calls[0][2].where).toMatchObject({
179
+ method: { not: null },
180
+ });
181
+
182
+ await service.list({ type: 'mcp' });
183
+ expect(pagination.paginate.mock.calls[1][2].where).toMatchObject({
184
+ mcp_tool: { not: null },
185
+ });
186
+ });
187
+
188
+ it('enriquece cada log com dados de user-agent e geoip', async () => {
189
+ const { service, pagination } = makeService();
190
+ pagination.paginate.mockResolvedValue(
191
+ paginateResult([{ id: 1, user_agent: 'UA', ip: '8.8.8.8' }]),
192
+ );
193
+
194
+ const result = await service.list({});
195
+
196
+ expect(result.data[0]).toMatchObject({
197
+ browser_name: 'Chrome',
198
+ browser_version: '120',
199
+ os_name: 'Windows',
200
+ os_version: '11',
201
+ country: 'BR',
202
+ city: 'Sao Paulo',
203
+ });
204
+ });
205
+
206
+ it('usa null quando não há user_agent nem ip', async () => {
207
+ const { service, pagination } = makeService();
208
+ pagination.paginate.mockResolvedValue(
209
+ paginateResult([{ id: 2, user_agent: null, ip: null }]),
210
+ );
211
+
212
+ const result = await service.list({});
213
+
214
+ expect(result.data[0]).toMatchObject({
215
+ browser_name: null,
216
+ os_name: null,
217
+ country: null,
218
+ city: null,
219
+ });
220
+ expect(mockUAParser).not.toHaveBeenCalled();
221
+ expect(mockLookup).not.toHaveBeenCalled();
222
+ });
223
+ });
224
+
225
+ describe('clearOld', () => {
226
+ it('executa o DELETE em lote via $executeRaw', async () => {
227
+ const { service, prisma } = makeService();
228
+ prisma.$executeRaw.mockResolvedValue(1);
229
+
230
+ await service.clearOld(30, 500);
231
+
232
+ expect(prisma.$executeRaw).toHaveBeenCalledTimes(1);
233
+ });
234
+ });
235
+ });