@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,197 @@
1
+ require('source-map-support').install();
2
+
3
+ import { EnduserSession } from "../../../sdk"
4
+ import {
5
+ assert_checks,
6
+ Check,
7
+ setup_isolated_tenant,
8
+ } from "../../setup_isolated"
9
+
10
+ const host = process.env.API_URL || 'http://localhost:8080' as const
11
+
12
+ const RAND = () => Math.random().toString(36).slice(2, 10)
13
+
14
+ /**
15
+ * Form "Rich Text" answers must be sanitized when they are stored, not only when they are rendered.
16
+ *
17
+ * A Rich Text answer is HTML authored by whoever fills the form — including an unauthenticated
18
+ * visitor on a public intake page — and it is stored verbatim by the generic write path (the only
19
+ * thing it strips is `<script>`). Every read path re-sanitizes before `dangerouslySetInnerHTML`,
20
+ * but the staff-side rich text *editor* did not: `@tellescope/react-components`'s `RichTextInput`
21
+ * fed the stored answer straight into `html-to-draftjs`, whose `<iframe>` handling produces a
22
+ * Draft.js EMBEDDED_LINK block that `react-draft-wysiwyg` renders as a real, unsandboxed iframe.
23
+ * So an enduser could submit `<iframe src="javascript:…">` in an answer and have it execute in the
24
+ * session of the staff member who opened that response for editing (enduser -> staff stored XSS,
25
+ * i.e. a PHI-egress class). The editor was fixed to sanitize before parsing; this test covers the
26
+ * second layer: the answer is sanitized once, on write, so exports, PDFs, integrations, AI
27
+ * summaries and any future consumer inherit it instead of each having to remember.
28
+ *
29
+ * Every client write of answers goes through `formResponseValidator` — generic create/update
30
+ * (endusers may PATCH `responses`), `submit_form_response` and `save_field_response` — so all three
31
+ * are exercised here. The server-side writers that bypass validation (Formsort, Healthie sync, AI
32
+ * form filling) never construct a `'Rich Text'` answer; see the note on the validator in
33
+ * `@tellescope/validation`.
34
+ *
35
+ * The negative checks are paired with positive ones: formatting and legitimate external links must
36
+ * survive, or a "sanitized" result could just mean the answer was discarded. The last two checks
37
+ * guard the two ways this change could break normal use: an empty answer must round-trip
38
+ * unchanged (the portal compares against `''` / `'<p></p>'`), and a long answer must not be
39
+ * rejected because the sanitizer's own `target`/`rel` additions pushed it over the length limit.
40
+ */
41
+ export const rich_text_answer_sanitized_on_write_checks = async (): Promise<Check[]> => {
42
+ const checks: Check[] = []
43
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
44
+
45
+ const PAYLOAD = '<p>hi <b>bold</b> <a href="https://x.io">l</a></p>'
46
+ + '<iframe src="javascript:alert(1)"></iframe>'
47
+ + '<img src="x" onerror="alert(1)">'
48
+ + '<script>alert(1)</script>'
49
+ + '<a href="javascript:alert(2)">j</a>'
50
+
51
+ const dangerous = (stored: string) => ([
52
+ ['<iframe', stored.toLowerCase().includes('<iframe')],
53
+ ['onerror', stored.toLowerCase().includes('onerror')],
54
+ ['<script', stored.toLowerCase().includes('<script')],
55
+ ['javascript:', stored.toLowerCase().includes('javascript:')],
56
+ ] as [string, boolean][]).filter(([, present]) => present).map(([name]) => name)
57
+
58
+ const record_sanitized = (label: string, stored: string | undefined) => {
59
+ if (typeof stored !== 'string') {
60
+ record(label, false, `no answer stored (got ${JSON.stringify(stored)})`)
61
+ return
62
+ }
63
+ const bad = dangerous(stored)
64
+ const keptFormatting = stored.includes('<b>bold</b>')
65
+ const keptLink = stored.includes('href="https://x.io"') && stored.includes('rel="noopener noreferrer"')
66
+
67
+ record(label, bad.length === 0 && keptFormatting && keptLink,
68
+ bad.length !== 0 ? `dangerous markup stored: ${bad.join(', ')} — ${stored.slice(0, 300)}`
69
+ : !keptFormatting ? `formatting did not survive — ${stored.slice(0, 300)}`
70
+ : !keptLink ? `external link lost its href or rel — ${stored.slice(0, 300)}`
71
+ : `stored sanitized: ${stored.slice(0, 300)}`
72
+ )
73
+ }
74
+
75
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
76
+
77
+ try {
78
+ tenant = await setup_isolated_tenant({ name: `RichText ${RAND()}`, users: [{ isAdmin: true }], apiKeys: 0 })
79
+ const sdk = tenant.sdk
80
+
81
+ const form = await sdk.api.forms.createOne({ title: `rich text ${RAND()}` })
82
+ const field = await sdk.api.form_fields.createOne({
83
+ formId: form.id, title: 'Tell us more', type: 'Rich Text', previousFields: [{ type: 'root', info: {} }],
84
+ })
85
+ const enduser = await sdk.api.endusers.createOne({
86
+ email: `richtext-${RAND()}@tellescope.com`, fname: 'Rich', lname: 'Text',
87
+ })
88
+ // generate_auth_token rather than .authenticate — repeated logins hit the rate limiter.
89
+ const { authToken } = await sdk.api.endusers.generate_auth_token({ id: enduser.id })
90
+ const enduserSDK = new EnduserSession({ host, businessId: tenant.businessId, authToken })
91
+
92
+ const prepare = () => sdk.api.form_responses.prepare_form_response({ formId: form.id, enduserId: enduser.id })
93
+ const answer = (value: string) => ({
94
+ fieldId: field.id, fieldTitle: field.title, answer: { type: 'Rich Text' as const, value },
95
+ })
96
+ /** What is actually in the database, read back by an admin session. */
97
+ const stored_answer = async (formResponseId: string) => {
98
+ const formResponse = await sdk.api.form_responses.getOne(formResponseId)
99
+ const response = (formResponse.responses || []).find(r => r.fieldId === field.id)
100
+ return (response?.answer as { value?: string } | undefined)?.value
101
+ }
102
+
103
+ /* ===== 1. submit_form_response — the path a patient filling out a form takes ===== */
104
+
105
+ const submitted = await prepare()
106
+ await enduserSDK.api.form_responses.submit_form_response({
107
+ accessCode: submitted.accessCode, responses: [answer(PAYLOAD)],
108
+ })
109
+ const submittedValue = await stored_answer(submitted.response.id)
110
+ record_sanitized('1: submit_form_response stores a sanitized answer', submittedValue)
111
+
112
+ /* ===== 2. save_field_response — the partial/draft save the portal fires per question ===== */
113
+
114
+ const partial = await prepare()
115
+ await enduserSDK.api.form_responses.save_field_response({
116
+ accessCode: partial.accessCode, response: answer(PAYLOAD),
117
+ })
118
+ record_sanitized('2: save_field_response stores a sanitized answer', await stored_answer(partial.response.id))
119
+
120
+ /* ===== 3. generic PATCH of responses — endusers are allowed to update their own ===== */
121
+
122
+ const patched = await prepare()
123
+ await enduserSDK.api.form_responses.updateOne(patched.response.id, { responses: [answer(PAYLOAD)] })
124
+ record_sanitized('3: enduser PATCH of responses stores a sanitized answer', await stored_answer(patched.response.id))
125
+
126
+ /* ===== 4. Idempotence: re-submitting what was stored must not change it again ===== */
127
+
128
+ if (typeof submittedValue === 'string') {
129
+ const resubmitted = await prepare()
130
+ await enduserSDK.api.form_responses.submit_form_response({
131
+ accessCode: resubmitted.accessCode, responses: [answer(submittedValue)],
132
+ })
133
+ const again = await stored_answer(resubmitted.response.id)
134
+ record('4: re-submitting a sanitized answer is idempotent', again === submittedValue,
135
+ again === submittedValue ? 'unchanged on the second pass' : `changed — ${String(again).slice(0, 300)}`)
136
+ } else {
137
+ record('4: re-submitting a sanitized answer is idempotent', false, 'skipped: check 1 stored nothing')
138
+ }
139
+
140
+ /* ===== 5. Empty answers round-trip: the portal treats '' and '<p></p>' as "unanswered" ===== */
141
+
142
+ const empties = await prepare()
143
+ const roundTrips: string[] = []
144
+ for (const value of ['<p></p>', '']) {
145
+ await enduserSDK.api.form_responses.save_field_response({
146
+ accessCode: empties.accessCode, response: answer(value),
147
+ })
148
+ roundTrips.push(`${JSON.stringify(value)} -> ${JSON.stringify(await stored_answer(empties.response.id))}`)
149
+ }
150
+ const emptiesOk = roundTrips[0] === '"<p></p>" -> "<p></p>"' && roundTrips[1] === '"" -> ""'
151
+ record('5: empty and <p></p> answers round-trip unchanged', emptiesOk, roundTrips.join(', '))
152
+
153
+ /* ===== 6. Length headroom: the sanitizer ADDS target/rel to every external link ===== */
154
+
155
+ // ~24.9k of legitimate answer, just under the old 25000 limit. Sanitizing adds ~41 characters
156
+ // per external link, so at the old limit the sanitized result would be rejected on a re-save.
157
+ const LINKS = 12
158
+ const longAnswer = `<p>${'a'.repeat(24400)}</p>`
159
+ + `<a href="https://example.com/a">link</a>`.repeat(LINKS)
160
+ const longResponse = await prepare()
161
+ const longResult = await enduserSDK.api.form_responses.submit_form_response({
162
+ accessCode: longResponse.accessCode, responses: [answer(longAnswer)],
163
+ }).then(() => stored_answer(longResponse.response.id))
164
+ .catch((e: any) => `rejected: ${e?.message || e}`)
165
+
166
+ const longOk = typeof longResult === 'string'
167
+ && longResult.split('rel="noopener noreferrer"').length - 1 === LINKS
168
+ record('6: a ~24.9k answer with external links is accepted after sanitizing', longOk,
169
+ longOk ? `stored ${longResult.length} characters with ${LINKS} rel-tagged links (submitted ${longAnswer.length})`
170
+ : `submitted ${longAnswer.length} characters — ${String(longResult).slice(0, 300)}`)
171
+ } finally {
172
+ // Teardown BEFORE anything asserts — assert calls process.exit, which skips pending finallys and
173
+ // would strand the provisioned tenant permanently.
174
+ if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
175
+ }
176
+
177
+ return checks
178
+ }
179
+
180
+ /** Asserting wrapper, for standalone runs and for sequential use in the suite. */
181
+ export const rich_text_answer_sanitized_on_write_tests = async () => {
182
+ assert_checks('Rich Text answers sanitized on write', await rich_text_answer_sanitized_on_write_checks())
183
+ }
184
+
185
+ // Allow running this test file independently
186
+ if (require.main === module) {
187
+ console.log(`🌐 Using API URL: ${host}`)
188
+ rich_text_answer_sanitized_on_write_tests()
189
+ .then(() => {
190
+ console.log("✅ rich text answer sanitization test suite completed successfully")
191
+ process.exit(0)
192
+ })
193
+ .catch((error) => {
194
+ console.error("❌ rich text answer sanitization test suite failed:", error)
195
+ process.exit(1)
196
+ })
197
+ }
@@ -147,7 +147,10 @@ export const sanitize_user_html_with_iframes_tests = async () => {
147
147
  // sandbox="" is the MOST restrictive value and must not be replaced by the default
148
148
  const emptySandbox = sanitize_user_html_with_iframes(`<iframe src="https://ok.example" sandbox=""></iframe>`)
149
149
  if (emptySandbox.includes('allow-')) fail(`empty sandbox was replaced with the permissive default: ${emptySandbox}`)
150
- if (!/\ssandbox(\s|>)/.test(emptySandbox)) fail(`empty sandbox attribute lost entirely: ${emptySandbox}`)
150
+ // Accepts either spelling — a bare `sandbox` and `sandbox=""` are identical per the HTML spec,
151
+ // and which one sanitize-html emits has changed between releases. What must never happen is the
152
+ // attribute disappearing, which leaves the frame with no sandbox at all.
153
+ if (!/\ssandbox(=""|\s|>)/.test(emptySandbox)) fail(`empty sandbox attribute lost entirely: ${emptySandbox}`)
151
154
 
152
155
  // author referrerpolicy honored
153
156
  const noref = sanitize_user_html_with_iframes(`<iframe src="https://ok.example" referrerpolicy="no-referrer"></iframe>`)
@@ -0,0 +1,146 @@
1
+ require('source-map-support').install();
2
+
3
+ import {
4
+ assert_checks,
5
+ Check,
6
+ setup_isolated_tenant,
7
+ } from "../setup_isolated"
8
+
9
+ const host = process.env.API_URL || 'http://localhost:8080' as const
10
+
11
+ /**
12
+ * Coverage for how `get_templated_message` renders enduser field values that LOOK like dates.
13
+ *
14
+ * The rule: a value stored as text renders as the author stored it. `message_templating.ts` only
15
+ * reformats an actual `Date` object, and a custom field is always a string.
16
+ *
17
+ * This exists because that rule was silently broken IN PRODUCTION, and had been for a long time.
18
+ * The templating code read `if (fieldValue instanceof Date || isDate(fieldValue))`. Validator
19
+ * 13.11.0's `isDate` was timezone-dependent — it compared a UTC-parsed date against a local-time
20
+ * `.getDate()` — so it returned false on a US developer's machine but TRUE in UTC, which is what
21
+ * the servers run. A date of birth stored as "1985-03-12" therefore took the Date branch in
22
+ * production, was parsed as UTC midnight and localized to the enduser's timezone, and rendered in a
23
+ * patient-facing SMS or email as "3/11/1985, 7:00:00 PM" — the PREVIOUS day, with a time nobody
24
+ * entered. A January date lost a year: "2024-01-01" rendered "12/31/2023".
25
+ *
26
+ * The validator bump did not cause this; it made it reproducible locally, which is how it surfaced.
27
+ * Removing the `isDate` half of the condition fixes it in every timezone.
28
+ *
29
+ * This is the ONE intentional production behavior change in an otherwise behavior-neutral
30
+ * dependency PR, made deliberately rather than shipping wrong dates to patients for another
31
+ * cycle. Every other validator touched here was pinned to production's existing behavior.
32
+ *
33
+ * So the assertions below are about the day not moving and no clock time appearing, which is what a
34
+ * patient would actually notice. A pure-function test could not have caught this: the function that
35
+ * does the work is module-private and the endpoint is the real surface.
36
+ *
37
+ * Runs standalone or as part of the main suite:
38
+ * npm run test:modular -- api_tests/templated_message_date_fields
39
+ */
40
+
41
+ /** Values a customer realistically stores in a date-ish custom field, and what must come back. */
42
+ const DATE_SHAPED_VALUES = [
43
+ { label: 'ISO date (the shape that regressed)', value: '1985-03-12' },
44
+ { label: 'ISO date, recent', value: '2024-01-01' },
45
+ { label: 'slash-separated date', value: '1999/12/31' },
46
+ { label: 'MM-DD-YYYY (the dateOfBirth format)', value: '03-12-1985' },
47
+ { label: 'ISO datetime string', value: '2024-06-15T14:30:00.000Z' },
48
+ { label: 'leap day', value: '2000-02-29' },
49
+ ]
50
+
51
+ /** A rendered date-only value must not have grown a clock time or a timezone abbreviation. */
52
+ const LOOKS_REFORMATTED = /\d{1,2}:\d{2}|\bAM\b|\bPM\b|\bE[SD]T\b|\bUTC\b|\bGMT\b/
53
+
54
+ const templated_message_date_field_checks = async (): Promise<Check[]> => {
55
+ const checks: Check[] = []
56
+ const tenant = await setup_isolated_tenant({ users: [{ isAdmin: true }] })
57
+
58
+ try {
59
+ const { sdk } = tenant
60
+ const userId = tenant.users[0].id
61
+
62
+ const fields = Object.fromEntries(DATE_SHAPED_VALUES.map((c, i) => [`DateField${i}`, c.value]))
63
+ const enduser = await sdk.api.endusers.createOne({
64
+ fname: 'Date',
65
+ lname: 'Fields',
66
+ timezone: 'America/New_York', // the timezone the shifted rendering localized into
67
+ fields,
68
+ })
69
+
70
+ for (let i = 0; i < DATE_SHAPED_VALUES.length; i++) {
71
+ const { label, value } = DATE_SHAPED_VALUES[i]
72
+ const { plaintext } = await sdk.api.templates.get_templated_message({
73
+ enduserId: enduser.id,
74
+ userId,
75
+ channel: 'Chat',
76
+ message: `[{{enduser.DateField${i}}}]`,
77
+ })
78
+
79
+ checks.push({
80
+ label: `${label} renders exactly as stored`,
81
+ pass: plaintext === `[${value}]`,
82
+ detail: `stored ${JSON.stringify(value)}, rendered ${JSON.stringify(plaintext)}`,
83
+ })
84
+ // A second, independent signal for the specific symptom, so this still catches a shifted
85
+ // rendering even if the equality check above is ever loosened. Only meaningful for values
86
+ // that carry no time of their own — an ISO datetime legitimately contains one.
87
+ if (!LOOKS_REFORMATTED.test(value)) {
88
+ checks.push({
89
+ label: `${label} does not gain a clock time`,
90
+ pass: !LOOKS_REFORMATTED.test(plaintext),
91
+ detail: `rendered ${JSON.stringify(plaintext)}`,
92
+ })
93
+ }
94
+ }
95
+
96
+ // The specific patient-visible symptom, asserted on its own so a failure names it.
97
+ const { plaintext: dob } = await sdk.api.templates.get_templated_message({
98
+ enduserId: enduser.id,
99
+ userId,
100
+ channel: 'Chat',
101
+ message: `DOB: {{enduser.DateField0}}`,
102
+ })
103
+ checks.push({
104
+ label: 'a date of birth does not render as the previous day',
105
+ pass: dob.includes('1985-03-12') && !dob.includes('3/11/1985'),
106
+ detail: `rendered ${JSON.stringify(dob)}`,
107
+ })
108
+
109
+ // Positive control: if templating itself were broken, every check above would pass vacuously
110
+ // (an unreplaced `{{enduser.X}}` is neither reformatted nor time-stamped).
111
+ const { plaintext: control } = await sdk.api.templates.get_templated_message({
112
+ enduserId: enduser.id,
113
+ userId,
114
+ channel: 'Chat',
115
+ message: `{{enduser.fname}} {{enduser.lname}}`,
116
+ })
117
+ checks.push({
118
+ label: 'control: non-date fields are still templated at all',
119
+ pass: control === 'Date Fields',
120
+ detail: `rendered ${JSON.stringify(control)}`,
121
+ })
122
+
123
+ return checks
124
+ } finally {
125
+ await tenant.teardown()
126
+ }
127
+ }
128
+
129
+ /** Asserting wrapper, for standalone runs and for sequential use in the suite. */
130
+ export const templated_message_date_field_tests = async () => {
131
+ assert_checks('Templated message date-shaped fields', await templated_message_date_field_checks())
132
+ }
133
+
134
+ // Allow running this test file independently
135
+ if (require.main === module) {
136
+ console.log(`🌐 Using API URL: ${host}`)
137
+ templated_message_date_field_tests()
138
+ .then(() => {
139
+ console.log("✅ Templated message date field test suite completed successfully")
140
+ process.exit(0)
141
+ })
142
+ .catch((error) => {
143
+ console.error("❌ Templated message date field test suite failed:", error)
144
+ process.exit(1)
145
+ })
146
+ }
@@ -0,0 +1,192 @@
1
+ require('source-map-support').install();
2
+
3
+ import {
4
+ assert_checks,
5
+ Check,
6
+ setup_isolated_tenant,
7
+ } from "../setup_isolated"
8
+
9
+ const host = process.env.API_URL || 'http://localhost:8080' as const
10
+
11
+ /**
12
+ * Link templating through the real `templates.get_templated_message` — the same endpoint the worker
13
+ * calls on every outbound email and SMS.
14
+ *
15
+ * The exhaustive coverage of this path lives in the API's own module test
16
+ * (`packages/private/api/api/modules/message_templating.test.ts`), which stubs the database so a case
17
+ * costs microseconds. This suite exists to prove those stubs still describe the real thing: that a
18
+ * rendered link really is a `/r/<token>` redirect, and that `linkOpenTrackingIds` really is one id per
19
+ * redirect record created.
20
+ *
21
+ * Deliberately small. Every case here writes real `url_redirects` rows, and `teardown_tenant` does not
22
+ * remove them — that collection is in neither `schema` nor `getInternalQueries()` — so each extra case
23
+ * leaves rows behind in the database forever.
24
+ *
25
+ * Runs standalone or as part of the main suite:
26
+ * npm run test:modular -- api_tests/templated_message_links
27
+ */
28
+
29
+ /** A tracked link is `<portal or api url>/r/<token>`; the token itself is not predictable. */
30
+ const REDIRECT = /https?:\/\/[^\s"'<>]+\/r\/[A-Za-z0-9_-]+/
31
+ /** Bound on the one case that used to hang the request, so a regression fails instead of stalling. */
32
+ const CLIENT_WAIT_MS = 20_000
33
+ const LINK = 'https://example.com/landing'
34
+
35
+ const templated_message_link_checks = async (): Promise<Check[]> => {
36
+ const checks: Check[] = []
37
+ const tenant = await setup_isolated_tenant({ users: [{ isAdmin: true }] })
38
+
39
+ try {
40
+ const { sdk } = tenant
41
+ const userId = tenant.users[0].id
42
+ const enduser = await sdk.api.endusers.createOne({ fname: 'Link', lname: 'Templating' })
43
+
44
+ const templated = (message: string, html: string) => sdk.api.templates.get_templated_message({
45
+ enduserId: enduser.id, userId, channel: 'Email', message, html,
46
+ })
47
+
48
+ // 1. one link: an anchor in html, "text (url)" in plaintext, one tracking id
49
+ {
50
+ const body = `Go: {${LINK}}[Click]`
51
+ const { html, plaintext, linkOpenTrackingIds } = await templated(body, `<p>${body}</p>`)
52
+
53
+ checks.push({
54
+ label: 'html renders an anchor whose href is a /r/ redirect',
55
+ pass: new RegExp(`^<p>Go: <a href="${REDIRECT.source}">Click</a></p>$`).test(html),
56
+ detail: `rendered ${JSON.stringify(html)}`,
57
+ })
58
+ checks.push({
59
+ label: 'plaintext appends the same redirect in parentheses',
60
+ pass: new RegExp(`^Go: Click \\(${REDIRECT.source}\\)$`).test(plaintext),
61
+ detail: `rendered ${JSON.stringify(plaintext)}`,
62
+ })
63
+ checks.push({
64
+ label: 'one link creates exactly one redirect record',
65
+ pass: linkOpenTrackingIds.length === 1,
66
+ detail: `linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
67
+ })
68
+ }
69
+
70
+ // 2. several links: one record each, all replaced
71
+ {
72
+ const body = `One {${LINK}/a}[A], two {${LINK}/b}[B], three {${LINK}/c}[C]`
73
+ const { html, linkOpenTrackingIds } = await templated(body, `<p>${body}</p>`)
74
+
75
+ checks.push({
76
+ label: 'every link in one message is replaced',
77
+ pass: (html.match(/<a href="/g) || []).length === 3 && !html.includes('{'),
78
+ detail: `rendered ${JSON.stringify(html)}`,
79
+ })
80
+ checks.push({
81
+ label: 'three links create exactly three redirect records',
82
+ pass: linkOpenTrackingIds.length === 3,
83
+ detail: `linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
84
+ })
85
+ }
86
+
87
+ // 3. $LINK_ONLY: the bare redirect url, no anchor
88
+ {
89
+ const body = `Go: {${LINK}}[$LINK_ONLY]`
90
+ const { html, plaintext, linkOpenTrackingIds } = await templated(body, `<p>${body}</p>`)
91
+
92
+ checks.push({
93
+ label: '$LINK_ONLY substitutes the bare redirect url',
94
+ pass: new RegExp(`^<p>Go: ${REDIRECT.source}</p>$`).test(html)
95
+ && new RegExp(`^Go: ${REDIRECT.source}$`).test(plaintext),
96
+ detail: `html ${JSON.stringify(html)}, plaintext ${JSON.stringify(plaintext)}`,
97
+ })
98
+ checks.push({
99
+ label: '$LINK_ONLY still creates its redirect record',
100
+ pass: linkOpenTrackingIds.length === 1,
101
+ detail: `linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
102
+ })
103
+ }
104
+
105
+ // 4. a link merge field resolves to the same {url}[text] form, then to an anchor
106
+ {
107
+ const form = await sdk.api.forms.createOne({ title: `Link templating ${Date.now()}` })
108
+ try {
109
+ const body = `Start here: {{forms.${form.id}.link:Begin}}`
110
+ const { html, linkOpenTrackingIds } = await templated(body, `<p>${body}</p>`)
111
+
112
+ checks.push({
113
+ label: 'a form link merge field becomes an anchor on a redirect',
114
+ pass: new RegExp(`^<p>Start here: <a href="${REDIRECT.source}">Begin</a></p>$`).test(html),
115
+ detail: `rendered ${JSON.stringify(html)}`,
116
+ })
117
+ checks.push({
118
+ label: 'the form link creates exactly one redirect record',
119
+ pass: linkOpenTrackingIds.length === 1,
120
+ detail: `linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
121
+ })
122
+ } finally {
123
+ await sdk.api.forms.deleteOne(form.id)
124
+ }
125
+ }
126
+
127
+ // 5. The loop-safety case, end to end. A space between the two halves of a link template is one
128
+ // find_link matches but the replacement key cannot, and this request used to never come back:
129
+ // the server looped on that same match, inserting a url_redirects row every time round. The
130
+ // client-side bound is what turns a regression here into a failure rather than a hung suite.
131
+ {
132
+ const body = `Go: {${LINK}} [Click]`
133
+ const outcome = await Promise.race([
134
+ templated(body, `<p>${body}</p>`).then(r => r).catch(e => ({ error: e?.message ?? String(e) } as any)),
135
+ new Promise<any>(resolve => setTimeout(() => resolve({ timedOut: true }), CLIENT_WAIT_MS)),
136
+ ])
137
+
138
+ checks.push({
139
+ label: 'a template the server cannot replace still returns a response',
140
+ pass: !outcome.timedOut && !outcome.error,
141
+ detail: outcome.timedOut
142
+ ? `no response within ${CLIENT_WAIT_MS}ms — the templating loop stopped terminating`
143
+ : outcome.error ? `errored: ${outcome.error}` : 'responded',
144
+ })
145
+ if (!outcome.timedOut && !outcome.error) {
146
+ checks.push({
147
+ label: 'it is left literal in both the html and the plaintext',
148
+ pass: outcome.html === `<p>${body}</p>` && outcome.plaintext === body,
149
+ detail: `html ${JSON.stringify(outcome.html)}, plaintext ${JSON.stringify(outcome.plaintext)}`,
150
+ })
151
+ checks.push({
152
+ label: 'and it creates no redirect records at all',
153
+ pass: outcome.linkOpenTrackingIds.length === 0,
154
+ detail: `linkOpenTrackingIds=${JSON.stringify(outcome.linkOpenTrackingIds)}`,
155
+ })
156
+ }
157
+ }
158
+
159
+ // Positive control: a body with no link syntax is left alone and creates nothing.
160
+ {
161
+ const { html, linkOpenTrackingIds } = await templated('No links here', '<p>No links here</p>')
162
+ checks.push({
163
+ label: 'control: a body without link syntax is untouched and creates no records',
164
+ pass: html === '<p>No links here</p>' && linkOpenTrackingIds.length === 0,
165
+ detail: `rendered ${JSON.stringify(html)}, linkOpenTrackingIds=${JSON.stringify(linkOpenTrackingIds)}`,
166
+ })
167
+ }
168
+
169
+ return checks
170
+ } finally {
171
+ await tenant.teardown()
172
+ }
173
+ }
174
+
175
+ /** Asserting wrapper, for standalone runs and for sequential use in the suite. */
176
+ export const templated_message_link_tests = async () => {
177
+ assert_checks('Templated message links', await templated_message_link_checks())
178
+ }
179
+
180
+ // Allow running this test file independently
181
+ if (require.main === module) {
182
+ console.log(`🌐 Using API URL: ${host}`)
183
+ templated_message_link_tests()
184
+ .then(() => {
185
+ console.log("✅ Templated message link test suite completed successfully")
186
+ process.exit(0)
187
+ })
188
+ .catch((error) => {
189
+ console.error("❌ Templated message link test suite failed:", error)
190
+ process.exit(1)
191
+ })
192
+ }
@@ -6,7 +6,7 @@ import { setup_tests } from "../setup"
6
6
 
7
7
  const log = console.log
8
8
 
9
- const host = process.env.REACT_APP_TELLESCOPE_API_URL || 'http://localhost:8080'
9
+ const host = process.env.API_URL || process.env.REACT_APP_TELLESCOPE_API_URL || 'http://localhost:8080'
10
10
 
11
11
  // Helper to assert that an async function throws an error
12
12
  const assert_throws = async (fn: () => Promise<any>, description: string) => {