@tellescope/sdk 2.0.0-beta.0 → 2.0.0-beta.2

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