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