@tellescope/sdk 1.256.16 → 2.0.0-beta.0
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 +3 -1
- 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/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_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/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 +180 -166
- 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 +6 -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/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_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/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 +180 -166
- 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 +5 -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_login_rate_limits.test.ts +7 -7
- package/src/tests/api_tests/enduser_session_invalidation.test.ts +4 -4
- 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_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/allowed-paths-confinement.test.ts +1 -1
- 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/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 +10 -2
- package/src/tests/webhooks_tests.ts +4 -2
- package/test_generated.pdf +0 -0
- package/tsconfig.cjs.json +17 -0
package/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
4
|
"description": "Code for interacting with the Tellescope API",
|
|
5
5
|
"main": "./lib/cjs/sdk.js",
|
|
6
6
|
"module": "./lib/esm/sdk.js",
|
|
7
7
|
"types": "./lib/esm/sdk.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "npm run-script build:cjs && npm run-script retest",
|
|
9
|
+
"test": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest",
|
|
10
10
|
"retest": "npm run-script retest-api && npm run-script retest-public-api && npm run-script retest-webhooks",
|
|
11
|
-
"test-api": "npm run-script build:cjs && npm run-script retest-api",
|
|
11
|
+
"test-api": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest-api",
|
|
12
12
|
"retest-api": "node -r dotenv/config lib/cjs/tests/tests.js",
|
|
13
|
-
"test-public-api": "npm run-script build:cjs && npm run-script retest-public-api",
|
|
13
|
+
"test-public-api": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest-public-api",
|
|
14
14
|
"retest-public-api": "node -r dotenv/config lib/cjs/tests/public_endpoint_tests.js",
|
|
15
|
-
"test-sockets": "npm run-script build:cjs && npm run-script retest-sockets",
|
|
15
|
+
"test-sockets": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest-sockets",
|
|
16
16
|
"retest-sockets": "node -r dotenv/config lib/cjs/tests/socket_tests.js",
|
|
17
|
-
"test-webhooks": "npm run-script build:cjs && npm run-script retest-webhooks",
|
|
17
|
+
"test-webhooks": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest-webhooks",
|
|
18
18
|
"retest-webhooks": "node -r dotenv/config lib/cjs/tests/webhooks_tests.js",
|
|
19
19
|
"build": "npm run-script build:esm && npm run-script build:cjs",
|
|
20
20
|
"build:esm": "tsc",
|
|
21
|
-
"build:cjs": "tsc
|
|
22
|
-
"watch:cjs": "tsc
|
|
21
|
+
"build:cjs": "tsc -b tsconfig.cjs.json",
|
|
22
|
+
"watch:cjs": "tsc -b -w --preserveWatchOutput tsconfig.cjs.json"
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
@@ -31,18 +31,21 @@
|
|
|
31
31
|
"url": "https://github.com/tellescope-os/tellescope/issues"
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://github.com/tellescope-os/tellescope#readme",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22.12.0"
|
|
36
|
+
},
|
|
34
37
|
"dependencies": {
|
|
35
|
-
"@tellescope/constants": "
|
|
36
|
-
"@tellescope/schema": "
|
|
37
|
-
"@tellescope/testing": "
|
|
38
|
-
"@tellescope/types-client": "
|
|
39
|
-
"@tellescope/types-models": "
|
|
40
|
-
"@tellescope/types-utilities": "
|
|
41
|
-
"@tellescope/utilities": "
|
|
42
|
-
"@tellescope/validation": "
|
|
43
|
-
"axios": "0.
|
|
38
|
+
"@tellescope/constants": "2.0.0-beta.0",
|
|
39
|
+
"@tellescope/schema": "2.0.0-beta.0",
|
|
40
|
+
"@tellescope/testing": "2.0.0-beta.0",
|
|
41
|
+
"@tellescope/types-client": "2.0.0-beta.0",
|
|
42
|
+
"@tellescope/types-models": "2.0.0-beta.0",
|
|
43
|
+
"@tellescope/types-utilities": "2.0.0-beta.0",
|
|
44
|
+
"@tellescope/utilities": "2.0.0-beta.0",
|
|
45
|
+
"@tellescope/validation": "2.0.0-beta.0",
|
|
46
|
+
"axios": "0.33.0",
|
|
44
47
|
"dotenv": "14.3.2",
|
|
45
|
-
"form-data": "4.0.
|
|
48
|
+
"form-data": "4.0.6",
|
|
46
49
|
"source-map-support": "0.5.21"
|
|
47
50
|
},
|
|
48
51
|
"devDependencies": {
|
|
@@ -55,5 +58,5 @@
|
|
|
55
58
|
"publishConfig": {
|
|
56
59
|
"access": "public"
|
|
57
60
|
},
|
|
58
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "95b2448bb219e7634e29ef4b7850f8f74d73dcbc"
|
|
59
62
|
}
|
package/src/sdk.ts
CHANGED
|
@@ -313,6 +313,9 @@ type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
|
|
|
313
313
|
get_form_statistics: (args: extractFields<CustomActions['forms']['get_form_statistics']['parameters']>) => (
|
|
314
314
|
Promise<extractFields<CustomActions['forms']['get_form_statistics']['returns']>>
|
|
315
315
|
),
|
|
316
|
+
public_form_details: (args: extractFields<PublicActions['forms']['public_form_details']['parameters']>) => (
|
|
317
|
+
Promise<extractFields<PublicActions['forms']['public_form_details']['returns']>>
|
|
318
|
+
),
|
|
316
319
|
},
|
|
317
320
|
phone_trees: {
|
|
318
321
|
start_outbound_call: (args: extractFields<CustomActions['phone_trees']['start_outbound_call']['parameters']>) => (
|
|
@@ -1052,6 +1055,7 @@ export class Session extends SessionManager {
|
|
|
1052
1055
|
queries.organizations.invite_user = a => this._POST(`/v1/${schema.users.customActions.invite_user.path}`, a)
|
|
1053
1056
|
|
|
1054
1057
|
queries.forms.get_form_statistics = args => this._GET(`/v1${schema.forms.customActions.get_form_statistics.path}`, args)
|
|
1058
|
+
queries.forms.public_form_details = args => this._GET(`/v1${schema.forms.publicActions.public_form_details.path}`, args)
|
|
1055
1059
|
|
|
1056
1060
|
queries.form_fields.load_choices_from_database = args => this._GET(`/v1${schema.form_fields.customActions.load_choices_from_database.path}`, args)
|
|
1057
1061
|
queries.form_fields.booking_info = args => this._GET(`/v1${schema.form_fields.customActions.booking_info.path}`, args)
|
|
@@ -1350,4 +1354,4 @@ export class Session extends SessionManager {
|
|
|
1350
1354
|
sync = (a: { from: Date }) => this.GET<typeof a, { results: Pick<DataSyncRecord, 'modelName' | 'recordId' | 'data'>[], to: string }>('/v1/data-sync', a)
|
|
1351
1355
|
}
|
|
1352
1356
|
|
|
1353
|
-
export { SessionOptions }
|
|
1357
|
+
export type { SessionOptions }
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { authenticate_enduser_via_token, setup_tests } from "../setup"
|
|
11
11
|
|
|
12
12
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
13
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
14
14
|
|
|
15
15
|
// Second-tenant session, used to prove the active patient count never crosses tenants.
|
|
16
16
|
// Same key used by the org-isolation tests in tests.ts.
|
|
@@ -3,7 +3,7 @@ import { log_header, wait, async_test } from "@tellescope/testing"
|
|
|
3
3
|
import { Enduser } from "@tellescope/types-client"
|
|
4
4
|
import { setup_tests } from "../setup"
|
|
5
5
|
|
|
6
|
-
const host = process.env.TEST_HOST || "http://localhost:8080"
|
|
6
|
+
const host = process.env.API_URL || process.env.TEST_HOST || "http://localhost:8080"
|
|
7
7
|
|
|
8
8
|
export const appointment_rescheduled_trigger_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
9
9
|
log_header("Automation Trigger Tests (Appointment Rescheduled)")
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { setup_tests, authenticate_enduser_via_token } 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
|
/**
|
|
14
14
|
* CU-86e2dvrn9: calendar_events.references switched from updatesDisabled to
|
|
@@ -12,7 +12,7 @@ import { ModelName } from "@tellescope/types-models"
|
|
|
12
12
|
import { authenticate_enduser_via_token, setup_tests } from "../setup"
|
|
13
13
|
|
|
14
14
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
15
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
15
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
16
16
|
|
|
17
17
|
type OwnerField = 'enduserId' | 'enduserIds' | 'attendees.id'
|
|
18
18
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
async_test,
|
|
7
7
|
log_header,
|
|
8
8
|
} from "@tellescope/testing"
|
|
9
|
-
import { setup_tests } from "../setup"
|
|
9
|
+
import { reset_test_tenant, setup_tests } from "../setup"
|
|
10
10
|
|
|
11
11
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
12
|
|
|
@@ -43,7 +43,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
|
|
|
43
43
|
const businessId = sdk.userInfo.businessId
|
|
44
44
|
|
|
45
45
|
// Ensure throttled_events from prior tests don't bleed in.
|
|
46
|
-
await sdk
|
|
46
|
+
await reset_test_tenant(sdk)
|
|
47
47
|
|
|
48
48
|
// ---- /v1/login-enduser per-IP cap (20/min) ----
|
|
49
49
|
// Bogus emails ensure we never reach the actual DB lookup / login work.
|
|
@@ -58,7 +58,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
|
|
|
58
58
|
{ expectedResult: 'tripped' }
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
await sdk
|
|
61
|
+
await reset_test_tenant(sdk)
|
|
62
62
|
|
|
63
63
|
// ---- /v1/begin-enduser-login-flow per-IP cap (10/min) ----
|
|
64
64
|
// Use distinct emails so the per-identifier limit (5/10min) does NOT trip first;
|
|
@@ -73,7 +73,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
|
|
|
73
73
|
{ expectedResult: 'tripped' }
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
-
await sdk
|
|
76
|
+
await reset_test_tenant(sdk)
|
|
77
77
|
|
|
78
78
|
// ---- /v1/begin-enduser-login-flow per-identifier cap (5 / 10 min per email) ----
|
|
79
79
|
// Hit a single email below the per-IP cap.
|
|
@@ -88,7 +88,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
|
|
|
88
88
|
{ expectedResult: 'tripped' }
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
-
await sdk
|
|
91
|
+
await reset_test_tenant(sdk)
|
|
92
92
|
|
|
93
93
|
// ---- /v1/endusers/send-otp-code per-IP cap (5/min) ----
|
|
94
94
|
// Use a bogus JWT-shaped token so we trip the IP guard first (it runs before any DB work).
|
|
@@ -116,7 +116,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
|
|
|
116
116
|
|
|
117
117
|
// ---- Legitimate-login regression: a single successful login should still go through ----
|
|
118
118
|
// Reset state, then create a real enduser with a password and confirm one login succeeds.
|
|
119
|
-
await sdk
|
|
119
|
+
await reset_test_tenant(sdk)
|
|
120
120
|
|
|
121
121
|
const ts = Date.now()
|
|
122
122
|
const enduser = await sdk.api.endusers.createOne({
|
|
@@ -151,7 +151,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
|
|
|
151
151
|
)
|
|
152
152
|
} finally {
|
|
153
153
|
await sdk.api.endusers.deleteOne(enduser.id).catch(() => null)
|
|
154
|
-
await sdk
|
|
154
|
+
await reset_test_tenant(sdk).catch(() => null)
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -7,10 +7,10 @@ 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
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
13
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
14
14
|
|
|
15
15
|
// Main test function that can be called independently
|
|
16
16
|
export const enduser_session_invalidation_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
@@ -126,7 +126,7 @@ export const enduser_session_invalidation_tests = async ({ sdk, sdkNonAdmin } :
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// Ensure OTP is off and db is clean before starting
|
|
129
|
-
await sdk
|
|
129
|
+
await reset_test_tenant(sdk)
|
|
130
130
|
await wait(undefined, 500)
|
|
131
131
|
await resetOTPSettings(sdk)
|
|
132
132
|
|
|
@@ -242,7 +242,7 @@ export const enduser_session_invalidation_tests = async ({ sdk, sdkNonAdmin } :
|
|
|
242
242
|
)
|
|
243
243
|
|
|
244
244
|
// Reset rate limiting state before continuing
|
|
245
|
-
await sdk
|
|
245
|
+
await reset_test_tenant(sdk)
|
|
246
246
|
await wait(undefined, 500)
|
|
247
247
|
|
|
248
248
|
// Test 8: Disabling OTP does NOT invalidate sessions
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { setup_tests } from "../setup"
|
|
10
10
|
|
|
11
11
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
12
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Tests for managed_content_records enduser access filtering.
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { setup_tests } from "../setup"
|
|
10
10
|
|
|
11
11
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
12
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Tests for file_download_URL enduser access via managed content fallback.
|
|
@@ -75,6 +75,15 @@ export const mdi_webhooks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, s
|
|
|
75
75
|
)
|
|
76
76
|
assert(status === 401, `message_created with bad auth should 401 (got ${status})`, "MDI webhook authenticates before patient lookup")
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
// 5. case_order_created / case_order_updated with bogus auth -> still 401.
|
|
80
|
+
for (const event_type of ['case_order_created', 'case_order_updated']) {
|
|
81
|
+
const { status } = await postMDI(
|
|
82
|
+
{ event_type, case_id: 'mdi-case-test-1', case_order_id: 'mdi-order-test-1', order_status: 'ready' },
|
|
83
|
+
{ 'Authorization': `Bearer not-a-real-mdi-token-${Date.now()}` },
|
|
84
|
+
)
|
|
85
|
+
assert(status === 401, `${event_type} with bad auth should 401 (got ${status})`, `MDI webhook authenticates before ${event_type} dispatch`)
|
|
86
|
+
}
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
// Allow running this test file independently
|
|
@@ -9,7 +9,7 @@ import { ObservationComponent } from "@tellescope/types-models"
|
|
|
9
9
|
import { authenticate_enduser_via_token, setup_tests } from "../setup"
|
|
10
10
|
|
|
11
11
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
12
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
13
13
|
|
|
14
14
|
const ENDUSER_PASSWORD = 'NutritionTestPassword!123'
|
|
15
15
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { setup_tests } from "../setup"
|
|
11
11
|
|
|
12
12
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
|
-
const businessId = '60398b1131a295e64f084ff6'
|
|
13
|
+
const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
|
|
14
14
|
|
|
15
15
|
const v1Url = `${host}/v1/webhooks/openloop/${businessId}`
|
|
16
16
|
const v2Url = `${host}/v1/webhooks/openloop-v2/${businessId}`
|
|
@@ -631,7 +631,7 @@ export const allowed_paths_confinement_tests = async ({ sdk } : { sdk: Session,
|
|
|
631
631
|
// record. `allowedPaths` is a JWT claim only (WithAllowedPaths in types-models is used solely on
|
|
632
632
|
// the session type; there is no schema validator for it), so an API-key session can never carry
|
|
633
633
|
// one and is never path-confined. Assert that rather than reason about it.
|
|
634
|
-
const apiKeySDK = new Session({ host, apiKey: '3n5q0SCBT_iUvZz-b9BJtX7o7HQUVJ9v132PgHJNJsg.' })
|
|
634
|
+
const apiKeySDK = new Session({ host, apiKey: process.env.TEST_API_KEY || '3n5q0SCBT_iUvZz-b9BJtX7o7HQUVJ9v132PgHJNJsg.' })
|
|
635
635
|
for (const path of ['/journeys', '/form-responses', '/endusers', '/calendar-events', '/files']) {
|
|
636
636
|
const r = await axios.request({
|
|
637
637
|
method: 'get',
|
|
@@ -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',
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { EnduserSession } from "../../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
assert_checks,
|
|
6
|
+
Check,
|
|
7
|
+
setup_isolated_tenant,
|
|
8
|
+
} from "../../setup_isolated"
|
|
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
|
+
/**
|
|
15
|
+
* Form "Rich Text" answers must be sanitized when they are stored, not only when they are rendered.
|
|
16
|
+
*
|
|
17
|
+
* A Rich Text answer is HTML authored by whoever fills the form — including an unauthenticated
|
|
18
|
+
* visitor on a public intake page — and it is stored verbatim by the generic write path (the only
|
|
19
|
+
* thing it strips is `<script>`). Every read path re-sanitizes before `dangerouslySetInnerHTML`,
|
|
20
|
+
* but the staff-side rich text *editor* did not: `@tellescope/react-components`'s `RichTextInput`
|
|
21
|
+
* fed the stored answer straight into `html-to-draftjs`, whose `<iframe>` handling produces a
|
|
22
|
+
* Draft.js EMBEDDED_LINK block that `react-draft-wysiwyg` renders as a real, unsandboxed iframe.
|
|
23
|
+
* So an enduser could submit `<iframe src="javascript:…">` in an answer and have it execute in the
|
|
24
|
+
* session of the staff member who opened that response for editing (enduser -> staff stored XSS,
|
|
25
|
+
* i.e. a PHI-egress class). The editor was fixed to sanitize before parsing; this test covers the
|
|
26
|
+
* second layer: the answer is sanitized once, on write, so exports, PDFs, integrations, AI
|
|
27
|
+
* summaries and any future consumer inherit it instead of each having to remember.
|
|
28
|
+
*
|
|
29
|
+
* Every client write of answers goes through `formResponseValidator` — generic create/update
|
|
30
|
+
* (endusers may PATCH `responses`), `submit_form_response` and `save_field_response` — so all three
|
|
31
|
+
* are exercised here. The server-side writers that bypass validation (Formsort, Healthie sync, AI
|
|
32
|
+
* form filling) never construct a `'Rich Text'` answer; see the note on the validator in
|
|
33
|
+
* `@tellescope/validation`.
|
|
34
|
+
*
|
|
35
|
+
* The negative checks are paired with positive ones: formatting and legitimate external links must
|
|
36
|
+
* survive, or a "sanitized" result could just mean the answer was discarded. The last two checks
|
|
37
|
+
* guard the two ways this change could break normal use: an empty answer must round-trip
|
|
38
|
+
* unchanged (the portal compares against `''` / `'<p></p>'`), and a long answer must not be
|
|
39
|
+
* rejected because the sanitizer's own `target`/`rel` additions pushed it over the length limit.
|
|
40
|
+
*/
|
|
41
|
+
export const rich_text_answer_sanitized_on_write_checks = async (): Promise<Check[]> => {
|
|
42
|
+
const checks: Check[] = []
|
|
43
|
+
const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
|
|
44
|
+
|
|
45
|
+
const PAYLOAD = '<p>hi <b>bold</b> <a href="https://x.io">l</a></p>'
|
|
46
|
+
+ '<iframe src="javascript:alert(1)"></iframe>'
|
|
47
|
+
+ '<img src="x" onerror="alert(1)">'
|
|
48
|
+
+ '<script>alert(1)</script>'
|
|
49
|
+
+ '<a href="javascript:alert(2)">j</a>'
|
|
50
|
+
|
|
51
|
+
const dangerous = (stored: string) => ([
|
|
52
|
+
['<iframe', stored.toLowerCase().includes('<iframe')],
|
|
53
|
+
['onerror', stored.toLowerCase().includes('onerror')],
|
|
54
|
+
['<script', stored.toLowerCase().includes('<script')],
|
|
55
|
+
['javascript:', stored.toLowerCase().includes('javascript:')],
|
|
56
|
+
] as [string, boolean][]).filter(([, present]) => present).map(([name]) => name)
|
|
57
|
+
|
|
58
|
+
const record_sanitized = (label: string, stored: string | undefined) => {
|
|
59
|
+
if (typeof stored !== 'string') {
|
|
60
|
+
record(label, false, `no answer stored (got ${JSON.stringify(stored)})`)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const bad = dangerous(stored)
|
|
64
|
+
const keptFormatting = stored.includes('<b>bold</b>')
|
|
65
|
+
const keptLink = stored.includes('href="https://x.io"') && stored.includes('rel="noopener noreferrer"')
|
|
66
|
+
|
|
67
|
+
record(label, bad.length === 0 && keptFormatting && keptLink,
|
|
68
|
+
bad.length !== 0 ? `dangerous markup stored: ${bad.join(', ')} — ${stored.slice(0, 300)}`
|
|
69
|
+
: !keptFormatting ? `formatting did not survive — ${stored.slice(0, 300)}`
|
|
70
|
+
: !keptLink ? `external link lost its href or rel — ${stored.slice(0, 300)}`
|
|
71
|
+
: `stored sanitized: ${stored.slice(0, 300)}`
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
tenant = await setup_isolated_tenant({ name: `RichText ${RAND()}`, users: [{ isAdmin: true }], apiKeys: 0 })
|
|
79
|
+
const sdk = tenant.sdk
|
|
80
|
+
|
|
81
|
+
const form = await sdk.api.forms.createOne({ title: `rich text ${RAND()}` })
|
|
82
|
+
const field = await sdk.api.form_fields.createOne({
|
|
83
|
+
formId: form.id, title: 'Tell us more', type: 'Rich Text', previousFields: [{ type: 'root', info: {} }],
|
|
84
|
+
})
|
|
85
|
+
const enduser = await sdk.api.endusers.createOne({
|
|
86
|
+
email: `richtext-${RAND()}@tellescope.com`, fname: 'Rich', lname: 'Text',
|
|
87
|
+
})
|
|
88
|
+
// generate_auth_token rather than .authenticate — repeated logins hit the rate limiter.
|
|
89
|
+
const { authToken } = await sdk.api.endusers.generate_auth_token({ id: enduser.id })
|
|
90
|
+
const enduserSDK = new EnduserSession({ host, businessId: tenant.businessId, authToken })
|
|
91
|
+
|
|
92
|
+
const prepare = () => sdk.api.form_responses.prepare_form_response({ formId: form.id, enduserId: enduser.id })
|
|
93
|
+
const answer = (value: string) => ({
|
|
94
|
+
fieldId: field.id, fieldTitle: field.title, answer: { type: 'Rich Text' as const, value },
|
|
95
|
+
})
|
|
96
|
+
/** What is actually in the database, read back by an admin session. */
|
|
97
|
+
const stored_answer = async (formResponseId: string) => {
|
|
98
|
+
const formResponse = await sdk.api.form_responses.getOne(formResponseId)
|
|
99
|
+
const response = (formResponse.responses || []).find(r => r.fieldId === field.id)
|
|
100
|
+
return (response?.answer as { value?: string } | undefined)?.value
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ===== 1. submit_form_response — the path a patient filling out a form takes ===== */
|
|
104
|
+
|
|
105
|
+
const submitted = await prepare()
|
|
106
|
+
await enduserSDK.api.form_responses.submit_form_response({
|
|
107
|
+
accessCode: submitted.accessCode, responses: [answer(PAYLOAD)],
|
|
108
|
+
})
|
|
109
|
+
const submittedValue = await stored_answer(submitted.response.id)
|
|
110
|
+
record_sanitized('1: submit_form_response stores a sanitized answer', submittedValue)
|
|
111
|
+
|
|
112
|
+
/* ===== 2. save_field_response — the partial/draft save the portal fires per question ===== */
|
|
113
|
+
|
|
114
|
+
const partial = await prepare()
|
|
115
|
+
await enduserSDK.api.form_responses.save_field_response({
|
|
116
|
+
accessCode: partial.accessCode, response: answer(PAYLOAD),
|
|
117
|
+
})
|
|
118
|
+
record_sanitized('2: save_field_response stores a sanitized answer', await stored_answer(partial.response.id))
|
|
119
|
+
|
|
120
|
+
/* ===== 3. generic PATCH of responses — endusers are allowed to update their own ===== */
|
|
121
|
+
|
|
122
|
+
const patched = await prepare()
|
|
123
|
+
await enduserSDK.api.form_responses.updateOne(patched.response.id, { responses: [answer(PAYLOAD)] })
|
|
124
|
+
record_sanitized('3: enduser PATCH of responses stores a sanitized answer', await stored_answer(patched.response.id))
|
|
125
|
+
|
|
126
|
+
/* ===== 4. Idempotence: re-submitting what was stored must not change it again ===== */
|
|
127
|
+
|
|
128
|
+
if (typeof submittedValue === 'string') {
|
|
129
|
+
const resubmitted = await prepare()
|
|
130
|
+
await enduserSDK.api.form_responses.submit_form_response({
|
|
131
|
+
accessCode: resubmitted.accessCode, responses: [answer(submittedValue)],
|
|
132
|
+
})
|
|
133
|
+
const again = await stored_answer(resubmitted.response.id)
|
|
134
|
+
record('4: re-submitting a sanitized answer is idempotent', again === submittedValue,
|
|
135
|
+
again === submittedValue ? 'unchanged on the second pass' : `changed — ${String(again).slice(0, 300)}`)
|
|
136
|
+
} else {
|
|
137
|
+
record('4: re-submitting a sanitized answer is idempotent', false, 'skipped: check 1 stored nothing')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ===== 5. Empty answers round-trip: the portal treats '' and '<p></p>' as "unanswered" ===== */
|
|
141
|
+
|
|
142
|
+
const empties = await prepare()
|
|
143
|
+
const roundTrips: string[] = []
|
|
144
|
+
for (const value of ['<p></p>', '']) {
|
|
145
|
+
await enduserSDK.api.form_responses.save_field_response({
|
|
146
|
+
accessCode: empties.accessCode, response: answer(value),
|
|
147
|
+
})
|
|
148
|
+
roundTrips.push(`${JSON.stringify(value)} -> ${JSON.stringify(await stored_answer(empties.response.id))}`)
|
|
149
|
+
}
|
|
150
|
+
const emptiesOk = roundTrips[0] === '"<p></p>" -> "<p></p>"' && roundTrips[1] === '"" -> ""'
|
|
151
|
+
record('5: empty and <p></p> answers round-trip unchanged', emptiesOk, roundTrips.join(', '))
|
|
152
|
+
|
|
153
|
+
/* ===== 6. Length headroom: the sanitizer ADDS target/rel to every external link ===== */
|
|
154
|
+
|
|
155
|
+
// ~24.9k of legitimate answer, just under the old 25000 limit. Sanitizing adds ~41 characters
|
|
156
|
+
// per external link, so at the old limit the sanitized result would be rejected on a re-save.
|
|
157
|
+
const LINKS = 12
|
|
158
|
+
const longAnswer = `<p>${'a'.repeat(24400)}</p>`
|
|
159
|
+
+ `<a href="https://example.com/a">link</a>`.repeat(LINKS)
|
|
160
|
+
const longResponse = await prepare()
|
|
161
|
+
const longResult = await enduserSDK.api.form_responses.submit_form_response({
|
|
162
|
+
accessCode: longResponse.accessCode, responses: [answer(longAnswer)],
|
|
163
|
+
}).then(() => stored_answer(longResponse.response.id))
|
|
164
|
+
.catch((e: any) => `rejected: ${e?.message || e}`)
|
|
165
|
+
|
|
166
|
+
const longOk = typeof longResult === 'string'
|
|
167
|
+
&& longResult.split('rel="noopener noreferrer"').length - 1 === LINKS
|
|
168
|
+
record('6: a ~24.9k answer with external links is accepted after sanitizing', longOk,
|
|
169
|
+
longOk ? `stored ${longResult.length} characters with ${LINKS} rel-tagged links (submitted ${longAnswer.length})`
|
|
170
|
+
: `submitted ${longAnswer.length} characters — ${String(longResult).slice(0, 300)}`)
|
|
171
|
+
} finally {
|
|
172
|
+
// Teardown BEFORE anything asserts — assert calls process.exit, which skips pending finallys and
|
|
173
|
+
// would strand the provisioned tenant permanently.
|
|
174
|
+
if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return checks
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Asserting wrapper, for standalone runs and for sequential use in the suite. */
|
|
181
|
+
export const rich_text_answer_sanitized_on_write_tests = async () => {
|
|
182
|
+
assert_checks('Rich Text answers sanitized on write', await rich_text_answer_sanitized_on_write_checks())
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Allow running this test file independently
|
|
186
|
+
if (require.main === module) {
|
|
187
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
188
|
+
rich_text_answer_sanitized_on_write_tests()
|
|
189
|
+
.then(() => {
|
|
190
|
+
console.log("✅ rich text answer sanitization test suite completed successfully")
|
|
191
|
+
process.exit(0)
|
|
192
|
+
})
|
|
193
|
+
.catch((error) => {
|
|
194
|
+
console.error("❌ rich text answer sanitization test suite failed:", error)
|
|
195
|
+
process.exit(1)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
@@ -147,7 +147,10 @@ export const sanitize_user_html_with_iframes_tests = async () => {
|
|
|
147
147
|
// sandbox="" is the MOST restrictive value and must not be replaced by the default
|
|
148
148
|
const emptySandbox = sanitize_user_html_with_iframes(`<iframe src="https://ok.example" sandbox=""></iframe>`)
|
|
149
149
|
if (emptySandbox.includes('allow-')) fail(`empty sandbox was replaced with the permissive default: ${emptySandbox}`)
|
|
150
|
-
|
|
150
|
+
// Accepts either spelling — a bare `sandbox` and `sandbox=""` are identical per the HTML spec,
|
|
151
|
+
// and which one sanitize-html emits has changed between releases. What must never happen is the
|
|
152
|
+
// attribute disappearing, which leaves the frame with no sandbox at all.
|
|
153
|
+
if (!/\ssandbox(=""|\s|>)/.test(emptySandbox)) fail(`empty sandbox attribute lost entirely: ${emptySandbox}`)
|
|
151
154
|
|
|
152
155
|
// author referrerpolicy honored
|
|
153
156
|
const noref = sanitize_user_html_with_iframes(`<iframe src="https://ok.example" referrerpolicy="no-referrer"></iframe>`)
|