@tellescope/sdk 1.256.15 → 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 +5 -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/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/file-link-session-confinement.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +357 -0
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -0
- 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 +182 -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 +8 -5
- 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/file-link-session-confinement.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +349 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -0
- 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 +182 -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/file-link-session-confinement.test.ts +291 -0
- 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 +12 -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
|
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import axios from "axios"
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
assert_checks,
|
|
7
|
+
Check,
|
|
8
|
+
setup_isolated_tenant,
|
|
9
|
+
} from "../../setup_isolated"
|
|
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
|
+
/**
|
|
16
|
+
* The `{{files.<id>.link}}` merge field must mint a session that can download ONE file, and nothing
|
|
17
|
+
* else.
|
|
18
|
+
*
|
|
19
|
+
* That merge field is how the webapp's "Generate PDF -> Save and Share -> Email" flow on the enduser
|
|
20
|
+
* timeline (DownloadFormResponse.tsx, ComposeSharePDFEmail) delivers a document to a hand-typed
|
|
21
|
+
* address — a referral contact, a caregiver, an outside provider. The link is resolved at send time
|
|
22
|
+
* by message_templating.ts, which called login_enduser with no confinement at all, so the recipient
|
|
23
|
+
* received a full 24-hour patient-portal session: chat history, every form response, every file,
|
|
24
|
+
* observations, medications, and every write route including PATCH /enduser/:id.
|
|
25
|
+
*
|
|
26
|
+
* Route access was not the only leak. Both portals call refresh-enduser-session during URL-token
|
|
27
|
+
* intake, and for a session not flagged `fromPublicSession` that handler returns the whole patient
|
|
28
|
+
* document (name, dateOfBirth, email, phone, insurance, fields, notes) — so the chart was handed
|
|
29
|
+
* over on page load, before a single route was touched. Probe 7b covers that.
|
|
30
|
+
*
|
|
31
|
+
* Two variants, because one bit — is the recipient the patient? — drives two behaviours the merge
|
|
32
|
+
* field previously could not express:
|
|
33
|
+
*
|
|
34
|
+
* `link` goes to the patient. Keeps the org's MFA policy, keeps OTP recovery.
|
|
35
|
+
* `shared-link` goes to someone who is NOT the patient. An OTP code is delivered to the PATIENT's
|
|
36
|
+
* own email or phone, so for this variant recovery is both unanswerable by the
|
|
37
|
+
* holder and a way to make the platform message the patient on demand. It is
|
|
38
|
+
* refused, and the token is minted bypassMFA so it never needs a challenge either.
|
|
39
|
+
*
|
|
40
|
+
* Probe 12 is the one that decides whether the OTP rule is real: the three OTP endpoints read their
|
|
41
|
+
* `token` argument with `jwtDecode`, which does not verify the signature. The file link puts a
|
|
42
|
+
* decodable JWT in the recipient's URL bar, so they know the enduser id, and a scope name in an
|
|
43
|
+
* unverified token is a suggestion rather than a claim. Without signature verification the rule is
|
|
44
|
+
* a paper wall.
|
|
45
|
+
*
|
|
46
|
+
* Every confinement probe is paired against the tenant's own admin session, so a red check means
|
|
47
|
+
* confinement rather than a broken fixture.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
type Probe = { status: number, body: any }
|
|
51
|
+
|
|
52
|
+
const probe = async (
|
|
53
|
+
method: 'get' | 'post' | 'patch',
|
|
54
|
+
path: string,
|
|
55
|
+
o: { token?: string, params?: object, data?: object } = {},
|
|
56
|
+
): Promise<Probe> => {
|
|
57
|
+
const r = await axios.request({
|
|
58
|
+
method,
|
|
59
|
+
url: `${host}/v1${path}`,
|
|
60
|
+
params: o.params,
|
|
61
|
+
data: o.data,
|
|
62
|
+
headers: o.token ? { Authorization: `Bearer ${o.token}` } : undefined,
|
|
63
|
+
validateStatus: () => true,
|
|
64
|
+
})
|
|
65
|
+
return { status: r.status, body: r.data }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const file_link_session_confinement_checks = async (): Promise<Check[]> => {
|
|
69
|
+
const checks: Check[] = []
|
|
70
|
+
const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
|
|
71
|
+
|
|
72
|
+
let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
tenant = await setup_isolated_tenant({
|
|
76
|
+
name: `FileLink ${RAND()}`, users: [{ isAdmin: true }], apiKeys: 0,
|
|
77
|
+
})
|
|
78
|
+
const sdk = tenant.sdk
|
|
79
|
+
|
|
80
|
+
// dontsend- so the OTP probes cannot deliver anything real, same convention the provisioner uses
|
|
81
|
+
const enduser = await sdk.api.endusers.createOne({
|
|
82
|
+
fname: 'FileLink', lname: 'Recipient',
|
|
83
|
+
email: `dontsend-filelink-${RAND()}@tellescope.com`,
|
|
84
|
+
phone: '+15555550100',
|
|
85
|
+
dateOfBirth: '01-01-1990',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// No S3 upload needed: prepare_file_upload inserts the files record with its secureName
|
|
89
|
+
// immediately, and file_download_URL only SIGNS a GET url — it never checks the object exists.
|
|
90
|
+
const newFile = async (name: string) => (
|
|
91
|
+
(await sdk.api.files.prepare_file_upload({
|
|
92
|
+
name, type: 'application/pdf', size: 1000, enduserId: enduser.id,
|
|
93
|
+
})).file
|
|
94
|
+
)
|
|
95
|
+
const fileA = await newFile(`shared-${RAND()}.pdf`) // the one that gets shared
|
|
96
|
+
const fileB = await newFile(`private-${RAND()}.pdf`) // the rest of the patient's chart
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Mints a token exactly the way a real send does — get_templated_message is the endpoint the
|
|
100
|
+
* worker calls (worker/app.js handleOutgoingEmail).
|
|
101
|
+
*
|
|
102
|
+
* replaceLinks always rewrites the resolved link into a tracking redirect, so `plaintext` holds
|
|
103
|
+
* `doc (${API_URL}/r/<redirectToken>)` rather than the portal URL. Follow the 302 to recover it.
|
|
104
|
+
*/
|
|
105
|
+
const mint = async (verb: 'link' | 'shared-link') => {
|
|
106
|
+
const { plaintext } = await sdk.api.templates.get_templated_message({
|
|
107
|
+
channel: 'Email',
|
|
108
|
+
userId: tenant!.users[0].id,
|
|
109
|
+
enduserId: enduser.id,
|
|
110
|
+
message: `{{files.${fileA.id}.${verb}:doc}}`,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const redirectURL = plaintext.match(/\((https?:\/\/[^)\s]+)\)/)?.[1]
|
|
114
|
+
if (!redirectURL) return { token: undefined, detail: `no link resolved from: ${plaintext}` }
|
|
115
|
+
|
|
116
|
+
const r = await axios.get(redirectURL, { maxRedirects: 0, validateStatus: () => true })
|
|
117
|
+
const location = r.headers['location']
|
|
118
|
+
if (!location) return { token: undefined, detail: `redirect did not 302 (${r.status})` }
|
|
119
|
+
|
|
120
|
+
const token = new URL(location).searchParams.get('token') ?? undefined
|
|
121
|
+
return { token, detail: token ? 'minted' : `no token in ${location}` }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ================================ confinement + the pin ================================ */
|
|
125
|
+
|
|
126
|
+
// Positive control first: the admin session reads both files fine, so a 403 below is confinement.
|
|
127
|
+
const adminFiles = await probe('get', '/files', { token: tenant.users[0].authToken })
|
|
128
|
+
record(
|
|
129
|
+
'control: admin session lists files',
|
|
130
|
+
adminFiles.status === 200, `status ${adminFiles.status}`,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const confinement_probes = async (variant: 'link' | 'shared-link', token: string) => {
|
|
134
|
+
const p = (label: string, expected: number, r: Probe) => record(
|
|
135
|
+
`${variant} — ${label}`, r.status === expected, `status ${r.status} (expected ${expected})`,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
p('1: downloads the shared file', 200,
|
|
139
|
+
await probe('get', '/file-download-URL', { token, params: { secureName: fileA.secureName } }))
|
|
140
|
+
|
|
141
|
+
p('2: cannot download another file on the same patient', 403,
|
|
142
|
+
await probe('get', '/file-download-URL', { token, params: { secureName: fileB.secureName } }))
|
|
143
|
+
|
|
144
|
+
// validateInput merges query, then body, then params — body wins over query, so a pin checked
|
|
145
|
+
// against req.query rather than req.validated would be sidesteppable exactly like this.
|
|
146
|
+
p('3: cannot smuggle another secureName through the body', 403,
|
|
147
|
+
await probe('get', '/file-download-URL', {
|
|
148
|
+
token, params: { secureName: fileA.secureName }, data: { secureName: fileB.secureName },
|
|
149
|
+
}))
|
|
150
|
+
|
|
151
|
+
// a listing would turn the pin into a directory of the patient's chart
|
|
152
|
+
p('4: cannot list files', 403, await probe('get', '/files', { token }))
|
|
153
|
+
|
|
154
|
+
p('5a: cannot read chats', 403, await probe('get', '/chats', { token }))
|
|
155
|
+
p('5b: cannot read form responses', 403, await probe('get', '/form-responses', { token }))
|
|
156
|
+
p('5c: cannot read observations', 403, await probe('get', '/enduser-observations', { token }))
|
|
157
|
+
|
|
158
|
+
p('6: cannot update the patient', 403,
|
|
159
|
+
await probe('patch', `/enduser/${enduser.id}`, { token, data: { updates: { fname: 'Overwritten' } } }))
|
|
160
|
+
|
|
161
|
+
const refreshed = await probe('post', '/refresh-enduser-session', { token })
|
|
162
|
+
p('7: can refresh its own session', 200, refreshed)
|
|
163
|
+
|
|
164
|
+
// 7b — the refresh payload must be the fromPublicSession shape, not the whole chart
|
|
165
|
+
const leaked = ['dateOfBirth', 'email', 'phone', 'fields', 'insurance', 'lname']
|
|
166
|
+
.filter(f => (refreshed.body?.enduser ?? {})[f] !== undefined)
|
|
167
|
+
record(
|
|
168
|
+
`${variant} — 7b: refresh response withholds patient fields`,
|
|
169
|
+
refreshed.status === 200 && leaked.length === 0,
|
|
170
|
+
leaked.length ? `leaked: ${leaked.join(', ')}` : 'no patient fields in payload',
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
// 8 — confinement has to survive the re-mint, or one refresh launders it away
|
|
174
|
+
const refreshedToken = refreshed.body?.authToken
|
|
175
|
+
if (typeof refreshedToken === 'string' && refreshedToken) {
|
|
176
|
+
p('8a: refreshed token still cannot download another file', 403,
|
|
177
|
+
await probe('get', '/file-download-URL', {
|
|
178
|
+
token: refreshedToken, params: { secureName: fileB.secureName },
|
|
179
|
+
}))
|
|
180
|
+
p('8b: refreshed token still cannot update the patient', 403,
|
|
181
|
+
await probe('patch', `/enduser/${enduser.id}`, {
|
|
182
|
+
token: refreshedToken, data: { updates: { fname: 'Overwritten' } },
|
|
183
|
+
}))
|
|
184
|
+
} else {
|
|
185
|
+
record(`${variant} — 8: refreshed token available`, false, 'refresh returned no authToken')
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const plain = await mint('link')
|
|
190
|
+
if (plain.token) {
|
|
191
|
+
await confinement_probes('link', plain.token)
|
|
192
|
+
} else {
|
|
193
|
+
record('link — minted', false, plain.detail)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const shared = await mint('shared-link')
|
|
197
|
+
if (shared.token) {
|
|
198
|
+
await confinement_probes('shared-link', shared.token)
|
|
199
|
+
} else {
|
|
200
|
+
// Before the shared-link verb exists the merge field does not resolve. Record it and skip the
|
|
201
|
+
// shared probes rather than throwing, so the first run reports the whole picture at once.
|
|
202
|
+
record('shared-link — minted', false, shared.detail)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ========================================= OTP ========================================= */
|
|
206
|
+
|
|
207
|
+
if (shared.token) {
|
|
208
|
+
const methods = await probe('get', '/endusers/otp-methods', { params: { token: shared.token } })
|
|
209
|
+
record(
|
|
210
|
+
'shared-link — 9: OTP methods are not offered',
|
|
211
|
+
methods.status === 403, `status ${methods.status} (expected 403)`,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const send = await probe('post', '/endusers/send-otp-code', {
|
|
215
|
+
data: { token: shared.token, method: 'email' },
|
|
216
|
+
})
|
|
217
|
+
record(
|
|
218
|
+
'shared-link — 10: cannot make us send the patient a login code',
|
|
219
|
+
send.status === 403, `status ${send.status} (expected 403)`,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
// rejected on scope BEFORE the code lookup, so it is not an oracle on code validity
|
|
223
|
+
const verify = await probe('post', '/endusers/verify-otp-code', {
|
|
224
|
+
data: { token: shared.token, code: '000000' },
|
|
225
|
+
})
|
|
226
|
+
record(
|
|
227
|
+
'shared-link — 11: OTP verification refuses the scope before checking the code',
|
|
228
|
+
verify.status === 403, `status ${verify.status} (expected 403, 400 = reached the code check)`,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
// 12 — the same call with the scope claim stripped and the signature broken. The OTP endpoints
|
|
232
|
+
// jwtDecode without verifying, so without signature verification this sails straight through
|
|
233
|
+
// and every check above is decorative.
|
|
234
|
+
const [header, payload] = shared.token.split('.')
|
|
235
|
+
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString())
|
|
236
|
+
delete decoded.sessionScopes
|
|
237
|
+
delete decoded.scopeContext
|
|
238
|
+
const forged = [
|
|
239
|
+
header,
|
|
240
|
+
Buffer.from(JSON.stringify(decoded)).toString('base64url'),
|
|
241
|
+
'not-a-real-signature',
|
|
242
|
+
].join('.')
|
|
243
|
+
|
|
244
|
+
const forgedSend = await probe('post', '/endusers/send-otp-code', {
|
|
245
|
+
data: { token: forged, method: 'email' },
|
|
246
|
+
})
|
|
247
|
+
record(
|
|
248
|
+
'shared-link — 12: a forged token cannot strip the scope to unlock OTP',
|
|
249
|
+
forgedSend.status === 403 || forgedSend.status === 401 || forgedSend.status === 404,
|
|
250
|
+
`status ${forgedSend.status} (expected 401/403/404; 204 = signature never verified)`,
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (plain.token) {
|
|
255
|
+
// 13 — the patient-facing variant must keep its recovery path. Deliverable to a dontsend-
|
|
256
|
+
// address, so nothing leaves the system.
|
|
257
|
+
const send = await probe('post', '/endusers/send-otp-code', {
|
|
258
|
+
data: { token: plain.token, method: 'email' },
|
|
259
|
+
})
|
|
260
|
+
record(
|
|
261
|
+
'link — 13: patient OTP recovery still works',
|
|
262
|
+
send.status === 204, `status ${send.status} (expected 204)`,
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
// Teardown BEFORE anything asserts — assert calls process.exit, which skips pending finallys and
|
|
267
|
+
// would strand the provisioned tenant permanently.
|
|
268
|
+
if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return checks
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Asserting wrapper, for standalone runs and for sequential use in the suite. */
|
|
275
|
+
export const file_link_session_confinement_tests = async () => {
|
|
276
|
+
assert_checks('Shared file link session confinement', await file_link_session_confinement_checks())
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Allow running this test file independently
|
|
280
|
+
if (require.main === module) {
|
|
281
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
282
|
+
file_link_session_confinement_tests()
|
|
283
|
+
.then(() => {
|
|
284
|
+
console.log("✅ file link session confinement test suite completed successfully")
|
|
285
|
+
process.exit(0)
|
|
286
|
+
})
|
|
287
|
+
.catch((error) => {
|
|
288
|
+
console.error("❌ file link session confinement test suite failed:", error)
|
|
289
|
+
process.exit(1)
|
|
290
|
+
})
|
|
291
|
+
}
|