@tellescope/sdk 1.251.0 → 1.252.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.d.ts +6 -0
  2. package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.d.ts.map +1 -0
  3. package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.js +139 -0
  4. package/lib/cjs/tests/api_tests/calendar_canvas_coding_clear.test.js.map +1 -0
  5. package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.js +337 -0
  8. package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.js.map +1 -0
  9. package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.d.ts +6 -0
  10. package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.d.ts.map +1 -0
  11. package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js +287 -0
  12. package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -0
  13. package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts.map +1 -1
  14. package/lib/cjs/tests/api_tests/integrations_redacted.test.js +30 -20
  15. package/lib/cjs/tests/api_tests/integrations_redacted.test.js.map +1 -1
  16. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -1
  17. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js +234 -198
  18. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -1
  19. package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts +28 -0
  20. package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts.map +1 -0
  21. package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js +349 -0
  22. package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js.map +1 -0
  23. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +28 -0
  24. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -0
  25. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +247 -0
  26. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -0
  27. package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts +29 -0
  28. package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts.map +1 -0
  29. package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.js +278 -0
  30. package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.js.map +1 -0
  31. package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts +24 -0
  32. package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
  33. package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js +201 -0
  34. package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js.map +1 -0
  35. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts +2 -0
  36. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts.map +1 -0
  37. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js +148 -0
  38. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js.map +1 -0
  39. package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts +2 -0
  40. package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts.map +1 -0
  41. package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js +88 -0
  42. package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js.map +1 -0
  43. package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts +32 -0
  44. package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts.map +1 -0
  45. package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js +237 -0
  46. package/lib/cjs/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js.map +1 -0
  47. package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts +38 -0
  48. package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts.map +1 -0
  49. package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js +222 -0
  50. package/lib/cjs/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js.map +1 -0
  51. package/lib/cjs/tests/api_tests/user_portal_settings.test.d.ts +6 -0
  52. package/lib/cjs/tests/api_tests/user_portal_settings.test.d.ts.map +1 -0
  53. package/lib/cjs/tests/api_tests/user_portal_settings.test.js +301 -0
  54. package/lib/cjs/tests/api_tests/user_portal_settings.test.js.map +1 -0
  55. package/lib/cjs/tests/tests.d.ts.map +1 -1
  56. package/lib/cjs/tests/tests.js +198 -151
  57. package/lib/cjs/tests/tests.js.map +1 -1
  58. package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.d.ts +6 -0
  59. package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.d.ts.map +1 -0
  60. package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.js +135 -0
  61. package/lib/esm/tests/api_tests/calendar_canvas_coding_clear.test.js.map +1 -0
  62. package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.d.ts +6 -0
  63. package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.d.ts.map +1 -0
  64. package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.js +333 -0
  65. package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.js.map +1 -0
  66. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.d.ts +6 -0
  67. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.d.ts.map +1 -0
  68. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js +280 -0
  69. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -0
  70. package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts.map +1 -1
  71. package/lib/esm/tests/api_tests/integrations_redacted.test.js +30 -20
  72. package/lib/esm/tests/api_tests/integrations_redacted.test.js.map +1 -1
  73. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -1
  74. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js +235 -199
  75. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -1
  76. package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts +28 -0
  77. package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts.map +1 -0
  78. package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js +345 -0
  79. package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js.map +1 -0
  80. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +28 -0
  81. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -0
  82. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +243 -0
  83. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -0
  84. package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts +29 -0
  85. package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts.map +1 -0
  86. package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.js +271 -0
  87. package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.js.map +1 -0
  88. package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts +24 -0
  89. package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
  90. package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js +194 -0
  91. package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js.map +1 -0
  92. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts +2 -0
  93. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts.map +1 -0
  94. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.js +144 -0
  95. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.js.map +1 -0
  96. package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts +2 -0
  97. package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts.map +1 -0
  98. package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.js +84 -0
  99. package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.js.map +1 -0
  100. package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts +32 -0
  101. package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.d.ts.map +1 -0
  102. package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js +233 -0
  103. package/lib/esm/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.js.map +1 -0
  104. package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts +38 -0
  105. package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.d.ts.map +1 -0
  106. package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js +218 -0
  107. package/lib/esm/tests/api_tests/security/F-0076-self-admin-role-assignment.test.js.map +1 -0
  108. package/lib/esm/tests/api_tests/user_portal_settings.test.d.ts +6 -0
  109. package/lib/esm/tests/api_tests/user_portal_settings.test.d.ts.map +1 -0
  110. package/lib/esm/tests/api_tests/user_portal_settings.test.js +297 -0
  111. package/lib/esm/tests/api_tests/user_portal_settings.test.js.map +1 -0
  112. package/lib/esm/tests/tests.d.ts.map +1 -1
  113. package/lib/esm/tests/tests.js +198 -151
  114. package/lib/esm/tests/tests.js.map +1 -1
  115. package/lib/tsconfig.tsbuildinfo +1 -1
  116. package/package.json +10 -10
  117. package/src/tests/api_tests/calendar_event_webhook_template.test.ts +204 -0
  118. package/src/tests/api_tests/enduser_login_rate_limits.test.ts +178 -0
  119. package/src/tests/api_tests/integrations_redacted.test.ts +8 -0
  120. package/src/tests/api_tests/push_forms_to_portal_group_completion.test.ts +113 -88
  121. package/src/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.ts +236 -0
  122. package/src/tests/api_tests/security/F-0005-ai-conversations-rbac.test.ts +154 -0
  123. package/src/tests/api_tests/security/F-0007-invite-user-enumeration.test.ts +198 -0
  124. package/src/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.ts +130 -0
  125. package/src/tests/api_tests/security/F-0013-sanitize-user-html.test.ts +109 -0
  126. package/src/tests/api_tests/security/F-0016-prototype-pollution.test.ts +50 -0
  127. package/src/tests/api_tests/security/F-0053-cascade-role-rename-cross-tenant.test.ts +161 -0
  128. package/src/tests/api_tests/security/F-0076-self-admin-role-assignment.test.ts +165 -0
  129. package/src/tests/api_tests/user_portal_settings.test.ts +217 -0
  130. package/src/tests/tests.ts +25 -2
  131. package/test_generated.pdf +0 -0
