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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/CLAUDE.md +24 -0
  2. package/env.example +6 -0
  3. package/lib/cjs/sdk.d.ts +373 -3
  4. package/lib/cjs/sdk.d.ts.map +1 -1
  5. package/lib/cjs/sdk.js.map +1 -1
  6. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  7. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  8. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +506 -0
  9. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  10. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  11. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  12. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +3 -1
  13. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  14. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  15. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  16. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js +3 -1
  17. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  18. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  19. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  20. package/lib/cjs/tests/api_tests/form_vital_reading.test.js +461 -0
  21. package/lib/cjs/tests/api_tests/form_vital_reading.test.js.map +1 -0
  22. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  23. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  24. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js +232 -0
  25. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  26. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  27. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  28. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js +243 -0
  29. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  30. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  31. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  32. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +3 -1
  33. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  34. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts +3 -1
  35. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  36. package/lib/cjs/tests/api_tests/organization_signup.test.js +3 -1
  37. package/lib/cjs/tests/api_tests/organization_signup.test.js.map +1 -1
  38. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  39. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  40. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js +571 -0
  41. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  42. package/lib/cjs/tests/api_tests/programs.test.d.ts +3 -1
  43. package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -1
  44. package/lib/cjs/tests/api_tests/programs.test.js +73 -17
  45. package/lib/cjs/tests/api_tests/programs.test.js.map +1 -1
  46. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  47. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  48. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js +733 -0
  49. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  50. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  51. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  52. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +3 -1
  53. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  54. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  55. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  56. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +3 -1
  57. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  58. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  59. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  60. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +3 -1
  61. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  62. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  63. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  64. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js +3 -1
  65. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  66. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  67. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  68. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  69. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  70. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  71. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  72. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  73. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +3 -1
  74. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  75. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  76. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  77. package/lib/cjs/tests/api_tests/session_for_join_link.test.js +147 -0
  78. package/lib/cjs/tests/api_tests/session_for_join_link.test.js.map +1 -0
  79. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  80. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +191 -88
  81. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  82. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  83. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  84. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js +219 -0
  85. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  86. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  87. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  88. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js +396 -0
  89. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  90. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  91. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  92. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js +3 -1
  93. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  94. package/lib/cjs/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  95. package/lib/cjs/tests/api_tests/totp_mfa.test.js +37 -31
  96. package/lib/cjs/tests/api_tests/totp_mfa.test.js.map +1 -1
  97. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  98. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  99. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +3 -1
  100. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  101. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  102. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  103. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +3 -1
  104. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  105. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  106. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  107. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +3 -1
  108. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  109. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts +7 -0
  110. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  111. package/lib/cjs/tests/api_tests/vitl_push.test.js +341 -0
  112. package/lib/cjs/tests/api_tests/vitl_push.test.js.map +1 -0
  113. package/lib/cjs/tests/setup_isolated.d.ts +48 -0
  114. package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
  115. package/lib/cjs/tests/setup_isolated.js +119 -1
  116. package/lib/cjs/tests/setup_isolated.js.map +1 -1
  117. package/lib/cjs/tests/tests.d.ts.map +1 -1
  118. package/lib/cjs/tests/tests.js +374 -292
  119. package/lib/cjs/tests/tests.js.map +1 -1
  120. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  121. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  122. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js +201 -0
  123. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  124. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  125. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  126. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js +120 -0
  127. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  128. package/lib/cjs/tests/webhooks_tests.js +44 -34
  129. package/lib/cjs/tests/webhooks_tests.js.map +1 -1
  130. package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  131. package/lib/esm/sdk.d.ts +373 -3
  132. package/lib/esm/sdk.d.ts.map +1 -1
  133. package/lib/esm/sdk.js.map +1 -1
  134. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  135. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  136. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +501 -0
  137. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  138. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  139. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  140. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +2 -0
  141. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  142. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  143. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  144. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js +2 -0
  145. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  146. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  147. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  148. package/lib/esm/tests/api_tests/form_vital_reading.test.js +456 -0
  149. package/lib/esm/tests/api_tests/form_vital_reading.test.js.map +1 -0
  150. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  151. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  152. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js +227 -0
  153. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  154. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  155. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  156. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js +238 -0
  157. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  158. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  159. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  160. package/lib/esm/tests/api_tests/load_threads_care_team.test.js +2 -0
  161. package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  162. package/lib/esm/tests/api_tests/organization_signup.test.d.ts +3 -1
  163. package/lib/esm/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  164. package/lib/esm/tests/api_tests/organization_signup.test.js +2 -0
  165. package/lib/esm/tests/api_tests/organization_signup.test.js.map +1 -1
  166. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  167. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  168. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js +566 -0
  169. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  170. package/lib/esm/tests/api_tests/programs.test.d.ts +3 -1
  171. package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -1
  172. package/lib/esm/tests/api_tests/programs.test.js +72 -16
  173. package/lib/esm/tests/api_tests/programs.test.js.map +1 -1
  174. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  175. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  176. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js +728 -0
  177. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  178. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  179. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  180. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +2 -0
  181. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  182. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  183. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  184. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +2 -0
  185. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  186. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  187. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  188. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +2 -0
  189. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  190. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  191. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  192. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js +2 -0
  193. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  194. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  195. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  196. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  197. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  198. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  199. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  200. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  201. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +2 -0
  202. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  203. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  204. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  205. package/lib/esm/tests/api_tests/session_for_join_link.test.js +142 -0
  206. package/lib/esm/tests/api_tests/session_for_join_link.test.js.map +1 -0
  207. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  208. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +191 -88
  209. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  210. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  211. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  212. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js +214 -0
  213. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  214. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  215. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  216. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js +388 -0
  217. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  218. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  219. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  220. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js +2 -0
  221. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  222. package/lib/esm/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  223. package/lib/esm/tests/api_tests/totp_mfa.test.js +37 -31
  224. package/lib/esm/tests/api_tests/totp_mfa.test.js.map +1 -1
  225. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  226. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  227. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +2 -0
  228. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  229. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  230. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  231. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +2 -0
  232. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  233. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  234. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  235. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +2 -0
  236. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  237. package/lib/esm/tests/api_tests/vitl_push.test.d.ts +7 -0
  238. package/lib/esm/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  239. package/lib/esm/tests/api_tests/vitl_push.test.js +333 -0
  240. package/lib/esm/tests/api_tests/vitl_push.test.js.map +1 -0
  241. package/lib/esm/tests/setup_isolated.d.ts +48 -0
  242. package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
  243. package/lib/esm/tests/setup_isolated.js +114 -0
  244. package/lib/esm/tests/setup_isolated.js.map +1 -1
  245. package/lib/esm/tests/tests.d.ts.map +1 -1
  246. package/lib/esm/tests/tests.js +375 -293
  247. package/lib/esm/tests/tests.js.map +1 -1
  248. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  249. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  250. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js +174 -0
  251. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  252. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  253. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  254. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js +117 -0
  255. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  256. package/lib/esm/tests/webhooks_tests.js +44 -34
  257. package/lib/esm/tests/webhooks_tests.js.map +1 -1
  258. package/lib/tsconfig.tsbuildinfo +1 -1
  259. package/package.json +12 -11
  260. package/src/sdk.ts +3 -2
  261. package/src/tests/api_tests/booking_prevent_upcoming_duplicate.test.ts +541 -0
  262. package/src/tests/api_tests/enduser_fields_sync.test.ts +4 -0
  263. package/src/tests/api_tests/enduser_observations_auto_review.test.ts +4 -0
  264. package/src/tests/api_tests/form_vital_reading.test.ts +393 -0
  265. package/src/tests/api_tests/journey_statistics_cache.test.ts +237 -0
  266. package/src/tests/api_tests/journey_step_manual_count.test.ts +220 -0
  267. package/src/tests/api_tests/load_threads_care_team.test.ts +4 -0
  268. package/src/tests/api_tests/organization_signup.test.ts +4 -0
  269. package/src/tests/api_tests/phone_embed_leave_message.test.ts +541 -0
  270. package/src/tests/api_tests/programs.test.ts +351 -0
  271. package/src/tests/api_tests/public_form_case_insensitive_match.test.ts +632 -0
  272. package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +4 -0
  273. package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +4 -0
  274. package/src/tests/api_tests/security/file-link-session-confinement.test.ts +4 -0
  275. package/src/tests/api_tests/security/form-logic-js-execution.test.ts +4 -0
  276. package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +225 -0
  277. package/src/tests/api_tests/security/register_business_id_injection.test.ts +2 -2
  278. package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +4 -0
  279. package/src/tests/api_tests/session_for_join_link.test.ts +99 -0
  280. package/src/tests/api_tests/set_fields_order_templates.test.ts +94 -9
  281. package/src/tests/api_tests/stripe_checkout_items.test.ts +186 -0
  282. package/src/tests/api_tests/tickets_bulk_assign_from_queue.test.ts +4 -0
  283. package/src/tests/api_tests/totp_mfa.test.ts +10 -5
  284. package/src/tests/api_tests/trigger_contains_matching.test.ts +4 -0
  285. package/src/tests/api_tests/user_logs_restore_deleted.test.ts +4 -0
  286. package/src/tests/api_tests/vital_review_trigger_action.test.ts +455 -0
  287. package/src/tests/api_tests/vitl_push.test.ts +323 -0
  288. package/src/tests/setup_isolated.ts +140 -0
  289. package/src/tests/tests.ts +124 -30
  290. package/src/tests/unit_tests/isolated_suite_discovery.test.ts +178 -0
  291. package/src/tests/unit_tests/programs_conditional_logic.test.ts +189 -0
  292. package/src/tests/webhooks_tests.ts +6 -1
  293. package/test_generated.pdf +0 -0
