@zeniai/client-epic-state 5.0.8 → 5.0.9-beta0ND

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 (32) hide show
  1. package/lib/coreEpics.js +2 -1
  2. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  3. package/lib/entity/snackbar/snackbarTypes.js +1 -0
  4. package/lib/entity/tenant/SessionManager.d.ts +38 -0
  5. package/lib/entity/tenant/SessionManager.js +171 -0
  6. package/lib/entity/tenant/epic/sessionHeartbeatEpic.d.ts +16 -0
  7. package/lib/entity/tenant/epic/sessionHeartbeatEpic.js +16 -0
  8. package/lib/entity/tenant/sessionTypes.d.ts +26 -0
  9. package/lib/entity/tenant/sessionTypes.js +12 -0
  10. package/lib/entity/tenant/tenantReducer.d.ts +5 -1
  11. package/lib/entity/tenant/tenantReducer.js +23 -2
  12. package/lib/entity/tenant/tenantState.d.ts +1 -0
  13. package/lib/esm/coreEpics.js +2 -1
  14. package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
  15. package/lib/esm/entity/tenant/SessionManager.js +167 -0
  16. package/lib/esm/entity/tenant/epic/sessionHeartbeatEpic.js +12 -0
  17. package/lib/esm/entity/tenant/sessionTypes.js +9 -0
  18. package/lib/esm/entity/tenant/tenantReducer.js +21 -1
  19. package/lib/esm/index.js +5 -2
  20. package/lib/esm/view/aiAccountantView/aiAccountantViewReducer.js +3 -0
  21. package/lib/esm/view/aiAccountantView/aiAccountantViewSelector.js +5 -0
  22. package/lib/esm/view/aiAccountantView/epics/triggerAiAccountantJobEpic.js +29 -5
  23. package/lib/index.d.ts +5 -2
  24. package/lib/index.js +36 -30
  25. package/lib/tsconfig.typecheck.tsbuildinfo +1 -1
  26. package/lib/view/aiAccountantView/aiAccountantViewPayload.d.ts +2 -0
  27. package/lib/view/aiAccountantView/aiAccountantViewReducer.js +3 -0
  28. package/lib/view/aiAccountantView/aiAccountantViewSelector.d.ts +3 -1
  29. package/lib/view/aiAccountantView/aiAccountantViewSelector.js +5 -0
  30. package/lib/view/aiAccountantView/epics/triggerAiAccountantJobEpic.js +29 -5
  31. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  32. package/package.json +1 -1
