@tellescope/sdk 1.256.16 → 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) 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 +3 -1
  6. package/lib/cjs/sdk.d.ts.map +1 -1
  7. package/lib/cjs/sdk.js +1 -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_webhooks.test.d.ts.map +1 -1
  38. package/lib/cjs/tests/api_tests/mdi_webhooks.test.js +21 -8
  39. package/lib/cjs/tests/api_tests/mdi_webhooks.test.js.map +1 -1
  40. package/lib/cjs/tests/api_tests/nutrition_tracking.test.js +1 -1
  41. package/lib/cjs/tests/api_tests/nutrition_tracking.test.js.map +1 -1
  42. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +1 -1
  43. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  44. package/lib/cjs/tests/api_tests/programs.test.d.ts +5 -0
  45. package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -0
  46. package/lib/cjs/tests/api_tests/programs.test.js +370 -0
  47. package/lib/cjs/tests/api_tests/programs.test.js.map +1 -0
  48. package/lib/cjs/tests/api_tests/programs_trigger_filters.test.d.ts +5 -0
  49. package/lib/cjs/tests/api_tests/programs_trigger_filters.test.d.ts.map +1 -0
  50. package/lib/cjs/tests/api_tests/programs_trigger_filters.test.js +295 -0
  51. package/lib/cjs/tests/api_tests/programs_trigger_filters.test.js.map +1 -0
  52. package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.js +1 -1
  53. package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.js.map +1 -1
  54. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +42 -0
  55. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -0
  56. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +484 -0
  57. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -0
  58. package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.js +1 -1
  59. package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.js.map +1 -1
  60. package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +1 -1
  61. package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -1
  62. package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.js +1 -1
  63. package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.js.map +1 -1
  64. package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.js +1 -1
  65. package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.js.map +1 -1
  66. package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.js +2 -2
  67. package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.js.map +1 -1
  68. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +32 -0
  69. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -0
  70. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +297 -0
  71. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -0
  72. package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts.map +1 -1
  73. package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.js +4 -1
  74. package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.js.map +1 -1
  75. package/lib/cjs/tests/api_tests/templated_message_date_fields.test.d.ts +3 -0
  76. package/lib/cjs/tests/api_tests/templated_message_date_fields.test.d.ts.map +1 -0
  77. package/lib/cjs/tests/api_tests/templated_message_date_fields.test.js +202 -0
  78. package/lib/cjs/tests/api_tests/templated_message_date_fields.test.js.map +1 -0
  79. package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.d.ts +3 -0
  80. package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.d.ts.map +1 -0
  81. package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.js +233 -0
  82. package/lib/cjs/tests/api_tests/templated_message_link_in_href.test.js.map +1 -0
  83. package/lib/cjs/tests/api_tests/templated_message_links.test.d.ts +3 -0
  84. package/lib/cjs/tests/api_tests/templated_message_links.test.d.ts.map +1 -0
  85. package/lib/cjs/tests/api_tests/templated_message_links.test.js +235 -0
  86. package/lib/cjs/tests/api_tests/templated_message_links.test.js.map +1 -0
  87. package/lib/cjs/tests/api_tests/time_tracks.test.js +1 -1
  88. package/lib/cjs/tests/api_tests/time_tracks.test.js.map +1 -1
  89. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +5 -0
  90. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -0
  91. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +395 -0
  92. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -0
  93. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +5 -0
  94. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -0
  95. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +471 -0
  96. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -0
  97. package/lib/cjs/tests/api_tests/user_portal_settings.test.js +1 -1
  98. package/lib/cjs/tests/api_tests/user_portal_settings.test.js.map +1 -1
  99. package/lib/cjs/tests/api_tests/validation_behavior_locks.test.d.ts +2 -0
  100. package/lib/cjs/tests/api_tests/validation_behavior_locks.test.d.ts.map +1 -0
  101. package/lib/cjs/tests/api_tests/validation_behavior_locks.test.js +233 -0
  102. package/lib/cjs/tests/api_tests/validation_behavior_locks.test.js.map +1 -0
  103. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +5 -0
  104. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -0
  105. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +569 -0
  106. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -0
  107. package/lib/cjs/tests/public_endpoint_tests.js +3 -2
  108. package/lib/cjs/tests/public_endpoint_tests.js.map +1 -1
  109. package/lib/cjs/tests/setup.d.ts +16 -0
  110. package/lib/cjs/tests/setup.d.ts.map +1 -1
  111. package/lib/cjs/tests/setup.js +55 -8
  112. package/lib/cjs/tests/setup.js.map +1 -1
  113. package/lib/cjs/tests/setup_isolated.d.ts +9 -0
  114. package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
  115. package/lib/cjs/tests/setup_isolated.js +23 -2
  116. package/lib/cjs/tests/setup_isolated.js.map +1 -1
  117. package/lib/cjs/tests/socket_tests.d.ts.map +1 -1
  118. package/lib/cjs/tests/socket_tests.js +3 -2
  119. package/lib/cjs/tests/socket_tests.js.map +1 -1
  120. package/lib/cjs/tests/tests.d.ts.map +1 -1
  121. package/lib/cjs/tests/tests.js +180 -166
  122. package/lib/cjs/tests/tests.js.map +1 -1
  123. package/lib/cjs/tests/unit_tests/trigger_title_partials.test.d.ts +3 -0
  124. package/lib/cjs/tests/unit_tests/trigger_title_partials.test.d.ts.map +1 -0
  125. package/lib/cjs/tests/unit_tests/trigger_title_partials.test.js +118 -0
  126. package/lib/cjs/tests/unit_tests/trigger_title_partials.test.js.map +1 -0
  127. package/lib/cjs/tests/webhooks_tests.js +3 -2
  128. package/lib/cjs/tests/webhooks_tests.js.map +1 -1
  129. package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  130. package/lib/esm/enduser.d.ts +2 -0
  131. package/lib/esm/enduser.d.ts.map +1 -1
  132. package/lib/esm/sdk.d.ts +6 -3
  133. package/lib/esm/sdk.d.ts.map +1 -1
  134. package/lib/esm/sdk.js +1 -0
  135. package/lib/esm/sdk.js.map +1 -1
  136. package/lib/esm/session.d.ts +2 -0
  137. package/lib/esm/session.d.ts.map +1 -1
  138. package/lib/esm/tests/api_tests/active_patient_count.test.js +1 -1
  139. package/lib/esm/tests/api_tests/active_patient_count.test.js.map +1 -1
  140. package/lib/esm/tests/api_tests/appointment_rescheduled_trigger.test.js +1 -1
  141. package/lib/esm/tests/api_tests/appointment_rescheduled_trigger.test.js.map +1 -1
  142. package/lib/esm/tests/api_tests/calendar_event_limits.test.js +5 -5
  143. package/lib/esm/tests/api_tests/calendar_event_limits.test.js.map +1 -1
  144. package/lib/esm/tests/api_tests/calendar_event_references_update.test.js +1 -1
  145. package/lib/esm/tests/api_tests/calendar_event_references_update.test.js.map +1 -1
  146. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js +1 -1
  147. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
  148. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +21 -0
  149. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -0
  150. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +210 -0
  151. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -0
  152. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js +7 -7
  153. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -1
  154. package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js +4 -4
  155. package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js.map +1 -1
  156. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +32 -0
  157. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -0
  158. package/lib/esm/tests/api_tests/load_threads_care_team.test.js +252 -0
  159. package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -0
  160. package/lib/esm/tests/api_tests/managed_content_enduser_access.test.js +1 -1
  161. package/lib/esm/tests/api_tests/managed_content_enduser_access.test.js.map +1 -1
  162. package/lib/esm/tests/api_tests/managed_content_file_access.test.js +1 -1
  163. package/lib/esm/tests/api_tests/managed_content_file_access.test.js.map +1 -1
  164. package/lib/esm/tests/api_tests/mdi_webhooks.test.d.ts.map +1 -1
  165. package/lib/esm/tests/api_tests/mdi_webhooks.test.js +21 -8
  166. package/lib/esm/tests/api_tests/mdi_webhooks.test.js.map +1 -1
  167. package/lib/esm/tests/api_tests/nutrition_tracking.test.js +1 -1
  168. package/lib/esm/tests/api_tests/nutrition_tracking.test.js.map +1 -1
  169. package/lib/esm/tests/api_tests/openloop_webhooks.test.js +1 -1
  170. package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  171. package/lib/esm/tests/api_tests/programs.test.d.ts +5 -0
  172. package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -0
  173. package/lib/esm/tests/api_tests/programs.test.js +365 -0
  174. package/lib/esm/tests/api_tests/programs.test.js.map +1 -0
  175. package/lib/esm/tests/api_tests/programs_trigger_filters.test.d.ts +5 -0
  176. package/lib/esm/tests/api_tests/programs_trigger_filters.test.d.ts.map +1 -0
  177. package/lib/esm/tests/api_tests/programs_trigger_filters.test.js +290 -0
  178. package/lib/esm/tests/api_tests/programs_trigger_filters.test.js.map +1 -0
  179. package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.js +1 -1
  180. package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.js.map +1 -1
  181. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +42 -0
  182. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -0
  183. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +479 -0
  184. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -0
  185. package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.js +1 -1
  186. package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.js.map +1 -1
  187. package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +1 -1
  188. package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -1
  189. package/lib/esm/tests/api_tests/security/enduser-cross-create.test.js +1 -1
  190. package/lib/esm/tests/api_tests/security/enduser-cross-create.test.js.map +1 -1
  191. package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.js +1 -1
  192. package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.js.map +1 -1
  193. package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.js +3 -3
  194. package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.js.map +1 -1
  195. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +32 -0
  196. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -0
  197. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +292 -0
  198. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -0
  199. package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts.map +1 -1
  200. package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.js +4 -1
  201. package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.js.map +1 -1
  202. package/lib/esm/tests/api_tests/templated_message_date_fields.test.d.ts +3 -0
  203. package/lib/esm/tests/api_tests/templated_message_date_fields.test.d.ts.map +1 -0
  204. package/lib/esm/tests/api_tests/templated_message_date_fields.test.js +198 -0
  205. package/lib/esm/tests/api_tests/templated_message_date_fields.test.js.map +1 -0
  206. package/lib/esm/tests/api_tests/templated_message_link_in_href.test.d.ts +3 -0
  207. package/lib/esm/tests/api_tests/templated_message_link_in_href.test.d.ts.map +1 -0
  208. package/lib/esm/tests/api_tests/templated_message_link_in_href.test.js +229 -0
  209. package/lib/esm/tests/api_tests/templated_message_link_in_href.test.js.map +1 -0
  210. package/lib/esm/tests/api_tests/templated_message_links.test.d.ts +3 -0
  211. package/lib/esm/tests/api_tests/templated_message_links.test.d.ts.map +1 -0
  212. package/lib/esm/tests/api_tests/templated_message_links.test.js +231 -0
  213. package/lib/esm/tests/api_tests/templated_message_links.test.js.map +1 -0
  214. package/lib/esm/tests/api_tests/time_tracks.test.js +1 -1
  215. package/lib/esm/tests/api_tests/time_tracks.test.js.map +1 -1
  216. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +5 -0
  217. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -0
  218. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +390 -0
  219. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -0
  220. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +5 -0
  221. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -0
  222. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +466 -0
  223. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -0
  224. package/lib/esm/tests/api_tests/user_portal_settings.test.js +1 -1
  225. package/lib/esm/tests/api_tests/user_portal_settings.test.js.map +1 -1
  226. package/lib/esm/tests/api_tests/validation_behavior_locks.test.d.ts +2 -0
  227. package/lib/esm/tests/api_tests/validation_behavior_locks.test.d.ts.map +1 -0
  228. package/lib/esm/tests/api_tests/validation_behavior_locks.test.js +229 -0
  229. package/lib/esm/tests/api_tests/validation_behavior_locks.test.js.map +1 -0
  230. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +5 -0
  231. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -0
  232. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +564 -0
  233. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -0
  234. package/lib/esm/tests/public_endpoint_tests.js +3 -2
  235. package/lib/esm/tests/public_endpoint_tests.js.map +1 -1
  236. package/lib/esm/tests/setup.d.ts +16 -0
  237. package/lib/esm/tests/setup.d.ts.map +1 -1
  238. package/lib/esm/tests/setup.js +53 -7
  239. package/lib/esm/tests/setup.js.map +1 -1
  240. package/lib/esm/tests/setup_isolated.d.ts +9 -0
  241. package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
  242. package/lib/esm/tests/setup_isolated.js +21 -1
  243. package/lib/esm/tests/setup_isolated.js.map +1 -1
  244. package/lib/esm/tests/socket_tests.d.ts.map +1 -1
  245. package/lib/esm/tests/socket_tests.js +3 -2
  246. package/lib/esm/tests/socket_tests.js.map +1 -1
  247. package/lib/esm/tests/tests.d.ts.map +1 -1
  248. package/lib/esm/tests/tests.js +180 -166
  249. package/lib/esm/tests/tests.js.map +1 -1
  250. package/lib/esm/tests/unit_tests/trigger_title_partials.test.d.ts +3 -0
  251. package/lib/esm/tests/unit_tests/trigger_title_partials.test.d.ts.map +1 -0
  252. package/lib/esm/tests/unit_tests/trigger_title_partials.test.js +115 -0
  253. package/lib/esm/tests/unit_tests/trigger_title_partials.test.js.map +1 -0
  254. package/lib/esm/tests/webhooks_tests.js +3 -2
  255. package/lib/esm/tests/webhooks_tests.js.map +1 -1
  256. package/lib/tsconfig.tsbuildinfo +1 -1
  257. package/package.json +22 -19
  258. package/src/sdk.ts +5 -1
  259. package/src/tests/api_tests/active_patient_count.test.ts +1 -1
  260. package/src/tests/api_tests/appointment_rescheduled_trigger.test.ts +1 -1
  261. package/src/tests/api_tests/calendar_event_references_update.test.ts +1 -1
  262. package/src/tests/api_tests/enduser_cross_access_isolation.test.ts +1 -1
  263. package/src/tests/api_tests/enduser_login_rate_limits.test.ts +7 -7
  264. package/src/tests/api_tests/enduser_session_invalidation.test.ts +4 -4
  265. package/src/tests/api_tests/managed_content_enduser_access.test.ts +1 -1
  266. package/src/tests/api_tests/managed_content_file_access.test.ts +1 -1
  267. package/src/tests/api_tests/mdi_webhooks.test.ts +9 -0
  268. package/src/tests/api_tests/nutrition_tracking.test.ts +1 -1
  269. package/src/tests/api_tests/openloop_webhooks.test.ts +1 -1
  270. package/src/tests/api_tests/security/allowed-paths-confinement.test.ts +1 -1
  271. package/src/tests/api_tests/security/booking-info-idor-token-mint.test.ts +1 -1
  272. package/src/tests/api_tests/security/enduser-ai-summary-trust-gate.test.ts +1 -1
  273. package/src/tests/api_tests/security/enduser-cross-create.test.ts +1 -1
  274. package/src/tests/api_tests/security/enduser-write-restrictions.test.ts +1 -1
  275. package/src/tests/api_tests/security/invite-user-enumeration.test.ts +3 -3
  276. package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +197 -0
  277. package/src/tests/api_tests/security/sanitize-user-html-with-iframes.test.ts +4 -1
  278. package/src/tests/api_tests/templated_message_date_fields.test.ts +146 -0
  279. package/src/tests/api_tests/templated_message_links.test.ts +192 -0
  280. package/src/tests/api_tests/time_tracks.test.ts +1 -1
  281. package/src/tests/api_tests/user_portal_settings.test.ts +1 -1
  282. package/src/tests/api_tests/validation_behavior_locks.test.ts +204 -0
  283. package/src/tests/public_endpoint_tests.ts +3 -2
  284. package/src/tests/setup.ts +43 -7
  285. package/src/tests/setup_isolated.ts +10 -0
  286. package/src/tests/socket_tests.ts +3 -2
  287. package/src/tests/tests.ts +10 -2
  288. package/src/tests/webhooks_tests.ts +4 -2
  289. package/test_generated.pdf +0 -0
  290. package/tsconfig.cjs.json +17 -0
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@tellescope/sdk",
3
- "version": "1.256.16",
3
+ "version": "2.0.0-beta.0",
4
4
  "description": "Code for interacting with the Tellescope API",
