@hed-hog/core 0.0.398 → 0.0.400
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/ai-agent.sort.d.ts +6 -0
- package/dist/ai/ai-agent.sort.d.ts.map +1 -0
- package/dist/ai/ai-agent.sort.js +21 -0
- package/dist/ai/ai-agent.sort.js.map +1 -0
- package/dist/ai/ai.controller.d.ts +4 -4
- package/dist/ai/ai.service.d.ts +35 -4
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +51 -137
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai-instruction/ai-instruction.service.d.ts.map +1 -1
- package/dist/ai-instruction/ai-instruction.service.js +26 -23
- package/dist/ai-instruction/ai-instruction.service.js.map +1 -1
- package/dist/ai-instruction/ai-instruction.sort.d.ts +16 -0
- package/dist/ai-instruction/ai-instruction.sort.d.ts.map +1 -0
- package/dist/ai-instruction/ai-instruction.sort.js +32 -0
- package/dist/ai-instruction/ai-instruction.sort.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +24 -8
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +8 -1
- package/dist/auth/auth.module.js.map +1 -1
- package/dist/auth/auth.service.d.ts +72 -25
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +41 -4
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/guards/auth.guard.d.ts +3 -1
- package/dist/auth/guards/auth.guard.d.ts.map +1 -1
- package/dist/auth/guards/auth.guard.js +12 -3
- package/dist/auth/guards/auth.guard.js.map +1 -1
- package/dist/authorize/authorize.controller.d.ts +30 -6
- package/dist/authorize/authorize.controller.d.ts.map +1 -1
- package/dist/authorize/authorize.controller.js +120 -13
- package/dist/authorize/authorize.controller.js.map +1 -1
- package/dist/authorize/authorize.errors.d.ts +34 -0
- package/dist/authorize/authorize.errors.d.ts.map +1 -0
- package/dist/authorize/authorize.errors.js +37 -0
- package/dist/authorize/authorize.errors.js.map +1 -0
- package/dist/authorize/authorize.module.d.ts.map +1 -1
- package/dist/authorize/authorize.module.js +6 -1
- package/dist/authorize/authorize.module.js.map +1 -1
- package/dist/authorize/authorize.service.d.ts +78 -4
- package/dist/authorize/authorize.service.d.ts.map +1 -1
- package/dist/authorize/authorize.service.js +212 -21
- package/dist/authorize/authorize.service.js.map +1 -1
- package/dist/authorize/dto/authorize-info.dto.d.ts +10 -1
- package/dist/authorize/dto/authorize-info.dto.d.ts.map +1 -1
- package/dist/authorize/dto/authorize-info.dto.js +16 -1
- package/dist/authorize/dto/authorize-info.dto.js.map +1 -1
- package/dist/authorize/dto/consent.dto.d.ts +10 -1
- package/dist/authorize/dto/consent.dto.d.ts.map +1 -1
- package/dist/authorize/dto/consent.dto.js +16 -1
- package/dist/authorize/dto/consent.dto.js.map +1 -1
- package/dist/authorize/dto/token.dto.d.ts +12 -0
- package/dist/authorize/dto/token.dto.d.ts.map +1 -1
- package/dist/authorize/dto/token.dto.js +21 -1
- package/dist/authorize/dto/token.dto.js.map +1 -1
- package/dist/authorize/oauth-exception.filter.d.ts +17 -0
- package/dist/authorize/oauth-exception.filter.d.ts.map +1 -0
- package/dist/authorize/oauth-exception.filter.js +54 -0
- package/dist/authorize/oauth-exception.filter.js.map +1 -0
- package/dist/authorize/oauth-metadata.service.d.ts +39 -0
- package/dist/authorize/oauth-metadata.service.d.ts.map +1 -0
- package/dist/authorize/oauth-metadata.service.js +81 -0
- package/dist/authorize/oauth-metadata.service.js.map +1 -0
- package/dist/authorize/well-known.controller.d.ts +67 -0
- package/dist/authorize/well-known.controller.d.ts.map +1 -0
- package/dist/authorize/well-known.controller.js +126 -0
- package/dist/authorize/well-known.controller.js.map +1 -0
- package/dist/captcha/captcha.service.d.ts +32 -2
- package/dist/captcha/captcha.service.d.ts.map +1 -1
- package/dist/captcha/captcha.service.js +85 -12
- package/dist/captcha/captcha.service.js.map +1 -1
- package/dist/challenge/challenge.service.d.ts +2 -2
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +6 -0
- package/dist/core.module.js.map +1 -1
- package/dist/dashboard/dashboard/dashboard.controller.d.ts +9 -9
- package/dist/dashboard/dashboard/dashboard.service.d.ts +9 -9
- package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +3 -3
- package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +3 -3
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +3 -3
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +3 -3
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.js +2 -4
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.js.map +1 -1
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.sort.d.ts +8 -0
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.sort.d.ts.map +1 -0
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.sort.js +18 -0
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.sort.js.map +1 -0
- package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-item/dashboard-item.service.js +2 -4
- package/dist/dashboard/dashboard-item/dashboard-item.service.js.map +1 -1
- package/dist/dashboard/dashboard-item/dashboard-item.sort.d.ts +9 -0
- package/dist/dashboard/dashboard-item/dashboard-item.sort.d.ts.map +1 -0
- package/dist/dashboard/dashboard-item/dashboard-item.sort.js +19 -0
- package/dist/dashboard/dashboard-item/dashboard-item.sort.js.map +1 -0
- package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +3 -3
- package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +3 -3
- package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-role/dashboard-role.service.js +2 -4
- package/dist/dashboard/dashboard-role/dashboard-role.service.js.map +1 -1
- package/dist/dashboard/dashboard-role/dashboard-role.sort.d.ts +8 -0
- package/dist/dashboard/dashboard-role/dashboard-role.sort.d.ts.map +1 -0
- package/dist/dashboard/dashboard-role/dashboard-role.sort.js +16 -0
- package/dist/dashboard/dashboard-role/dashboard-role.sort.js.map +1 -0
- package/dist/file/file-upload-ticket.service.d.ts +160 -0
- package/dist/file/file-upload-ticket.service.d.ts.map +1 -0
- package/dist/file/file-upload-ticket.service.js +382 -0
- package/dist/file/file-upload-ticket.service.js.map +1 -0
- package/dist/file/file.controller.d.ts +24 -1
- package/dist/file/file.controller.d.ts.map +1 -1
- package/dist/file/file.controller.js +63 -2
- package/dist/file/file.controller.js.map +1 -1
- package/dist/file/file.module.d.ts.map +1 -1
- package/dist/file/file.module.js +14 -2
- package/dist/file/file.module.js.map +1 -1
- package/dist/file/file.service.d.ts +182 -6
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +385 -36
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/filename-encoding.util.d.ts +24 -0
- package/dist/file/filename-encoding.util.d.ts.map +1 -0
- package/dist/file/filename-encoding.util.js +63 -0
- package/dist/file/filename-encoding.util.js.map +1 -0
- package/dist/file/image-optimization.service.d.ts +27 -0
- package/dist/file/image-optimization.service.d.ts.map +1 -1
- package/dist/file/image-optimization.service.js +63 -14
- package/dist/file/image-optimization.service.js.map +1 -1
- package/dist/file/mcp-file-source.schema.d.ts +33 -0
- package/dist/file/mcp-file-source.schema.d.ts.map +1 -0
- package/dist/file/mcp-file-source.schema.js +75 -0
- package/dist/file/mcp-file-source.schema.js.map +1 -0
- package/dist/file/mcp-file-source.service.d.ts +124 -0
- package/dist/file/mcp-file-source.service.d.ts.map +1 -0
- package/dist/file/mcp-file-source.service.js +262 -0
- package/dist/file/mcp-file-source.service.js.map +1 -0
- package/dist/file/mimetype-sniff.util.d.ts +62 -0
- package/dist/file/mimetype-sniff.util.d.ts.map +1 -0
- package/dist/file/mimetype-sniff.util.js +226 -0
- package/dist/file/mimetype-sniff.util.js.map +1 -0
- package/dist/file/remote-fetch.util.d.ts +35 -0
- package/dist/file/remote-fetch.util.d.ts.map +1 -0
- package/dist/file/remote-fetch.util.js +236 -0
- package/dist/file/remote-fetch.util.js.map +1 -0
- package/dist/impersonation/dto/start-impersonation.dto.d.ts +4 -1
- package/dist/impersonation/dto/start-impersonation.dto.d.ts.map +1 -1
- package/dist/impersonation/dto/start-impersonation.dto.js +1 -1
- package/dist/impersonation/dto/start-impersonation.dto.js.map +1 -1
- package/dist/impersonation/impersonation.service.d.ts +8 -3
- package/dist/impersonation/impersonation.service.d.ts.map +1 -1
- package/dist/impersonation/impersonation.service.js +14 -4
- package/dist/impersonation/impersonation.service.js.map +1 -1
- package/dist/incident/incident.controller.d.ts +2 -1
- package/dist/incident/incident.controller.d.ts.map +1 -1
- package/dist/incident/incident.controller.js +10 -6
- package/dist/incident/incident.controller.js.map +1 -1
- package/dist/incident/incident.service.d.ts +2 -0
- package/dist/incident/incident.service.d.ts.map +1 -1
- package/dist/incident/incident.service.js +4 -0
- package/dist/incident/incident.service.js.map +1 -1
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +7 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +0 -13
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +5 -40
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +5 -5
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/integration-profile/testers/index.d.ts.map +1 -1
- package/dist/integration-profile/testers/index.js +2 -0
- package/dist/integration-profile/testers/index.js.map +1 -1
- package/dist/integration-profile/testers/shipping.testers.d.ts +3 -0
- package/dist/integration-profile/testers/shipping.testers.d.ts.map +1 -0
- package/dist/integration-profile/testers/shipping.testers.js +71 -0
- package/dist/integration-profile/testers/shipping.testers.js.map +1 -0
- package/dist/locale/locale.controller.d.ts +1 -1
- package/dist/locale/locale.service.d.ts +1 -1
- package/dist/mail/dto/send.dto.d.ts +6 -1
- package/dist/mail/dto/send.dto.d.ts.map +1 -1
- package/dist/mail/dto/send.dto.js.map +1 -1
- package/dist/mail/mail.service.d.ts +9 -0
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +34 -14
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mail-sent/mail-sent.controller.d.ts +1 -1
- package/dist/mail-sent/mail-sent.service.d.ts +1 -1
- package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
- package/dist/mail-sent/mail-sent.service.js +2 -1
- package/dist/mail-sent/mail-sent.service.js.map +1 -1
- package/dist/mail-sent/mail-sent.sort.d.ts +9 -0
- package/dist/mail-sent/mail-sent.sort.d.ts.map +1 -0
- package/dist/mail-sent/mail-sent.sort.js +19 -0
- package/dist/mail-sent/mail-sent.sort.js.map +1 -0
- package/dist/mcp/filters/mcp-challenge.filter.d.ts +20 -0
- package/dist/mcp/filters/mcp-challenge.filter.d.ts.map +1 -0
- package/dist/mcp/filters/mcp-challenge.filter.js +69 -0
- package/dist/mcp/filters/mcp-challenge.filter.js.map +1 -0
- package/dist/mcp/guards/mcp-auth.guard.d.ts +4 -1
- package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -1
- package/dist/mcp/guards/mcp-auth.guard.js +19 -5
- package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
- package/dist/mcp/guards/mcp-permission.guard.d.ts +25 -3
- package/dist/mcp/guards/mcp-permission.guard.d.ts.map +1 -1
- package/dist/mcp/guards/mcp-permission.guard.js +46 -26
- package/dist/mcp/guards/mcp-permission.guard.js.map +1 -1
- package/dist/mcp/mcp-audit.service.d.ts +17 -0
- package/dist/mcp/mcp-audit.service.d.ts.map +1 -1
- package/dist/mcp/mcp-audit.service.js +32 -3
- package/dist/mcp/mcp-audit.service.js.map +1 -1
- package/dist/mcp/mcp-permission.service.d.ts +64 -0
- package/dist/mcp/mcp-permission.service.d.ts.map +1 -0
- package/dist/mcp/mcp-permission.service.js +178 -0
- package/dist/mcp/mcp-permission.service.js.map +1 -0
- package/dist/mcp/mcp-server.service.d.ts +3 -1
- package/dist/mcp/mcp-server.service.d.ts.map +1 -1
- package/dist/mcp/mcp-server.service.js +36 -15
- package/dist/mcp/mcp-server.service.js.map +1 -1
- package/dist/mcp/mcp.controller.d.ts.map +1 -1
- package/dist/mcp/mcp.controller.js +10 -2
- package/dist/mcp/mcp.controller.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +5 -1
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp/tools/core-ai.mcp-tools.js +2 -9
- package/dist/mcp/tools/core-ai.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-dashboard-items.mcp-tools.js +1 -1
- package/dist/mcp/tools/core-dashboard-items.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-dashboard.mcp-tools.js +2 -9
- package/dist/mcp/tools/core-dashboard.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-event-webhooks.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-event-webhooks.mcp-tools.js +2 -10
- package/dist/mcp/tools/core-event-webhooks.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-files.mcp-tools.d.ts +17 -5
- package/dist/mcp/tools/core-files.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-files.mcp-tools.js +186 -24
- package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-integration-events.mcp-tools.js +2 -9
- package/dist/mcp/tools/core-integration-events.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-mail-sent.mcp-tools.js +2 -14
- package/dist/mcp/tools/core-mail-sent.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-mail.mcp-tools.js +2 -9
- package/dist/mcp/tools/core-mail.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-menus.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-menus.mcp-tools.js +2 -10
- package/dist/mcp/tools/core-menus.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-notifications.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-notifications.mcp-tools.js +0 -1
- package/dist/mcp/tools/core-notifications.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-roles.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-roles.mcp-tools.js +2 -13
- package/dist/mcp/tools/core-roles.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-routes.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-routes.mcp-tools.js +2 -11
- package/dist/mcp/tools/core-routes.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-screens.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-screens.mcp-tools.js +2 -11
- package/dist/mcp/tools/core-screens.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-sessions.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-sessions.mcp-tools.js +0 -2
- package/dist/mcp/tools/core-sessions.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-settings.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-settings.mcp-tools.js +2 -10
- package/dist/mcp/tools/core-settings.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-users.mcp-tools.js +2 -9
- package/dist/mcp/tools/core-users.mcp-tools.js.map +1 -1
- package/dist/mcp/tools/core-webhooks.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-webhooks.mcp-tools.js +2 -10
- package/dist/mcp/tools/core-webhooks.mcp-tools.js.map +1 -1
- package/dist/mcp/types/mcp-context.type.d.ts +9 -0
- package/dist/mcp/types/mcp-context.type.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +8 -0
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/meeting/meeting-provider.registry.d.ts +115 -0
- package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
- package/dist/meeting/meeting-provider.registry.js.map +1 -1
- package/dist/meeting/meeting.constants.d.ts +70 -0
- package/dist/meeting/meeting.constants.d.ts.map +1 -1
- package/dist/meeting/meeting.constants.js +9 -1
- package/dist/meeting/meeting.constants.js.map +1 -1
- package/dist/menu/menu.service.d.ts.map +1 -1
- package/dist/menu/menu.service.js +29 -9
- package/dist/menu/menu.service.js.map +1 -1
- package/dist/message-template/dto/update-message-template.dto.d.ts +18 -0
- package/dist/message-template/dto/update-message-template.dto.d.ts.map +1 -0
- package/dist/message-template/dto/update-message-template.dto.js +38 -0
- package/dist/message-template/dto/update-message-template.dto.js.map +1 -0
- package/dist/message-template/message-template-admin.service.d.ts +70 -0
- package/dist/message-template/message-template-admin.service.d.ts.map +1 -0
- package/dist/message-template/message-template-admin.service.js +226 -0
- package/dist/message-template/message-template-admin.service.js.map +1 -0
- package/dist/message-template/message-template.controller.d.ts +29 -0
- package/dist/message-template/message-template.controller.d.ts.map +1 -0
- package/dist/message-template/message-template.controller.js +101 -0
- package/dist/message-template/message-template.controller.js.map +1 -0
- package/dist/message-template/message-template.module.d.ts +10 -0
- package/dist/message-template/message-template.module.d.ts.map +1 -0
- package/dist/message-template/message-template.module.js +33 -0
- package/dist/message-template/message-template.module.js.map +1 -0
- package/dist/message-template/message-template.registry.d.ts +55 -0
- package/dist/message-template/message-template.registry.d.ts.map +1 -0
- package/dist/message-template/message-template.registry.js +64 -0
- package/dist/message-template/message-template.registry.js.map +1 -0
- package/dist/message-template/message-template.service.d.ts +59 -0
- package/dist/message-template/message-template.service.d.ts.map +1 -0
- package/dist/message-template/message-template.service.js +166 -0
- package/dist/message-template/message-template.service.js.map +1 -0
- package/dist/notification/notification.constants.d.ts +11 -0
- package/dist/notification/notification.constants.d.ts.map +1 -0
- package/dist/notification/notification.constants.js +14 -0
- package/dist/notification/notification.constants.js.map +1 -0
- package/dist/notification/notification.controller.d.ts +20 -20
- package/dist/notification/notification.module.d.ts.map +1 -1
- package/dist/notification/notification.module.js +3 -0
- package/dist/notification/notification.module.js.map +1 -1
- package/dist/notification/notification.service.d.ts +33 -21
- package/dist/notification/notification.service.d.ts.map +1 -1
- package/dist/notification/notification.service.js +36 -2
- package/dist/notification/notification.service.js.map +1 -1
- package/dist/oauth/oauth.controller.d.ts +4 -6
- package/dist/oauth/oauth.controller.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.js +0 -1
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.errors.d.ts +15 -1
- package/dist/oauth/oauth.errors.d.ts.map +1 -1
- package/dist/oauth/oauth.errors.js +9 -1
- package/dist/oauth/oauth.errors.js.map +1 -1
- package/dist/oauth/oauth.module.d.ts.map +1 -1
- package/dist/oauth/oauth.module.js +6 -1
- package/dist/oauth/oauth.module.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +25 -5
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +63 -27
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/oauth/providers/abstract.provider.d.ts +10 -0
- package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
- package/dist/oauth/providers/abstract.provider.js +39 -12
- package/dist/oauth/providers/abstract.provider.js.map +1 -1
- package/dist/oauth/providers/github.provider.d.ts +1 -1
- package/dist/oauth/providers/github.provider.d.ts.map +1 -1
- package/dist/oauth/providers/github.provider.js +48 -22
- package/dist/oauth/providers/github.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +6 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/observability/monitored-cron.d.ts +29 -12
- package/dist/observability/monitored-cron.d.ts.map +1 -1
- package/dist/observability/monitored-cron.js +131 -46
- package/dist/observability/monitored-cron.js.map +1 -1
- package/dist/pairing/pairing.controller.d.ts +9 -9
- package/dist/pairing/pairing.service.d.ts +15 -11
- package/dist/pairing/pairing.service.d.ts.map +1 -1
- package/dist/profile/dto/change-password.dto.d.ts.map +1 -1
- package/dist/profile/dto/change-password.dto.js +4 -6
- package/dist/profile/dto/change-password.dto.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +1 -1
- package/dist/profile/profile.service.d.ts +1 -1
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +9 -3
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/realtime/realtime-tick.coalescer.d.ts +34 -0
- package/dist/realtime/realtime-tick.coalescer.d.ts.map +1 -0
- package/dist/realtime/realtime-tick.coalescer.js +60 -0
- package/dist/realtime/realtime-tick.coalescer.js.map +1 -0
- package/dist/realtime/realtime.service.d.ts +2 -0
- package/dist/realtime/realtime.service.d.ts.map +1 -1
- package/dist/realtime/realtime.service.js +22 -2
- package/dist/realtime/realtime.service.js.map +1 -1
- package/dist/redis/redis-kv.service.d.ts +90 -0
- package/dist/redis/redis-kv.service.d.ts.map +1 -0
- package/dist/redis/redis-kv.service.js +247 -0
- package/dist/redis/redis-kv.service.js.map +1 -0
- package/dist/redis/redis-master.d.ts +29 -0
- package/dist/redis/redis-master.d.ts.map +1 -0
- package/dist/redis/redis-master.js +81 -0
- package/dist/redis/redis-master.js.map +1 -0
- package/dist/redis/redis-pubsub.d.ts +12 -0
- package/dist/redis/redis-pubsub.d.ts.map +1 -1
- package/dist/redis/redis-pubsub.js +25 -2
- package/dist/redis/redis-pubsub.js.map +1 -1
- package/dist/redis/redis.module.d.ts +11 -0
- package/dist/redis/redis.module.d.ts.map +1 -0
- package/dist/redis/redis.module.js +30 -0
- package/dist/redis/redis.module.js.map +1 -0
- package/dist/session/session.controller.d.ts +6 -2
- package/dist/session/session.controller.d.ts.map +1 -1
- package/dist/session/session.service.d.ts +118 -15
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +251 -77
- package/dist/session/session.service.js.map +1 -1
- package/dist/setting/setting.controller.d.ts +5 -0
- package/dist/setting/setting.controller.d.ts.map +1 -1
- package/dist/setting/setting.service.d.ts +7 -0
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +38 -3
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +17 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +48 -13
- package/dist/task/task.service.js.map +1 -1
- package/dist/token/token.service.d.ts +19 -1
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +39 -2
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user-credential.util.d.ts +10 -0
- package/dist/user/user-credential.util.d.ts.map +1 -1
- package/dist/user/user-credential.util.js +14 -1
- package/dist/user/user-credential.util.js.map +1 -1
- package/dist/user/user.service.d.ts +12 -4
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +2 -11
- package/dist/user/user.service.js.map +1 -1
- package/dist/user/user.sort.d.ts +16 -0
- package/dist/user/user.sort.d.ts.map +1 -0
- package/dist/user/user.sort.js +25 -0
- package/dist/user/user.sort.js.map +1 -0
- package/dist/utils/abbreviate-name.d.ts +13 -0
- package/dist/utils/abbreviate-name.d.ts.map +1 -0
- package/dist/utils/abbreviate-name.js +42 -0
- package/dist/utils/abbreviate-name.js.map +1 -0
- package/dist/utils/date-range-query.dto.d.ts +11 -0
- package/dist/utils/date-range-query.dto.d.ts.map +1 -0
- package/dist/utils/date-range-query.dto.js +63 -0
- package/dist/utils/date-range-query.dto.js.map +1 -0
- package/dist/utils/date-range.d.ts +96 -0
- package/dist/utils/date-range.d.ts.map +1 -0
- package/dist/utils/date-range.js +172 -0
- package/dist/utils/date-range.js.map +1 -0
- package/dist/webhook-integration/webhook-utils.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-utils.js +24 -0
- package/dist/webhook-integration/webhook-utils.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-recipients.d.ts +49 -0
- package/dist/whatsapp-sender/whatsapp-recipients.d.ts.map +1 -0
- package/dist/whatsapp-sender/whatsapp-recipients.js +66 -0
- package/dist/whatsapp-sender/whatsapp-recipients.js.map +1 -0
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +25 -0
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.js +46 -5
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
- package/hedhog/data/integration_provider.yaml +9 -0
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/locale.yaml +0 -1
- package/hedhog/data/mail.yaml +2 -2
- package/hedhog/data/mail_layout.yaml +6 -3
- package/hedhog/data/menu.yaml +25 -5
- package/hedhog/data/route.yaml +140 -0
- package/hedhog/data/setting_group.yaml +137 -3
- package/hedhog/frontend/app/access-log/page.tsx.ejs +47 -53
- package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +33 -8
- package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +54 -13
- package/hedhog/frontend/app/account/components/mfa-method-card.tsx.ejs +7 -1
- package/hedhog/frontend/app/account/components/recovery-codes-dialog.tsx.ejs +1 -0
- package/hedhog/frontend/app/account/components/tokens.tsx.ejs +47 -17
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +46 -5
- package/hedhog/frontend/app/ai_instruction/_components/ai-instruction-form.tsx.ejs +101 -30
- package/hedhog/frontend/app/ai_instruction/_components/ai-instruction-versions.tsx.ejs +24 -8
- package/hedhog/frontend/app/ai_instruction/page.tsx.ejs +80 -15
- package/hedhog/frontend/app/configurations/[slug]/components/image-optimization-action.tsx.ejs +166 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +145 -45
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +99 -219
- package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +19 -1
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +19 -5
- package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +2 -8
- package/hedhog/frontend/app/dashboard/components/dynamic-widget.tsx.ejs +3 -0
- package/hedhog/frontend/app/dashboard/components/widget-wrapper.tsx.ejs +4 -0
- package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +43 -44
- package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +41 -3
- package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +42 -2
- package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +41 -3
- package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +39 -4
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +42 -3
- package/hedhog/frontend/app/incidents/page.tsx.ejs +60 -31
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +45 -5
- package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +92 -15
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +109 -61
- package/hedhog/frontend/app/mail/template/page.tsx.ejs +125 -23
- package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +13 -15
- package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +116 -110
- package/hedhog/frontend/app/message-templates/_components/message-template-sheet.tsx.ejs +245 -0
- package/hedhog/frontend/app/message-templates/_lib/types.ts.ejs +24 -0
- package/hedhog/frontend/app/message-templates/page.tsx.ejs +182 -0
- package/hedhog/frontend/app/pages/_components/page-wizard-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/pages/page.tsx.ejs +94 -9
- package/hedhog/frontend/app/roles/menus.tsx.ejs +4 -4
- package/hedhog/frontend/app/roles/routes.tsx.ejs +4 -4
- package/hedhog/frontend/app/roles/users.tsx.ejs +4 -4
- package/hedhog/frontend/app/setting-list/page.tsx.ejs +71 -9
- package/hedhog/frontend/app/users/page.tsx.ejs +77 -67
- package/hedhog/frontend/messages/en.json +183 -36
- package/hedhog/frontend/messages/pt.json +183 -36
- package/hedhog/table/cron_lock.yaml +17 -0
- package/hedhog/table/file.yaml +13 -1
- package/hedhog/table/file_upload_chunk.yaml +23 -0
- package/hedhog/table/file_upload_ticket.yaml +67 -0
- package/hedhog/table/mcp_tool_call_log.yaml +30 -22
- package/hedhog/table/message_template.yaml +33 -0
- package/hedhog/table/setting.yaml +8 -0
- package/hedhog/table/user_session.yaml +36 -1
- package/package.json +8 -6
- package/src/ai/ai-agent.sort.ts +21 -0
- package/src/ai/ai.service.spec.ts +173 -1
- package/src/ai/ai.service.ts +64 -193
- package/src/ai-instruction/ai-instruction.service.ts +32 -28
- package/src/ai-instruction/ai-instruction.sort.ts +32 -0
- package/src/auth/auth.module.ts +8 -1
- package/src/auth/auth.service.spec.ts +108 -1
- package/src/auth/auth.service.ts +59 -5
- package/src/auth/guards/auth.guard.ts +9 -1
- package/src/authorize/authorize.controller.ts +126 -11
- package/src/authorize/authorize.errors.ts +55 -0
- package/src/authorize/authorize.module.ts +6 -1
- package/src/authorize/authorize.service.spec.ts +379 -7
- package/src/authorize/authorize.service.ts +309 -23
- package/src/authorize/dto/authorize-info.dto.ts +21 -3
- package/src/authorize/dto/consent.dto.ts +21 -3
- package/src/authorize/dto/token.dto.ts +24 -2
- package/src/authorize/oauth-exception.filter.ts +58 -0
- package/src/authorize/oauth-metadata.service.ts +67 -0
- package/src/authorize/well-known.controller.spec.ts +84 -0
- package/src/authorize/well-known.controller.ts +96 -0
- package/src/captcha/captcha.service.spec.ts +211 -4
- package/src/captcha/captcha.service.ts +127 -10
- package/src/core.module.ts +6 -0
- package/src/dashboard/dashboard-component-role/dashboard-component-role.service.ts +2 -3
- package/src/dashboard/dashboard-component-role/dashboard-component-role.sort.ts +18 -0
- package/src/dashboard/dashboard-item/dashboard-item.service.ts +2 -3
- package/src/dashboard/dashboard-item/dashboard-item.sort.ts +19 -0
- package/src/dashboard/dashboard-role/dashboard-role.service.ts +2 -3
- package/src/dashboard/dashboard-role/dashboard-role.sort.ts +16 -0
- package/src/file/file-upload-ticket.service.spec.ts +422 -0
- package/src/file/file-upload-ticket.service.ts +513 -0
- package/src/file/file.controller.ts +65 -0
- package/src/file/file.module.ts +14 -2
- package/src/file/file.service.spec.ts +94 -0
- package/src/file/file.service.ts +432 -30
- package/src/file/filename-encoding.util.spec.ts +54 -0
- package/src/file/filename-encoding.util.ts +67 -0
- package/src/file/image-optimization.service.spec.ts +156 -19
- package/src/file/image-optimization.service.ts +106 -21
- package/src/file/mcp-file-source.schema.ts +83 -0
- package/src/file/mcp-file-source.service.spec.ts +473 -0
- package/src/file/mcp-file-source.service.ts +445 -0
- package/src/file/mimetype-sniff.util.spec.ts +126 -0
- package/src/file/mimetype-sniff.util.ts +265 -0
- package/src/file/remote-fetch.util.spec.ts +117 -0
- package/src/file/remote-fetch.util.ts +330 -0
- package/src/impersonation/dto/start-impersonation.dto.ts +5 -2
- package/src/impersonation/impersonation.service.spec.ts +24 -1
- package/src/impersonation/impersonation.service.ts +14 -4
- package/src/incident/incident.controller.ts +9 -1
- package/src/incident/incident.service.ts +7 -0
- package/src/index.ts +17 -0
- package/src/integration-profile/integration-profile.secrets.ts +7 -0
- package/src/integration-profile/integration-profile.service.ts +7 -64
- package/src/integration-profile/integration-profile.utils.spec.ts +9 -0
- package/src/integration-profile/integration-profile.utils.ts +5 -5
- package/src/integration-profile/testers/index.ts +2 -0
- package/src/integration-profile/testers/remaining.testers.spec.ts +79 -0
- package/src/integration-profile/testers/shipping.testers.ts +94 -0
- package/src/mail/dto/send.dto.ts +6 -1
- package/src/mail/mail-template.spec.ts +58 -1
- package/src/mail/mail.service.spec.ts +112 -0
- package/src/mail/mail.service.ts +42 -4
- package/src/mail-sent/mail-sent.service.ts +3 -0
- package/src/mail-sent/mail-sent.sort.ts +19 -0
- package/src/mcp/filters/mcp-challenge.filter.spec.ts +76 -0
- package/src/mcp/filters/mcp-challenge.filter.ts +70 -0
- package/src/mcp/guards/mcp-auth.guard.spec.ts +51 -2
- package/src/mcp/guards/mcp-auth.guard.ts +24 -1
- package/src/mcp/guards/mcp-permission.guard.spec.ts +109 -15
- package/src/mcp/guards/mcp-permission.guard.ts +76 -51
- package/src/mcp/mcp-audit.service.spec.ts +107 -0
- package/src/mcp/mcp-audit.service.ts +37 -1
- package/src/mcp/mcp-permission.service.spec.ts +197 -0
- package/src/mcp/mcp-permission.service.ts +184 -0
- package/src/mcp/mcp-server.service.spec.ts +84 -2
- package/src/mcp/mcp-server.service.ts +111 -87
- package/src/mcp/mcp.controller.ts +8 -1
- package/src/mcp/mcp.module.ts +5 -1
- package/src/mcp/tools/core-ai.mcp-tools.ts +1 -1
- package/src/mcp/tools/core-dashboard-items.mcp-tools.ts +1 -1
- package/src/mcp/tools/core-dashboard.mcp-tools.ts +1 -1
- package/src/mcp/tools/core-event-webhooks.mcp-tools.ts +1 -2
- package/src/mcp/tools/core-files.mcp-tools.ts +202 -22
- package/src/mcp/tools/core-integration-events.mcp-tools.ts +1 -1
- package/src/mcp/tools/core-mail-sent.mcp-tools.ts +1 -1
- package/src/mcp/tools/core-mail.mcp-tools.ts +1 -1
- package/src/mcp/tools/core-menus.mcp-tools.ts +1 -2
- package/src/mcp/tools/core-notifications.mcp-tools.ts +0 -1
- package/src/mcp/tools/core-roles.mcp-tools.ts +1 -5
- package/src/mcp/tools/core-routes.mcp-tools.ts +1 -3
- package/src/mcp/tools/core-screens.mcp-tools.ts +1 -3
- package/src/mcp/tools/core-sessions.mcp-tools.ts +0 -2
- package/src/mcp/tools/core-settings.mcp-tools.ts +1 -2
- package/src/mcp/tools/core-users.mcp-tools.ts +1 -1
- package/src/mcp/tools/core-webhooks.mcp-tools.ts +1 -2
- package/src/mcp/types/mcp-context.type.ts +15 -6
- package/src/mcp-chat/mcp-chat.service.ts +8 -0
- package/src/meeting/meeting-provider.registry.ts +121 -0
- package/src/meeting/meeting.constants.ts +74 -0
- package/src/menu/menu.service.spec.ts +89 -0
- package/src/menu/menu.service.ts +29 -11
- package/src/message-template/dto/update-message-template.dto.ts +22 -0
- package/src/message-template/message-template-admin.service.spec.ts +163 -0
- package/src/message-template/message-template-admin.service.ts +239 -0
- package/src/message-template/message-template.controller.ts +52 -0
- package/src/message-template/message-template.module.ts +21 -0
- package/src/message-template/message-template.registry.spec.ts +62 -0
- package/src/message-template/message-template.registry.ts +125 -0
- package/src/message-template/message-template.service.spec.ts +149 -0
- package/src/message-template/message-template.service.ts +149 -0
- package/src/notification/notification.constants.ts +10 -0
- package/src/notification/notification.module.ts +3 -0
- package/src/notification/notification.service.spec.ts +75 -1
- package/src/notification/notification.service.ts +39 -1
- package/src/oauth/oauth.controller.ts +0 -1
- package/src/oauth/oauth.errors.ts +7 -0
- package/src/oauth/oauth.module.ts +6 -1
- package/src/oauth/oauth.service.spec.ts +58 -7
- package/src/oauth/oauth.service.ts +71 -32
- package/src/oauth/providers/abstract.provider.spec.ts +192 -0
- package/src/oauth/providers/abstract.provider.ts +46 -14
- package/src/oauth/providers/github.provider.ts +57 -34
- package/src/oauth/providers/microsoft-entra-id.provider.ts +8 -1
- package/src/observability/monitored-cron.spec.ts +164 -31
- package/src/observability/monitored-cron.ts +154 -62
- package/src/profile/dto/change-password.dto.ts +25 -7
- package/src/profile/profile.service.spec.ts +97 -0
- package/src/profile/profile.service.ts +9 -3
- package/src/realtime/realtime-tick.coalescer.spec.ts +92 -0
- package/src/realtime/realtime-tick.coalescer.ts +60 -0
- package/src/realtime/realtime.service.spec.ts +33 -0
- package/src/realtime/realtime.service.ts +28 -2
- package/src/redis/redis-kv.service.ts +261 -0
- package/src/redis/redis-master.spec.ts +111 -0
- package/src/redis/redis-master.ts +86 -0
- package/src/redis/redis-pubsub.ts +27 -3
- package/src/redis/redis.module.ts +17 -0
- package/src/session/session.service.spec.ts +121 -42
- package/src/session/session.service.ts +279 -82
- package/src/setting/setting.service.ts +44 -3
- package/src/task/task.service.spec.ts +41 -0
- package/src/task/task.service.ts +53 -13
- package/src/token/token.service.spec.ts +90 -0
- package/src/token/token.service.ts +48 -2
- package/src/user/user-credential.util.ts +14 -5
- package/src/user/user.service.ts +2 -12
- package/src/user/user.sort.ts +25 -0
- package/src/utils/abbreviate-name.spec.ts +50 -0
- package/src/utils/abbreviate-name.ts +44 -0
- package/src/utils/date-range-query.dto.ts +56 -0
- package/src/utils/date-range.spec.ts +175 -0
- package/src/utils/date-range.ts +246 -0
- package/src/webhook-integration/webhook-utils.spec.ts +67 -0
- package/src/webhook-integration/webhook-utils.ts +26 -0
- package/src/whatsapp-sender/whatsapp-recipients.spec.ts +89 -0
- package/src/whatsapp-sender/whatsapp-recipients.ts +83 -0
- package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +112 -0
- package/src/whatsapp-sender/whatsapp-sender.service.ts +58 -5
|
@@ -24,6 +24,7 @@ import { SecurityService } from '../security/security.service';
|
|
|
24
24
|
import { SettingService } from '../setting/setting.service';
|
|
25
25
|
import { TokenService } from '../token/token.service';
|
|
26
26
|
import { UserService } from '../user/user.service';
|
|
27
|
+
import { isActivePasswordCredential } from '../user/user-credential.util';
|
|
27
28
|
import { buildSecurityEventVars } from '../utils/security-event-vars';
|
|
28
29
|
import { resolveWebAuthnOrigin } from '../utils/webauthn-origin';
|
|
29
30
|
import { ChangeEmailDto } from './dto/change-email.dto';
|
|
@@ -101,7 +102,7 @@ export class ProfileService {
|
|
|
101
102
|
throw new NotFoundException(getLocaleText('userNotFound', locale, 'User not found.'));
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
const credentials = (user as unknown as User).user_credential?.filter(
|
|
105
|
+
const credentials = (user as unknown as User).user_credential?.filter(isActivePasswordCredential) || [];
|
|
105
106
|
|
|
106
107
|
if (!await this.security.validatePassword(locale, credentials, password)) {
|
|
107
108
|
throw new BadRequestException(getLocaleText('accessDenied', locale, 'Access denied.'));
|
|
@@ -325,7 +326,9 @@ export class ProfileService {
|
|
|
325
326
|
async getProfile(userId: number) {
|
|
326
327
|
const [user, passwordCredentials] = await Promise.all([
|
|
327
328
|
this.prisma.user.findUnique({ where: { id: userId } }),
|
|
328
|
-
this.prisma.user_credential.count({
|
|
329
|
+
this.prisma.user_credential.count({
|
|
330
|
+
where: { user_id: userId, type: 'password', revoked_at: null },
|
|
331
|
+
}),
|
|
329
332
|
]);
|
|
330
333
|
|
|
331
334
|
if (!user) return user;
|
|
@@ -457,7 +460,10 @@ export class ProfileService {
|
|
|
457
460
|
|
|
458
461
|
async changePassword(locale:string, userId: number, {currentPassword, newPassword}: ChangePasswordDto, ipAddress?: string, userAgent?: string, origin?: string) {
|
|
459
462
|
const credential = await this.prisma.user_credential.findFirst({
|
|
460
|
-
|
|
463
|
+
// Credencial revogada não é senha atual: sem este filtro, `changePassword`
|
|
464
|
+
// aceitava a senha antiga de uma credencial revogada e a reativava por via
|
|
465
|
+
// indireta, gravando um hash novo na mesma linha.
|
|
466
|
+
where: { user_id: userId, type: 'password', revoked_at: null },
|
|
461
467
|
});
|
|
462
468
|
|
|
463
469
|
if (!credential) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
|
4
|
+
import { RealtimeTickCoalescer } from './realtime-tick.coalescer';
|
|
5
|
+
|
|
6
|
+
describe('RealtimeTickCoalescer', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.useFakeTimers();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
jest.useRealTimers();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('junta uma rajada num único disparo', () => {
|
|
16
|
+
// O heartbeat bate a cada 15s por aluno: com 200 ao vivo seriam ~13
|
|
17
|
+
// eventos/s. É esta compressão que torna o push viável.
|
|
18
|
+
const onFlush = jest.fn();
|
|
19
|
+
const coalescer = new RealtimeTickCoalescer(2000, onFlush);
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < 200; i += 1) coalescer.signal();
|
|
22
|
+
expect(onFlush).not.toHaveBeenCalled();
|
|
23
|
+
|
|
24
|
+
jest.advanceTimersByTime(2000);
|
|
25
|
+
|
|
26
|
+
expect(onFlush).toHaveBeenCalledTimes(1);
|
|
27
|
+
expect(onFlush).toHaveBeenCalledWith({ seq: 1, coalesced: 200 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('não dispara sozinho quando não houve sinal', () => {
|
|
31
|
+
const onFlush = jest.fn();
|
|
32
|
+
new RealtimeTickCoalescer(2000, onFlush);
|
|
33
|
+
|
|
34
|
+
jest.advanceTimersByTime(10_000);
|
|
35
|
+
|
|
36
|
+
expect(onFlush).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('abre uma janela nova depois do disparo', () => {
|
|
40
|
+
const onFlush = jest.fn();
|
|
41
|
+
const coalescer = new RealtimeTickCoalescer(2000, onFlush);
|
|
42
|
+
|
|
43
|
+
coalescer.signal();
|
|
44
|
+
jest.advanceTimersByTime(2000);
|
|
45
|
+
expect(onFlush).toHaveBeenCalledTimes(1);
|
|
46
|
+
|
|
47
|
+
coalescer.signal();
|
|
48
|
+
jest.advanceTimersByTime(2000);
|
|
49
|
+
|
|
50
|
+
expect(onFlush).toHaveBeenCalledTimes(2);
|
|
51
|
+
expect(onFlush).toHaveBeenLastCalledWith({ seq: 2, coalesced: 1 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('não se auto-perpetua depois de esvaziar', () => {
|
|
55
|
+
const onFlush = jest.fn();
|
|
56
|
+
const coalescer = new RealtimeTickCoalescer(2000, onFlush);
|
|
57
|
+
|
|
58
|
+
coalescer.signal();
|
|
59
|
+
jest.advanceTimersByTime(2000);
|
|
60
|
+
jest.advanceTimersByTime(60_000);
|
|
61
|
+
|
|
62
|
+
expect(onFlush).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('dispose cancela o disparo pendente', () => {
|
|
66
|
+
// Um timer vivo no encerramento segura o processo e atrasa o drain do pod.
|
|
67
|
+
const onFlush = jest.fn();
|
|
68
|
+
const coalescer = new RealtimeTickCoalescer(2000, onFlush);
|
|
69
|
+
|
|
70
|
+
coalescer.signal();
|
|
71
|
+
coalescer.dispose();
|
|
72
|
+
jest.advanceTimersByTime(10_000);
|
|
73
|
+
|
|
74
|
+
expect(onFlush).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('flush manual entrega o que estiver pendente', () => {
|
|
78
|
+
const onFlush = jest.fn();
|
|
79
|
+
const coalescer = new RealtimeTickCoalescer(2000, onFlush);
|
|
80
|
+
|
|
81
|
+
coalescer.signal();
|
|
82
|
+
coalescer.signal();
|
|
83
|
+
coalescer.flush();
|
|
84
|
+
|
|
85
|
+
expect(onFlush).toHaveBeenCalledWith({ seq: 1, coalesced: 2 });
|
|
86
|
+
|
|
87
|
+
// Depois do flush manual, o timer que já estava agendado não deve emitir de
|
|
88
|
+
// novo com a fila vazia.
|
|
89
|
+
jest.advanceTimersByTime(2000);
|
|
90
|
+
expect(onFlush).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Junta uma rajada de sinais num único disparo.
|
|
3
|
+
*
|
|
4
|
+
* Nasceu no LMS e vive aqui porque o problema não é de lá: qualquer feature com
|
|
5
|
+
* uma tela de acompanhamento tem a mesma rajada. No LMS é o heartbeat do player,
|
|
6
|
+
* que bate a cada 15s por aluno — com 200 alunos ao vivo são ~13 sinais por
|
|
7
|
+
* segundo, e um evento de realtime por batida faria cada admin com a tela aberta
|
|
8
|
+
* refazer a consulta 13x/s. Na loja é o beacon de navegação dos visitantes. O
|
|
9
|
+
* coalescer transforma isso num disparo a cada `windowMs`.
|
|
10
|
+
*
|
|
11
|
+
* A borda escolhida é a de saída (trailing): o primeiro sinal agenda o disparo
|
|
12
|
+
* para daqui a `windowMs` e os que chegarem no meio são absorvidos. Isso limita
|
|
13
|
+
* o pior caso a um disparo por janela, ao custo de até `windowMs` de latência —
|
|
14
|
+
* aceitável para uma tela de acompanhamento, e o número de sinais absorvidos vai
|
|
15
|
+
* junto no disparo para quem quiser instrumentar.
|
|
16
|
+
*/
|
|
17
|
+
export class RealtimeTickCoalescer {
|
|
18
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
19
|
+
private pending = 0;
|
|
20
|
+
private sequence = 0;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly windowMs: number,
|
|
24
|
+
private readonly onFlush: (info: { seq: number; coalesced: number }) => void,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
/** Registra um sinal. Só o primeiro da janela agenda o disparo. */
|
|
28
|
+
signal(): void {
|
|
29
|
+
this.pending += 1;
|
|
30
|
+
if (this.timer !== null) return;
|
|
31
|
+
|
|
32
|
+
this.timer = setTimeout(() => {
|
|
33
|
+
this.timer = null;
|
|
34
|
+
this.flush();
|
|
35
|
+
}, this.windowMs);
|
|
36
|
+
|
|
37
|
+
// Um timer pendente não deve segurar o processo no shutdown do worker.
|
|
38
|
+
this.timer.unref?.();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Dispara agora o que estiver pendente, se houver. */
|
|
42
|
+
flush(): void {
|
|
43
|
+
if (this.pending === 0) return;
|
|
44
|
+
|
|
45
|
+
const coalesced = this.pending;
|
|
46
|
+
this.pending = 0;
|
|
47
|
+
this.sequence += 1;
|
|
48
|
+
|
|
49
|
+
this.onFlush({ seq: this.sequence, coalesced });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Cancela o disparo pendente. Usado no encerramento do módulo. */
|
|
53
|
+
dispose(): void {
|
|
54
|
+
if (this.timer !== null) {
|
|
55
|
+
clearTimeout(this.timer);
|
|
56
|
+
this.timer = null;
|
|
57
|
+
}
|
|
58
|
+
this.pending = 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -37,6 +37,39 @@ describe('RealtimeService', () => {
|
|
|
37
37
|
expect(() => bare.emitToUser(1, 'x', {})).not.toThrow();
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
// Caminho que só existe com mais de um pod: quem cria a notificação quase
|
|
41
|
+
// nunca é o pod que segura o socket do usuário (4 réplicas, sem sticky
|
|
42
|
+
// session), então a entrega real depende deste re-emit a partir do Redis.
|
|
43
|
+
describe('handleRedisMessage', () => {
|
|
44
|
+
const message = (over: Record<string, unknown> = {}) =>
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
target: { type: 'user', target: '7' },
|
|
47
|
+
event: 'notification:event',
|
|
48
|
+
payload: { type: 'notification.created' },
|
|
49
|
+
originInstanceId: 'outro-pod',
|
|
50
|
+
...over,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('re-emite localmente o que veio de outro pod', () => {
|
|
54
|
+
(service as any).handleRedisMessage(message());
|
|
55
|
+
expect(server.to).toHaveBeenCalledWith('user:7');
|
|
56
|
+
expect(server.emit).toHaveBeenCalledWith('notification:event', {
|
|
57
|
+
type: 'notification.created',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('ignora a própria mensagem, já emitida localmente', () => {
|
|
62
|
+
const ownId = (service as any).instanceId;
|
|
63
|
+
(service as any).handleRedisMessage(message({ originInstanceId: ownId }));
|
|
64
|
+
expect(server.to).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('ignora payload malformado', () => {
|
|
68
|
+
expect(() => (service as any).handleRedisMessage('{')).not.toThrow();
|
|
69
|
+
expect(server.to).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
40
73
|
it('tracks local connections per user', () => {
|
|
41
74
|
expect(service.trackConnect(1)).toBe(1);
|
|
42
75
|
expect(service.trackConnect(1)).toBe(2);
|
|
@@ -36,6 +36,15 @@ export class RealtimeService implements OnModuleInit, OnModuleDestroy {
|
|
|
36
36
|
private server: Server | null = null;
|
|
37
37
|
private redisPublisher: RedisClientType | null = null;
|
|
38
38
|
private redisSubscriber: RedisClientType | null = null;
|
|
39
|
+
// A `reconnectStrategy` que nunca devolve `false` faz `client.connect()` ficar
|
|
40
|
+
// retentando pra sempre quando o Redis está indisponível: a Promise nunca
|
|
41
|
+
// resolve nem rejeita, e `onModuleInit` trava o boot inteiro da API esperando
|
|
42
|
+
// por ela. Este teto bound só a tentativa inicial; a reconexão de um cliente
|
|
43
|
+
// já conectado continua indefinida.
|
|
44
|
+
private readonly redisConnectTimeoutMs = Math.max(
|
|
45
|
+
1000,
|
|
46
|
+
Number(process.env.REDIS_CONNECT_TIMEOUT_MS || 3000),
|
|
47
|
+
);
|
|
39
48
|
|
|
40
49
|
/** Local (this-instance) socket count per user id, for presence/liveness. */
|
|
41
50
|
private readonly localConnections = new Map<number, number>();
|
|
@@ -162,8 +171,8 @@ export class RealtimeService implements OnModuleInit, OnModuleDestroy {
|
|
|
162
171
|
this.redisSubscriber = createClient(opts);
|
|
163
172
|
this.redisPublisher.on('error', (e) => this.logger.warn(`Realtime Redis publisher error: ${e?.message ?? e}`));
|
|
164
173
|
this.redisSubscriber.on('error', (e) => this.logger.warn(`Realtime Redis subscriber error: ${e?.message ?? e}`));
|
|
165
|
-
await this.redisPublisher.connect();
|
|
166
|
-
await this.redisSubscriber.connect();
|
|
174
|
+
await this.withConnectTimeout(this.redisPublisher.connect(), this.redisConnectTimeoutMs);
|
|
175
|
+
await this.withConnectTimeout(this.redisSubscriber.connect(), this.redisConnectTimeoutMs);
|
|
167
176
|
await this.redisSubscriber.subscribe(this.channel, (msg) => this.handleRedisMessage(msg));
|
|
168
177
|
this.logger.log(`Realtime Redis fan-out enabled on channel ${this.channel}`);
|
|
169
178
|
} catch (error) {
|
|
@@ -184,4 +193,21 @@ export class RealtimeService implements OnModuleInit, OnModuleDestroy {
|
|
|
184
193
|
/* ignore */
|
|
185
194
|
}
|
|
186
195
|
}
|
|
196
|
+
|
|
197
|
+
private async withConnectTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
198
|
+
let timer: NodeJS.Timeout;
|
|
199
|
+
try {
|
|
200
|
+
return await Promise.race([
|
|
201
|
+
promise,
|
|
202
|
+
new Promise<never>((_, reject) => {
|
|
203
|
+
timer = setTimeout(
|
|
204
|
+
() => reject(new Error(`Redis connection timed out after ${timeoutMs}ms`)),
|
|
205
|
+
timeoutMs,
|
|
206
|
+
);
|
|
207
|
+
}),
|
|
208
|
+
]);
|
|
209
|
+
} finally {
|
|
210
|
+
clearTimeout(timer!);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
187
213
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { createClient, type RedisClientType } from 'redis';
|
|
3
|
+
import { extractRedisErrorMessage, withConnectTimeout } from './redis-pubsub';
|
|
4
|
+
import { isReadOnlyReplicaError, resolveMasterUrl } from './redis-master';
|
|
5
|
+
|
|
6
|
+
/** Uma entrada do índice ordenado por tempo, já decodificada. */
|
|
7
|
+
export type RedisScoredMember = {
|
|
8
|
+
member: string;
|
|
9
|
+
score: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Chave/valor com TTL sobre o Redis, para estado efêmero compartilhado entre as
|
|
14
|
+
* réplicas da API.
|
|
15
|
+
*
|
|
16
|
+
* Existe ao lado do `redis-pubsub.ts` — que só publica e assina — porque
|
|
17
|
+
* algumas features precisam GUARDAR estado por um tempo curto, não só avisar
|
|
18
|
+
* que algo mudou. O caso que motivou: o painel de visitantes ao vivo da loja,
|
|
19
|
+
* onde gravar cada navegação no Postgres colocaria carga de escrita no banco
|
|
20
|
+
* que serve a própria loja, exatamente o que o recurso não pode fazer. Aqui o
|
|
21
|
+
* dado vive na memória do Redis e expira sozinho, sem job de limpeza.
|
|
22
|
+
*
|
|
23
|
+
* **Toda falha degrada para inerte.** Mesma postura do pub/sub: sem
|
|
24
|
+
* `REDIS_URL`, ou com o Redis fora, cada método vira no-op (ou devolve vazio) e
|
|
25
|
+
* quem chamou segue seu caminho. Um recurso de observação nunca pode derrubar o
|
|
26
|
+
* fluxo que ele observa — daí nenhum método propagar erro.
|
|
27
|
+
*/
|
|
28
|
+
@Injectable()
|
|
29
|
+
export class RedisKvService implements OnModuleInit, OnModuleDestroy {
|
|
30
|
+
private readonly logger = new Logger(RedisKvService.name);
|
|
31
|
+
private readonly url = String(process.env.REDIS_URL || '').trim();
|
|
32
|
+
private readonly connectTimeoutMs = Math.max(
|
|
33
|
+
1000,
|
|
34
|
+
Number(process.env.REDIS_CONNECT_TIMEOUT_MS || 3000),
|
|
35
|
+
);
|
|
36
|
+
/** Uma queda do Redis dispara `error` a cada tentativa de reconexão. */
|
|
37
|
+
private readonly errorLogCooldownMs = 30000;
|
|
38
|
+
private lastErrorLogAt = 0;
|
|
39
|
+
|
|
40
|
+
private client: RedisClientType | null = null;
|
|
41
|
+
/** Reconexão em andamento, para não abrir uma por escrita durante o failover. */
|
|
42
|
+
private reconnecting: Promise<void> | null = null;
|
|
43
|
+
|
|
44
|
+
/** `false` = sem Redis; todo método é no-op e quem chama não precisa saber. */
|
|
45
|
+
get enabled(): boolean {
|
|
46
|
+
return this.client !== null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async onModuleInit(): Promise<void> {
|
|
50
|
+
if (!this.url) {
|
|
51
|
+
this.logger.log('Redis key-value disabled because REDIS_URL is not configured.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.client = await this.connectToMaster();
|
|
56
|
+
if (this.client) this.logger.log('Redis key-value enabled.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Conecta no master, e não em quem o Service devolver.
|
|
61
|
+
*
|
|
62
|
+
* `REDIS_URL` aponta para um Service que balanceia entre master e réplica
|
|
63
|
+
* (veja `resolveMasterUrl`), então conectar direto na URL deixa metade dos
|
|
64
|
+
* processos escrevendo numa réplica e falhando com `READONLY`.
|
|
65
|
+
*/
|
|
66
|
+
private async connectToMaster(): Promise<RedisClientType | null> {
|
|
67
|
+
try {
|
|
68
|
+
const url = await resolveMasterUrl(
|
|
69
|
+
this.url,
|
|
70
|
+
this.logger,
|
|
71
|
+
this.connectTimeoutMs,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const client = createClient({
|
|
75
|
+
url,
|
|
76
|
+
socket: {
|
|
77
|
+
reconnectStrategy: (retries: number) => Math.min(retries * 250, 5000),
|
|
78
|
+
},
|
|
79
|
+
}) as RedisClientType;
|
|
80
|
+
|
|
81
|
+
// Registrado antes do connect: uma falha no handshake vira erro tratado
|
|
82
|
+
// em vez de um evento 'error' sem dono derrubando o processo.
|
|
83
|
+
client.on('error', (error: unknown) => this.logClientError(error));
|
|
84
|
+
|
|
85
|
+
await withConnectTimeout(client.connect(), this.connectTimeoutMs);
|
|
86
|
+
return client;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.logger.warn(
|
|
89
|
+
`Redis key-value disabled: ${extractRedisErrorMessage(error)}`,
|
|
90
|
+
);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Refaz a conexão depois de um `READONLY`.
|
|
97
|
+
*
|
|
98
|
+
* Um failover troca quem é o master, e o client fica preso no nó que virou
|
|
99
|
+
* réplica — daí em diante toda escrita falha. Reconectar redescobre o master.
|
|
100
|
+
* O guard serializa as tentativas: sem ele, um pico de escritas em cima do
|
|
101
|
+
* failover abriria uma conexão nova por requisição.
|
|
102
|
+
*/
|
|
103
|
+
private async reconnectToMaster(): Promise<void> {
|
|
104
|
+
if (this.reconnecting) return this.reconnecting;
|
|
105
|
+
|
|
106
|
+
this.reconnecting = (async () => {
|
|
107
|
+
const stale = this.client;
|
|
108
|
+
this.client = null;
|
|
109
|
+
if (stale) {
|
|
110
|
+
try {
|
|
111
|
+
await stale.quit();
|
|
112
|
+
} catch {
|
|
113
|
+
// O nó virou réplica ou caiu; encerrando de qualquer forma.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this.client = await this.connectToMaster();
|
|
117
|
+
})().finally(() => {
|
|
118
|
+
this.reconnecting = null;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return this.reconnecting;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async onModuleDestroy(): Promise<void> {
|
|
125
|
+
const client = this.client;
|
|
126
|
+
this.client = null;
|
|
127
|
+
if (!client) return;
|
|
128
|
+
try {
|
|
129
|
+
await client.quit();
|
|
130
|
+
} catch {
|
|
131
|
+
// Encerrando de qualquer forma.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Grava um objeto como JSON com expiração em segundos. */
|
|
136
|
+
async setJson(key: string, value: unknown, ttlSeconds: number): Promise<void> {
|
|
137
|
+
await this.run(async (client) => {
|
|
138
|
+
await client.set(key, JSON.stringify(value), { EX: Math.max(1, ttlSeconds) });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Lê e desserializa. Devolve `null` se não existe, expirou ou está corrompido. */
|
|
143
|
+
async getJson<T>(key: string): Promise<T | null> {
|
|
144
|
+
return (
|
|
145
|
+
(await this.run(async (client) => {
|
|
146
|
+
const raw = await client.get(key);
|
|
147
|
+
if (raw == null) return null;
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(raw) as T;
|
|
150
|
+
} catch {
|
|
151
|
+
// Valor gravado por uma versão anterior com outro formato: tratar como
|
|
152
|
+
// ausente é melhor do que estourar na leitura do painel.
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
})) ?? null
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Lê várias chaves numa ida só.
|
|
161
|
+
*
|
|
162
|
+
* `MGET` em vez de um `get` por chave: com dezenas de visitantes ao vivo, uma
|
|
163
|
+
* chamada por chave multiplicaria o round-trip pelo número de sessões a cada
|
|
164
|
+
* atualização do painel. Devolve na MESMA ordem das chaves pedidas, com `null`
|
|
165
|
+
* onde não havia valor.
|
|
166
|
+
*/
|
|
167
|
+
async getJsonMany<T>(keys: string[]): Promise<Array<T | null>> {
|
|
168
|
+
if (!keys.length) return [];
|
|
169
|
+
const values = await this.run(async (client) => client.mGet(keys));
|
|
170
|
+
if (!values) return keys.map(() => null);
|
|
171
|
+
|
|
172
|
+
return values.map((raw) => {
|
|
173
|
+
if (raw == null) return null;
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(raw) as T;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async delete(key: string): Promise<void> {
|
|
183
|
+
await this.run(async (client) => {
|
|
184
|
+
await client.del(key);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Põe (ou repontua) um membro no índice ordenado. */
|
|
189
|
+
async zadd(key: string, member: string, score: number): Promise<void> {
|
|
190
|
+
await this.run(async (client) => {
|
|
191
|
+
await client.zAdd(key, [{ value: member, score }]);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Membros com score dentro do intervalo, do mais antigo para o mais recente. */
|
|
196
|
+
async zrangeByScore(
|
|
197
|
+
key: string,
|
|
198
|
+
min: number,
|
|
199
|
+
max: number,
|
|
200
|
+
): Promise<RedisScoredMember[]> {
|
|
201
|
+
const rows = await this.run(async (client) =>
|
|
202
|
+
client.zRangeByScoreWithScores(key, min, max),
|
|
203
|
+
);
|
|
204
|
+
return (rows ?? []).map((row) => ({ member: row.value, score: row.score }));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Remove do índice o que caiu fora da janela.
|
|
209
|
+
*
|
|
210
|
+
* O TTL cuida das chaves individuais, mas não do índice: sem esta poda o
|
|
211
|
+
* sorted set acumularia para sempre todo visitante que já passou pela loja, e
|
|
212
|
+
* cada leitura do painel ficaria mais cara que a anterior.
|
|
213
|
+
*/
|
|
214
|
+
async zremRangeByScore(key: string, min: number, max: number): Promise<void> {
|
|
215
|
+
await this.run(async (client) => {
|
|
216
|
+
await client.zRemRangeByScore(key, min, max);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Executa contra o client, engolindo qualquer falha.
|
|
222
|
+
*
|
|
223
|
+
* É aqui que mora a promessa de degradar para inerte: quem chama recebe
|
|
224
|
+
* `undefined` e trata como "sem dado", em vez de precisar de try/catch em
|
|
225
|
+
* cada ponto de uso.
|
|
226
|
+
*/
|
|
227
|
+
private async run<T>(fn: (client: RedisClientType) => Promise<T>): Promise<T | undefined> {
|
|
228
|
+
const client = this.client;
|
|
229
|
+
if (!client) return undefined;
|
|
230
|
+
try {
|
|
231
|
+
return await fn(client);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
// `READONLY` quer dizer que este client está falando com uma réplica —
|
|
234
|
+
// um failover trocou o master debaixo dele. Reconectar redescobre quem é
|
|
235
|
+
// o master e a operação é refeita uma vez; qualquer outro erro segue a
|
|
236
|
+
// regra da casa e degrada para inerte.
|
|
237
|
+
if (isReadOnlyReplicaError(error)) {
|
|
238
|
+
await this.reconnectToMaster();
|
|
239
|
+
const reconnected = this.client;
|
|
240
|
+
if (reconnected) {
|
|
241
|
+
try {
|
|
242
|
+
return await fn(reconnected);
|
|
243
|
+
} catch (retryError) {
|
|
244
|
+
this.logClientError(retryError);
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.logClientError(error);
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private logClientError(error: unknown): void {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
if (now - this.lastErrorLogAt < this.errorLogCooldownMs) return;
|
|
258
|
+
this.lastErrorLogAt = now;
|
|
259
|
+
this.logger.warn(`Redis key-value error: ${extractRedisErrorMessage(error)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
2
|
+
import { Logger } from '@nestjs/common';
|
|
3
|
+
|
|
4
|
+
const createClient = jest.fn();
|
|
5
|
+
jest.mock('redis', () => ({ createClient }));
|
|
6
|
+
|
|
7
|
+
// Importado depois do mock: `redis-master` resolve `createClient` no topo.
|
|
8
|
+
const { isReadOnlyReplicaError, resolveMasterUrl } = require('./redis-master');
|
|
9
|
+
|
|
10
|
+
const makeProbe = (info: string) => {
|
|
11
|
+
const probe = {
|
|
12
|
+
on: jest.fn(),
|
|
13
|
+
connect: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
14
|
+
info: jest.fn<() => Promise<string>>().mockResolvedValue(info),
|
|
15
|
+
quit: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
16
|
+
};
|
|
17
|
+
createClient.mockReturnValue(probe);
|
|
18
|
+
return probe;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const logger = new Logger('test');
|
|
22
|
+
const REPLICA_INFO = [
|
|
23
|
+
'role:slave',
|
|
24
|
+
'master_host:redis-hub-node-1.redis-hub-headless.hcode.svc.cluster.local',
|
|
25
|
+
'master_port:6379',
|
|
26
|
+
'master_link_status:up',
|
|
27
|
+
].join('\r\n');
|
|
28
|
+
|
|
29
|
+
describe('resolveMasterUrl', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
|
|
33
|
+
jest.spyOn(logger, 'log').mockImplementation(() => undefined);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('redireciona para o master quando a URL caiu numa replica', async () => {
|
|
37
|
+
// O Service redis-hub tem os dois pods como endpoint, entao a URL do env
|
|
38
|
+
// aponta para o master metade das vezes e para a replica na outra metade.
|
|
39
|
+
makeProbe(REPLICA_INFO);
|
|
40
|
+
|
|
41
|
+
const url = await resolveMasterUrl(
|
|
42
|
+
'redis://:senha@redis-hub:6379/0',
|
|
43
|
+
logger,
|
|
44
|
+
3000,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(url).toBe(
|
|
48
|
+
'redis://:senha@redis-hub-node-1.redis-hub-headless.hcode.svc.cluster.local:6379/0',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('mantem a URL quando ja caiu no master', async () => {
|
|
53
|
+
makeProbe('role:master\r\nconnected_slaves:1');
|
|
54
|
+
|
|
55
|
+
const url = await resolveMasterUrl('redis://redis-hub:6379/0', logger, 3000);
|
|
56
|
+
|
|
57
|
+
expect(url).toBe('redis://redis-hub:6379/0');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('mantem a URL quando o probe falha, em vez de derrubar o boot', async () => {
|
|
61
|
+
createClient.mockReturnValue({
|
|
62
|
+
on: jest.fn(),
|
|
63
|
+
connect: jest
|
|
64
|
+
.fn<() => Promise<void>>()
|
|
65
|
+
.mockRejectedValue(new Error('ECONNREFUSED')),
|
|
66
|
+
info: jest.fn(),
|
|
67
|
+
quit: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const url = await resolveMasterUrl('redis://redis-hub:6379/0', logger, 3000);
|
|
71
|
+
|
|
72
|
+
expect(url).toBe('redis://redis-hub:6379/0');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('mantem a URL quando a replica nao informa o master', async () => {
|
|
76
|
+
makeProbe('role:slave\r\nmaster_link_status:down');
|
|
77
|
+
|
|
78
|
+
const url = await resolveMasterUrl('redis://redis-hub:6379/0', logger, 3000);
|
|
79
|
+
|
|
80
|
+
expect(url).toBe('redis://redis-hub:6379/0');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('fecha o probe mesmo no caminho feliz', async () => {
|
|
84
|
+
const probe = makeProbe('role:master');
|
|
85
|
+
|
|
86
|
+
await resolveMasterUrl('redis://redis-hub:6379/0', logger, 3000);
|
|
87
|
+
|
|
88
|
+
expect(probe.quit).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('nao tenta descobrir nada sem URL', async () => {
|
|
92
|
+
const url = await resolveMasterUrl('', logger, 3000);
|
|
93
|
+
|
|
94
|
+
expect(url).toBe('');
|
|
95
|
+
expect(createClient).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('isReadOnlyReplicaError', () => {
|
|
100
|
+
it('reconhece a recusa de escrita da replica', () => {
|
|
101
|
+
expect(
|
|
102
|
+
isReadOnlyReplicaError(
|
|
103
|
+
new Error("READONLY You can't write against a read only replica."),
|
|
104
|
+
),
|
|
105
|
+
).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('nao confunde com outros erros', () => {
|
|
109
|
+
expect(isReadOnlyReplicaError(new Error('ECONNRESET'))).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|