@tellescope/sdk 1.256.15 → 2.0.0-beta.0

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