package/lib/coreEpics.js CHANGED
@@ -24,6 +24,7 @@ const resendVerifyDeviceOTPEpic_1 = require("./entity/tenant/epic/resendVerifyDe
24
24
  const saveExternalConnectionEpic_1 = require("./entity/tenant/epic/saveExternalConnectionEpic");
25
25
  const sendEmailMagicLinkToUserEpic_1 = require("./entity/tenant/epic/sendEmailMagicLinkToUserEpic");
26
26
  const signInUserEpic_1 = require("./entity/tenant/epic/signInUserEpic");
27
+ const sessionHeartbeatEpic_1 = require("./entity/tenant/epic/sessionHeartbeatEpic");
27
28
  const signOutUserEpic_1 = require("./entity/tenant/epic/signOutUserEpic");
28
29
  const verifyDeviceWithTwoFAEpic_1 = require("./entity/tenant/epic/verifyDeviceWithTwoFAEpic");
29
30
  // ── Toast Notification Epic ──────────────────────────────────────────
@@ -48,7 +49,7 @@ const verifyOtpEpic_1 = require("./view/twoFactorAuthentication/verifyOtpEpic");
48
49
  // RootActionType is only available in the dynamically imported epic.ts.
49
50
  const coreRootEpic = (0, redux_observable_1.combineEpics)(
50
51
  // Auth & tenant
51
- clearAllEpic_1.clearAllEpic, doMagicLinkSignInEpic_1.doMagicLinkSignInEpic, signInUserEpic_1.doSignInEpic, signOutUserEpic_1.doSignOutEpic, fetchActiveTenantEpic_1.fetchActiveTenantEpic, fetchAllTenantsEpic_1.fetchAllTenantsEpic, fetchExcludedResourcesEpic_1.fetchExcludedResourcesEpic, fetchExternalConnectionsEpic_1.fetchExternalConnectionsEpic, fetchSubscriptionSummaryForTenantEpic_1.fetchSubscriptionSummaryForTenantEpic, resendVerifyDeviceOTPEpic_1.resendVerifyDeviceOTPEpic, saveExternalConnectionEpic_1.saveExternalConnectionEpic, sendEmailMagicLinkToUserEpic_1.sendEmailMagicLinkToUserEpic, verifyDeviceWithTwoFAEpic_1.verifyDeviceWithTwoFAEpic,
52
+ clearAllEpic_1.clearAllEpic, doMagicLinkSignInEpic_1.doMagicLinkSignInEpic, signInUserEpic_1.doSignInEpic, signOutUserEpic_1.doSignOutEpic, sessionHeartbeatEpic_1.sessionHeartbeatEpic, fetchActiveTenantEpic_1.fetchActiveTenantEpic, fetchAllTenantsEpic_1.fetchAllTenantsEpic, fetchExcludedResourcesEpic_1.fetchExcludedResourcesEpic, fetchExternalConnectionsEpic_1.fetchExternalConnectionsEpic, fetchSubscriptionSummaryForTenantEpic_1.fetchSubscriptionSummaryForTenantEpic, resendVerifyDeviceOTPEpic_1.resendVerifyDeviceOTPEpic, saveExternalConnectionEpic_1.saveExternalConnectionEpic, sendEmailMagicLinkToUserEpic_1.sendEmailMagicLinkToUserEpic, verifyDeviceWithTwoFAEpic_1.verifyDeviceWithTwoFAEpic,
52
53
  // Toast
53
54
  pushToastNotificationEpic_1.pushToastNotificationEpic,
54
55
  // Collaboration
@@ -1,4 +1,4 @@
1
- export declare const toSnackbarMessageSection: (v: string) => "common" | "transactionDetails_updatingPastTransactions" | "transactionDetails_transactionUpdated" | "cockpit_month_end_email_sent" | "cockpit_month_end_email_save" | "cockpit_month_end_email_attachment_upload" | "je_bill_link" | "je_posted" | "audit_score_updated" | "people_invite_people" | "people_invite_multiple_people" | "people_delete_person" | "people_update_person" | "reimbursement_updated" | "reimbursement_deleted" | "reimbursement_cancelled_deleted" | "reimbursement_cancelled" | "reimbursement_approved" | "reimbursement_rejected" | "reimbursement_sent_for_approval" | "account_added" | "business_verification_save" | "business_verification_submit" | "business_verification_from_bills_submit" | "plaid_connection" | "unlink_deposit_account" | "reimbursement_setup" | "reimbursement_approval_create" | "reimbursement_duplicate_approval_create" | "reimbursement_approval_update" | "reimbursement_duplicate_approval_update" | "reimbursement_approval_delete" | "reimbursement_create_mileage" | "reimbursement_update_mileage" | "reimbursement_accept_term" | "reimbursement_accept_employee_term" | "reimbursement_bulk_submit" | "reimbursement_bulk_processed" | "bill_pay_setup" | "zeni_accounts_setup" | "bill_pay_approval_create" | "bill_pay_duplicate_approval_create" | "bill_pay_approval_update" | "bill_pay_duplicate_approval_update" | "bill_pay_approval_delete" | "bill_pay_updated" | "bill_pay_deleted" | "bill_pay_cancelled_deleted" | "bill_pay_cancelled" | "bill_pay_approved" | "real_time_approver_added" | "bill_pay_rejected" | "bill_pay_sent_for_approval" | "bill_pay_accept_term" | "bill_pay_bulk_submit" | "bill_pay_bulk_processed" | "bill_pay_refund" | "bill_pay_retry" | "bill_pay_marked_as_paid" | "zeni_account_accept_term" | "update_vendor" | "data_refresh_update" | "delete_bank_account" | "create_bank_account" | "create_bank_account_ach" | "create_bank_account_wire" | "create_bank_account_international" | "transfer_money" | "update_zeni_account_nickname" | "create_checking_account" | "deposit_check" | "create_vendor" | "onboarding_customer_view" | "onboarding_customer_view_complete" | "onboarding_customer_identity_verification_save" | "onboarding_customer_identity_verification_submit" | "onboarding_customer_business_verification_save" | "onboarding_customer_business_verification_submit" | "retry_bank_account_connection" | "dashboard_invite_sent" | "onboarding_info_saved" | "approve_original_merchant" | "approve_global_merchant" | "reject_global_merchant" | "create_global_merchant" | "fetch_global_merchant_no_recommendation" | "save_vendor_renamed" | "save_vendor_sent_for_review" | "save_vendor_marked_as_employee" | "save_vendor_marked_as_local_contractor" | "charge_card_setup" | "charge_card_accept_term" | "charge_card_receipt_upload" | "charge_card_resend_invite" | "charge_card_revoke_invite" | "charge_card_update_limit" | "charge_cards_update_limit" | "close_charge_card" | "lock_charge_card" | "lock_charge_card_card_user" | "lock_charge_cards" | "unlock_charge_card" | "unlock_charge_card_card_user" | "unlock_charge_cards" | "close_charge_cards" | "revoke_invite_charge_cards" | "charge_card_express_interest" | "create_schedule" | "save_schedule" | "delete_schedule" | "ignore_schedule" | "save_task_detail" | "fetch_task_detail" | "delete_task" | "archive_task" | "create_tag" | "delete_tag" | "update_charge_card" | "update_charge_card_name" | "update_charge_card_name_card_user" | "issue_charge_card" | "issue_charge_cards" | "notification_settings_saved" | "referral_invite_sent" | "notification_mark_as_read" | "mark_as_complete_schedule" | "cancel_journal_entry" | "settings_accounting_accounts_updated" | "create_card_setup" | "confirm_card_setup" | "add_card_payment_source" | "fetch_payment_sources" | "task_assigned_toast_notification" | "task_due_tomorrow_toast_notification" | "task_deleted_toast_notification" | "task_archived_toast_notification" | "task_overdue_toast_notification" | "task_overdue_toast_notification_creator" | "task_notification_count" | "task_activities_toast_notification" | "task_created_toast_notification" | "task_group_creation_success" | "task_group_deletion_success" | "task_group_update_success" | "primary_funding_account_updated" | "task_time_spent_validation" | "missing_receipts_attachment" | "flux_analysis_unreviewed" | "flux_analysis_reviewed" | "receipt_match" | "receipts_upload" | "receipts_bulk_match" | "billing_address_view" | "express_pay_submit" | "exclude_transaction" | "reconcile" | "save_reconcile_for_later" | "recon_transaction_categorize" | "recon_transaction_match" | "update_debit_card_pin_attempt" | "set_debit_card_pin" | "ai_cfo_create_session_and_submit" | "ai_cfo_chat_session_deleted" | "charge_card_auto_pay_enable" | "charge_card_auto_pay_disable" | "treasury_setup" | "treasury_accept_term" | "treasury_transfer_money" | "treasury_transfer_money_failed" | "auto_transfer_rule_create" | "auto_transfer_rule_update" | "auto_transfer_rule_delete" | "auto_transfer_rule_pause" | "auto_transfer_rule_resume" | "treasury_update_portfolio_allocation" | "send_email_magic_link_to_user" | "complete_profile_done" | "reports_resync" | "invalid_phone_number" | "transactions_categorized_updated_failed" | "transaction_categorized_updated_failed" | "transactionsCategorized_transactionsUpdated" | "transactionsCategorized_transactionUpdated" | "transactionCategorized_transactionsUpdated" | "transactionCategorized_transactionUpdated" | "transactionsCategorized_transactionsFailed" | "transactionsCategorized_transactionFailed" | "transactionCategorized_transactionsFailed" | "transactionCategorized_transactionFailed" | "transactionsUpdated_transactionsFailed" | "transactionsUpdated_transactionFailed" | "transactionUpdated_transactionsFailed" | "transactionUpdated_transactionFailed" | "transactionsCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionsUpdated_transactionFailed" | "transactionsCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionUpdated_transactionFailed" | "transactionCategorized_transactionsUpdated_transactionFailed" | "transactionCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionUpdated_transactionFailed" | "accounting_classes_enabled_update" | "account_excluded_from_reconciliation" | "account_included_in_reconciliation";
1
+ export declare const toSnackbarMessageSection: (v: string) => "common" | "transactionDetails_updatingPastTransactions" | "transactionDetails_transactionUpdated" | "cockpit_month_end_email_sent" | "cockpit_month_end_email_save" | "cockpit_month_end_email_attachment_upload" | "je_bill_link" | "je_posted" | "audit_score_updated" | "people_invite_people" | "people_invite_multiple_people" | "people_delete_person" | "people_update_person" | "reimbursement_updated" | "reimbursement_deleted" | "reimbursement_cancelled_deleted" | "reimbursement_cancelled" | "reimbursement_approved" | "reimbursement_rejected" | "reimbursement_sent_for_approval" | "account_added" | "business_verification_save" | "business_verification_submit" | "business_verification_from_bills_submit" | "plaid_connection" | "unlink_deposit_account" | "reimbursement_setup" | "reimbursement_approval_create" | "reimbursement_duplicate_approval_create" | "reimbursement_approval_update" | "reimbursement_duplicate_approval_update" | "reimbursement_approval_delete" | "reimbursement_create_mileage" | "reimbursement_update_mileage" | "reimbursement_accept_term" | "reimbursement_accept_employee_term" | "reimbursement_bulk_submit" | "reimbursement_bulk_processed" | "bill_pay_setup" | "zeni_accounts_setup" | "bill_pay_approval_create" | "bill_pay_duplicate_approval_create" | "bill_pay_approval_update" | "bill_pay_duplicate_approval_update" | "bill_pay_approval_delete" | "bill_pay_updated" | "bill_pay_deleted" | "bill_pay_cancelled_deleted" | "bill_pay_cancelled" | "bill_pay_approved" | "real_time_approver_added" | "bill_pay_rejected" | "bill_pay_sent_for_approval" | "bill_pay_accept_term" | "bill_pay_bulk_submit" | "bill_pay_bulk_processed" | "bill_pay_refund" | "bill_pay_retry" | "bill_pay_marked_as_paid" | "zeni_account_accept_term" | "update_vendor" | "data_refresh_update" | "delete_bank_account" | "create_bank_account" | "create_bank_account_ach" | "create_bank_account_wire" | "create_bank_account_international" | "transfer_money" | "update_zeni_account_nickname" | "create_checking_account" | "deposit_check" | "create_vendor" | "onboarding_customer_view" | "onboarding_customer_view_complete" | "onboarding_customer_identity_verification_save" | "onboarding_customer_identity_verification_submit" | "onboarding_customer_business_verification_save" | "onboarding_customer_business_verification_submit" | "retry_bank_account_connection" | "dashboard_invite_sent" | "onboarding_info_saved" | "approve_original_merchant" | "approve_global_merchant" | "reject_global_merchant" | "create_global_merchant" | "fetch_global_merchant_no_recommendation" | "save_vendor_renamed" | "save_vendor_sent_for_review" | "save_vendor_marked_as_employee" | "save_vendor_marked_as_local_contractor" | "charge_card_setup" | "charge_card_accept_term" | "charge_card_receipt_upload" | "charge_card_resend_invite" | "charge_card_revoke_invite" | "charge_card_update_limit" | "charge_cards_update_limit" | "close_charge_card" | "lock_charge_card" | "lock_charge_card_card_user" | "lock_charge_cards" | "unlock_charge_card" | "unlock_charge_card_card_user" | "unlock_charge_cards" | "close_charge_cards" | "revoke_invite_charge_cards" | "charge_card_express_interest" | "create_schedule" | "save_schedule" | "delete_schedule" | "ignore_schedule" | "save_task_detail" | "fetch_task_detail" | "delete_task" | "archive_task" | "create_tag" | "delete_tag" | "update_charge_card" | "update_charge_card_name" | "update_charge_card_name_card_user" | "issue_charge_card" | "issue_charge_cards" | "notification_settings_saved" | "referral_invite_sent" | "notification_mark_as_read" | "mark_as_complete_schedule" | "cancel_journal_entry" | "settings_accounting_accounts_updated" | "create_card_setup" | "confirm_card_setup" | "add_card_payment_source" | "fetch_payment_sources" | "task_assigned_toast_notification" | "task_due_tomorrow_toast_notification" | "task_deleted_toast_notification" | "task_archived_toast_notification" | "task_overdue_toast_notification" | "task_overdue_toast_notification_creator" | "task_notification_count" | "task_activities_toast_notification" | "task_created_toast_notification" | "task_group_creation_success" | "task_group_deletion_success" | "task_group_update_success" | "primary_funding_account_updated" | "task_time_spent_validation" | "missing_receipts_attachment" | "flux_analysis_unreviewed" | "flux_analysis_reviewed" | "receipt_match" | "receipts_upload" | "receipts_bulk_match" | "billing_address_view" | "express_pay_submit" | "exclude_transaction" | "reconcile" | "save_reconcile_for_later" | "recon_transaction_categorize" | "recon_transaction_match" | "update_debit_card_pin_attempt" | "set_debit_card_pin" | "ai_cfo_create_session_and_submit" | "ai_cfo_chat_session_deleted" | "charge_card_auto_pay_enable" | "charge_card_auto_pay_disable" | "treasury_setup" | "treasury_accept_term" | "treasury_transfer_money" | "treasury_transfer_money_failed" | "auto_transfer_rule_create" | "auto_transfer_rule_update" | "auto_transfer_rule_delete" | "auto_transfer_rule_pause" | "auto_transfer_rule_resume" | "treasury_update_portfolio_allocation" | "send_email_magic_link_to_user" | "complete_profile_done" | "reports_resync" | "invalid_phone_number" | "transactions_categorized_updated_failed" | "transaction_categorized_updated_failed" | "transactionsCategorized_transactionsUpdated" | "transactionsCategorized_transactionUpdated" | "transactionCategorized_transactionsUpdated" | "transactionCategorized_transactionUpdated" | "transactionsCategorized_transactionsFailed" | "transactionsCategorized_transactionFailed" | "transactionCategorized_transactionsFailed" | "transactionCategorized_transactionFailed" | "transactionsUpdated_transactionsFailed" | "transactionsUpdated_transactionFailed" | "transactionUpdated_transactionsFailed" | "transactionUpdated_transactionFailed" | "transactionsCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionsUpdated_transactionFailed" | "transactionsCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionUpdated_transactionFailed" | "transactionCategorized_transactionsUpdated_transactionFailed" | "transactionCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionUpdated_transactionFailed" | "accounting_classes_enabled_update" | "account_excluded_from_reconciliation" | "account_included_in_reconciliation" | "ai_accountant_trigger_job";
2
2
  export type SnackbarMessageSections = ReturnType<typeof toSnackbarMessageSection>;
3
3
  export declare const toSnackbarMessageSectionTexts: (v: string) => "notification" | "failed" | "success";
4
4
  export type SnackbarMessageSectionTexts = ReturnType<typeof toSnackbarMessageSectionTexts>;
@@ -207,6 +207,7 @@ const ALL_SNACKBAR_MESSAGE_SECTIONS = [
207
207
  'accounting_classes_enabled_update',
208
208
  'account_excluded_from_reconciliation',
209
209
  'account_included_in_reconciliation',
210
+ 'ai_accountant_trigger_job',
210
211
  ];
211
212
  const toSnackbarMessageSection = (v) => (0, stringToUnion_1.stringToUnion)(v, ALL_SNACKBAR_MESSAGE_SECTIONS);
212
213
  exports.toSnackbarMessageSection = toSnackbarMessageSection;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SessionManager — Central session lifecycle manager.
3
+ *
4
+ * Responsibilities:
5
+ * 1. Track user activity (mousemove, keydown, click, scroll, touch)
6
+ * 2. Send heartbeat at configurable intervals while user is active
7
+ * 3. Show warning popup after idle timeout
8
+ * 4. Auto-logout after warning countdown expires
9
+ */
10
+ import { SessionCallbacks, SessionConfig } from './sessionTypes';
11
+ export declare class SessionManager {
12
+ private config;
13
+ private callbacks;
14
+ /** Timestamps and timers */
15
+ private lastActivityTime;
16
+ private idleCheckInterval;
17
+ private heartbeatInterval;
18
+ private countdownInterval;
19
+ private activityDebounceTimer;
20
+ /** State */
21
+ private running;
22
+ private warningActive;
23
+ private secondsRemaining;
24
+ /** Bound handler for event listener cleanup. */
25
+ private boundOnActivity;
26
+ private boundOnVisibilityChange;
27
+ start(config: Partial<SessionConfig> | undefined, callbacks: SessionCallbacks): void;
28
+ stop(): void;
29
+ continueSession(): void;
30
+ isWarningActive(): boolean;
31
+ getSecondsRemaining(): number;
32
+ private onActivity;
33
+ private onVisibilityChange;
34
+ private checkIdle;
35
+ private startWarning;
36
+ private startHeartbeat;
37
+ private clearTimer;
38
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ /**
3
+ * SessionManager — Central session lifecycle manager.
4
+ *
5
+ * Responsibilities:
6
+ * 1. Track user activity (mousemove, keydown, click, scroll, touch)
7
+ * 2. Send heartbeat at configurable intervals while user is active
8
+ * 3. Show warning popup after idle timeout
9
+ * 4. Auto-logout after warning countdown expires
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SessionManager = void 0;
13
+ const sessionTypes_1 = require("./sessionTypes");
14
+ /** Events that indicate user activity. */
15
+ const ACTIVITY_EVENTS = [
16
+ 'mousemove',
17
+ 'mousedown',
18
+ 'keydown',
19
+ 'scroll',
20
+ 'touchstart',
21
+ 'click',
22
+ ];
23
+ /** Debounce interval for activity events (ms). */
24
+ const ACTIVITY_DEBOUNCE_MS = 1000;
25
+ class SessionManager {
26
+ constructor() {
27
+ this.config = sessionTypes_1.DEFAULT_SESSION_CONFIG;
28
+ this.callbacks = null;
29
+ /** Timestamps and timers */
30
+ this.lastActivityTime = 0;
31
+ this.idleCheckInterval = null;
32
+ this.heartbeatInterval = null;
33
+ this.countdownInterval = null;
34
+ this.activityDebounceTimer = null;
35
+ /** State */
36
+ this.running = false;
37
+ this.warningActive = false;
38
+ this.secondsRemaining = 0;
39
+ /** Bound handler for event listener cleanup. */
40
+ this.boundOnActivity = this.onActivity.bind(this);
41
+ this.boundOnVisibilityChange = this.onVisibilityChange.bind(this);
42
+ }
43
+ // ─── Public API ────────────────────────────────────────────
44
+ start(config = {}, callbacks) {
45
+ if (this.running) {
46
+ this.stop();
47
+ }
48
+ this.config = { ...sessionTypes_1.DEFAULT_SESSION_CONFIG, ...config };
49
+ this.callbacks = callbacks;
50
+ this.running = true;
51
+ this.warningActive = false;
52
+ this.lastActivityTime = Date.now();
53
+ if (!this.config.isAutoLogoutEnabled) {
54
+ return;
55
+ }
56
+ ACTIVITY_EVENTS.forEach((event) => {
57
+ document.addEventListener(event, this.boundOnActivity, {
58
+ capture: true,
59
+ passive: true,
60
+ });
61
+ });
62
+ document.addEventListener('visibilitychange', this.boundOnVisibilityChange);
63
+ this.startHeartbeat();
64
+ this.callbacks?.onHeartbeat();
65
+ this.idleCheckInterval = setInterval(() => this.checkIdle(), 1000);
66
+ }
67
+ stop() {
68
+ this.running = false;
69
+ this.warningActive = false;
70
+ ACTIVITY_EVENTS.forEach((event) => {
71
+ document.removeEventListener(event, this.boundOnActivity, {
72
+ capture: true,
73
+ });
74
+ });
75
+ document.removeEventListener('visibilitychange', this.boundOnVisibilityChange);
76
+ this.clearTimer('idleCheckInterval');
77
+ this.clearTimer('heartbeatInterval');
78
+ this.clearTimer('countdownInterval');
79
+ this.clearTimer('activityDebounceTimer');
80
+ }
81
+ continueSession() {
82
+ if (!this.running) {
83
+ return;
84
+ }
85
+ this.warningActive = false;
86
+ this.secondsRemaining = 0;
87
+ this.lastActivityTime = Date.now();
88
+ this.clearTimer('countdownInterval');
89
+ this.callbacks?.onHeartbeat();
90
+ this.callbacks?.onSessionExtended();
91
+ this.startHeartbeat();
92
+ }
93
+ isWarningActive() {
94
+ return this.warningActive;
95
+ }
96
+ getSecondsRemaining() {
97
+ return this.secondsRemaining;
98
+ }
99
+ // ─── Private ───────────────────────────────────────────────
100
+ onActivity() {
101
+ if (!this.running || this.warningActive) {
102
+ return;
103
+ }
104
+ if (this.activityDebounceTimer != null) {
105
+ return;
106
+ }
107
+ this.lastActivityTime = Date.now();
108
+ this.activityDebounceTimer = setTimeout(() => {
109
+ this.activityDebounceTimer = null;
110
+ }, ACTIVITY_DEBOUNCE_MS);
111
+ }
112
+ onVisibilityChange() {
113
+ if (!this.running) {
114
+ return;
115
+ }
116
+ if (document.visibilityState === 'visible') {
117
+ this.lastActivityTime = Date.now();
118
+ this.checkIdle();
119
+ }
120
+ }
121
+ checkIdle() {
122
+ if (!this.running || this.warningActive) {
123
+ return;
124
+ }
125
+ const idleMs = Date.now() - this.lastActivityTime;
126
+ const idleTimeoutMs = this.config.idleTimeoutMinutes * 60 * 1000;
127
+ if (idleMs >= idleTimeoutMs) {
128
+ this.startWarning();
129
+ }
130
+ }
131
+ startWarning() {
132
+ this.warningActive = true;
133
+ this.secondsRemaining = this.config.warningDurationSeconds;
134
+ this.clearTimer('heartbeatInterval');
135
+ this.callbacks?.onWarningStart(this.secondsRemaining);
136
+ this.countdownInterval = setInterval(() => {
137
+ this.secondsRemaining -= 1;
138
+ if (this.secondsRemaining <= 0) {
139
+ this.clearTimer('countdownInterval');
140
+ this.warningActive = false;
141
+ this.callbacks?.onAutoLogout();
142
+ }
143
+ else {
144
+ this.callbacks?.onWarningTick(this.secondsRemaining);
145
+ }
146
+ }, 1000);
147
+ }
148
+ startHeartbeat() {
149
+ this.clearTimer('heartbeatInterval');
150
+ const intervalMs = this.config.heartbeatIntervalMinutes * 60 * 1000;
151
+ this.heartbeatInterval = setInterval(() => {
152
+ if (!this.running || this.warningActive) {
153
+ return;
154
+ }
155
+ this.callbacks?.onHeartbeat();
156
+ }, intervalMs);
157
+ }
158
+ clearTimer(name) {
159
+ const timer = this[name];
160
+ if (timer != null) {
161
+ if (name === 'activityDebounceTimer') {
162
+ clearTimeout(timer);
163
+ }
164
+ else {
165
+ clearInterval(timer);
166
+ }
167
+ this[name] = null;
168
+ }
169
+ }
170
+ }
171
+ exports.SessionManager = SessionManager;
@@ -0,0 +1,16 @@
1
+ import { ActionsObservable, StateObservable } from 'redux-observable';
2
+ import { RootState } from '../../../reducer';
3
+ import { ZeniAPI } from '../../../zeniAPI';
4
+ import { sendSessionHeartbeat, sessionHeartbeatFailure, sessionHeartbeatSuccess } from '../tenantReducer';
5
+ export type ActionType = ReturnType<typeof sendSessionHeartbeat> | ReturnType<typeof sessionHeartbeatSuccess> | ReturnType<typeof sessionHeartbeatFailure>;
6
+ export declare const sessionHeartbeatEpic: (actions$: ActionsObservable<ActionType>, _: StateObservable<RootState>, zeniAPI: ZeniAPI) => import("rxjs").Observable<{
7
+ payload: {
8
+ expiresAt: string;
9
+ };
10
+ type: "tenant/sessionHeartbeatSuccess";
11
+ } | {
12
+ payload: {
13
+ error: import("../../../responsePayload").ZeniAPIStatus<Record<string, unknown>>;
14
+ };
15
+ type: "tenant/sessionHeartbeatFailure";
16
+ }>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sessionHeartbeatEpic = void 0;
4
+ const rxjs_1 = require("rxjs");
5
+ const operators_1 = require("rxjs/operators");
6
+ const responsePayload_1 = require("../../../responsePayload");
7
+ const tenantReducer_1 = require("../tenantReducer");
8
+ const sessionHeartbeatEpic = (actions$, _, zeniAPI) => actions$.pipe((0, operators_1.filter)(tenantReducer_1.sendSessionHeartbeat.match), (0, operators_1.switchMap)(() => zeniAPI
9
+ .postAndGetJSON(`${zeniAPI.apiEndPoints.authMicroServiceBaseUrl}/1.0/sessions/heartbeat`)
10
+ .pipe((0, operators_1.mergeMap)((response) => {
11
+ if ((0, responsePayload_1.isSuccessResponse)(response)) {
12
+ return (0, rxjs_1.of)((0, tenantReducer_1.sessionHeartbeatSuccess)(response.data?.expiry ?? ''));
13
+ }
14
+ return (0, rxjs_1.of)((0, tenantReducer_1.sessionHeartbeatFailure)(response.status));
15
+ }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, tenantReducer_1.sessionHeartbeatFailure)((0, responsePayload_1.createZeniAPIStatus)('Heartbeat failed', 'Session heartbeat API call errored: ' + JSON.stringify(error))))))));
16
+ exports.sessionHeartbeatEpic = sessionHeartbeatEpic;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Session management types for idle timeout and heartbeat.
3
+ */
4
+ export interface SessionConfig {
5
+ /** Minutes between heartbeat API calls while user is active. Default: 10 */
6
+ heartbeatIntervalMinutes: number;
7
+ /** Minutes of inactivity before the warning popup appears. Default: 30 */
8
+ idleTimeoutMinutes: number;
9
+ /** Whether auto-logout is enabled. Default: true */
10
+ isAutoLogoutEnabled: boolean;
11
+ /** Seconds to count down in the warning popup before auto-logout. Default: 60 */
12
+ warningDurationSeconds: number;
13
+ }
14
+ export declare const DEFAULT_SESSION_CONFIG: SessionConfig;
15
+ export interface SessionCallbacks {
16
+ /** Called when the countdown reaches zero — app should sign out. */
17
+ onAutoLogout: () => void;
18
+ /** Called to send a heartbeat API request. */
19
+ onHeartbeat: () => void;
20
+ /** Called when the session is extended (heartbeat success or "Continue Session"). */
21
+ onSessionExtended: () => void;
22
+ /** Called when the warning countdown starts. */
23
+ onWarningStart: (secondsRemaining: number) => void;
24
+ /** Called every second during the countdown. */
25
+ onWarningTick: (secondsRemaining: number) => void;
26
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /**
3
+ * Session management types for idle timeout and heartbeat.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_SESSION_CONFIG = void 0;
7
+ exports.DEFAULT_SESSION_CONFIG = {
8
+ heartbeatIntervalMinutes: 10,
9
+ idleTimeoutMinutes: 30,
10
+ isAutoLogoutEnabled: true,
11
+ warningDurationSeconds: 60,
12
+ };
@@ -51,7 +51,11 @@ export declare const updateTenants: import("@reduxjs/toolkit").ActionCreatorWith
51
51
  accountLockTime?: string;
52
52
  accountUnlockTime?: string;
53
53
  isAccountLocked?: boolean;
54
- }, "tenant/sendEmailMagicLinkToUserFailure">, updateSignInState: import("@reduxjs/toolkit").ActionCreatorWithPayload<FetchState, "tenant/updateSignInState">, doSignOut: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"tenant/doSignOut">, signOutSuccess: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"tenant/signOutSuccess">, updateLoggedInUser: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[userPayload: LoggedInUserPayload, zeniSessionId: string, authProvider: AuthProvider], {
54
+ }, "tenant/sendEmailMagicLinkToUserFailure">, updateSignInState: import("@reduxjs/toolkit").ActionCreatorWithPayload<FetchState, "tenant/updateSignInState">, doSignOut: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"tenant/doSignOut">, signOutSuccess: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"tenant/signOutSuccess">, sendSessionHeartbeat: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"tenant/sendSessionHeartbeat">, sessionHeartbeatSuccess: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[expiresAt: string], {
55
+ expiresAt: string;
56
+ }, "tenant/sessionHeartbeatSuccess", never, never>, sessionHeartbeatFailure: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[error: ZeniAPIStatus<Record<string, unknown>>], {
57
+ error: ZeniAPIStatus<Record<string, unknown>>;
58
+ }, "tenant/sessionHeartbeatFailure", never, never>, updateLoggedInUser: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[userPayload: LoggedInUserPayload, zeniSessionId: string, authProvider: AuthProvider], {
55
59
  userPayload: LoggedInUserPayload;
56
60
  zeniSessionId: string;
57
61
  authProvider: AuthProvider;
@@ -4,7 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  var _a;
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.toConnection = exports.toRoleResource = exports.resendVerifyDeviceOTPFailure = exports.resendVerifyDeviceOTPSuccess = exports.resendVerifyDeviceOTP = exports.verifyDeviceWithTwoFAFailure = exports.verifyDeviceWithTwoFASuccess = exports.verifyDeviceWithTwoFA = exports.updateTenantMasterTOSInfo = exports.updateTenantAccountingClassesEnabled = exports.trigger2FA = exports.resetSignInState = exports.updateTreasuryVideoViewedForLoggedInUser = exports.updateReferViewedForLoggedInUser = exports.updateTenantReimbursementInfo = exports.removeOnboardingTenant = exports.updateOnboardingTenants = exports.updateSubscriptionSummaryForTenantFailure = exports.updateSubscriptionSummaryForTenantSuccess = exports.fetchSubscriptionSummaryForTenant = exports.clearAll = exports.fetchExternalConnectionsSuccess = exports.fetchExternalConnectionsFailure = exports.saveExternalConnectionFailure = exports.saveExternalConnectionSuccess = exports.saveExternalConnection = exports.fetchExternalConnections = exports.updateLoggedInUser = exports.signOutSuccess = exports.doSignOut = exports.updateSignInState = exports.sendEmailMagicLinkToUserFailure = exports.sendEmailMagicLinkToUserSuccess = exports.sendEmailMagicLinkToUser = exports.magicLinkSignInFailure = exports.magicLinkSignInSuccess = exports.doMagicLinkSignIn = exports.doSignIn = exports.updateExcludedResourcesFailure = exports.updateExcludedResourcesSuccess = exports.fetchExcludedResources = exports.updateCurrentTenant = exports.updateTenantFailure = exports.updateTenantSuccess = exports.fetchActiveTenant = exports.updateTenantsFailure = exports.updateTenantsSuccess = exports.fetchAllTenants = exports.updateTenants = exports.initialState = void 0;
7
+ exports.resendVerifyDeviceOTPSuccess = exports.resendVerifyDeviceOTP = exports.verifyDeviceWithTwoFAFailure = exports.verifyDeviceWithTwoFASuccess = exports.verifyDeviceWithTwoFA = exports.updateTenantMasterTOSInfo = exports.updateTenantAccountingClassesEnabled = exports.trigger2FA = exports.resetSignInState = exports.updateTreasuryVideoViewedForLoggedInUser = exports.updateReferViewedForLoggedInUser = exports.updateTenantReimbursementInfo = exports.removeOnboardingTenant = exports.updateOnboardingTenants = exports.updateSubscriptionSummaryForTenantFailure = exports.updateSubscriptionSummaryForTenantSuccess = exports.fetchSubscriptionSummaryForTenant = exports.clearAll = exports.fetchExternalConnectionsSuccess = exports.fetchExternalConnectionsFailure = exports.saveExternalConnectionFailure = exports.saveExternalConnectionSuccess = exports.saveExternalConnection = exports.fetchExternalConnections = exports.updateLoggedInUser = exports.sessionHeartbeatFailure = exports.sessionHeartbeatSuccess = exports.sendSessionHeartbeat = exports.signOutSuccess = exports.doSignOut = exports.updateSignInState = exports.sendEmailMagicLinkToUserFailure = exports.sendEmailMagicLinkToUserSuccess = exports.sendEmailMagicLinkToUser = exports.magicLinkSignInFailure = exports.magicLinkSignInSuccess = exports.doMagicLinkSignIn = exports.doSignIn = exports.updateExcludedResourcesFailure = exports.updateExcludedResourcesSuccess = exports.fetchExcludedResources = exports.updateCurrentTenant = exports.updateTenantFailure = exports.updateTenantSuccess = exports.fetchActiveTenant = exports.updateTenantsFailure = exports.updateTenantsSuccess = exports.fetchAllTenants = exports.updateTenants = exports.initialState = void 0;
8
+ exports.toConnection = exports.toRoleResource = exports.resendVerifyDeviceOTPFailure = void 0;
8
9
  exports.mapConnectionsPayloadToState = mapConnectionsPayloadToState;
9
10
  const toolkit_1 = require("@reduxjs/toolkit");
10
11
  const js_base64_1 = require("js-base64");
@@ -436,6 +437,26 @@ const tenant = (0, toolkit_1.createSlice)({
436
437
  signOutSuccess(draft) {
437
438
  draft.loggedInUser = undefined;
438
439
  draft.currentTenantId = undefined;
440
+ draft.sessionExpiresAt = undefined;
441
+ },
442
+ sendSessionHeartbeat() {
443
+ // No state change needed — epic handles the API call
444
+ },
445
+ sessionHeartbeatSuccess: {
446
+ prepare(expiresAt) {
447
+ return { payload: { expiresAt } };
448
+ },
449
+ reducer(draft, action) {
450
+ draft.sessionExpiresAt = action.payload.expiresAt;
451
+ },
452
+ },
453
+ sessionHeartbeatFailure: {
454
+ prepare(error) {
455
+ return { payload: { error } };
456
+ },
457
+ reducer(draft, action) {
458
+ draft.error = action.payload.error;
459
+ },
439
460
  },
440
461
  fetchSubscriptionSummaryForTenant: {
441
462
  prepare(tenantId) {
@@ -543,7 +564,7 @@ const tenant = (0, toolkit_1.createSlice)({
543
564
  },
544
565
  },
545
566
  });
546
- _a = tenant.actions, exports.updateTenants = _a.updateTenants, exports.fetchAllTenants = _a.fetchAllTenants, exports.updateTenantsSuccess = _a.updateTenantsSuccess, exports.updateTenantsFailure = _a.updateTenantsFailure, exports.fetchActiveTenant = _a.fetchActiveTenant, exports.updateTenantSuccess = _a.updateTenantSuccess, exports.updateTenantFailure = _a.updateTenantFailure, exports.updateCurrentTenant = _a.updateCurrentTenant, exports.fetchExcludedResources = _a.fetchExcludedResources, exports.updateExcludedResourcesSuccess = _a.updateExcludedResourcesSuccess, exports.updateExcludedResourcesFailure = _a.updateExcludedResourcesFailure, exports.doSignIn = _a.doSignIn, exports.doMagicLinkSignIn = _a.doMagicLinkSignIn, exports.magicLinkSignInSuccess = _a.magicLinkSignInSuccess, exports.magicLinkSignInFailure = _a.magicLinkSignInFailure, exports.sendEmailMagicLinkToUser = _a.sendEmailMagicLinkToUser, exports.sendEmailMagicLinkToUserSuccess = _a.sendEmailMagicLinkToUserSuccess, exports.sendEmailMagicLinkToUserFailure = _a.sendEmailMagicLinkToUserFailure, exports.updateSignInState = _a.updateSignInState, exports.doSignOut = _a.doSignOut, exports.signOutSuccess = _a.signOutSuccess, exports.updateLoggedInUser = _a.updateLoggedInUser, exports.fetchExternalConnections = _a.fetchExternalConnections, exports.saveExternalConnection = _a.saveExternalConnection, exports.saveExternalConnectionSuccess = _a.saveExternalConnectionSuccess, exports.saveExternalConnectionFailure = _a.saveExternalConnectionFailure, exports.fetchExternalConnectionsFailure = _a.fetchExternalConnectionsFailure, exports.fetchExternalConnectionsSuccess = _a.fetchExternalConnectionsSuccess, exports.clearAll = _a.clearAll, exports.fetchSubscriptionSummaryForTenant = _a.fetchSubscriptionSummaryForTenant, exports.updateSubscriptionSummaryForTenantSuccess = _a.updateSubscriptionSummaryForTenantSuccess, exports.updateSubscriptionSummaryForTenantFailure = _a.updateSubscriptionSummaryForTenantFailure, exports.updateOnboardingTenants = _a.updateOnboardingTenants, exports.removeOnboardingTenant = _a.removeOnboardingTenant, exports.updateTenantReimbursementInfo = _a.updateTenantReimbursementInfo, exports.updateReferViewedForLoggedInUser = _a.updateReferViewedForLoggedInUser, exports.updateTreasuryVideoViewedForLoggedInUser = _a.updateTreasuryVideoViewedForLoggedInUser, exports.resetSignInState = _a.resetSignInState, exports.trigger2FA = _a.trigger2FA, exports.updateTenantAccountingClassesEnabled = _a.updateTenantAccountingClassesEnabled, exports.updateTenantMasterTOSInfo = _a.updateTenantMasterTOSInfo, exports.verifyDeviceWithTwoFA = _a.verifyDeviceWithTwoFA, exports.verifyDeviceWithTwoFASuccess = _a.verifyDeviceWithTwoFASuccess, exports.verifyDeviceWithTwoFAFailure = _a.verifyDeviceWithTwoFAFailure, exports.resendVerifyDeviceOTP = _a.resendVerifyDeviceOTP, exports.resendVerifyDeviceOTPSuccess = _a.resendVerifyDeviceOTPSuccess, exports.resendVerifyDeviceOTPFailure = _a.resendVerifyDeviceOTPFailure;
567
+ _a = tenant.actions, exports.updateTenants = _a.updateTenants, exports.fetchAllTenants = _a.fetchAllTenants, exports.updateTenantsSuccess = _a.updateTenantsSuccess, exports.updateTenantsFailure = _a.updateTenantsFailure, exports.fetchActiveTenant = _a.fetchActiveTenant, exports.updateTenantSuccess = _a.updateTenantSuccess, exports.updateTenantFailure = _a.updateTenantFailure, exports.updateCurrentTenant = _a.updateCurrentTenant, exports.fetchExcludedResources = _a.fetchExcludedResources, exports.updateExcludedResourcesSuccess = _a.updateExcludedResourcesSuccess, exports.updateExcludedResourcesFailure = _a.updateExcludedResourcesFailure, exports.doSignIn = _a.doSignIn, exports.doMagicLinkSignIn = _a.doMagicLinkSignIn, exports.magicLinkSignInSuccess = _a.magicLinkSignInSuccess, exports.magicLinkSignInFailure = _a.magicLinkSignInFailure, exports.sendEmailMagicLinkToUser = _a.sendEmailMagicLinkToUser, exports.sendEmailMagicLinkToUserSuccess = _a.sendEmailMagicLinkToUserSuccess, exports.sendEmailMagicLinkToUserFailure = _a.sendEmailMagicLinkToUserFailure, exports.updateSignInState = _a.updateSignInState, exports.doSignOut = _a.doSignOut, exports.signOutSuccess = _a.signOutSuccess, exports.sendSessionHeartbeat = _a.sendSessionHeartbeat, exports.sessionHeartbeatSuccess = _a.sessionHeartbeatSuccess, exports.sessionHeartbeatFailure = _a.sessionHeartbeatFailure, exports.updateLoggedInUser = _a.updateLoggedInUser, exports.fetchExternalConnections = _a.fetchExternalConnections, exports.saveExternalConnection = _a.saveExternalConnection, exports.saveExternalConnectionSuccess = _a.saveExternalConnectionSuccess, exports.saveExternalConnectionFailure = _a.saveExternalConnectionFailure, exports.fetchExternalConnectionsFailure = _a.fetchExternalConnectionsFailure, exports.fetchExternalConnectionsSuccess = _a.fetchExternalConnectionsSuccess, exports.clearAll = _a.clearAll, exports.fetchSubscriptionSummaryForTenant = _a.fetchSubscriptionSummaryForTenant, exports.updateSubscriptionSummaryForTenantSuccess = _a.updateSubscriptionSummaryForTenantSuccess, exports.updateSubscriptionSummaryForTenantFailure = _a.updateSubscriptionSummaryForTenantFailure, exports.updateOnboardingTenants = _a.updateOnboardingTenants, exports.removeOnboardingTenant = _a.removeOnboardingTenant, exports.updateTenantReimbursementInfo = _a.updateTenantReimbursementInfo, exports.updateReferViewedForLoggedInUser = _a.updateReferViewedForLoggedInUser, exports.updateTreasuryVideoViewedForLoggedInUser = _a.updateTreasuryVideoViewedForLoggedInUser, exports.resetSignInState = _a.resetSignInState, exports.trigger2FA = _a.trigger2FA, exports.updateTenantAccountingClassesEnabled = _a.updateTenantAccountingClassesEnabled, exports.updateTenantMasterTOSInfo = _a.updateTenantMasterTOSInfo, exports.verifyDeviceWithTwoFA = _a.verifyDeviceWithTwoFA, exports.verifyDeviceWithTwoFASuccess = _a.verifyDeviceWithTwoFASuccess, exports.verifyDeviceWithTwoFAFailure = _a.verifyDeviceWithTwoFAFailure, exports.resendVerifyDeviceOTP = _a.resendVerifyDeviceOTP, exports.resendVerifyDeviceOTPSuccess = _a.resendVerifyDeviceOTPSuccess, exports.resendVerifyDeviceOTPFailure = _a.resendVerifyDeviceOTPFailure;
547
568
  exports.default = tenant.reducer;
548
569
  /**
549
570
  * Converts tenants payload to Tenant and User State
@@ -45,6 +45,7 @@ export interface TenantState extends FetchedState {
45
45
  magicLinkCodeVerifier?: string;
46
46
  magicLinkEmail?: string;
47
47
  otpChallenge?: string;
48
+ sessionExpiresAt?: string;
48
49
  twoFAPhoneNumber?: string;
49
50
  }
50
51
  export interface TenantProductSettings {
@@ -22,6 +22,7 @@ import { resendVerifyDeviceOTPEpic } from './entity/tenant/epic/resendVerifyDevi
22
22
  import { saveExternalConnectionEpic } from './entity/tenant/epic/saveExternalConnectionEpic';
23
23
  import { sendEmailMagicLinkToUserEpic } from './entity/tenant/epic/sendEmailMagicLinkToUserEpic';
24
24
  import { doSignInEpic } from './entity/tenant/epic/signInUserEpic';
25
+ import { sessionHeartbeatEpic } from './entity/tenant/epic/sessionHeartbeatEpic';
25
26
  import { doSignOutEpic } from './entity/tenant/epic/signOutUserEpic';
26
27
  import { verifyDeviceWithTwoFAEpic } from './entity/tenant/epic/verifyDeviceWithTwoFAEpic';
27
28
  // ── Toast Notification Epic ──────────────────────────────────────────
@@ -46,7 +47,7 @@ import { verifyOtpEpic } from './view/twoFactorAuthentication/verifyOtpEpic';
46
47
  // RootActionType is only available in the dynamically imported epic.ts.
47
48
  const coreRootEpic = combineEpics(
48
49
  // Auth & tenant
49
- clearAllEpic, doMagicLinkSignInEpic, doSignInEpic, doSignOutEpic, fetchActiveTenantEpic, fetchAllTenantsEpic, fetchExcludedResourcesEpic, fetchExternalConnectionsEpic, fetchSubscriptionSummaryForTenantEpic, resendVerifyDeviceOTPEpic, saveExternalConnectionEpic, sendEmailMagicLinkToUserEpic, verifyDeviceWithTwoFAEpic,
50
+ clearAllEpic, doMagicLinkSignInEpic, doSignInEpic, doSignOutEpic, sessionHeartbeatEpic, fetchActiveTenantEpic, fetchAllTenantsEpic, fetchExcludedResourcesEpic, fetchExternalConnectionsEpic, fetchSubscriptionSummaryForTenantEpic, resendVerifyDeviceOTPEpic, saveExternalConnectionEpic, sendEmailMagicLinkToUserEpic, verifyDeviceWithTwoFAEpic,
50
51
  // Toast
51
52
  pushToastNotificationEpic,
52
53
  // Collaboration
@@ -204,6 +204,7 @@ const ALL_SNACKBAR_MESSAGE_SECTIONS = [
204
204
  'accounting_classes_enabled_update',
205
205
  'account_excluded_from_reconciliation',
206
206
  'account_included_in_reconciliation',
207
+ 'ai_accountant_trigger_job',
207
208
  ];
208
209
  export const toSnackbarMessageSection = (v) => stringToUnion(v, ALL_SNACKBAR_MESSAGE_SECTIONS);
209
210
  // Keeping this different from the message type, since we may not have messages in all instances
@@ -0,0 +1,167 @@
1
+ /**
2
+ * SessionManager — Central session lifecycle manager.
3
+ *
4
+ * Responsibilities:
5
+ * 1. Track user activity (mousemove, keydown, click, scroll, touch)
6
+ * 2. Send heartbeat at configurable intervals while user is active
7
+ * 3. Show warning popup after idle timeout
8
+ * 4. Auto-logout after warning countdown expires
9
+ */
10
+ import { DEFAULT_SESSION_CONFIG, } from './sessionTypes';
11
+ /** Events that indicate user activity. */
12
+ const ACTIVITY_EVENTS = [
13
+ 'mousemove',
14
+ 'mousedown',
15
+ 'keydown',
16
+ 'scroll',
17
+ 'touchstart',
18
+ 'click',
19
+ ];
20
+ /** Debounce interval for activity events (ms). */
21
+ const ACTIVITY_DEBOUNCE_MS = 1000;
22
+ export class SessionManager {
23
+ constructor() {
24
+ this.config = DEFAULT_SESSION_CONFIG;
25
+ this.callbacks = null;
26
+ /** Timestamps and timers */
27
+ this.lastActivityTime = 0;
28
+ this.idleCheckInterval = null;
29
+ this.heartbeatInterval = null;
30
+ this.countdownInterval = null;
31
+ this.activityDebounceTimer = null;
32
+ /** State */
33
+ this.running = false;
34
+ this.warningActive = false;
35
+ this.secondsRemaining = 0;
36
+ /** Bound handler for event listener cleanup. */
37
+ this.boundOnActivity = this.onActivity.bind(this);
38
+ this.boundOnVisibilityChange = this.onVisibilityChange.bind(this);
39
+ }
40
+ // ─── Public API ────────────────────────────────────────────
41
+ start(config = {}, callbacks) {
42
+ if (this.running) {
43
+ this.stop();
44
+ }
45
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
46
+ this.callbacks = callbacks;
47
+ this.running = true;
48
+ this.warningActive = false;
49
+ this.lastActivityTime = Date.now();
50
+ if (!this.config.isAutoLogoutEnabled) {
51
+ return;
52
+ }
53
+ ACTIVITY_EVENTS.forEach((event) => {
54
+ document.addEventListener(event, this.boundOnActivity, {
55
+ capture: true,
56
+ passive: true,
57
+ });
58
+ });
59
+ document.addEventListener('visibilitychange', this.boundOnVisibilityChange);
60
+ this.startHeartbeat();
61
+ this.callbacks?.onHeartbeat();
62
+ this.idleCheckInterval = setInterval(() => this.checkIdle(), 1000);
63
+ }
64
+ stop() {
65
+ this.running = false;
66
+ this.warningActive = false;
67
+ ACTIVITY_EVENTS.forEach((event) => {
68
+ document.removeEventListener(event, this.boundOnActivity, {
69
+ capture: true,
70
+ });
71
+ });
72
+ document.removeEventListener('visibilitychange', this.boundOnVisibilityChange);
73
+ this.clearTimer('idleCheckInterval');
74
+ this.clearTimer('heartbeatInterval');
75
+ this.clearTimer('countdownInterval');
76
+ this.clearTimer('activityDebounceTimer');
77
+ }
78
+ continueSession() {
79
+ if (!this.running) {
80
+ return;
81
+ }
82
+ this.warningActive = false;
83
+ this.secondsRemaining = 0;
84
+ this.lastActivityTime = Date.now();
85
+ this.clearTimer('countdownInterval');
86
+ this.callbacks?.onHeartbeat();
87
+ this.callbacks?.onSessionExtended();
88
+ this.startHeartbeat();
89
+ }
90
+ isWarningActive() {
91
+ return this.warningActive;
92
+ }
93
+ getSecondsRemaining() {
94
+ return this.secondsRemaining;
95
+ }
96
+ // ─── Private ───────────────────────────────────────────────
97
+ onActivity() {
98
+ if (!this.running || this.warningActive) {
99
+ return;
100
+ }
101
+ if (this.activityDebounceTimer != null) {
102
+ return;
103
+ }
104
+ this.lastActivityTime = Date.now();
105
+ this.activityDebounceTimer = setTimeout(() => {
106
+ this.activityDebounceTimer = null;
107
+ }, ACTIVITY_DEBOUNCE_MS);
108
+ }
109
+ onVisibilityChange() {
110
+ if (!this.running) {
111
+ return;
112
+ }
113
+ if (document.visibilityState === 'visible') {
114
+ this.lastActivityTime = Date.now();
115
+ this.checkIdle();
116
+ }
117
+ }
118
+ checkIdle() {
119
+ if (!this.running || this.warningActive) {
120
+ return;
121
+ }
122
+ const idleMs = Date.now() - this.lastActivityTime;
123
+ const idleTimeoutMs = this.config.idleTimeoutMinutes * 60 * 1000;
124
+ if (idleMs >= idleTimeoutMs) {
125
+ this.startWarning();
126
+ }
127
+ }
128
+ startWarning() {
129
+ this.warningActive = true;
130
+ this.secondsRemaining = this.config.warningDurationSeconds;
131
+ this.clearTimer('heartbeatInterval');
132
+ this.callbacks?.onWarningStart(this.secondsRemaining);
133
+ this.countdownInterval = setInterval(() => {
134
+ this.secondsRemaining -= 1;
135
+ if (this.secondsRemaining <= 0) {
136
+ this.clearTimer('countdownInterval');
137
+ this.warningActive = false;
138
+ this.callbacks?.onAutoLogout();
139
+ }
140
+ else {
141
+ this.callbacks?.onWarningTick(this.secondsRemaining);
142
+ }
143
+ }, 1000);
144
+ }
145
+ startHeartbeat() {
146
+ this.clearTimer('heartbeatInterval');
147
+ const intervalMs = this.config.heartbeatIntervalMinutes * 60 * 1000;
148
+ this.heartbeatInterval = setInterval(() => {
149
+ if (!this.running || this.warningActive) {
150
+ return;
151
+ }
152
+ this.callbacks?.onHeartbeat();
153
+ }, intervalMs);
154
+ }
155
+ clearTimer(name) {
156
+ const timer = this[name];
157
+ if (timer != null) {
158
+ if (name === 'activityDebounceTimer') {
159
+ clearTimeout(timer);
160
+ }
161
+ else {
162
+ clearInterval(timer);
163
+ }
164
+ this[name] = null;
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,12 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
4
+ import { sendSessionHeartbeat, sessionHeartbeatFailure, sessionHeartbeatSuccess, } from '../tenantReducer';
5
+ export const sessionHeartbeatEpic = (actions$, _, zeniAPI) => actions$.pipe(filter(sendSessionHeartbeat.match), switchMap(() => zeniAPI
6
+ .postAndGetJSON(`${zeniAPI.apiEndPoints.authMicroServiceBaseUrl}/1.0/sessions/heartbeat`)
7
+ .pipe(mergeMap((response) => {
8
+ if (isSuccessResponse(response)) {
9
+ return of(sessionHeartbeatSuccess(response.data?.expiry ?? ''));
10
+ }
11
+ return of(sessionHeartbeatFailure(response.status));
12
+ }), catchError((error) => of(sessionHeartbeatFailure(createZeniAPIStatus('Heartbeat failed', 'Session heartbeat API call errored: ' + JSON.stringify(error))))))));