@@ -0,0 +1,204 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../sdk"
4
+ import {
5
+ async_test,
6
+ log_header,
7
+ wait,
8
+ } from "@tellescope/testing"
9
+ import { setup_tests } from "../setup"
10
+
11
+ const host = process.env.API_URL || 'http://localhost:8080' as const
12
+
13
+ const HEALTHIE_TITLE = 'Healthie' // mirror @tellescope/constants HEALTHIE_TITLE
14
+
15
+ // Unreachable webhook target — the request will fail quickly but the API still writes
16
+ // the resolved payload to webhook_logs, which is what we're verifying.
17
+ const WEBHOOK_TARGET_URL = 'http://127.0.0.1:9'
18
+
19
+ const find_log_for_url = async (sdk: Session, url: string, expectedHealthieId: string) => {
20
+ const start = Date.now()
21
+ while (Date.now() - start < 10_000) {
22
+ const logs = await sdk.api.webhook_logs.getSome({ limit: 50, sort: 'newFirst' as any }) as Array<any>
23
+ const match = logs.find(l => l.url === url && (l.payload as any)?.healthieId === expectedHealthieId)
24
+ if (match) return match
25
+ await wait(undefined, 500)
26
+ }
27
+ return null
28
+ }
29
+
30
+ // Main test function that can be called independently
31
+ export const calendar_event_webhook_template_tests = async (
32
+ { sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }
33
+ ) => {
34
+ log_header("Calendar Event Webhook Template Tests")
35
+
36
+ const enduser = await sdk.api.endusers.createOne({})
37
+
38
+ // Case 1: Healthie ID resolved via source + externalId
39
+ const externalIdEvent = await sdk.api.calendar_events.createOne({
40
+ title: 'External ID Calendar Event',
41
+ durationInMinutes: 30,
42
+ startTimeInMS: Date.now(),
43
+ attendees: [{ type: 'enduser', id: enduser.id }],
44
+ source: HEALTHIE_TITLE,
45
+ externalId: 'evt_HEALTHIE_123',
46
+ } as any)
47
+
48
+ // Case 2: Healthie ID resolved via references
49
+ const referencesEvent = await sdk.api.calendar_events.createOne({
50
+ title: 'References Calendar Event',
51
+ durationInMinutes: 30,
52
+ startTimeInMS: Date.now(),
53
+ attendees: [{ type: 'enduser', id: enduser.id }],
54
+ references: [{ type: HEALTHIE_TITLE, id: 'evt_REF_456' }],
55
+ } as any)
56
+
57
+ // create an automation step to satisfy schema validation (automationStepId is required)
58
+ const journey = await sdk.api.journeys.createOne({
59
+ title: 'Calendar Event Webhook Template Tests',
60
+ defaultState: 'State 1',
61
+ })
62
+ const step = await sdk.api.automation_steps.createOne({
63
+ journeyId: journey.id,
64
+ events: [{ type: 'onJourneyStart', info: {} }],
65
+ action: {
66
+ type: 'sendWebhook',
67
+ info: {
68
+ message: 'placeholder',
69
+ url: WEBHOOK_TARGET_URL,
70
+ },
71
+ },
72
+ })
73
+
74
+ try {
75
+ await async_test(
76
+ 'Send Webhook resolves {{calendar_event.Healthie ID}} from source+externalId',
77
+ async () => {
78
+ const url = `${WEBHOOK_TARGET_URL}/external/${externalIdEvent.id}`
79
+ await sdk.api.webhooks.send_automation_webhook({
80
+ message: 'placeholder',
81
+ enduserId: enduser.id,
82
+ automationStepId: step.id,
83
+ action: {
84
+ type: 'sendWebhook',
85
+ info: {
86
+ message: 'placeholder',
87
+ url,
88
+ fields: [
89
+ { field: 'healthieId', value: '{{calendar_event.Healthie ID}}' },
90
+ { field: 'title', value: '{{calendar_event.title}}' },
91
+ ],
92
+ },
93
+ },
94
+ context: { calendarEventId: externalIdEvent.id },
95
+ }).catch(() => {}) // the unreachable URL is expected to error after logging
96
+
97
+ const log = await find_log_for_url(sdk, url, 'evt_HEALTHIE_123')
98
+ if (!log) return false
99
+ const payload = log.payload as any
100
+ return payload?.healthieId === 'evt_HEALTHIE_123'
101
+ && payload?.title === externalIdEvent.title
102
+ },
103
+ { onResult: r => r === true }
104
+ )
105
+
106
+ await async_test(
107
+ 'Send Webhook resolves {{calendar_event.Healthie ID}} from references',
108
+ async () => {
109
+ const url = `${WEBHOOK_TARGET_URL}/refs/${referencesEvent.id}`
110
+ await sdk.api.webhooks.send_automation_webhook({
111
+ message: 'placeholder',
112
+ enduserId: enduser.id,
113
+ automationStepId: step.id,
114
+ action: {
115
+ type: 'sendWebhook',
116
+ info: {
117
+ message: 'placeholder',
118
+ url,
119
+ fields: [
120
+ { field: 'healthieId', value: '{{calendar_event.Healthie ID}}' },
121
+ { field: 'title', value: '{{calendar_event.title}}' },
122
+ ],
123
+ },
124
+ },
125
+ context: { calendarEventId: referencesEvent.id },
126
+ }).catch(() => {})
127
+
128
+ const log = await find_log_for_url(sdk, url, 'evt_REF_456')
129
+ if (!log) return false
130
+ const payload = log.payload as any
131
+ return payload?.healthieId === 'evt_REF_456'
132
+ && payload?.title === referencesEvent.title
133
+ },
134
+ { onResult: r => r === true }
135
+ )
136
+
137
+ await async_test(
138
+ 'Send Webhook leaves {{calendar_event.Healthie ID}} blank when no calendarEventId in context',
139
+ async () => {
140
+ const url = `${WEBHOOK_TARGET_URL}/nocontext`
141
+ await sdk.api.webhooks.send_automation_webhook({
142
+ message: 'placeholder',
143
+ enduserId: enduser.id,
144
+ automationStepId: step.id,
145
+ action: {
146
+ type: 'sendWebhook',
147
+ info: {
148
+ message: 'placeholder',
149
+ url,
150
+ fields: [
151
+ { field: 'healthieId', value: '{{calendar_event.Healthie ID}}' },
152
+ ],
153
+ },
154
+ },
155
+ // no calendarEventId
156
+ }).catch(() => {})
157
+
158
+ // Without a calendar event in context, the templating helper returns the input
159
+ // unchanged, so the literal template string remains in the payload.
160
+ const start = Date.now()
161
+ while (Date.now() - start < 10_000) {
162
+ const logs = await sdk.api.webhook_logs.getSome({ limit: 50, sort: 'newFirst' as any }) as Array<any>
163
+ const match = logs.find(l => l.url === url)
164
+ if (match) {
165
+ const payload = match.payload as any
166
+ return payload?.healthieId === '{{calendar_event.Healthie ID}}'
167
+ }
168
+ await wait(undefined, 500)
169
+ }
170
+ return false
171
+ },
172
+ { onResult: r => r === true }
173
+ )
174
+
175
+ } finally {
176
+ try { await sdk.api.calendar_events.deleteOne(externalIdEvent.id) } catch {}
177
+ try { await sdk.api.calendar_events.deleteOne(referencesEvent.id) } catch {}
178
+ try { await sdk.api.automation_steps.deleteOne(step.id) } catch {}
179
+ try { await sdk.api.journeys.deleteOne(journey.id) } catch {}
180
+ try { await sdk.api.endusers.deleteOne(enduser.id) } catch {}
181
+ }
182
+ }
183
+
184
+ // Allow running this test file independently
185
+ if (require.main === module) {
186
+ console.log(`🌐 Using API URL: ${host}`)
187
+ const sdk = new Session({ host })
188
+ const sdkNonAdmin = new Session({ host })
189
+
190
+ const runTests = async () => {
191
+ await setup_tests(sdk, sdkNonAdmin)
192
+ await calendar_event_webhook_template_tests({ sdk, sdkNonAdmin })
193
+ }
194
+
195
+ runTests()
196
+ .then(() => {
197
+ console.log("✅ Calendar event webhook template test suite completed successfully")
198
+ process.exit(0)
199
+ })
200
+ .catch((error) => {
201
+ console.error("❌ Calendar event webhook template test suite failed:", error)
202
+ process.exit(1)
203
+ })
204
+ }
@@ -0,0 +1,178 @@
1
+ require('source-map-support').install();
2
+
3
+ import axios from "axios"
4
+ import { Session } from "../../sdk"
5
+ import {
6
+ async_test,
7
+ log_header,
8
+ } from "@tellescope/testing"
9
+ import { setup_tests } from "../setup"
10
+
11
+ const host = process.env.API_URL || 'http://localhost:8080' as const
12
+
13
+ // Per-IP rate limits applied to enduser public endpoints:
14
+ // POST /v1/login-enduser 20 / min, 100 / hour
15
+ // POST /v1/begin-enduser-login-flow 10 / min, 50 / hour
16
+ // POST /v1/endusers/send-otp-code 5 / min, 30 / hour
17
+ // Plus a per-identifier limit on begin_login_flow: 5 / 10 min per email|phone.
18
+
19
+ const post = async (path: string, body: any) => {
20
+ try {
21
+ const res = await axios.post(`${host}${path}`, body, { validateStatus: () => true })
22
+ return { status: res.status, data: res.data }
23
+ } catch (err: any) {
24
+ return { status: err?.response?.status, data: err?.response?.data }
25
+ }
26
+ }
27
+
28
+ const fire_until_429 = async (cap: number, send: (i: number) => Promise<{ status: number }>) => {
29
+ let triggeredAt = -1
30
+ for (let i = 0; i < cap + 5; i++) {
31
+ const { status } = await send(i)
32
+ if (status === 429) {
33
+ triggeredAt = i
34
+ break
35
+ }
36
+ }
37
+ return triggeredAt
38
+ }
39
+
40
+ export const enduser_login_rate_limits_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
41
+ log_header("Enduser Login Rate Limit Tests")
42
+
43
+ const businessId = sdk.userInfo.businessId
44
+
45
+ // Ensure throttled_events from prior tests don't bleed in.
46
+ await sdk.reset_db()
47
+
48
+ // ---- /v1/login-enduser per-IP cap (20/min) ----
49
+ // Bogus emails ensure we never reach the actual DB lookup / login work.
50
+ const loginTrip = await fire_until_429(20, i => post('/v1/login-enduser', {
51
+ email: `rl-login-${Date.now()}-${i}@tellescope.com`,
52
+ password: 'NotARealPassword!2025',
53
+ businessId,
54
+ }))
55
+ await async_test(
56
+ 'login per-IP throttle trips within first 21 requests',
57
+ async () => (loginTrip >= 0 && loginTrip <= 20) ? 'tripped' : `not-tripped:${loginTrip}`,
58
+ { expectedResult: 'tripped' }
59
+ )
60
+
61
+ await sdk.reset_db()
62
+
63
+ // ---- /v1/begin-enduser-login-flow per-IP cap (10/min) ----
64
+ // Use distinct emails so the per-identifier limit (5/10min) does NOT trip first;
65
+ // we want the IP cap to be the first thing to fire.
66
+ const beginIpTrip = await fire_until_429(10, i => post('/v1/begin-enduser-login-flow', {
67
+ email: `rl-begin-ip-${Date.now()}-${i}@tellescope.com`,
68
+ businessId,
69
+ }))
70
+ await async_test(
71
+ 'begin_login_flow per-IP throttle trips within first 11 requests',
72
+ async () => (beginIpTrip >= 0 && beginIpTrip <= 10) ? 'tripped' : `not-tripped:${beginIpTrip}`,
73
+ { expectedResult: 'tripped' }
74
+ )
75
+
76
+ await sdk.reset_db()
77
+
78
+ // ---- /v1/begin-enduser-login-flow per-identifier cap (5 / 10 min per email) ----
79
+ // Hit a single email below the per-IP cap.
80
+ const fixedEmail = `rl-begin-id-${Date.now()}@tellescope.com`
81
+ const beginIdTrip = await fire_until_429(5, () => post('/v1/begin-enduser-login-flow', {
82
+ email: fixedEmail,
83
+ businessId,
84
+ }))
85
+ await async_test(
86
+ 'begin_login_flow per-identifier throttle trips within first 6 requests',
87
+ async () => (beginIdTrip >= 0 && beginIdTrip <= 5) ? 'tripped' : `not-tripped:${beginIdTrip}`,
88
+ { expectedResult: 'tripped' }
89
+ )
90
+
91
+ await sdk.reset_db()
92
+
93
+ // ---- /v1/endusers/send-otp-code per-IP cap (5/min) ----
94
+ // Use a bogus JWT-shaped token so we trip the IP guard first (it runs before any DB work).
95
+ const fakeToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9.sig'
96
+ const sendOtpTrip = await fire_until_429(5, () => post('/v1/endusers/send-otp-code', {
97
+ token: fakeToken,
98
+ method: 'email',
99
+ }))
100
+ await async_test(
101
+ 'send_otp per-IP throttle trips within first 6 requests',
102
+ async () => (sendOtpTrip >= 0 && sendOtpTrip <= 5) ? 'tripped' : `not-tripped:${sendOtpTrip}`,
103
+ { expectedResult: 'tripped' }
104
+ )
105
+
106
+ // Confirm 429 response does not leak the keying strategy (no mention of "ip" or "address").
107
+ const tripped = await post('/v1/endusers/send-otp-code', { token: fakeToken, method: 'email' })
108
+ await async_test(
109
+ '429 response does not mention "ip" or "address"',
110
+ async () => {
111
+ const msg = (tripped.data?.message ?? '').toLowerCase()
112
+ return (msg.includes('ip') || msg.includes('address')) ? 'leaked' : 'safe'
113
+ },
114
+ { expectedResult: 'safe' }
115
+ )
116
+
117
+ // ---- Legitimate-login regression: a single successful login should still go through ----
118
+ // Reset state, then create a real enduser with a password and confirm one login succeeds.
119
+ await sdk.reset_db()
120
+
121
+ const ts = Date.now()
122
+ const enduser = await sdk.api.endusers.createOne({
123
+ fname: 'RateLimitOk', lname: 'Enduser',
124
+ email: `rl-legit-${ts}@tellescope.com`,
125
+ })
126
+ try {
127
+ await sdk.api.endusers.set_password({ id: enduser.id, password: 'CorrectPassword123!' })
128
+
129
+ const goodLogin = await post('/v1/login-enduser', {
130
+ email: enduser.email,
131
+ password: 'CorrectPassword123!',
132
+ businessId,
133
+ })
134
+ await async_test(
135
+ 'legitimate login still succeeds (returns authToken, not 429)',
136
+ async () => goodLogin.status === 200 && !!goodLogin.data?.authToken ? 'ok' : `failed:${goodLogin.status}`,
137
+ { expectedResult: 'ok' }
138
+ )
139
+
140
+ // A subsequent successful login by the same user/IP should also succeed —
141
+ // a single legitimate user retrying must not trip the per-IP cap.
142
+ const goodLoginRetry = await post('/v1/login-enduser', {
143
+ email: enduser.email,
144
+ password: 'CorrectPassword123!',
145
+ businessId,
146
+ })
147
+ await async_test(
148
+ 'legitimate login retry still succeeds',
149
+ async () => goodLoginRetry.status === 200 && !!goodLoginRetry.data?.authToken ? 'ok' : `failed:${goodLoginRetry.status}`,
150
+ { expectedResult: 'ok' }
151
+ )
152
+ } finally {
153
+ await sdk.api.endusers.deleteOne(enduser.id).catch(() => null)
154
+ await sdk.reset_db().catch(() => null)
155
+ }
156
+ }
157
+
158
+ // Allow running this test file independently
159
+ if (require.main === module) {
160
+ console.log(`🌐 Using API URL: ${host}`)
161
+ const sdk = new Session({ host })
162
+ const sdkNonAdmin = new Session({ host })
163
+
164
+ const runTests = async () => {
165
+ await setup_tests(sdk, sdkNonAdmin)
166
+ await enduser_login_rate_limits_tests({ sdk, sdkNonAdmin })
167
+ }
168
+
169
+ runTests()
170
+ .then(() => {
171
+ console.log("✅ Enduser login rate limit test suite completed successfully")
172
+ process.exit(0)
173
+ })
174
+ .catch((error) => {
175
+ console.error("❌ Enduser login rate limit test suite failed:", error)
176
+ process.exit(1)
177
+ })
178
+ }
@@ -205,6 +205,14 @@ export const integrations_redacted_tests = async ({ sdk, sdkNonAdmin } : { sdk:
205
205
  }}