5
5
  "main": "./lib/cjs/sdk.js",
6
6
  "module": "./lib/esm/sdk.js",
7
7
  "types": "./lib/esm/sdk.d.ts",
8
8
  "scripts": {
9
- "test": "npm run-script build:cjs && npm run-script retest",
9
+ "test": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest",
10
10
  "retest": "npm run-script retest-api && npm run-script retest-public-api && npm run-script retest-webhooks",
11
- "test-api": "npm run-script build:cjs && npm run-script retest-api",
11
+ "test-api": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest-api",
12
12
  "retest-api": "node -r dotenv/config lib/cjs/tests/tests.js",
13
- "test-public-api": "npm run-script build:cjs && npm run-script retest-public-api",
13
+ "test-public-api": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest-public-api",
14
14
  "retest-public-api": "node -r dotenv/config lib/cjs/tests/public_endpoint_tests.js",
15
- "test-sockets": "npm run-script build:cjs && npm run-script retest-sockets",
15
+ "test-sockets": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest-sockets",
16
16
  "retest-sockets": "node -r dotenv/config lib/cjs/tests/socket_tests.js",
17
- "test-webhooks": "npm run-script build:cjs && npm run-script retest-webhooks",
17
+ "test-webhooks": "npm run-script build:cjs && node ../../../scripts/dev/check-build-fresh.mjs && npm run-script retest-webhooks",
18
18
  "retest-webhooks": "node -r dotenv/config lib/cjs/tests/webhooks_tests.js",
19
19
  "build": "npm run-script build:esm && npm run-script build:cjs",
20
20
  "build:esm": "tsc",
21
- "build:cjs": "tsc --module commonjs --outDir lib/cjs",
22
- "watch:cjs": "tsc --module commonjs --outDir lib/cjs --watch"
21
+ "build:cjs": "tsc -b tsconfig.cjs.json",
22
+ "watch:cjs": "tsc -b -w --preserveWatchOutput tsconfig.cjs.json"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
@@ -31,18 +31,21 @@
31
31
  "url": "https://github.com/tellescope-os/tellescope/issues"
32
32
  },