@@ -6,6 +6,7 @@ import {
6
6
  assert_checks,
7
7
  Check,
8
8
  setup_isolated_tenant,
9
+ IsolatedSuite,
9
10
  } from "../../setup_isolated"
10
11
 
11
12
  const host = process.env.API_URL || 'http://localhost:8080' as const
@@ -276,6 +277,9 @@ export const file_link_session_confinement_tests = async () => {
276
277
  assert_checks('Shared file link session confinement', await file_link_session_confinement_checks())
277
278
  }
278
279
 
280
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
281
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Shared file link session confinement', run: file_link_session_confinement_checks }
282
+
279
283
  // Allow running this test file independently
280
284
  if (require.main === module) {
281
285
  console.log(`🌐 Using API URL: ${host}`)
@@ -4,6 +4,7 @@ import {
4
4
  assert_checks,
5
5
  Check,
6
6
  setup_isolated_tenant,
7
+ IsolatedSuite,
7
8
  } from "../../setup_isolated"
8
9
 
9
10
  const host = process.env.API_URL || 'http://localhost:8080' as const
@@ -168,6 +169,9 @@ export const form_logic_js_execution_tests = async () => {
168
169
  assert_checks('$JS in stored conditional logic', await form_logic_js_execution_checks())
169
170
  }
170
171
 
172
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
173
+ export const ISOLATED_SUITE: IsolatedSuite = { name: '$JS in stored conditional logic', run: form_logic_js_execution_checks }
174
+
171
175
  // Allow running this test file independently
172
176
  if (require.main === module) {
173
177
  console.log(`🌐 Using API URL: ${host}`)
@@ -65,6 +65,19 @@ const host = process.env.API_URL || 'http://localhost:8080' as const
65
65
  * R14 both grace markers are load-bearing: neither source nor stripeCustomerId alone opens it
66
66
  * R15 the grace is per-request — it does not clear the stored flag
67
67
  *
68
+ * ?alwaysSetPhone=true (CU-86e3k4ptv) is a second per-request override, narrower than ?enduserId=:
69
+ * it allows PHONE ONLY through the lock, on every finalized webhook, overwriting an existing and
70
+ * different number. Email — the password-reset channel this lock exists to protect — stays blocked.
71
+ * G16 (GREEN) the flag overwrites an existing, different phone on a guessed match
72
+ * G17 (GREEN) ...and in that same request the submitted email is still dropped
73
+ * R16 (RED) the identical request WITHOUT the flag leaves both phone and email blocked
74
+ * R17 (RED) the flag does not open email on a phone match either
75
+ * R18 (RED) only the literal string 'true' enables it — ?alwaysSetPhone=1 keeps the lock
76
+ * G18 (GREEN) a flagged webhook still stamps authFieldIntakeDisabled on the response, so later
77
+ * writes through save_enduser_updates_for_form_response stay blocked
78
+ * R19 (RED) it is request-scoped: a flag passed on an earlier webhook for the same
79
+ * responder_uuid does not unlock a later one that omits it
80
+ *
68
81
  * The Stripe grace is deliberately confined to THIS endpoint, which is API-key authenticated. The
69
82
  * anonymous public-form path gets no exemption; that boundary is pinned by G28 in
70
83
  * public_endpoint_auth_field_lock.test.ts.
@@ -155,12 +168,15 @@ export const formsort_webhook_auth_field_lock_tests = async (
155
168
  finalized: boolean,
156
169
  enduserId?: string,
157
170
  createNewEnduser?: boolean,
171
+ // sent verbatim, so a scenario can pin that only the literal string 'true' enables the opt-in
172
+ alwaysSetPhone?: string,
158
173
  }) => {
159
174
  const url = new URL(`${host}/v1/webhooks/form-ingestion/${apiKey.key}`)
160
175
  url.searchParams.set('formId', form.id)
161
176
  url.searchParams.set('returnJSON', 'true')
162
177
  if (o.enduserId) url.searchParams.set('enduserId', o.enduserId)
163
178
  if (o.createNewEnduser) url.searchParams.set('createNewEnduser', 'true')
179
+ if (o.alwaysSetPhone !== undefined) url.searchParams.set('alwaysSetPhone', o.alwaysSetPhone)
164
180
 
165
181
  const res = await axios.post(
166
182
  url.toString(),
@@ -688,6 +704,215 @@ export const formsort_webhook_auth_field_lock_tests = async (
688
704
  }
689
705
  })
690
706
 
707
+ // ---------------------------------------------------------------------------------------------
708
+ // G16 — ?alwaysSetPhone=true: the opt-in allows phone through the lock on a GUESSED match (the
709
+ // path R2 blocks), overwriting a phone that is already stored and different. Phone is also the
710
+ // only field this submission changes, which exercises the "at least one update provided" guard —
711
+ // if the delete happened before that guard, nothing would be written at all.
712
+ // ---------------------------------------------------------------------------------------------
713
+ await runScenario('G16', 'alwaysSetPhone=true overwrites an existing phone on a guessed match', async () => {
714
+ const email = emailFor('g16')
715
+ const originalPhone = nextPhone()
716
+ const victim = await makeVictim('g16', { email, phone: originalPhone })
717
+ const intakePhone = nextPhone()
718
+
719
+ const { data } = await postWebhook({
720
+ label: 'g16',
721
+ finalized: true,
722
+ alwaysSetPhone: 'true',
723
+ answers: [{ key: 'email', value: email }, { key: 'phone', value: intakePhone }],
724
+ })
725
+ if (!matched('G16', 'alwaysSetPhone=true overwrites an existing phone on a guessed match', data, victim.id)) return
726
+
727
+ const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
728
+ record('G16', 'alwaysSetPhone=true overwrites an existing phone on a guessed match',
729
+ enduser?.phone === intakePhone,
730
+ `phone=${enduser?.phone || '(unset)'} expected=${intakePhone}`)
731
+ })
732
+
733
+ // ---------------------------------------------------------------------------------------------
734
+ // G17 — the same request, with BOTH a new phone and a different email, so the two halves of the
735
+ // rule are observed together: phone lands, email does not. Shaped like R3 (a draft formed by a
736
+ // guess, finalized through the sticky flag) because that is the only resolution path where the
737
+ // submitted email can differ from the stored one while the patient still matches.
738
+ // ---------------------------------------------------------------------------------------------
739
+ await runScenario('G17', 'alwaysSetPhone=true writes phone and still drops email', async () => {
740
+ const originalEmail = emailFor('g17')
741
+ const originalPhone = nextPhone()
742
+ const victim = await makeVictim('g17', { email: originalEmail, phone: originalPhone })
743
+ const attackerEmail = emailFor('g17-attacker')
744
+ const intakePhone = nextPhone()
745
+
746
+ // draft matches the pre-existing patient by phone -> authFieldIntakeDisabled is stamped on it
747
+ await postWebhook({ label: 'g17', finalized: false, answers: [{ key: 'phone', value: originalPhone }] })
748
+
749
+ const { data } = await postWebhook({
750
+ label: 'g17',
751
+ finalized: true,
752
+ alwaysSetPhone: 'true',
753
+ answers: [{ key: 'phone', value: intakePhone }, { key: 'email', value: attackerEmail }],
754
+ })
755
+ if (!matched('G17', 'alwaysSetPhone=true writes phone and still drops email', data, victim.id)) return
756
+
757
+ const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
758
+ record('G17', 'alwaysSetPhone=true writes phone through the sticky flag', enduser?.phone === intakePhone,
759
+ `phone=${enduser?.phone || '(unset)'} expected=${intakePhone}`)
760
+ record('G17', 'alwaysSetPhone=true leaves email blocked in the same request',
761
+ enduser?.email === originalEmail,
762
+ `email=${enduser?.email || '(unset)'} expected=${originalEmail}`)
763
+ })
764
+
765
+ // ---------------------------------------------------------------------------------------------
766
+ // R16 — the control for G17: byte-identical request without the flag. Both fields stay blocked,
767
+ // i.e. the default behavior is unchanged.
768
+ // ---------------------------------------------------------------------------------------------
769
+ await runScenario('R16', 'without the flag the same request blocks phone and email', async () => {
770
+ const originalEmail = emailFor('r16')
771
+ const originalPhone = nextPhone()
772
+ const victim = await makeVictim('r16', { email: originalEmail, phone: originalPhone })
773
+ const attackerEmail = emailFor('r16-attacker')
774
+ const intakePhone = nextPhone()
775
+
776
+ await postWebhook({ label: 'r16', finalized: false, answers: [{ key: 'phone', value: originalPhone }] })
777
+
778
+ const { data } = await postWebhook({
779
+ label: 'r16',
780
+ finalized: true,
781
+ answers: [{ key: 'phone', value: intakePhone }, { key: 'email', value: attackerEmail }],
782
+ })
783
+ if (!matched('R16', 'without the flag the same request blocks phone and email', data, victim.id)) return
784
+
785
+ const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone || e.email === attackerEmail)
786
+ record('R16', 'without the flag phone stays blocked', enduser?.phone === originalPhone,
787
+ `phone=${enduser?.phone || '(unset)'} expected=${originalPhone}`)
788
+ record('R16', 'without the flag email stays blocked', enduser?.email === originalEmail,
789
+ `email=${enduser?.email || '(unset)'} expected=${originalEmail}`)
790
+ })
791
+
792
+ // ---------------------------------------------------------------------------------------------
793
+ // R17 — the flag is phone-only in BOTH match directions: matched on phone (R1's shape), the
794
+ // submitted email must still not land.
795
+ // ---------------------------------------------------------------------------------------------
796
+ await runScenario('R17', 'alwaysSetPhone=true does not repoint email on a phone match', async () => {
797
+ const originalEmail = emailFor('r17')
798
+ const phone = nextPhone()
799
+ const victim = await makeVictim('r17', { email: originalEmail, phone })
800
+ const attackerEmail = emailFor('r17-attacker')
801
+
802
+ const { data } = await postWebhook({
803
+ label: 'r17',
804
+ finalized: true,
805
+ alwaysSetPhone: 'true',
806
+ answers: [{ key: 'phone', value: phone }, { key: 'email', value: attackerEmail }],
807
+ })
808
+ if (!matched('R17', 'alwaysSetPhone=true does not repoint email on a phone match', data, victim.id)) return
809
+
810
+ const enduser = await pollEnduser(victim.id, e => e.email === attackerEmail)
811
+ record('R17', 'alwaysSetPhone=true does not repoint email on a phone match', enduser?.email === originalEmail,
812
+ `email=${enduser?.email || '(unset)'} expected=${originalEmail}`)
813
+ })
814
+
815
+ // ---------------------------------------------------------------------------------------------
816
+ // R18 — parsed as === 'true', matching createNewEnduser / matchByName. Any other value, including
817
+ // a truthy-looking one, keeps today's behavior.
818
+ // ---------------------------------------------------------------------------------------------
819
+ await runScenario('R18', 'alwaysSetPhone=1 does not enable the opt-in', async () => {
820
+ const email = emailFor('r18')
821
+ const originalPhone = nextPhone()
822
+ const victim = await makeVictim('r18', { email, phone: originalPhone })
823
+ const intakePhone = nextPhone()
824
+
825
+ const { data } = await postWebhook({
826
+ label: 'r18',
827
+ finalized: true,
828
+ alwaysSetPhone: '1',
829
+ answers: [{ key: 'email', value: email }, { key: 'phone', value: intakePhone }],
830
+ })
831
+ if (!matched('R18', 'alwaysSetPhone=1 does not enable the opt-in', data, victim.id)) return
832
+
833
+ const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
834
+ record('R18', 'alwaysSetPhone=1 does not enable the opt-in', enduser?.phone === originalPhone,
835
+ `phone=${enduser?.phone || '(unset)'} expected=${originalPhone}`)
836
+ })
837
+
838
+ // ---------------------------------------------------------------------------------------------
839
+ // G18 — the opt-in is a per-REQUEST allow, exactly like G11 (?enduserId=) and R15 (the Stripe
840
+ // grace): the response still records that it attached to a pre-existing patient, so the other
841
+ // consumer of that flag — save_enduser_updates_for_form_response, reached by later staff edits —
842
+ // stays protected.
843
+ // ---------------------------------------------------------------------------------------------
844
+ await runScenario('G18', 'alwaysSetPhone does not clear the stored flag', async () => {
845
+ const email = emailFor('g18')
846
+ const victim = await makeVictim('g18', { email, phone: nextPhone() })
847
+ const intakePhone = nextPhone()
848
+
849
+ const { data } = await postWebhook({
850
+ label: 'g18',
851
+ finalized: true,
852
+ alwaysSetPhone: 'true',
853
+ answers: [{ key: 'email', value: email }, { key: 'phone', value: intakePhone }],
854
+ })
855
+ if (!matched('G18', 'alwaysSetPhone does not clear the stored flag', data, victim.id)) return
856
+
857
+ const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
858
+ record('G18', 'flagged webhook landed the phone (probe precondition)', enduser?.phone === intakePhone,
859
+ `phone=${enduser?.phone || '(unset)'} expected=${intakePhone}`)
860
+
861
+ if (!data.formResponseId) {
862
+ skip('G18', 'alwaysSetPhone does not clear the stored flag', 'webhook returned no formResponseId')
863
+ return
864
+ }
865
+ const response: any = await sdk.api.form_responses.getOne(data.formResponseId).catch(() => null)
866
+ if (!response) {
867
+ skip('G18', 'alwaysSetPhone does not clear the stored flag', 'form response not readable')
868
+ } else if (response.authFieldIntakeDisabled === undefined) {
869
+ // deliberately absent from schema.ts; if reads project it away there is nothing to assert here
870
+ skip('G18', 'alwaysSetPhone does not clear the stored flag', 'authFieldIntakeDisabled not exposed on read')
871
+ } else {
872
+ record('G18', 'alwaysSetPhone does not clear the stored flag', response.authFieldIntakeDisabled === true,
873
+ `authFieldIntakeDisabled=${response.authFieldIntakeDisabled} expected=true`)
874
+ }
875
+ })
876
+
877
+ // ---------------------------------------------------------------------------------------------
878
+ // R19 — nothing about the opt-in is recorded on the response, so a later webhook for the same
879
+ // responder_uuid that omits it is blocked as usual. The earlier webhook here must be the DRAFT:
880
+ // once a response is submitted the handler returns 'Already submitted' before reaching the lock,
881
+ // which would make a finalized-then-finalized pair pass no matter what the implementation did.
882
+ // ---------------------------------------------------------------------------------------------
883
+ await runScenario('R19', 'alwaysSetPhone on an earlier webhook does not unlock a later one', async () => {
884
+ const email = emailFor('r19')
885
+ const originalPhone = nextPhone()
886
+ const victim = await makeVictim('r19', { email, phone: originalPhone })
887
+ const intakePhone = nextPhone()
888
+
889
+ // draft WITH the flag: matches the pre-existing patient by email and stamps the response
890
+ const draft = await postWebhook({
891
+ label: 'r19',
892
+ finalized: false,
893
+ alwaysSetPhone: 'true',
894
+ answers: [{ key: 'email', value: email }],
895
+ })
896
+ if (draft.status !== 200) {
897
+ record('R19', 'alwaysSetPhone on an earlier webhook does not unlock a later one', false,
898
+ `draft webhook returned ${draft.status}`)
899
+ return
900
+ }
901
+
902
+ // finalize WITHOUT the flag — if the opt-in were inherited from the response, this would land
903
+ const { data } = await postWebhook({
904
+ label: 'r19',
905
+ finalized: true,
906
+ answers: [{ key: 'email', value: email }, { key: 'phone', value: intakePhone }],
907
+ })
908
+ if (!matched('R19', 'alwaysSetPhone on an earlier webhook does not unlock a later one', data, victim.id)) return
909
+
910
+ const enduser = await pollEnduser(victim.id, e => e.phone === intakePhone)
911
+ record('R19', 'alwaysSetPhone on an earlier webhook does not unlock a later one',
912
+ enduser?.phone === originalPhone,
913
+ `phone=${enduser?.phone || '(unset)'} expected=${originalPhone}`)
914
+ })
915
+
691
916
  // ---------------------------------------------------------------------------------------------