206
206
  )
207
207
 
208
+ // F-0049: connect_stripe must reject caller-supplied accountId (Stripe-account-spoofing defense).
209
+ // Stripe Connect is deprecated in Tellescope; accountId was a test shortcut, kept rejected to preserve SDK shape.
210
+ await async_test(
211
+ "connect_stripe rejects caller-supplied accountId",
212
+ () => sdk.api.integrations.connect_stripe({ countryCode: 'US', accountId: 'acct_someoneElsesAccount' } as any),
213
+ { shouldError: true, onError: (e: any) => /accountId is not supported/i.test(e?.message || e?.toString() || '') }
214
+ )
215
+
208
216
  } finally {
209
217
  if (integrationId) {
210
218
  try {
@@ -1,6 +1,6 @@
1
1
  require('source-map-support').install();
2
2
 
3
- import { Session } from "../../sdk"
3
+ import { Session, EnduserSession } from "../../sdk"
4
4
  import { log_header, wait, async_test } from "@tellescope/testing"
5
5
  import { Enduser } from "@tellescope/types-client"
6
6
  import { setup_tests } from "../setup"
@@ -52,110 +52,135 @@ export const push_forms_to_portal_group_completion_tests = async ({ sdk, sdkNonA
52
52
  previousFields: [{ type: 'root', info: {} }],
53
53
  })
54
54
 
55
- // 2. Create a form group containing both forms
55
+ // 2. Create a form group containing both forms (shared across both submission flows)
56
56
  const formGroup = await sdk.api.form_groups.createOne({
57
57
  title: 'Push To Portal Test Group',
58
58
  formIds: [formA.id, formB.id],
59
59
  })
60
60
  createdFormGroupIds.push(formGroup.id)
61
61
 
62
- // 3. Configure trigger with event.info.groupId = the real formGroupId
63
- const trigger = await sdk.api.automation_triggers.createOne({
64
- event: { type: 'Form Group Completed', info: { groupId: formGroup.id } },
65
- action: { type: 'Add Tags', info: { tags: ['form-group-completed-push'] } },
66
- status: 'Active',
67
- title: 'Form Group Completed - Push to Portal',
68
- })
69
- createdTriggerIds.push(trigger.id)
62
+ // Helper: run the full push-to-portal → submit → trigger flow with a configurable submitter.
63
+ // Each invocation creates its own trigger/journey/step/enduser and asserts its own tag.
64
+ // We test both the admin (user-session) submitter and the enduser (portal-session) submitter
65
+ // because they exercise different DB scopes in submit_form_response and the regression QA caught
66
+ // was only triggered on the enduser-session path.
67
+ const runFlow = async ({ label, tag, submitAsEnduser } : { label: string, tag: string, submitAsEnduser: boolean }) => {
68
+ const trigger = await sdk.api.automation_triggers.createOne({
69
+ event: { type: 'Form Group Completed', info: { groupId: formGroup.id } },
70
+ action: { type: 'Add Tags', info: { tags: [tag] } },
71
+ status: 'Active',
72
+ title: `Form Group Completed - Push to Portal (${label})`,
73
+ })
74
+ createdTriggerIds.push(trigger.id)
70
75
 
71
- // 4. Create journey with a pushFormsToPortal step referencing the form group
72
- const journey = await sdk.api.journeys.createOne({
73
- title: 'Push To Portal Trigger Journey',
74
- })
75
- createdJourneyIds.push(journey.id)
76
+ const journey = await sdk.api.journeys.createOne({
77
+ title: `Push To Portal Trigger Journey (${label})`,
78
+ })
79
+ createdJourneyIds.push(journey.id)
76
80
 
77
- const pushStep = await sdk.api.automation_steps.createOne({
78
- journeyId: journey.id,
79
- action: { type: 'pushFormsToPortal', info: { formGroupIds: [formGroup.id] } },
80
- events: [{ type: 'onJourneyStart', info: {} }],
81
- })
81
+ const pushStep = await sdk.api.automation_steps.createOne({
82
+ journeyId: journey.id,
83
+ action: { type: 'pushFormsToPortal', info: { formGroupIds: [formGroup.id] } },
84
+ events: [{ type: 'onJourneyStart', info: {} }],
85
+ })
82
86
 
83
- // 5. Create enduser and add to journey
84
- const enduser = await sdk.api.endusers.createOne({ fname: 'PushPortal', lname: 'Tester' })
85
- createdEnduserIds.push(enduser.id)
87
+ const enduser = await sdk.api.endusers.createOne({ fname: 'PushPortal', lname: label })
88
+ createdEnduserIds.push(enduser.id)
86
89
 
87
- await sdk.api.endusers.add_to_journey({
88
- enduserIds: [enduser.id],
89
- journeyId: journey.id,
90
- })
90
+ await sdk.api.endusers.add_to_journey({
91
+ enduserIds: [enduser.id],
92
+ journeyId: journey.id,
93
+ })
91
94
 
92
- // 6. Poll for the worker to create the push-to-portal form_responses
93
- const pushedResponses = await pollFor(
94
- async () => {
95
- const responses = await sdk.api.form_responses.getSome({
96
- filter: { enduserId: enduser.id },
97
- })
98
- const pushed = responses.filter(r => !!r.pushedToPortalAt)
99
- return pushed.length >= 2 ? pushed : undefined
100
- },
101
- (result): result is any[] => Array.isArray(result) && result.length >= 2,
102
- 'pushed-to-portal form_responses to be created by worker',
103
- 500,
104
- 40,
105
- )
106
-
107
- // 7. Assert worker behavior: groupId === automationStepId and pushedToPortalAt is set
108
- for (const fr of pushedResponses) {
109
- if (!fr.pushedToPortalAt) {
110
- throw new Error(`Expected pushedToPortalAt to be set on form_response ${fr.id}`)
95
+ const pushedResponses = await pollFor(
96
+ async () => {
97
+ const responses = await sdk.api.form_responses.getSome({
98
+ filter: { enduserId: enduser.id },
99
+ })
100
+ const pushed = responses.filter(r => !!r.pushedToPortalAt)
101
+ return pushed.length >= 2 ? pushed : undefined
102
+ },
103
+ (result): result is any[] => Array.isArray(result) && result.length >= 2,
104
+ `pushed-to-portal form_responses to be created by worker (${label})`,
105
+ 500,
106
+ 40,
107
+ )
108
+
109
+ for (const fr of pushedResponses) {
110
+ if (!fr.pushedToPortalAt) {
111
+ throw new Error(`Expected pushedToPortalAt to be set on form_response ${fr.id} (${label})`)
112
+ }
113
+ if (fr.groupId !== pushStep.id) {
114
+ throw new Error(`Expected form_response.groupId (${fr.groupId}) to equal automation step id (${pushStep.id}) (${label})`)
115
+ }
116
+ if (fr.automationStepId !== pushStep.id) {
117
+ throw new Error(`Expected form_response.automationStepId (${fr.automationStepId}) to equal automation step id (${pushStep.id}) (${label})`)
118
+ }
111
119
  }
112
- if (fr.groupId !== pushStep.id) {
113
- throw new Error(`Expected form_response.groupId (${fr.groupId}) to equal automation step id (${pushStep.id})`)
120
+
121
+ await async_test(
122
+ `Worker writes groupId === automationStepId and pushedToPortalAt set (${label})`,
123
+ async () => true,
124
+ { onResult: r => r === true },
125
+ )
126
+
127
+ // Build the submitter session
128
+ let submitterApi: typeof sdk.api | EnduserSession['api']
129
+ if (submitAsEnduser) {
130
+ const { authToken } = await sdk.api.endusers.generate_auth_token({ id: enduser.id })
131
+ const enduserSDK = new EnduserSession({ host, authToken, businessId: sdk.userInfo.businessId })
132
+ submitterApi = enduserSDK.api
133
+ } else {
134
+ submitterApi = sdk.api
114
135
  }
115
- if (fr.automationStepId !== pushStep.id) {
116
- throw new Error(`Expected form_response.automationStepId (${fr.automationStepId}) to equal automation step id (${pushStep.id})`)
136
+
137
+ for (const fr of pushedResponses) {
138
+ const isFormA = fr.formId === formA.id
139
+ const targetFieldId = isFormA ? fieldA.id : fieldB.id
140
+ const targetFieldTitle = isFormA ? 'FieldA' : 'FieldB'
141
+ await submitterApi.form_responses.submit_form_response({
142
+ accessCode: fr.accessCode as string,
143
+ responses: [{
144
+ fieldId: targetFieldId,
145
+ fieldTitle: targetFieldTitle,
146
+ answer: { type: 'string', value: 'pushed-portal-answer' },
147
+ }],
148
+ })
117
149
  }
118
- }
119
150
 
120
- await async_test(
121
- "Worker writes groupId === automationStepId and pushedToPortalAt set",
122
- async () => true,
123
- { onResult: r => r === true },
124
- )
125
-
126
- // 8. Submit every form_response on behalf of the enduser
127
- // Identify which form_response corresponds to formA / formB via formId
128
- for (const fr of pushedResponses) {
129
- const isFormA = fr.formId === formA.id
130
- const targetFieldId = isFormA ? fieldA.id : fieldB.id
131
- const targetFieldTitle = isFormA ? 'FieldA' : 'FieldB'
132
- await sdk.api.form_responses.submit_form_response({
133
- accessCode: fr.accessCode as string,
134
- responses: [{
135
- fieldId: targetFieldId,
136
- fieldTitle: targetFieldTitle,
137
- answer: { type: 'string', value: 'pushed-portal-answer' },
138
- }],
139
- })
151
+ await pollFor(
152
+ async () => {
153
+ const e = await sdk.api.endusers.getOne(enduser.id)
154
+ return e.tags?.includes(tag) ? e : undefined
155
+ },
156
+ (result): result is Enduser => !!result,
157
+ `Form Group Completed trigger to apply tag after push-to-portal submissions (${label})`,
158
+ 500,
159
+ 30,
160
+ )
161
+
162
+ await async_test(
163
+ `Form Group Completed trigger fires for push-to-portal completion (${label})`,
164
+ () => sdk.api.endusers.getOne(enduser.id),
165
+ { onResult: (e: Enduser) => !!e.tags?.includes(tag) },
166
+ )
140
167
  }
141
168
 
142
- // 9. Poll for the trigger's side-effect (tag on enduser)
143
- await pollFor(
144
- async () => {
145
- const e = await sdk.api.endusers.getOne(enduser.id)
146
- return e.tags?.includes('form-group-completed-push') ? e : undefined
147
- },
148
- (result): result is Enduser => !!result,
149
- 'Form Group Completed trigger to apply tag after push-to-portal submissions',
150
- 500,
151
- 30,
152
- )
153
-
154
- await async_test(
155
- "Form Group Completed trigger fires for push-to-portal completion",
156
- () => sdk.api.endusers.getOne(enduser.id),
157
- { onResult: (e: Enduser) => !!e.tags?.includes('form-group-completed-push') },
158
- )
169
+ // Admin submitter: simulates a staff user filling in the form on behalf of the patient
170
+ // (uses a user-scoped DB in submit_form_response).
171
+ await runFlow({
172
+ label: 'admin-submit',
173
+ tag: 'form-group-completed-push-admin',
174
+ submitAsEnduser: false,
175
+ })
176
+
177
+ // Enduser submitter: simulates the patient submitting via the portal
178
+ // (uses an enduser-scoped DB in submit_form_response — exercises the path QA caught).
179
+ await runFlow({
180
+ label: 'enduser-submit',
181
+ tag: 'form-group-completed-push-enduser',
182
+ submitAsEnduser: true,
183
+ })
159
184
 
160
185
  } finally {
161
186
  for (const id of createdTriggerIds) {