33
33
  "homepage": "https://github.com/tellescope-os/tellescope#readme",
34
+ "engines": {
35
+ "node": ">=22.12.0"
36
+ },
34
37
  "dependencies": {
35
- "@tellescope/constants": "1.256.16",
36
- "@tellescope/schema": "1.256.16",
37
- "@tellescope/testing": "1.256.16",
38
- "@tellescope/types-client": "1.256.16",
39
- "@tellescope/types-models": "1.256.16",
40
- "@tellescope/types-utilities": "1.256.16",
41
- "@tellescope/utilities": "1.256.16",
42
- "@tellescope/validation": "1.256.16",
43
- "axios": "0.30.3",
38
+ "@tellescope/constants": "2.0.0-beta.0",
39
+ "@tellescope/schema": "2.0.0-beta.0",
40
+ "@tellescope/testing": "2.0.0-beta.0",
41
+ "@tellescope/types-client": "2.0.0-beta.0",
42
+ "@tellescope/types-models": "2.0.0-beta.0",
43
+ "@tellescope/types-utilities": "2.0.0-beta.0",
44
+ "@tellescope/utilities": "2.0.0-beta.0",
45
+ "@tellescope/validation": "2.0.0-beta.0",
46
+ "axios": "0.33.0",
44
47
  "dotenv": "14.3.2",
45
- "form-data": "4.0.4",
48
+ "form-data": "4.0.6",
46
49
  "source-map-support": "0.5.21"
47
50
  },
