@tellescope/sdk 1.256.16 → 2.0.0-beta.1

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