@tellescope/sdk 1.256.16 → 2.0.0-beta.1
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/CLAUDE.md +5 -0
- package/env.example +39 -0
- package/lib/cjs/enduser.d.ts +1 -0
- package/lib/cjs/enduser.d.ts.map +1 -1
- package/lib/cjs/sdk.d.ts +6 -1
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +2 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/session.d.ts +1 -0
- package/lib/cjs/session.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/active_patient_count.test.js +1 -1
- package/lib/cjs/tests/api_tests/active_patient_count.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/appointment_rescheduled_trigger.test.js +1 -1
- package/lib/cjs/tests/api_tests/appointment_rescheduled_trigger.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_limits.test.js +5 -5
- package/lib/cjs/tests/api_tests/calendar_event_limits.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_references_update.test.js +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_references_update.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js +1 -1
- package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +21 -0
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +215 -0
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js +6 -6
- package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js +3 -3
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +32 -0
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +257 -0
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/managed_content_enduser_access.test.js +1 -1
- package/lib/cjs/tests/api_tests/managed_content_enduser_access.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/managed_content_file_access.test.js +1 -1
- package/lib/cjs/tests/api_tests/managed_content_file_access.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js +139 -9
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_webhooks.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_webhooks.test.js +21 -8
- package/lib/cjs/tests/api_tests/mdi_webhooks.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/nutrition_tracking.test.js +1 -1
- package/lib/cjs/tests/api_tests/nutrition_tracking.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +1 -1
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/programs.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/programs.test.js +370 -0
- package/lib/cjs/tests/api_tests/programs.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/programs_trigger_filters.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/programs_trigger_filters.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/programs_trigger_filters.test.js +295 -0
- package/lib/cjs/tests/api_tests/programs_trigger_filters.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +55 -47
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +42 -0
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +484 -0
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.js +1 -1
- package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.js +2 -2
- package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +32 -0
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +297 -0
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.js +4 -1
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/templated_message_date_fields.test.d.ts +3 -0
- package/lib/cjs/tests/api_tests/templated_message_date_fields.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_date_fields.test.js +202 -0
- package/lib/cjs/tests/api_tests/templated_message_date_fields.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.d.ts +3 -0
- package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.js +233 -0
- package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_links.test.d.ts +3 -0
- package/lib/cjs/tests/api_tests/templated_message_links.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/templated_message_links.test.js +235 -0
- package/lib/cjs/tests/api_tests/templated_message_links.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/time_tracks.test.js +1 -1
- package/lib/cjs/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +395 -0
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +471 -0
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/user_portal_settings.test.js +1 -1
- package/lib/cjs/tests/api_tests/user_portal_settings.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/validation_behavior_locks.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/validation_behavior_locks.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/validation_behavior_locks.test.js +233 -0
- package/lib/cjs/tests/api_tests/validation_behavior_locks.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +569 -0
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -0
- package/lib/cjs/tests/public_endpoint_tests.js +3 -2
- package/lib/cjs/tests/public_endpoint_tests.js.map +1 -1
- package/lib/cjs/tests/setup.d.ts +16 -0
- package/lib/cjs/tests/setup.d.ts.map +1 -1
- package/lib/cjs/tests/setup.js +55 -8
- package/lib/cjs/tests/setup.js.map +1 -1
- package/lib/cjs/tests/setup_isolated.d.ts +9 -0
- package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
- package/lib/cjs/tests/setup_isolated.js +23 -2
- package/lib/cjs/tests/setup_isolated.js.map +1 -1
- package/lib/cjs/tests/socket_tests.d.ts.map +1 -1
- package/lib/cjs/tests/socket_tests.js +3 -2
- package/lib/cjs/tests/socket_tests.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +244 -216
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/cjs/tests/unit_tests/trigger_title_partials.test.d.ts +3 -0
- package/lib/cjs/tests/unit_tests/trigger_title_partials.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/trigger_title_partials.test.js +118 -0
- package/lib/cjs/tests/unit_tests/trigger_title_partials.test.js.map +1 -0
- package/lib/cjs/tests/webhooks_tests.js +3 -2
- package/lib/cjs/tests/webhooks_tests.js.map +1 -1
- package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/esm/enduser.d.ts +2 -0
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/sdk.d.ts +9 -3
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +2 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/session.d.ts +2 -0
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/active_patient_count.test.js +1 -1
- package/lib/esm/tests/api_tests/active_patient_count.test.js.map +1 -1
- package/lib/esm/tests/api_tests/appointment_rescheduled_trigger.test.js +1 -1
- package/lib/esm/tests/api_tests/appointment_rescheduled_trigger.test.js.map +1 -1
- package/lib/esm/tests/api_tests/calendar_event_limits.test.js +5 -5
- package/lib/esm/tests/api_tests/calendar_event_limits.test.js.map +1 -1
- package/lib/esm/tests/api_tests/calendar_event_references_update.test.js +1 -1
- package/lib/esm/tests/api_tests/calendar_event_references_update.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js +1 -1
- package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +21 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +210 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js +7 -7
- package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js +4 -4
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js.map +1 -1
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +32 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.js +252 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -0
- package/lib/esm/tests/api_tests/managed_content_enduser_access.test.js +1 -1
- package/lib/esm/tests/api_tests/managed_content_enduser_access.test.js.map +1 -1
- package/lib/esm/tests/api_tests/managed_content_file_access.test.js +1 -1
- package/lib/esm/tests/api_tests/managed_content_file_access.test.js.map +1 -1
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.js +139 -9
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.js.map +1 -1
- package/lib/esm/tests/api_tests/mdi_webhooks.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/mdi_webhooks.test.js +21 -8
- package/lib/esm/tests/api_tests/mdi_webhooks.test.js.map +1 -1
- package/lib/esm/tests/api_tests/nutrition_tracking.test.js +1 -1
- package/lib/esm/tests/api_tests/nutrition_tracking.test.js.map +1 -1
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js +1 -1
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/esm/tests/api_tests/programs.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/programs.test.js +365 -0
- package/lib/esm/tests/api_tests/programs.test.js.map +1 -0
- package/lib/esm/tests/api_tests/programs_trigger_filters.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/programs_trigger_filters.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/programs_trigger_filters.test.js +290 -0
- package/lib/esm/tests/api_tests/programs_trigger_filters.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +55 -47
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.js +1 -1
- package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +42 -0
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +479 -0
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.js +1 -1
- package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +1 -1
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/enduser-cross-create.test.js +1 -1
- package/lib/esm/tests/api_tests/security/enduser-cross-create.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.js +1 -1
- package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.js +3 -3
- package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +32 -0
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +292 -0
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.js +4 -1
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.js.map +1 -1
- package/lib/esm/tests/api_tests/templated_message_date_fields.test.d.ts +3 -0
- package/lib/esm/tests/api_tests/templated_message_date_fields.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_date_fields.test.js +198 -0
- package/lib/esm/tests/api_tests/templated_message_date_fields.test.js.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_link_in_href.test.d.ts +3 -0
- package/lib/esm/tests/api_tests/templated_message_link_in_href.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_link_in_href.test.js +229 -0
- package/lib/esm/tests/api_tests/templated_message_link_in_href.test.js.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_links.test.d.ts +3 -0
- package/lib/esm/tests/api_tests/templated_message_links.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/templated_message_links.test.js +231 -0
- package/lib/esm/tests/api_tests/templated_message_links.test.js.map +1 -0
- package/lib/esm/tests/api_tests/time_tracks.test.js +1 -1
- package/lib/esm/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +390 -0
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +466 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -0
- package/lib/esm/tests/api_tests/user_portal_settings.test.js +1 -1
- package/lib/esm/tests/api_tests/user_portal_settings.test.js.map +1 -1
- package/lib/esm/tests/api_tests/validation_behavior_locks.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/validation_behavior_locks.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/validation_behavior_locks.test.js +229 -0
- package/lib/esm/tests/api_tests/validation_behavior_locks.test.js.map +1 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +564 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -0
- package/lib/esm/tests/public_endpoint_tests.js +3 -2
- package/lib/esm/tests/public_endpoint_tests.js.map +1 -1
- package/lib/esm/tests/setup.d.ts +16 -0
- package/lib/esm/tests/setup.d.ts.map +1 -1
- package/lib/esm/tests/setup.js +53 -7
- package/lib/esm/tests/setup.js.map +1 -1
- package/lib/esm/tests/setup_isolated.d.ts +9 -0
- package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
- package/lib/esm/tests/setup_isolated.js +21 -1
- package/lib/esm/tests/setup_isolated.js.map +1 -1
- package/lib/esm/tests/socket_tests.d.ts.map +1 -1
- package/lib/esm/tests/socket_tests.js +3 -2
- package/lib/esm/tests/socket_tests.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +244 -216
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/esm/tests/unit_tests/trigger_title_partials.test.d.ts +3 -0
- package/lib/esm/tests/unit_tests/trigger_title_partials.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/trigger_title_partials.test.js +115 -0
- package/lib/esm/tests/unit_tests/trigger_title_partials.test.js.map +1 -0
- package/lib/esm/tests/webhooks_tests.js +3 -2
- package/lib/esm/tests/webhooks_tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -19
- package/src/sdk.ts +11 -1
- package/src/tests/api_tests/active_patient_count.test.ts +1 -1
- package/src/tests/api_tests/appointment_rescheduled_trigger.test.ts +1 -1
- package/src/tests/api_tests/calendar_event_references_update.test.ts +1 -1
- package/src/tests/api_tests/enduser_cross_access_isolation.test.ts +1 -1
- package/src/tests/api_tests/enduser_fields_sync.test.ts +150 -0
- package/src/tests/api_tests/enduser_login_rate_limits.test.ts +7 -7
- package/src/tests/api_tests/enduser_session_invalidation.test.ts +4 -4
- package/src/tests/api_tests/load_threads_care_team.test.ts +210 -0
- package/src/tests/api_tests/managed_content_enduser_access.test.ts +1 -1
- package/src/tests/api_tests/managed_content_file_access.test.ts +1 -1
- package/src/tests/api_tests/mdi_case_offerings.test.ts +138 -0
- package/src/tests/api_tests/mdi_webhooks.test.ts +9 -0
- package/src/tests/api_tests/nutrition_tracking.test.ts +1 -1
- package/src/tests/api_tests/openloop_webhooks.test.ts +1 -1
- package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +33 -26
- package/src/tests/api_tests/security/allowed-paths-confinement.test.ts +1 -1
- package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +432 -0
- package/src/tests/api_tests/security/booking-info-idor-token-mint.test.ts +1 -1
- package/src/tests/api_tests/security/enduser-ai-summary-trust-gate.test.ts +1 -1
- package/src/tests/api_tests/security/enduser-cross-create.test.ts +1 -1
- package/src/tests/api_tests/security/enduser-write-restrictions.test.ts +1 -1
- package/src/tests/api_tests/security/invite-user-enumeration.test.ts +3 -3
- package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +197 -0
- package/src/tests/api_tests/security/sanitize-user-html-with-iframes.test.ts +4 -1
- package/src/tests/api_tests/templated_message_date_fields.test.ts +146 -0
- package/src/tests/api_tests/templated_message_links.test.ts +192 -0
- package/src/tests/api_tests/time_tracks.test.ts +1 -1
- package/src/tests/api_tests/trigger_contains_matching.test.ts +345 -0
- package/src/tests/api_tests/user_logs_restore_deleted.test.ts +415 -0
- package/src/tests/api_tests/user_portal_settings.test.ts +1 -1
- package/src/tests/api_tests/validation_behavior_locks.test.ts +204 -0
- package/src/tests/public_endpoint_tests.ts +3 -2
- package/src/tests/setup.ts +43 -7
- package/src/tests/setup_isolated.ts +10 -0
- package/src/tests/socket_tests.ts +3 -2
- package/src/tests/tests.ts +22 -2
- package/src/tests/unit_tests/trigger_title_partials.test.ts +174 -0
- package/src/tests/webhooks_tests.ts +4 -2
- package/test_generated.pdf +0 -0
- package/tsconfig.cjs.json +17 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assert_checks,
|
|
5
|
+
Check,
|
|
6
|
+
setup_isolated_tenant,
|
|
7
|
+
} from "../../setup_isolated"
|
|
8
|
+
import { EnduserSession } from "../../../sdk"
|
|
9
|
+
|
|
10
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
11
|
+
|
|
12
|
+
const RAND = () => Math.random().toString(36).slice(2, 10)
|
|
13
|
+
|
|
14
|
+
const userAttendeeIds = (event: any): string[] => (event?.attendees ?? []).filter((a: any) => a.type === 'user').map((a: any) => a.id)
|
|
15
|
+
const enduserAttendees = (event: any): any[] => (event?.attendees ?? []).filter((a: any) => a.type === 'enduser')
|
|
16
|
+
/** the first enduser attendee — what routing.ts get_enduserId, event_handlers_v2, the EHR syncs and
|
|
17
|
+
* session_for_join_link all resolve as "the patient for this event" */
|
|
18
|
+
const primaryEnduserId = (event: any): string => enduserAttendees(event)[0]?.id ?? ''
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `calendar_events.book_appointment` must not act on a `rescheduledCalendarEventId` belonging to
|
|
22
|
+
* another patient.
|
|
23
|
+
*
|
|
24
|
+
* The handler loaded the reschedule target with `buildOrganizationWideQueries`, which is
|
|
25
|
+
* `buildAllQueries({ unrestricted: true })` and therefore never applies the `attendees.id` access
|
|
26
|
+
* constraint calendar_events declares — the effective filter was businessId only. Any authenticated
|
|
27
|
+
* enduser could pass a co-tenant's event id and (a) have that event stamped
|
|
28
|
+
* `rescheduledAt` / `rescheduledTo`, which the portal renders as "rescheduled or deleted", (b) have
|
|
29
|
+
* the victim attached as the primary enduser attendee of the replacement event, carrying the
|
|
30
|
+
* victim's `joinLinkToken` (a capability that resolves an event by token), and (c) receive the whole
|
|
31
|
+
* stored document back, including the victim's staff-authored `internalNotes`.
|
|
32
|
+
*
|
|
33
|
+
* Both lookup sites are guarded: the initial load and the re-lookup that performs the
|
|
34
|
+
* `rescheduledAt` write. A guard on only the first leaves the write path reachable.
|
|
35
|
+
*
|
|
36
|
+
* Also covered here, because they are the same "the portal UI was the only gate" bug:
|
|
37
|
+
* - `preventRescheduleInPortal` / `preventRescheduleMinutesInAdvance` were enforced only by
|
|
38
|
+
* portal/src/pages/Events.tsx, so a direct API call ignored both even for the owner.
|
|
39
|
+
* - Group events (`enduserAttendeeLimit > 1`): being an attendee is necessary but not sufficient,
|
|
40
|
+
* since any attendee rescheduling moves the appointment for everyone. The server now mirrors the
|
|
41
|
+
* portal's gate — a group event is reschedulable only when the template set
|
|
42
|
+
* `allowGroupReschedule`. The attendee the appointment belongs to (the first enduser attendee) is
|
|
43
|
+
* preserved on the replacement event, so a co-attendee rescheduling does not re-point the
|
|
44
|
+
* appointment at themselves. Carrying the REMAINING co-attendees over is unchanged pre-existing
|
|
45
|
+
* behaviour: they come along only when `replaceHostOnReschedule` is false, and not at all on the
|
|
46
|
+
* group-join branch, where a reschedule landing on an existing group event only appends the caller.
|
|
47
|
+
*
|
|
48
|
+
* Sections: A cross-enduser attempt, B the owner's own reschedule, C the reschedule window, D group
|
|
49
|
+
* events, E the caregiver / related-contact flow, F both replaceHostOnReschedule branches (E and F are
|
|
50
|
+
* regression controls — F mirrors tests.ts ~7372-7460, which only runs against the shared seeded
|
|
51
|
+
* tenant, and the attendee-copy loop changed here).
|
|
52
|
+
*
|
|
53
|
+
* RED (pre-fix): A1-A3 fail (the victim's event is stamped and a replacement carries them), C1/C2 and
|
|
54
|
+
* D1 fail (no server-side enforcement at all). GREEN: every positive control (B, D2, E, F) still
|
|
55
|
+
* passes, before and after.
|
|
56
|
+
*/
|
|
57
|
+
export const appointment_reschedule_ownership_checks = async (): Promise<Check[]> => {
|
|
58
|
+
const checks: Check[] = []
|
|
59
|
+
const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
|
|
60
|
+
|
|
61
|
+
let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
tenant = await setup_isolated_tenant({
|
|
65
|
+
name: `reschedule ownership ${RAND()}`,
|
|
66
|
+
users: [{ isAdmin: true }, { isAdmin: true }],
|
|
67
|
+
apiKeys: 0,
|
|
68
|
+
})
|
|
69
|
+
const sdk = tenant.sdk
|
|
70
|
+
const secondHostId = tenant.users[1].id
|
|
71
|
+
|
|
72
|
+
/* ================= fixtures: one host with a full day of availability ================= */
|
|
73
|
+
|
|
74
|
+
// deliberately not today, so "now" can never fall inside the availability window
|
|
75
|
+
const dayOfWeekStartingSundayIndexedByZero = (new Date().getDay() + 1) % 7
|
|
76
|
+
const availability = {
|
|
77
|
+
weeklyAvailabilities: [{
|
|
78
|
+
dayOfWeekStartingSundayIndexedByZero,
|
|
79
|
+
startTimeInMinutes: 60 * 9, // 9am
|
|
80
|
+
endTimeInMinutes: 60 * 17, // 5pm — 16 half-hour slots per occurrence
|
|
81
|
+
}],
|
|
82
|
+
timezone: 'America/New_York' as const,
|
|
83
|
+
}
|
|
84
|
+
// both hosts, so the replaceHostOnReschedule branches below have a second user to swap in
|
|
85
|
+
await sdk.api.users.updateOne(sdk.userInfo.id, availability, { replaceObjectFields: true })
|
|
86
|
+
await sdk.api.users.updateOne(secondHostId, availability, { replaceObjectFields: true })
|
|
87
|
+
|
|
88
|
+
const makeTemplate = (extra: object = {}) => sdk.api.calendar_event_templates.createOne({
|
|
89
|
+
title: `reschedule test ${RAND()}`,
|
|
90
|
+
durationInMinutes: 30,
|
|
91
|
+
confirmationEmailDisabled: true,
|
|
92
|
+
confirmationSMSDisabled: true,
|
|
93
|
+
...extra,
|
|
94
|
+
} as any)
|
|
95
|
+
|
|
96
|
+
const makeEnduser = async (label: string) => {
|
|
97
|
+
const enduser = await sdk.api.endusers.createOne({
|
|
98
|
+
email: `reschedule_${label}_${Date.now()}_${RAND()}@test.tellescope.com`,
|
|
99
|
+
})
|
|
100
|
+
const { authToken } = await sdk.api.endusers.generate_auth_token({ id: enduser.id })
|
|
101
|
+
const session = new EnduserSession({ host, authToken, enduser, businessId: tenant!.businessId })
|
|
102
|
+
return { enduser, session }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const [victim, attacker] = await Promise.all([makeEnduser('victim'), makeEnduser('attacker')])
|
|
106
|
+
|
|
107
|
+
// Every template here is a plain 30-minute one with no restrictions, so one availability query
|
|
108
|
+
// describes them all. Slots are handed out from that pool and never reused — deriving each booking
|
|
109
|
+
// from a fresh "earliest available" query instead makes the suite race with its own bookings
|
|
110
|
+
// (a booking from one template can land on a slot another template still advertises).
|
|
111
|
+
const slotTemplate = await makeTemplate()
|
|
112
|
+
const { availabilityBlocks: allBlocks } = await victim.session.api.calendar_events.get_appointment_availability({
|
|
113
|
+
calendarEventTemplateId: slotTemplate.id,
|
|
114
|
+
from: new Date(Date.now() - 10000),
|
|
115
|
+
to: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000),
|
|
116
|
+
})
|
|
117
|
+
const startTimes = allBlocks.map(b => b.startTimeInMS).filter((t, i, all) => all.indexOf(t) === i).sort((a, b) => a - b)
|
|
118
|
+
const hostsByStartTime = new Map(startTimes.map(t => [t, allBlocks.filter(b => b.startTimeInMS === t).map(b => b.userId)]))
|
|
119
|
+
const usedStartTimes = new Set<number>()
|
|
120
|
+
|
|
121
|
+
/** an unused start time this host is free for; the whole start time is consumed, for every host,
|
|
122
|
+
* because a multi-host booking takes the slot off all of them */
|
|
123
|
+
const takeSlot = (hostId?: string) => {
|
|
124
|
+
const startTimeInMS = startTimes.find(t => (
|
|
125
|
+
!usedStartTimes.has(t) && (!hostId || (hostsByStartTime.get(t) ?? []).includes(hostId))
|
|
126
|
+
))
|
|
127
|
+
if (startTimeInMS === undefined) throw new Error(`no unused availability left${hostId ? ` for host ${hostId}` : ''}`)
|
|
128
|
+
usedStartTimes.add(startTimeInMS)
|
|
129
|
+
return { startTimeInMS, userId: hostId || (hostsByStartTime.get(startTimeInMS) ?? [])[0] }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const book = async (as: EnduserSession, templateId: string, opts: { startTimeInMS?: number, userId?: string, hostId?: string, otherUserIds?: string[], bookingPageId?: string, rescheduledCalendarEventId?: string } = {}) => {
|
|
133
|
+
const slot = opts.startTimeInMS !== undefined && opts.userId
|
|
134
|
+
? { startTimeInMS: opts.startTimeInMS, userId: opts.userId }
|
|
135
|
+
: takeSlot(opts.hostId)
|
|
136
|
+
return (await as.api.calendar_events.book_appointment({
|
|
137
|
+
calendarEventTemplateId: templateId,
|
|
138
|
+
startTime: new Date(slot.startTimeInMS),
|
|
139
|
+
userId: slot.userId,
|
|
140
|
+
...opts.otherUserIds ? { otherUserIds: opts.otherUserIds } : {},
|
|
141
|
+
...opts.bookingPageId ? { bookingPageId: opts.bookingPageId } : {},
|
|
142
|
+
...opts.rescheduledCalendarEventId ? { rescheduledCalendarEventId: opts.rescheduledCalendarEventId } : {},
|
|
143
|
+
})).createdEvent
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** runs a booking and reports either the created event or a flattened error payload */
|
|
147
|
+
const attempt = async (run: () => Promise<any>) => (
|
|
148
|
+
run()
|
|
149
|
+
.then((createdEvent: any) => ({ rejected: false, createdEvent, payload: '' }))
|
|
150
|
+
.catch((err: any) => ({
|
|
151
|
+
rejected: true,
|
|
152
|
+
createdEvent: undefined,
|
|
153
|
+
payload: (() => {
|
|
154
|
+
try { return JSON.stringify({ data: err?.response?.data, message: err?.message }) }
|
|
155
|
+
catch { return String(err?.message || err) }
|
|
156
|
+
})(),
|
|
157
|
+
}))
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const template = slotTemplate
|
|
161
|
+
|
|
162
|
+
/* ================= A: a co-tenant's event is untouchable ================= */
|
|
163
|
+
|
|
164
|
+
const victimEvent = await book(victim.session, template.id)
|
|
165
|
+
const INTERNAL_NOTE = `staff-only note ${RAND()} not visible to another patient`
|
|
166
|
+
await sdk.api.calendar_events.updateOne(victimEvent.id, { internalNotes: INTERNAL_NOTE })
|
|
167
|
+
const victimJoinToken = victimEvent.attendees.find(a => a.type === 'enduser')?.joinLinkToken || ''
|
|
168
|
+
|
|
169
|
+
record(
|
|
170
|
+
'setup: victim booked an appointment with a joinLinkToken and staff internalNotes',
|
|
171
|
+
!!victimEvent.id && !!victimJoinToken
|
|
172
|
+
&& victimEvent.attendees.some(a => a.type === 'enduser' && a.id === victim.enduser.id),
|
|
173
|
+
`event=${victimEvent.id} attendees=${JSON.stringify(victimEvent.attendees.map(a => ({ type: a.type, id: a.id })))}`,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const eventsWithVictimBefore = (await sdk.api.calendar_events.getSome({ limit: 100 }))
|
|
177
|
+
.filter(e => e.attendees.some(a => a.id === victim.enduser.id))
|
|
178
|
+
.map(e => e.id)
|
|
179
|
+
|
|
180
|
+
const crossAttempt = await attempt(() => book(attacker.session, template.id, {
|
|
181
|
+
rescheduledCalendarEventId: victimEvent.id,
|
|
182
|
+
}))
|
|
183
|
+
|
|
184
|
+
record(
|
|
185
|
+
'A1: another enduser\'s book_appointment with the victim\'s rescheduledCalendarEventId is rejected',
|
|
186
|
+
crossAttempt.rejected,
|
|
187
|
+
crossAttempt.rejected
|
|
188
|
+
? `rejected: ${crossAttempt.payload}`
|
|
189
|
+
: `WRONGLY SUCCEEDED, created ${crossAttempt.createdEvent?.id}`,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const victimEventAfter = await sdk.api.calendar_events.getOne(victimEvent.id)
|
|
193
|
+
record(
|
|
194
|
+
'A2: the victim\'s event is not stamped rescheduledAt / rescheduledTo',
|
|
195
|
+
!victimEventAfter.rescheduledAt && !victimEventAfter.rescheduledTo,
|
|
196
|
+
`rescheduledAt=${String(victimEventAfter.rescheduledAt)} rescheduledTo=${String(victimEventAfter.rescheduledTo)}`,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const eventsWithVictimAfter = (await sdk.api.calendar_events.getSome({ limit: 100 }))
|
|
200
|
+
.filter(e => e.attendees.some(a => a.id === victim.enduser.id))
|
|
201
|
+
.map(e => e.id)
|
|
202
|
+
record(
|
|
203
|
+
'A3: no replacement event was created with the victim as an attendee',
|
|
204
|
+
eventsWithVictimAfter.length === eventsWithVictimBefore.length
|
|
205
|
+
&& eventsWithVictimAfter.every(id => eventsWithVictimBefore.includes(id)),
|
|
206
|
+
`before=${JSON.stringify(eventsWithVictimBefore)} after=${JSON.stringify(eventsWithVictimAfter)}`,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
record(
|
|
210
|
+
'A4: the rejection carries none of the victim\'s internalNotes, joinLinkToken or reminder config',
|
|
211
|
+
!crossAttempt.payload.includes(INTERNAL_NOTE)
|
|
212
|
+
&& !(victimJoinToken && crossAttempt.payload.includes(victimJoinToken))
|
|
213
|
+
&& !crossAttempt.payload.includes('"reminders"'),
|
|
214
|
+
`response=${crossAttempt.payload.slice(0, 400)}`,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
/* ================= B: the owner's own reschedule still works ================= */
|
|
218
|
+
|
|
219
|
+
const ownReschedule = await attempt(() => book(victim.session, template.id, {
|
|
220
|
+
rescheduledCalendarEventId: victimEvent.id,
|
|
221
|
+
}))
|
|
222
|
+
record(
|
|
223
|
+
'B1: the owner can still reschedule their own appointment',
|
|
224
|
+
!ownReschedule.rejected && !!ownReschedule.createdEvent?.id,
|
|
225
|
+
ownReschedule.rejected ? `rejected: ${ownReschedule.payload}` : `created ${ownReschedule.createdEvent.id}`,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if (!ownReschedule.rejected) {
|
|
229
|
+
const replacement = ownReschedule.createdEvent
|
|
230
|
+
const originalAfter = await sdk.api.calendar_events.getOne(victimEvent.id)
|
|
231
|
+
|
|
232
|
+
record(
|
|
233
|
+
'B2: the original event is stamped rescheduledAt / rescheduledTo -> the replacement',
|
|
234
|
+
!!originalAfter.rescheduledAt && originalAfter.rescheduledTo === replacement.id,
|
|
235
|
+
`rescheduledAt=${String(originalAfter.rescheduledAt)} rescheduledTo=${String(originalAfter.rescheduledTo)} replacement=${replacement.id}`,
|
|
236
|
+
)
|
|
237
|
+
record(
|
|
238
|
+
'B3: internalNotes and the attendee set carry over to the replacement',
|
|
239
|
+
replacement.internalNotes === INTERNAL_NOTE
|
|
240
|
+
&& replacement.attendees.some((a: any) => a.type === 'enduser' && a.id === victim.enduser.id)
|
|
241
|
+
&& replacement.attendees.some((a: any) => a.type === 'user'),
|
|
242
|
+
`internalNotes=${JSON.stringify(replacement.internalNotes)} attendees=${JSON.stringify(replacement.attendees.map((a: any) => ({ type: a.type, id: a.id })))}`,
|
|
243
|
+
)
|
|
244
|
+
record(
|
|
245
|
+
'B4: the owner stays the primary attendee, keeps their OWN joinLinkToken, and no other enduser is attached',
|
|
246
|
+
primaryEnduserId(replacement) === victim.enduser.id
|
|
247
|
+
&& replacement.attendees.filter((a: any) => a.type === 'enduser').every((a: any) => a.id === victim.enduser.id)
|
|
248
|
+
&& replacement.attendees.find((a: any) => a.type === 'enduser' && a.id === victim.enduser.id)?.joinLinkToken === victimJoinToken,
|
|
249
|
+
`enduser attendees=${JSON.stringify(replacement.attendees.filter((a: any) => a.type === 'enduser').map((a: any) => ({ id: a.id, sameToken: a.joinLinkToken === victimJoinToken })))}`,
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ================= C: reschedule window enforced server-side, not just in the portal ================= */
|
|
254
|
+
|
|
255
|
+
const noPortalRescheduleTemplate = await makeTemplate({ preventRescheduleInPortal: true })
|
|
256
|
+
const noPortalEvent = await book(victim.session, noPortalRescheduleTemplate.id)
|
|
257
|
+
const noPortalAttempt = await attempt(() => book(victim.session, noPortalRescheduleTemplate.id, {
|
|
258
|
+
rescheduledCalendarEventId: noPortalEvent.id,
|
|
259
|
+
}))
|
|
260
|
+
const noPortalAfter = await sdk.api.calendar_events.getOne(noPortalEvent.id)
|
|
261
|
+
record(
|
|
262
|
+
'C1: preventRescheduleInPortal is enforced on a direct API call by the owner',
|
|
263
|
+
noPortalAttempt.rejected && !noPortalAfter.rescheduledAt,
|
|
264
|
+
`rejected=${noPortalAttempt.rejected} detail=${noPortalAttempt.payload || `created ${noPortalAttempt.createdEvent?.id}`}`,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
// 100000 minutes (~69 days) is longer than the booking horizon, so any slot is inside the window
|
|
268
|
+
const windowTemplate = await makeTemplate({ preventRescheduleMinutesInAdvance: 100000 })
|
|
269
|
+
const windowEvent = await book(victim.session, windowTemplate.id)
|
|
270
|
+
const windowAttempt = await attempt(() => book(victim.session, windowTemplate.id, {
|
|
271
|
+
rescheduledCalendarEventId: windowEvent.id,
|
|
272
|
+
}))
|
|
273
|
+
const windowAfter = await sdk.api.calendar_events.getOne(windowEvent.id)
|
|
274
|
+
record(
|
|
275
|
+
'C2: preventRescheduleMinutesInAdvance is enforced on a direct API call by the owner',
|
|
276
|
+
windowAttempt.rejected && !windowAfter.rescheduledAt,
|
|
277
|
+
`rejected=${windowAttempt.rejected} detail=${windowAttempt.payload || `created ${windowAttempt.createdEvent?.id}`}`,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
/* ================= D: group events ================= */
|
|
281
|
+
|
|
282
|
+
// D1 — no allowGroupReschedule: not reschedulable by anyone, mirroring the portal's own gate
|
|
283
|
+
const groupTemplate = await makeTemplate({ enduserAttendeeLimit: 2 })
|
|
284
|
+
const groupEvent = await book(victim.session, groupTemplate.id)
|
|
285
|
+
await book(attacker.session, groupTemplate.id, {
|
|
286
|
+
startTimeInMS: groupEvent.startTimeInMS,
|
|
287
|
+
userId: groupEvent.attendees.find(a => a.type === 'user')!.id,
|
|
288
|
+
})
|
|
289
|
+
const groupAttempt = await attempt(() => book(attacker.session, groupTemplate.id, {
|
|
290
|
+
rescheduledCalendarEventId: groupEvent.id,
|
|
291
|
+
}))
|
|
292
|
+
const groupAfter = await sdk.api.calendar_events.getOne(groupEvent.id)
|
|
293
|
+
record(
|
|
294
|
+
'D1: a co-attendee cannot reschedule a group event without allowGroupReschedule',
|
|
295
|
+
groupAttempt.rejected && !groupAfter.rescheduledAt,
|
|
296
|
+
`rejected=${groupAttempt.rejected} rescheduledAt=${String(groupAfter.rescheduledAt)} detail=${groupAttempt.payload}`,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
// D2 — allowGroupReschedule: permitted, and the co-attendee is carried over rather than dropped
|
|
300
|
+
const groupReschedulableTemplate = await makeTemplate({ enduserAttendeeLimit: 2, allowGroupReschedule: true })
|
|
301
|
+
const groupReschedulable = await book(victim.session, groupReschedulableTemplate.id)
|
|
302
|
+
await book(attacker.session, groupReschedulableTemplate.id, {
|
|
303
|
+
startTimeInMS: groupReschedulable.startTimeInMS,
|
|
304
|
+
userId: groupReschedulable.attendees.find(a => a.type === 'user')!.id,
|
|
305
|
+
})
|
|
306
|
+
const groupRescheduleAttempt = await attempt(() => book(attacker.session, groupReschedulableTemplate.id, {
|
|
307
|
+
rescheduledCalendarEventId: groupReschedulable.id,
|
|
308
|
+
}))
|
|
309
|
+
record(
|
|
310
|
+
'D2: with allowGroupReschedule a co-attendee may reschedule; both attend and the original primary is kept',
|
|
311
|
+
!groupRescheduleAttempt.rejected
|
|
312
|
+
&& !!groupRescheduleAttempt.createdEvent?.attendees.some((a: any) => a.id === victim.enduser.id)
|
|
313
|
+
&& !!groupRescheduleAttempt.createdEvent?.attendees.some((a: any) => a.id === attacker.enduser.id)
|
|
314
|
+
// the co-attendee rescheduling must NOT become the event's derived patient
|
|
315
|
+
&& primaryEnduserId(groupRescheduleAttempt.createdEvent) === victim.enduser.id,
|
|
316
|
+
groupRescheduleAttempt.rejected
|
|
317
|
+
? `rejected: ${groupRescheduleAttempt.payload}`
|
|
318
|
+
: `attendees=${JSON.stringify(groupRescheduleAttempt.createdEvent.attendees.map((a: any) => ({ type: a.type, id: a.id })))}`
|
|
319
|
+
+ ` primaryEnduser=${primaryEnduserId(groupRescheduleAttempt.createdEvent)} (want ${victim.enduser.id})`,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
/* ================= E: the caregiver / related-contact flow still works ================= */
|
|
323
|
+
|
|
324
|
+
// A booking page with includeRelatedContactTypes adds the patient's related contacts as attendees
|
|
325
|
+
// at booking time, so the attendee-based guard has to keep letting the caregiver reschedule.
|
|
326
|
+
const caregiver = await makeEnduser('caregiver')
|
|
327
|
+
await sdk.api.endusers.updateOne(victim.enduser.id, {
|
|
328
|
+
relationships: [{ type: 'Caregiver', id: caregiver.enduser.id }],
|
|
329
|
+
} as any, { replaceObjectFields: true })
|
|
330
|
+
|
|
331
|
+
const relatedContactTemplate = await makeTemplate()
|
|
332
|
+
const bookingPage = await sdk.api.appointment_booking_pages.createOne({
|
|
333
|
+
title: `related contacts ${RAND()}`,
|
|
334
|
+
locationIds: [],
|
|
335
|
+
calendarEventTemplateIds: [relatedContactTemplate.id],
|
|
336
|
+
includeRelatedContactTypes: ['Caregiver'],
|
|
337
|
+
} as any)
|
|
338
|
+
|
|
339
|
+
const relatedContactEvent = await book(victim.session, relatedContactTemplate.id, { bookingPageId: bookingPage.id })
|
|
340
|
+
record(
|
|
341
|
+
'E1: a related contact is attached as an attendee at booking time',
|
|
342
|
+
relatedContactEvent.attendees.some((a: any) => a.id === caregiver.enduser.id),
|
|
343
|
+
`attendees=${JSON.stringify(relatedContactEvent.attendees.map((a: any) => ({ type: a.type, id: a.id })))} caregiver=${caregiver.enduser.id}`,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
const caregiverReschedule = await attempt(() => book(caregiver.session, relatedContactTemplate.id, {
|
|
347
|
+
rescheduledCalendarEventId: relatedContactEvent.id,
|
|
348
|
+
}))
|
|
349
|
+
record(
|
|
350
|
+
'E2: the caregiver can still reschedule, and the PATIENT stays the primary enduser attendee',
|
|
351
|
+
!caregiverReschedule.rejected
|
|
352
|
+
&& !!caregiverReschedule.createdEvent?.attendees.some((a: any) => a.id === caregiver.enduser.id)
|
|
353
|
+
// the appointment must still resolve to the patient: EHR sync, triggers/webhooks and
|
|
354
|
+
// session_for_join_link all read the first enduser attendee
|
|
355
|
+
&& primaryEnduserId(caregiverReschedule.createdEvent) === victim.enduser.id,
|
|
356
|
+
caregiverReschedule.rejected
|
|
357
|
+
? `rejected: ${caregiverReschedule.payload}`
|
|
358
|
+
: `attendees=${JSON.stringify(caregiverReschedule.createdEvent.attendees.map((a: any) => ({ type: a.type, id: a.id })))}`
|
|
359
|
+
+ ` primaryEnduser=${primaryEnduserId(caregiverReschedule.createdEvent)} (want patient ${victim.enduser.id})`,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
/* ================= F: replaceHostOnReschedule, both branches ================= */
|
|
363
|
+
|
|
364
|
+
// Equivalent to tests.ts ~7372-7460, which only runs against the shared seeded tenant. The
|
|
365
|
+
// attendee-copy loop changed here, so both branches need coverage that runs anywhere.
|
|
366
|
+
const preserveHostsTemplate = await makeTemplate({ replaceHostOnReschedule: false })
|
|
367
|
+
const preserveOriginal = await book(victim.session, preserveHostsTemplate.id, {
|
|
368
|
+
hostId: sdk.userInfo.id,
|
|
369
|
+
otherUserIds: [secondHostId],
|
|
370
|
+
})
|
|
371
|
+
const preserveReplacement = await attempt(() => book(victim.session, preserveHostsTemplate.id, {
|
|
372
|
+
hostId: secondHostId,
|
|
373
|
+
rescheduledCalendarEventId: preserveOriginal.id,
|
|
374
|
+
}))
|
|
375
|
+
record(
|
|
376
|
+
'F1: replaceHostOnReschedule false still preserves both user attendees',
|
|
377
|
+
!preserveReplacement.rejected
|
|
378
|
+
&& userAttendeeIds(preserveReplacement.createdEvent).length === 2
|
|
379
|
+
&& userAttendeeIds(preserveReplacement.createdEvent).includes(sdk.userInfo.id)
|
|
380
|
+
&& userAttendeeIds(preserveReplacement.createdEvent).includes(secondHostId),
|
|
381
|
+
preserveReplacement.rejected
|
|
382
|
+
? `rejected: ${preserveReplacement.payload}`
|
|
383
|
+
: `user attendees=${JSON.stringify(userAttendeeIds(preserveReplacement.createdEvent))}`,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
const replaceHostsTemplate = await makeTemplate({ replaceHostOnReschedule: true })
|
|
387
|
+
const replaceOriginal = await book(victim.session, replaceHostsTemplate.id, {
|
|
388
|
+
hostId: sdk.userInfo.id,
|
|
389
|
+
otherUserIds: [secondHostId],
|
|
390
|
+
})
|
|
391
|
+
const replaceReplacement = await attempt(() => book(victim.session, replaceHostsTemplate.id, {
|
|
392
|
+
hostId: secondHostId,
|
|
393
|
+
rescheduledCalendarEventId: replaceOriginal.id,
|
|
394
|
+
}))
|
|
395
|
+
record(
|
|
396
|
+
'F2: replaceHostOnReschedule true still replaces the hosts and keeps the enduser attendee',
|
|
397
|
+
!replaceReplacement.rejected
|
|
398
|
+
&& userAttendeeIds(replaceReplacement.createdEvent).length === 1
|
|
399
|
+
&& userAttendeeIds(replaceReplacement.createdEvent)[0] === secondHostId
|
|
400
|
+
&& replaceReplacement.createdEvent.attendees.filter((a: any) => a.type === 'enduser').length === 1
|
|
401
|
+
&& replaceReplacement.createdEvent.attendees.some((a: any) => a.id === victim.enduser.id),
|
|
402
|
+
replaceReplacement.rejected
|
|
403
|
+
? `rejected: ${replaceReplacement.payload}`
|
|
404
|
+
: `attendees=${JSON.stringify(replaceReplacement.createdEvent.attendees.map((a: any) => ({ type: a.type, id: a.id })))}`,
|
|
405
|
+
)
|
|
406
|
+
} finally {
|
|
407
|
+
// Teardown before anything asserts — assert calls process.exit, which skips pending finallys and
|
|
408
|
+
// would strand the provisioned tenant permanently.
|
|
409
|
+
if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return checks
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Asserting wrapper, for standalone runs and for sequential use in the suite. */
|
|
416
|
+
export const appointment_reschedule_ownership_tests = async () => {
|
|
417
|
+
assert_checks('book_appointment reschedule ownership', await appointment_reschedule_ownership_checks())
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Allow running this test file independently
|
|
421
|
+
if (require.main === module) {
|
|
422
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
423
|
+
appointment_reschedule_ownership_tests()
|
|
424
|
+
.then(() => {
|
|
425
|
+
console.log("✅ book_appointment reschedule ownership test suite completed successfully")
|
|
426
|
+
process.exit(0)
|
|
427
|
+
})
|
|
428
|
+
.catch((error) => {
|
|
429
|
+
console.error("❌ book_appointment reschedule ownership test suite failed:", error)
|
|
430
|
+
process.exit(1)
|
|
431
|
+
})
|
|
432
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { authenticate_enduser_via_token, setup_tests } from "../../setup"
|
|
9
9
|
|
|
10
10
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
11
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
11
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
12
12
|
|
|
13
13
|
const RAND = () => Math.random().toString(36).slice(2, 10)
|
|
14
14
|
|
|
@@ -6,7 +6,7 @@ import { FormResponse } from "@tellescope/types-client"
|
|
|
6
6
|
import { setup_tests } from "../../setup"
|
|
7
7
|
|
|
8
8
|
const host = process.env.API_URL || "http://localhost:8080"
|
|
9
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
9
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
10
10
|
|
|
11
11
|
const ENDUSER_PASSWORD = 'AISummaryGateTestPassword!123'
|
|
12
12
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { setup_tests } from "../../setup"
|
|
9
9
|
|
|
10
10
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
11
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
11
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
12
12
|
|
|
13
13
|
const ENDUSER_PASSWORD = 'F0116TestPassword!123'
|
|
14
14
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { setup_tests } from "../../setup"
|
|
9
9
|
|
|
10
10
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
11
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
11
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
12
12
|
|
|
13
13
|
const ENDUSER_PASSWORD = 'F0106TestPassword!123'
|
|
14
14
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
log_header,
|
|
8
8
|
wait,
|
|
9
9
|
} from "@tellescope/testing"
|
|
10
|
-
import { setup_tests } from "../../setup"
|
|
10
|
+
import { reset_test_tenant, setup_tests } from "../../setup"
|
|
11
11
|
|
|
12
12
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
13
|
|
|
@@ -53,7 +53,7 @@ export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk
|
|
|
53
53
|
log_header("users.invite_user cross-tenant enumeration regression")
|
|
54
54
|
|
|
55
55
|
// Reset state so prior tests' rate-limit accounting doesn't leak in.
|
|
56
|
-
await sdk
|
|
56
|
+
await reset_test_tenant(sdk)
|
|
57
57
|
|
|
58
58
|
const organizationId = sdk.userInfo.organizationIds?.[0] ?? sdk.userInfo.businessId
|
|
59
59
|
// Use the test admin's own email — guaranteed to exist in the test tenant, so every invite
|
|
@@ -98,7 +98,7 @@ export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk
|
|
|
98
98
|
// ====================================================================
|
|
99
99
|
// Let rate limit decay so this single call isn't blocked.
|
|
100
100
|
await wait(undefined, 5000)
|
|
101
|
-
await sdk
|
|
101
|
+
await reset_test_tenant(sdk)
|
|
102
102
|
|
|
103
103
|
const sameTenantRes = await post(
|
|
104
104
|
'/v1/invite-user-to-organization',
|