48
51
  "devDependencies": {
@@ -55,5 +58,5 @@
55
58
  "publishConfig": {
56
59
  "access": "public"
57
60
  },
58
- "gitHead": "5874f0e4ca75d8ce70768a5933cd5bb5b3b15c58"
61
+ "gitHead": "95b2448bb219e7634e29ef4b7850f8f74d73dcbc"
59
62
  }
package/src/sdk.ts CHANGED
@@ -313,6 +313,9 @@ type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
313
313
  get_form_statistics: (args: extractFields<CustomActions['forms']['get_form_statistics']['parameters']>) => (
314
314
  Promise<extractFields<CustomActions['forms']['get_form_statistics']['returns']>>
315
315
  ),
316
+ public_form_details: (args: extractFields<PublicActions['forms']['public_form_details']['parameters']>) => (
317
+ Promise<extractFields<PublicActions['forms']['public_form_details']['returns']>>
318
+ ),
316
319
  },
317
320
  phone_trees: {
318
321
  start_outbound_call: (args: extractFields<CustomActions['phone_trees']['start_outbound_call']['parameters']>) => (
@@ -1052,6 +1055,7 @@ export class Session extends SessionManager {
1052
1055
  queries.organizations.invite_user = a => this._POST(`/v1/${schema.users.customActions.invite_user.path}`, a)
1053
1056
 
1054
1057
  queries.forms.get_form_statistics = args => this._GET(`/v1${schema.forms.customActions.get_form_statistics.path}`, args)
1058
+ queries.forms.public_form_details = args => this._GET(`/v1${schema.forms.publicActions.public_form_details.path}`, args)
1055
1059
 
1056
1060
  queries.form_fields.load_choices_from_database = args => this._GET(`/v1${schema.form_fields.customActions.load_choices_from_database.path}`, args)
1057
1061
  queries.form_fields.booking_info = args => this._GET(`/v1${schema.form_fields.customActions.booking_info.path}`, args)
@@ -1350,4 +1354,4 @@ export class Session extends SessionManager {
1350
1354
  sync = (a: { from: Date }) => this.GET<typeof a, { results: Pick<DataSyncRecord, 'modelName' | 'recordId' | 'data'>[], to: string }>('/v1/data-sync', a)
1351
1355
  }
1352
1356
 
1353
- export { SessionOptions }
1357
+ export type { SessionOptions }
@@ -10,7 +10,7 @@ import {
10
10
  import { authenticate_enduser_via_token, setup_tests } from "../setup"
11
11
 
12
12
  const host = process.env.API_URL || 'http://localhost:8080' as const
13
- const businessId = '60398b1131a295e64f084ff6'
13
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
14
14
 
15
15
  // Second-tenant session, used to prove the active patient count never crosses tenants.
16
16
  // Same key used by the org-isolation tests in tests.ts.
@@ -3,7 +3,7 @@ import { log_header, wait, async_test } from "@tellescope/testing"
3
3
  import { Enduser } from "@tellescope/types-client"
4
4
  import { setup_tests } from "../setup"
5
5
 
6
- const host = process.env.TEST_HOST || "http://localhost:8080"
6
+ const host = process.env.API_URL || process.env.TEST_HOST || "http://localhost:8080"
7
7
 
8
8
  export const appointment_rescheduled_trigger_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
9
9
  log_header("Automation Trigger Tests (Appointment Rescheduled)")
@@ -8,7 +8,7 @@ import {
8
8
  import { setup_tests, authenticate_enduser_via_token } from "../setup"
9
9
 
10
10
  const host = process.env.API_URL || 'http://localhost:8080' as const
11
- const businessId = '60398b1131a295e64f084ff6'
11
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
12
12
 
13
13
  /**
14
14
  * CU-86e2dvrn9: calendar_events.references switched from updatesDisabled to
@@ -12,7 +12,7 @@ import { ModelName } from "@tellescope/types-models"
12
12
  import { authenticate_enduser_via_token, setup_tests } from "../setup"
13
13
 
14
14
  const host = process.env.API_URL || 'http://localhost:8080' as const
15
- const businessId = '60398b1131a295e64f084ff6'
15
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
16
16
 
17
17
  type OwnerField = 'enduserId' | 'enduserIds' | 'attendees.id'
18
18
 
@@ -6,7 +6,7 @@ import {
6
6
  async_test,
7
7
  log_header,
8
8
  } from "@tellescope/testing"
9
- import { setup_tests } from "../setup"
9
+ import { reset_test_tenant, setup_tests } from "../setup"
10
10
 
11
11
  const host = process.env.API_URL || 'http://localhost:8080' as const
12
12
 
@@ -43,7 +43,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
43
43
  const businessId = sdk.userInfo.businessId
44
44
 
45
45
  // Ensure throttled_events from prior tests don't bleed in.
46
- await sdk.reset_db()
46
+ await reset_test_tenant(sdk)
47
47
 
48
48
  // ---- /v1/login-enduser per-IP cap (20/min) ----
49
49
  // Bogus emails ensure we never reach the actual DB lookup / login work.
@@ -58,7 +58,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
58
58
  { expectedResult: 'tripped' }
59
59
  )
60
60
 
61
- await sdk.reset_db()
61
+ await reset_test_tenant(sdk)
62
62
 
63
63
  // ---- /v1/begin-enduser-login-flow per-IP cap (10/min) ----
64
64
  // Use distinct emails so the per-identifier limit (5/10min) does NOT trip first;
@@ -73,7 +73,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
73
73
  { expectedResult: 'tripped' }
74
74
  )
75
75
 
76
- await sdk.reset_db()
76
+ await reset_test_tenant(sdk)
77
77
 
78
78
  // ---- /v1/begin-enduser-login-flow per-identifier cap (5 / 10 min per email) ----
79
79
  // Hit a single email below the per-IP cap.
@@ -88,7 +88,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
88
88
  { expectedResult: 'tripped' }
89
89
  )
90
90
 
91
- await sdk.reset_db()
91
+ await reset_test_tenant(sdk)
92
92
 
93
93
  // ---- /v1/endusers/send-otp-code per-IP cap (5/min) ----
94
94
  // Use a bogus JWT-shaped token so we trip the IP guard first (it runs before any DB work).
@@ -116,7 +116,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
116
116
 
117
117
  // ---- Legitimate-login regression: a single successful login should still go through ----
118
118
  // Reset state, then create a real enduser with a password and confirm one login succeeds.
119
- await sdk.reset_db()
119
+ await reset_test_tenant(sdk)
120
120
 
121
121
  const ts = Date.now()
122
122
  const enduser = await sdk.api.endusers.createOne({
@@ -151,7 +151,7 @@ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { s
151
151
  )
152
152
  } finally {
153
153
  await sdk.api.endusers.deleteOne(enduser.id).catch(() => null)
154
- await sdk.reset_db().catch(() => null)
154
+ await reset_test_tenant(sdk).catch(() => null)
155
155
  }
156
156
  }
157
157
 
@@ -7,10 +7,10 @@ import {
7
7
  log_header,
8
8
  wait,
9
9
  } from "@tellescope/testing"
10
- import { setup_tests } from "../setup"
10
+ import { reset_test_tenant, setup_tests } from "../setup"
11
11
 
12
12
  const host = process.env.API_URL || 'http://localhost:8080' as const
13
- const businessId = '60398b1131a295e64f084ff6'
13
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
14
14
 
15
15
  // Main test function that can be called independently
16
16
  export const enduser_session_invalidation_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
@@ -126,7 +126,7 @@ export const enduser_session_invalidation_tests = async ({ sdk, sdkNonAdmin } :
126
126
  }
127
127
 
128
128
  // Ensure OTP is off and db is clean before starting
129
- await sdk.reset_db()
129
+ await reset_test_tenant(sdk)
130
130
  await wait(undefined, 500)
131
131
  await resetOTPSettings(sdk)
132
132
 
@@ -242,7 +242,7 @@ export const enduser_session_invalidation_tests = async ({ sdk, sdkNonAdmin } :
242
242
  )
243
243
 
244
244
  // Reset rate limiting state before continuing
245
- await sdk.reset_db()
245
+ await reset_test_tenant(sdk)
246
246
  await wait(undefined, 500)
247
247
 
248
248
  // Test 8: Disabling OTP does NOT invalidate sessions
@@ -9,7 +9,7 @@ import {
9
9
  import { setup_tests } from "../setup"
10
10
 
11
11
  const host = process.env.API_URL || 'http://localhost:8080' as const
12
- const businessId = '60398b1131a295e64f084ff6'
12
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
13
13
 
14
14
  /**
15
15
  * Tests for managed_content_records enduser access filtering.
@@ -9,7 +9,7 @@ import {
9
9
  import { setup_tests } from "../setup"
10
10
 
11
11
  const host = process.env.API_URL || 'http://localhost:8080' as const
12
- const businessId = '60398b1131a295e64f084ff6'
12
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
13
13
 
14
14
  /**
15
15
  * Tests for file_download_URL enduser access via managed content fallback.
@@ -75,6 +75,15 @@ export const mdi_webhooks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, s
75
75
  )
76
76
  assert(status === 401, `message_created with bad auth should 401 (got ${status})`, "MDI webhook authenticates before patient lookup")
77
77
  }
78
+
79
+ // 5. case_order_created / case_order_updated with bogus auth -> still 401.
80
+ for (const event_type of ['case_order_created', 'case_order_updated']) {
81
+ const { status } = await postMDI(
82
+ { event_type, case_id: 'mdi-case-test-1', case_order_id: 'mdi-order-test-1', order_status: 'ready' },
83
+ { 'Authorization': `Bearer not-a-real-mdi-token-${Date.now()}` },
84
+ )
85
+ assert(status === 401, `${event_type} with bad auth should 401 (got ${status})`, `MDI webhook authenticates before ${event_type} dispatch`)
86
+ }
78
87
  }
79
88
 
80
89
  // Allow running this test file independently
@@ -9,7 +9,7 @@ import { ObservationComponent } from "@tellescope/types-models"
9
9
  import { authenticate_enduser_via_token, setup_tests } from "../setup"
10
10
 
11
11
  const host = process.env.API_URL || 'http://localhost:8080' as const
12
- const businessId = '60398b1131a295e64f084ff6'
12
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
13
13
 
14
14
  const ENDUSER_PASSWORD = 'NutritionTestPassword!123'
15
15
 
@@ -10,7 +10,7 @@ import {
10
10
  import { setup_tests } from "../setup"
11
11
 
12
12
  const host = process.env.API_URL || 'http://localhost:8080' as const
13
- const businessId = '60398b1131a295e64f084ff6'
13
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
14
14
 
15
15
  const v1Url = `${host}/v1/webhooks/openloop/${businessId}`
16
16
  const v2Url = `${host}/v1/webhooks/openloop-v2/${businessId}`
@@ -631,7 +631,7 @@ export const allowed_paths_confinement_tests = async ({ sdk } : { sdk: Session,
631
631
  // record. `allowedPaths` is a JWT claim only (WithAllowedPaths in types-models is used solely on
632
632
  // the session type; there is no schema validator for it), so an API-key session can never carry
633
633
  // one and is never path-confined. Assert that rather than reason about it.
634
- const apiKeySDK = new Session({ host, apiKey: '3n5q0SCBT_iUvZz-b9BJtX7o7HQUVJ9v132PgHJNJsg.' })
634
+ const apiKeySDK = new Session({ host, apiKey: process.env.TEST_API_KEY || '3n5q0SCBT_iUvZz-b9BJtX7o7HQUVJ9v132PgHJNJsg.' })
635
635
  for (const path of ['/journeys', '/form-responses', '/endusers', '/calendar-events', '/files']) {
636
636
  const r = await axios.request({
637
637
  method: 'get',
@@ -8,7 +8,7 @@ import {
8
8
  import { authenticate_enduser_via_token, setup_tests } from "../../setup"
9
9
 
10
10
  const host = process.env.API_URL || 'http://localhost:8080' as const
11
- const businessId = '60398b1131a295e64f084ff6'
11
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
12
12
 
13
13
  const RAND = () => Math.random().toString(36).slice(2, 10)
14
14
 
@@ -6,7 +6,7 @@ import { FormResponse } from "@tellescope/types-client"
6
6
  import { setup_tests } from "../../setup"
7
7
 
8
8
  const host = process.env.API_URL || "http://localhost:8080"
9
- const businessId = '60398b1131a295e64f084ff6'
9
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
10
10
 
11
11
  const ENDUSER_PASSWORD = 'AISummaryGateTestPassword!123'
12
12
 
@@ -8,7 +8,7 @@ import {
8
8
  import { setup_tests } from "../../setup"
9
9
 
10
10
  const host = process.env.API_URL || 'http://localhost:8080' as const
11
- const businessId = '60398b1131a295e64f084ff6'
11
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
12
12
 
13
13
  const ENDUSER_PASSWORD = 'F0116TestPassword!123'
14
14
 
@@ -8,7 +8,7 @@ import {
8
8
  import { setup_tests } from "../../setup"
9
9
 
10
10
  const host = process.env.API_URL || 'http://localhost:8080' as const
11
- const businessId = '60398b1131a295e64f084ff6'
11
+ const businessId = process.env.TEST_BUSINESS_ID || '60398b1131a295e64f084ff6'
12
12
 
13
13
  const ENDUSER_PASSWORD = 'F0106TestPassword!123'
14
14
 
@@ -7,7 +7,7 @@ import {
7
7
  log_header,
8
8
  wait,
9
9
  } from "@tellescope/testing"
10
- import { setup_tests } from "../../setup"
10
+ import { reset_test_tenant, setup_tests } from "../../setup"
11
11
 
12
12
  const host = process.env.API_URL || 'http://localhost:8080' as const
13
13
 
@@ -53,7 +53,7 @@ export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk
53
53
  log_header("users.invite_user cross-tenant enumeration regression")
54
54
 
55
55
  // Reset state so prior tests' rate-limit accounting doesn't leak in.
56
- await sdk.reset_db()
56
+ await reset_test_tenant(sdk)
57
57
 
58
58
  const organizationId = sdk.userInfo.organizationIds?.[0] ?? sdk.userInfo.businessId
59
59
  // Use the test admin's own email — guaranteed to exist in the test tenant, so every invite
@@ -98,7 +98,7 @@ export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk
98
98
  // ====================================================================
99
99
  // Let rate limit decay so this single call isn't blocked.
100
100
  await wait(undefined, 5000)
101
- await sdk.reset_db()
101
+ await reset_test_tenant(sdk)
102
102
 
103
103
  const sameTenantRes = await post(
104
104
  '/v1/invite-user-to-organization',
@@ -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>`)