692
917
  // Report
693
918
  // ---------------------------------------------------------------------------------------------
@@ -65,7 +65,7 @@ export const register_business_id_injection_tests = async ({ sdk } : { sdk: Sess
65
65
  // An anonymous caller registers with businessId set to the victim org. On vulnerable
66
66
  // code this creates a Business user INSIDE the victim org that no one invited.
67
67
  // ==================================================================================
68
- const email1 = `security+reg-inject-${Date.now()}@wearehackerone.com`
68
+ const email1 = `dontsend+reg-inject-${Date.now()}@tellescope.com`
69
69
 
70
70
  const before = await sdk.api.users.getOne({ email: email1 }).catch(() => null)
71
71
  assert(before === null, `precondition failed: ${email1} already present in victim org`, 'precondition: victim org has no such user')
@@ -95,7 +95,7 @@ export const register_business_id_injection_tests = async ({ sdk } : { sdk: Sess
95
95
  // businessId. On vulnerable code the scoped duplicate check misses the org-less record,
96
96
  // so a SECOND user record for the same email is created inside the victim org.
97
97
  // ==================================================================================
98
- const email2 = `security+reg-dup-${Date.now()}@wearehackerone.com`
98
+ const email2 = `dontsend+reg-dup-${Date.now()}@tellescope.com`
99
99
 
100
100
  const resRoot = await post('/v1/users/register', { email: email2, password, termsVersion })
101
101
  console.log(` register(no businessId) -> HTTP ${resRoot.status}`)
@@ -5,6 +5,7 @@ import {
5
5
  assert_checks,
6
6
  Check,
7
7
  setup_isolated_tenant,
8
+ IsolatedSuite,
8
9
  } from "../../setup_isolated"
9
10
 
10
11
  const host = process.env.API_URL || 'http://localhost:8080' as const
@@ -182,6 +183,9 @@ export const rich_text_answer_sanitized_on_write_tests = async () => {
182
183
  assert_checks('Rich Text answers sanitized on write', await rich_text_answer_sanitized_on_write_checks())
183
184
  }
184
185
 
186
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
187
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Rich Text answers sanitized on write', run: rich_text_answer_sanitized_on_write_checks }
188
+
185
189
  // Allow running this test file independently
186
190
  if (require.main === module) {
187
191
  console.log(`🌐 Using API URL: ${host}`)
@@ -0,0 +1,99 @@
1
+ require('source-map-support').install();
2
+
3
+ import {
4
+ assert_checks,
5
+ Check,
6
+ setup_isolated_tenant,
7
+ IsolatedSuite,
8
+ } from "../setup_isolated"
9
+ import { EnduserSession } from "../../sdk"
10
+
11
+ const host = process.env.API_URL || 'http://localhost:8080' as const
12
+
13
+ const RAND = () => Math.random().toString(36).slice(2, 10)
14
+
15
+ // the session is confined to the video-join-link paths, so read the identity from the token itself
16
+ const enduserIdFromAuthToken = (authToken: string): string => (
17
+ JSON.parse(Buffer.from(authToken.split('.')[1], 'base64url').toString()).id
18
+ )
19
+
20
+ /**
21
+ * `calendar_events.session_for_join_link` must log in as the attendee who holds the token, not the
22
+ * first enduser attendee on the event.
23
+ */
24
+ export const session_for_join_link_checks = async (): Promise<Check[]> => {
25
+ const checks: Check[] = []
26
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
27
+
28
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
29
+
30
+ try {
31
+ tenant = await setup_isolated_tenant({
32
+ name: `join link ${RAND()}`,
33
+ users: [{ isAdmin: true }],
34
+ apiKeys: 0,
35
+ })
36
+ const { sdk, businessId } = tenant
37
+
38
+ const [enduserA, enduserB] = await Promise.all([
39
+ sdk.api.endusers.createOne({ email: `join_a_${RAND()}@test.tellescope.com` }),
40
+ sdk.api.endusers.createOne({ email: `join_b_${RAND()}@test.tellescope.com` }),
41
+ ])
42
+
43
+ const tokenA = `join-a-${RAND()}${RAND()}`
44
+ const tokenB = `join-b-${RAND()}${RAND()}`
45
+ const event = await sdk.api.calendar_events.createOne({
46
+ title: 'join link', durationInMinutes: 30, startTimeInMS: Date.now() + 3600_000,
47
+ attendees: [
48
+ { type: 'enduser', id: enduserA.id, joinLinkToken: tokenA },
49
+ { type: 'enduser', id: enduserB.id, joinLinkToken: tokenB },
50
+ ],
51
+ })
52
+
53
+ const sessionFor = (token: string) => (
54
+ new EnduserSession({ host, businessId }).api.calendar_events.session_for_join_link({ token })
55
+ )
56
+
57
+ const resultB = await sessionFor(tokenB)
58
+ const idB = enduserIdFromAuthToken(resultB.authToken)
59
+ record(
60
+ "second attendee's token logs in as the second attendee",
61
+ idB === enduserB.id && resultB.eventId === event.id,
62
+ `expected ${enduserB.id}, got ${idB}${idB === enduserA.id ? ' (the first attendee)' : ''}`,
63
+ )
64
+
65
+ const resultA = await sessionFor(tokenA)
66
+ const idA = enduserIdFromAuthToken(resultA.authToken)
67
+ record(
68
+ "first attendee's token logs in as the first attendee",
69
+ idA === enduserA.id && resultA.eventId === event.id,
70
+ `expected ${enduserA.id}, got ${idA}`,
71
+ )
72
+ } finally {
73
+ // teardown before anything asserts — assert calls process.exit, which skips pending finallys
74
+ if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
75
+ }
76
+
77
+ return checks
78
+ }
79
+
80
+ export const session_for_join_link_tests = async () => {
81
+ assert_checks('session_for_join_link attendee', await session_for_join_link_checks())
82
+ }
83
+
84
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
85
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'session_for_join_link attendee', run: session_for_join_link_checks }
86
+
87
+ // Allow running this test file independently
88
+ if (require.main === module) {
89
+ console.log(`🌐 Using API URL: ${host}`)
90
+ session_for_join_link_tests()
91
+ .then(() => {
92
+ console.log("✅ session_for_join_link test suite completed successfully")
93
+ process.exit(0)
94
+ })
95
+ .catch((error) => {
96
+ console.error("❌ session_for_join_link test suite failed:", error)
97
+ process.exit(1)
98
+ })
99
+ }
@@ -22,6 +22,7 @@ const buildSetFieldsAction = (prefix: string) => ({
22
22
  { name: `${prefix}_tracking`, value: '{{order.tracking}}', type: 'Custom Value' as const },
23
23
  { name: `${prefix}_carrier`, value: '{{order.carrier}}', type: 'Custom Value' as const },
24
24
  { name: `${prefix}_sku`, value: '{{order.sku}}', type: 'Custom Value' as const },
25
+ { name: `${prefix}_bookingLink`, value: '{{order.bookingLink}}', type: 'Custom Value' as const },
25
26
  { name: `${prefix}_externalId`, value: '{{order.externalId}}', type: 'Custom Value' as const },
26
27
  { name: `${prefix}_id`, value: '{{order.id}}', type: 'Custom Value' as const },
27
28
  { name: `${prefix}_protocol`, value: '{{order.protocol}}', type: 'Custom Value' as const },
@@ -55,6 +56,7 @@ const direct_order_creation_block = async ({ sdk }: { sdk: Session }) => {
55
56
  carrier: 'UPS',
56
57
  sku: 'SKU-A',
57
58
  protocol: 'wl1',
59
+ bookingLink: 'https://booking.example.com/a-123',
58
60
  })
59
61
  createdOrderId = order.id
60
62
 
@@ -70,6 +72,7 @@ const direct_order_creation_block = async ({ sdk }: { sdk: Session }) => {
70
72
  && e.fields?.pharmacy_sku === 'SKU-A'
71
73
  && e.fields?.pharmacy_externalId === 'EXT-A-123'
72
74
  && e.fields?.pharmacy_protocol === 'wl1'
75
+ && e.fields?.pharmacy_bookingLink === 'https://booking.example.com/a-123'
73
76
  && typeof e.fields?.pharmacy_id === 'string'
74
77
  && (e.fields?.pharmacy_id as string).length > 0
75
78
  && (e.fields?.pharmacy_id as string) === order.id
@@ -86,6 +89,7 @@ const direct_order_creation_block = async ({ sdk }: { sdk: Session }) => {
86
89
  pharmacy_externalId: beforeNonMatch.fields?.pharmacy_externalId,
87
90
  pharmacy_id: beforeNonMatch.fields?.pharmacy_id,
88
91
  pharmacy_protocol: beforeNonMatch.fields?.pharmacy_protocol,
92
+ pharmacy_bookingLink: beforeNonMatch.fields?.pharmacy_bookingLink,
89
93
  }
90
94
 
91
95
  const nonMatching = await sdk.api.enduser_orders.createOne({
@@ -114,6 +118,7 @@ const direct_order_creation_block = async ({ sdk }: { sdk: Session }) => {
114
118
  && e.fields?.pharmacy_externalId === snapshot.pharmacy_externalId
115
119
  && e.fields?.pharmacy_id === snapshot.pharmacy_id
116
120
  && e.fields?.pharmacy_protocol === snapshot.pharmacy_protocol
121
+ && e.fields?.pharmacy_bookingLink === snapshot.pharmacy_bookingLink
117
122
  )}
118
123
  )
119
124
  } finally {
@@ -130,6 +135,8 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
130
135
 
131
136
  const webhookUrl = `${host}/v1/webhooks/beluga`
132
137
  const externalOrderId = `EXT-B-${Date.now()}`
138
+ const noLinkOrderId = `EXT-B-NOLINK-${Date.now()}`
139
+ const bookingLink = 'https://booking.example.com/beluga-results'
133
140
 
134
141
  const enduser = await sdk.api.endusers.createOne({})
135
142
  const form = await sdk.api.forms.createOne({ title: 'Order Templates Beluga Form' })
@@ -139,6 +146,16 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
139
146
  formTitle: form.title,
140
147
  })
141
148
 
149
+ // The no-bookingLink case needs its OWN enduser: a fired trigger is throttled per
150
+ // (trigger, enduser) for a minute, so a second order for the same enduser would be
151
+ // silently skipped and the assertion would pass vacuously.
152
+ const noLinkEnduser = await sdk.api.endusers.createOne({})
153
+ const noLinkFormResponse = await sdk.api.form_responses.createOne({
154
+ formId: form.id,
155
+ enduserId: noLinkEnduser.id,
156
+ formTitle: form.title,
157
+ })
158
+
142
159
  const trigger = await sdk.api.automation_triggers.createOne({
143
160
  title: TRIGGER_TITLE_BLOCK_B,
144
161
  status: 'Active',
@@ -153,6 +170,13 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
153
170
  action: buildSetFieldsAction('pharmacy'),
154
171
  })
155
172
 
173
+ const resultsTrigger = await sdk.api.automation_triggers.createOne({
174
+ title: `${TRIGGER_TITLE_BLOCK_B} (Results Available)`,
175
+ status: 'Active',
176
+ event: { type: 'Order Status Equals', info: { source: 'Beluga', status: 'Results Available' } },
177
+ action: buildSetFieldsAction('pharmacy'),
178
+ })
179
+
156
180
  try {
157
181
  // Step 1: PHARMACY_ORDER_SHIPPED
158
182
  const shippedRes = await fetch(webhookUrl, {
@@ -179,9 +203,10 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
179
203
  && e.fields?.pharmacy_externalId === externalOrderId
180
204
  && typeof e.fields?.pharmacy_id === 'string'
181
205
  && (e.fields?.pharmacy_id as string).length > 0
182
- // Webhook does not set sku/protocol -> default branch in helper -> ''
206
+ // Webhook does not set sku/protocol/bookingLink -> default branch in helper -> ''
183
207
  && e.fields?.pharmacy_sku === ''
184
208
  && e.fields?.pharmacy_protocol === ''
209
+ && e.fields?.pharmacy_bookingLink === ''
185
210
  )}
186
211
  )
187
212
 
@@ -207,24 +232,84 @@ const beluga_webhook_block = async ({ sdk }: { sdk: Session }) => {
207
232
  && e.fields?.pharmacy_externalId === externalOrderId
208
233
  )}
209
234
  )
235
+
236
+ // Step 3: LAB_ORDER_RESULTS with a bookingLink -> status 'Results Available'
237
+ const resultsRes = await fetch(webhookUrl, {
238
+ method: 'POST',
239
+ headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify({
241
+ masterId: `tellescope_${formResponse.id}`,
242
+ event: 'LAB_ORDER_RESULTS',
243
+ orderId: externalOrderId,
244
+ bookingLink,
245
+ }),
246
+ })
247
+ assert(resultsRes.status === 200, `Beluga webhook (results) expected 200, got ${resultsRes.status}`)
248
+
249
+ await wait(undefined, 250)
250
+
251
+ await async_test(
252
+ "Block B: LAB_ORDER_RESULTS resolves {{order.bookingLink}} into an enduser field",
253
+ () => sdk.api.endusers.getOne(enduser.id),
254
+ { onResult: e => !!(
255
+ e.fields?.pharmacy_status === 'Results Available'
256
+ && e.fields?.pharmacy_bookingLink === bookingLink
257
+ && e.fields?.pharmacy_externalId === externalOrderId
258
+ )}
259
+ )
260
+
261
+ // Step 4: LAB_ORDER_RESULTS for a different enduser/order with NO bookingLink -> resolves to ''
262
+ const noLinkRes = await fetch(webhookUrl, {
263
+ method: 'POST',
264
+ headers: { 'Content-Type': 'application/json' },
265
+ body: JSON.stringify({
266
+ masterId: `tellescope_${noLinkFormResponse.id}`,
267
+ event: 'LAB_ORDER_RESULTS',
268
+ orderId: noLinkOrderId,
269
+ }),
270
+ })
271
+ assert(noLinkRes.status === 200, `Beluga webhook (results, no link) expected 200, got ${noLinkRes.status}`)
272
+
273
+ await wait(undefined, 250)
274
+
275
+ await async_test(
276
+ "Block B: LAB_ORDER_RESULTS without a bookingLink resolves to ''",
277
+ () => sdk.api.endusers.getOne(noLinkEnduser.id),
278
+ { onResult: e => !!(
279
+ e.fields?.pharmacy_status === 'Results Available' // proves the trigger did fire
280
+ && e.fields?.pharmacy_externalId === noLinkOrderId
281
+ && e.fields?.pharmacy_bookingLink === ''
282
+ )}
283
+ )
284
+
285
+ await async_test(
286
+ "Block B: the other enduser's bookingLink is untouched by the empty-link order",
287
+ () => sdk.api.endusers.getOne(enduser.id),
288
+ { onResult: e => e.fields?.pharmacy_bookingLink === bookingLink }
289
+ )
210
290
  } finally {
211
291
  // Clean up the order created by the webhook
212
- try {
213
- const orders = await sdk.api.enduser_orders.getSome({
214
- filter: { source: 'Beluga', externalId: externalOrderId } as any,
215
- })
216
- for (const o of orders) {
217
- await sdk.api.enduser_orders.deleteOne(o.id).catch(console.error)
292
+ for (const externalId of [externalOrderId, noLinkOrderId]) {
293
+ try {
294
+ const orders = await sdk.api.enduser_orders.getSome({
295
+ filter: { source: 'Beluga', externalId } as any,
296
+ })
297
+ for (const o of orders) {
298
+ await sdk.api.enduser_orders.deleteOne(o.id).catch(console.error)
299
+ }
300
+ } catch (err) {
301
+ console.error(err)
218
302
  }
219
- } catch (err) {
220
- console.error(err)
221
303
  }
222
304
 
223
305
  await sdk.api.automation_triggers.deleteOne(trigger.id).catch(console.error)
224
306
  await sdk.api.automation_triggers.deleteOne(deliveredTrigger.id).catch(console.error)
307
+ await sdk.api.automation_triggers.deleteOne(resultsTrigger.id).catch(console.error)
225
308
  await sdk.api.form_responses.deleteOne(formResponse.id).catch(console.error)
309
+ await sdk.api.form_responses.deleteOne(noLinkFormResponse.id).catch(console.error)
226
310
  await sdk.api.forms.deleteOne(form.id).catch(console.error)
227
311
  await sdk.api.endusers.deleteOne(enduser.id).catch(console.error)
312
+ await sdk.api.endusers.deleteOne(noLinkEnduser.id).catch(console.error)
228
313
  }
229
314
  }
230
315