@tellescope/sdk 1.250.2 → 1.252.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 (136) hide show
  1. package/lib/cjs/sdk.d.ts +9 -0
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/sdk.js +3 -0
  4. package/lib/cjs/sdk.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/account_switcher.test.d.ts.map +1 -1
  6. package/lib/cjs/tests/api_tests/account_switcher.test.js +1700 -306
  7. package/lib/cjs/tests/api_tests/account_switcher.test.js.map +1 -1
  8. package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.d.ts +6 -0
  9. package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.d.ts.map +1 -0
  10. package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.js +337 -0
  11. package/lib/cjs/tests/api_tests/calendar_event_webhook_template.test.js.map +1 -0
  12. package/lib/cjs/tests/api_tests/enduser_login.test.d.ts +6 -0
  13. package/lib/cjs/tests/api_tests/enduser_login.test.d.ts.map +1 -0
  14. package/lib/cjs/tests/api_tests/enduser_login.test.js +315 -0
  15. package/lib/cjs/tests/api_tests/enduser_login.test.js.map +1 -0
  16. package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.d.ts +6 -0
  17. package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.d.ts.map +1 -0
  18. package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js +287 -0
  19. package/lib/cjs/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -0
  20. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
  21. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
  22. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js +406 -0
  23. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
  24. package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts +28 -0
  25. package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts.map +1 -0
  26. package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js +349 -0
  27. package/lib/cjs/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js.map +1 -0
  28. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +28 -0
  29. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -0
  30. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +247 -0
  31. package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -0
  32. package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts +29 -0
  33. package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts.map +1 -0
  34. package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.js +278 -0
  35. package/lib/cjs/tests/api_tests/security/F-0007-invite-user-enumeration.test.js.map +1 -0
  36. package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts +24 -0
  37. package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
  38. package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js +201 -0
  39. package/lib/cjs/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js.map +1 -0
  40. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts +2 -0
  41. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts.map +1 -0
  42. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js +148 -0
  43. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js.map +1 -0
  44. package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts +2 -0
  45. package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts.map +1 -0
  46. package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js +88 -0
  47. package/lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js.map +1 -0
  48. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
  49. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
  50. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +373 -0
  51. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
  52. package/lib/cjs/tests/setup.d.ts.map +1 -1
  53. package/lib/cjs/tests/setup.js +47 -32
  54. package/lib/cjs/tests/setup.js.map +1 -1
  55. package/lib/cjs/tests/tests.d.ts.map +1 -1
  56. package/lib/cjs/tests/tests.js +215 -159
  57. package/lib/cjs/tests/tests.js.map +1 -1
  58. package/lib/esm/sdk.d.ts +9 -0
  59. package/lib/esm/sdk.d.ts.map +1 -1
  60. package/lib/esm/sdk.js +3 -0
  61. package/lib/esm/sdk.js.map +1 -1
  62. package/lib/esm/tests/api_tests/account_switcher.test.d.ts.map +1 -1
  63. package/lib/esm/tests/api_tests/account_switcher.test.js +1702 -305
  64. package/lib/esm/tests/api_tests/account_switcher.test.js.map +1 -1
  65. package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.d.ts +6 -0
  66. package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.d.ts.map +1 -0
  67. package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.js +333 -0
  68. package/lib/esm/tests/api_tests/calendar_event_webhook_template.test.js.map +1 -0
  69. package/lib/esm/tests/api_tests/enduser_login.test.d.ts +6 -0
  70. package/lib/esm/tests/api_tests/enduser_login.test.d.ts.map +1 -0
  71. package/lib/esm/tests/api_tests/enduser_login.test.js +308 -0
  72. package/lib/esm/tests/api_tests/enduser_login.test.js.map +1 -0
  73. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts +6 -0
  74. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts.map +1 -0
  75. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js +268 -0
  76. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js.map +1 -0
  77. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.d.ts +6 -0
  78. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.d.ts.map +1 -0
  79. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js +280 -0
  80. package/lib/esm/tests/api_tests/enduser_login_rate_limits.test.js.map +1 -0
  81. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
  82. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
  83. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js +402 -0
  84. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
  85. package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts +28 -0
  86. package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.d.ts.map +1 -0
  87. package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js +345 -0
  88. package/lib/esm/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.js.map +1 -0
  89. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +28 -0
  90. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -0
  91. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +243 -0
  92. package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -0
  93. package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts +29 -0
  94. package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.d.ts.map +1 -0
  95. package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.js +271 -0
  96. package/lib/esm/tests/api_tests/security/F-0007-invite-user-enumeration.test.js.map +1 -0
  97. package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts +24 -0
  98. package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
  99. package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js +194 -0
  100. package/lib/esm/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.js.map +1 -0
  101. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts +2 -0
  102. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.d.ts.map +1 -0
  103. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.js +144 -0
  104. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html.test.js.map +1 -0
  105. package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts +2 -0
  106. package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.d.ts.map +1 -0
  107. package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.js +84 -0
  108. package/lib/esm/tests/api_tests/security/F-0016-prototype-pollution.test.js.map +1 -0
  109. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
  110. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
  111. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +369 -0
  112. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
  113. package/lib/esm/tests/setup.d.ts.map +1 -1
  114. package/lib/esm/tests/setup.js +47 -32
  115. package/lib/esm/tests/setup.js.map +1 -1
  116. package/lib/esm/tests/tests.d.ts.map +1 -1
  117. package/lib/esm/tests/tests.js +215 -159
  118. package/lib/esm/tests/tests.js.map +1 -1
  119. package/lib/tsconfig.tsbuildinfo +1 -1
  120. package/package.json +10 -10
  121. package/src/sdk.ts +12 -0
  122. package/src/tests/api_tests/account_switcher.test.ts +1283 -0
  123. package/src/tests/api_tests/calendar_event_webhook_template.test.ts +204 -0
  124. package/src/tests/api_tests/enduser_login.test.ts +215 -0
  125. package/src/tests/api_tests/enduser_login_rate_limits.test.ts +178 -0
  126. package/src/tests/api_tests/push_forms_to_portal_group_completion.test.ts +223 -0
  127. package/src/tests/api_tests/security/F-0001-data-sync-redaction-bypass.test.ts +236 -0
  128. package/src/tests/api_tests/security/F-0005-ai-conversations-rbac.test.ts +154 -0
  129. package/src/tests/api_tests/security/F-0007-invite-user-enumeration.test.ts +198 -0
  130. package/src/tests/api_tests/security/F-0008-handle-incoming-communication-cross-tenant.test.ts +130 -0
  131. package/src/tests/api_tests/security/F-0013-sanitize-user-html.test.ts +109 -0
  132. package/src/tests/api_tests/security/F-0016-prototype-pollution.test.ts +50 -0
  133. package/src/tests/api_tests/set_fields_order_templates.test.ts +258 -0
  134. package/src/tests/setup.ts +8 -1
  135. package/src/tests/tests.ts +35 -5
  136. package/test_generated.pdf +0 -0
@@ -0,0 +1,198 @@
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
+ wait,
9
+ } from "@tellescope/testing"
10
+ import { setup_tests } from "../../setup"
11
+
12
+ const host = process.env.API_URL || 'http://localhost:8080' as const
13
+
14
+ const CROSS_ORG_API_KEY = process.env.CROSS_ORG_API_KEY
15
+ const CROSS_ORG_TARGET_BUSINESS_ID = process.env.CROSS_ORG_TARGET_BUSINESS_ID
16
+
17
+ const post = async (path: string, body: any, headers: Record<string, string> = {}) => {
18
+ try {
19
+ const res = await axios.post(`${host}${path}`, body, {
20
+ validateStatus: () => true,
21
+ headers,
22
+ })
23
+ return { status: res.status, data: res.data }
24
+ } catch (err: any) {
25
+ return { status: err?.response?.status, data: err?.response?.data }
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Regression test for F-0007 (security-audit/findings/F-0007-invite-user-cross-tenant-email-enumeration.md).
31
+ *
32
+ * `users.invite_user` previously used `buildAllQueries({ unrestricted: true, organizationIds: [] }).users.findOne({ email })`
33
+ * to enforce platform-wide email uniqueness and threw the distinctive `"A user with this email already exists"`
34
+ * error on duplicate — regardless of which tenant the existing user was in. Any authenticated user could
35
+ * therefore probe whether email X is registered to any tenant on the platform.
36
+ *
37
+ * **Tests only the negative case** — never drives a successful invite (which would create a real user
38
+ * record and send a real transactional email). All assertions use either:
39
+ * - An email that already exists in the test tenant (the admin's own email), so each call short-circuits
40
+ * at the same-tenant duplicate check or rate-limit check before any invite work happens, OR
41
+ * - The cross-org infrastructure (CROSS_ORG_API_KEY env var) targeting an email that exists in a different
42
+ * tenant — verifies the post-fix response does NOT distinguish "exists elsewhere" from a generic outcome.
43
+ *
44
+ * Assertions:
45
+ * 1. Rate-limit defense-in-depth: rapid same-tenant duplicate requests trip 429 within ~12 attempts.
46
+ * 2. Same-tenant duplicate: returns the same `"already exists"` error pre/post-fix (this branch is
47
+ * unchanged by the fix; asserted for regression-safety).
48
+ * 3. Cross-tenant duplicate (env-gated): post-fix response shape does NOT contain the `"already exists"`
49
+ * string and matches the silent-no-op shape `{ created: { id: ... } }`. Skipped when CROSS_ORG_*
50
+ * env vars are not set, mirroring cross_org_api_key.test.ts convention.
51
+ */
52
+ export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
53
+ log_header("F-0007: users.invite_user cross-tenant enumeration regression")
54
+
55
+ // Reset state so prior tests' rate-limit accounting doesn't leak in.
56
+ await sdk.reset_db()
57
+
58
+ const organizationId = sdk.userInfo.organizationIds?.[0] ?? sdk.userInfo.businessId
59
+ // Use the test admin's own email — guaranteed to exist in the test tenant, so every invite
60
+ // request short-circuits at the same-tenant duplicate check (or earlier at the rate-limit
61
+ // check). No real users get created, no emails get sent.
62
+ const sameTenantExistingEmail = process.env.TEST_EMAIL!
63
+
64
+ // ====================================================================
65
+ // Assertion 1: rate-limit defense-in-depth
66
+ // Fire 15 requests with the admin's own email. Post-fix: rate limit
67
+ // fires before the same-tenant duplicate check on call 11+. Pre-fix:
68
+ // every call returns 400 "already exists" forever.
69
+ // ====================================================================
70
+ let rateLimitedAt = -1
71
+ for (let i = 0; i < 15; i++) {
72
+ const r = await post(
73
+ '/v1/invite-user-to-organization',
74
+ {
75
+ email: sameTenantExistingEmail,
76
+ fname: 'F0007', lname: 'RateLimit',
77
+ organizationId,
78
+ },
79
+ { Authorization: `Bearer ${sdk.authToken}` },
80
+ )
81
+ if (r.status === 429) {
82
+ rateLimitedAt = i
83
+ break
84
+ }
85
+ }
86
+
87
+ await async_test(
88
+ "F-0007: invite_user rate-limits within ~12 rapid requests (defense-in-depth; no invites sent)",
89
+ async () => ({ rateLimitedAt }),
90
+ { onResult: r => r.rateLimitedAt >= 0 && r.rateLimitedAt <= 12 },
91
+ )
92
+
93
+ // ====================================================================
94
+ // Assertion 2: same-tenant duplicate returns the descriptive error
95
+ // (unchanged by the fix; regression guard so a future change to the
96
+ // duplicate-detection path doesn't accidentally suppress same-tenant
97
+ // errors that operators rely on).
98
+ // ====================================================================
99
+ // Let rate limit decay so this single call isn't blocked.
100
+ await wait(undefined, 5000)
101
+ await sdk.reset_db()
102
+
103
+ const sameTenantRes = await post(
104
+ '/v1/invite-user-to-organization',
105
+ {
106
+ email: sameTenantExistingEmail,
107
+ fname: 'F0007', lname: 'SameTenant',
108
+ organizationId,
109
+ },
110
+ { Authorization: `Bearer ${sdk.authToken}` },
111
+ )
112
+
113
+ await async_test(
114
+ "F-0007: invite_user same-tenant duplicate returns 400 'already exists' (unchanged)",
115
+ async () => ({
116
+ status: sameTenantRes.status,
117
+ message: sameTenantRes.data?.message ?? '',
118
+ }),
119
+ { onResult: r => r.status === 400 && r.message.toLowerCase().includes('already exists') },
120
+ )
121
+
122
+ // ====================================================================
123
+ // Assertion 3: cross-tenant duplicate must NOT reveal existence
124
+ // Env-gated; skipped when cross-org infra not configured.
125
+ // ====================================================================
126
+ if (!(CROSS_ORG_API_KEY && CROSS_ORG_TARGET_BUSINESS_ID)) {
127
+ console.log(" [F-0007] Skipping cross-tenant silent no-op assertion — CROSS_ORG_* env vars not set")
128
+ return
129
+ }
130
+
131
+ // The target email is the admin's email in the home (test) tenant. We then attempt to invite
132
+ // that same email FROM a session belonging to the cross-org target tenant. Post-fix, the
133
+ // response should NOT contain "already exists" — it must be indistinguishable from a
134
+ // successful invite OR a generic non-revealing response. Pre-fix, the response is the
135
+ // distinctive "already exists" error revealing cross-tenant existence.
136
+ const sdkCrossOrg = new Session({
137
+ host,
138
+ apiKey: CROSS_ORG_API_KEY,
139
+ headers: { 'x-tellescope-organization': CROSS_ORG_TARGET_BUSINESS_ID },
140
+ })
141
+
142
+ // Resolve an organizationId in the target business. Falls back to the businessId itself.
143
+ let targetOrgId = CROSS_ORG_TARGET_BUSINESS_ID
144
+ try {
145
+ const orgs = await sdkCrossOrg.api.organizations.getSome({ limit: 1 })
146
+ if (orgs?.[0]?.id) targetOrgId = orgs[0].id
147
+ } catch {}
148
+
149
+ const crossRes = await post(
150
+ '/v1/invite-user-to-organization',
151
+ {
152
+ email: sameTenantExistingEmail,
153
+ fname: 'F0007', lname: 'CrossTenantProbe',
154
+ organizationId: targetOrgId,
155
+ },
156
+ {
157
+ Authorization: `API_KEY ${CROSS_ORG_API_KEY}`,
158
+ 'x-tellescope-organization': CROSS_ORG_TARGET_BUSINESS_ID,
159
+ },
160
+ )
161
+
162
+ await async_test(
163
+ "F-0007: cross-tenant invite of existing email must NOT return 'already exists' (no enumeration)",
164
+ async () => ({
165
+ status: crossRes.status,
166
+ message: crossRes.data?.message ?? '',
167
+ hasCreatedShape: !!crossRes.data?.created?.id,
168
+ }),
169
+ {
170
+ onResult: r =>
171
+ // The response MUST NOT contain the "already exists" string regardless of status.
172
+ // Acceptable post-fix shapes: 200 with `{ created: { id: ... } }` (silent no-op), or
173
+ // 200 with a generic ack. Rate-limit 429 also OK if it slipped through to here.
174
+ !r.message.toLowerCase().includes('already exists'),
175
+ },
176
+ )
177
+ }
178
+
179
+ if (require.main === module) {
180
+ console.log(`🌐 Using API URL: ${host}`)
181
+ const sdk = new Session({ host })
182
+ const sdkNonAdmin = new Session({ host })
183
+
184
+ const runTests = async () => {
185
+ await setup_tests(sdk, sdkNonAdmin)
186
+ await invite_user_enumeration_tests({ sdk, sdkNonAdmin })
187
+ }
188
+
189
+ runTests()
190
+ .then(() => {
191
+ console.log("✅ F-0007 invite_user enumeration test suite completed successfully")
192
+ process.exit(0)
193
+ })
194
+ .catch((error) => {
195
+ console.error("❌ F-0007 invite_user enumeration test suite failed:", error)
196
+ process.exit(1)
197
+ })
198
+ }
@@ -0,0 +1,130 @@
1
+ require('source-map-support').install();
2
+
3
+ import axios from "axios"
4
+ import { ObjectId } from 'bson'
5
+ import { Session } from "../../../sdk"
6
+ import {
7
+ async_test,
8
+ log_header,
9
+ } from "@tellescope/testing"
10
+ import { setup_tests } from "../../setup"
11
+
12
+ const host = process.env.API_URL || 'http://localhost:8080' as const
13
+
14
+ const CROSS_ORG_API_KEY = process.env.CROSS_ORG_API_KEY
15
+ const CROSS_ORG_TARGET_BUSINESS_ID = process.env.CROSS_ORG_TARGET_BUSINESS_ID
16
+
17
+ const post = async (path: string, body: any, headers: Record<string, string> = {}) => {
18
+ try {
19
+ const res = await axios.post(`${host}${path}`, body, {
20
+ validateStatus: () => true,
21
+ headers,
22
+ })
23
+ return { status: res.status, data: res.data }
24
+ } catch (err: any) {
25
+ return { status: err?.response?.status, data: err?.response?.data }
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Regression test for F-0008 (security-audit/findings/F-0008-handle-incoming-communication-cross-tenant-enduser-lookup.md).
31
+ *
32
+ * `journeys.handle_incoming_communication` previously used `buildAllQueries({ unrestricted: true, organizationIds: [] }).endusers.findById(enduserId)`,
33
+ * permitting cross-tenant lookup of any enduser by id. The handler then called `handleIncomingCommunication(...)`
34
+ * against the matched enduser, triggering journey progression and automated actions on someone else's tenant.
35
+ *
36
+ * The fix switches to the standard tenant-scoped `DB.endusers.findById(enduserId)` wrapper, which automatically
37
+ * filters by `req.session.businessId`. Cross-tenant lookups now return null → handler returns 404 → no side effect.
38
+ *
39
+ * Note: the same-tenant happy path is already covered by the existing test at
40
+ * `packages/public/sdk/src/tests/tests.ts:7588` ("handle_incoming_communication test for other enduser") — that
41
+ * test creates endusers in the test tenant, sets up journeys, calls handle_incoming_communication, and asserts
42
+ * journey-step cancellation. This file covers the negative cases only.
43
+ *
44
+ * **Negative-only by design**: the test never drives `handleIncomingCommunication` against a cross-tenant
45
+ * enduser — the post-fix code returns 404 before any side effects fire, and the assertion confirms that.
46
+ */
47
+ export const handle_incoming_communication_cross_tenant_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
48
+ log_header("F-0008: handle_incoming_communication cross-tenant rejection")
49
+
50
+ // ====================================================================
51
+ // Assertion 1: nonexistent enduserId returns 404 (baseline; regression
52
+ // guard for the not-found path that any cross-tenant call now lands in).
53
+ // ====================================================================
54
+ const nonexistentId = new ObjectId().toHexString()
55
+ const nonexistentRes = await post(
56
+ '/v1/journeys/handle-incoming-communication',
57
+ { enduserId: nonexistentId },
58
+ { Authorization: `Bearer ${sdk.authToken}` },
59
+ )
60
+ await async_test(
61
+ "F-0008: handle_incoming_communication with nonexistent enduserId returns 404",
62
+ async () => ({ status: nonexistentRes.status }),
63
+ { onResult: r => r.status === 404 },
64
+ )
65
+
66
+ // ====================================================================
67
+ // Assertion 2: cross-tenant enduserId returns 404 (the actual F-0008 fix).
68
+ // Env-gated; skipped when cross-org infra isn't configured.
69
+ // Safe to run post-fix because the tenant-scoped DB returns null for
70
+ // cross-tenant lookups — no handleIncomingCommunication side effect fires.
71
+ // ====================================================================
72
+ if (!(CROSS_ORG_API_KEY && CROSS_ORG_TARGET_BUSINESS_ID)) {
73
+ console.log(" [F-0008] Skipping cross-tenant rejection assertion — CROSS_ORG_* env vars not set")
74
+ return
75
+ }
76
+
77
+ const sdkCrossOrg = new Session({
78
+ host,
79
+ apiKey: CROSS_ORG_API_KEY,
80
+ headers: { 'x-tellescope-organization': CROSS_ORG_TARGET_BUSINESS_ID },
81
+ })
82
+
83
+ // Create a sentinel enduser in the cross-org tenant. Use a clearly-test email
84
+ // so any accidental side-effect routing is obvious in logs.
85
+ const ts = Date.now()
86
+ const crossEnduser = await sdkCrossOrg.api.endusers.createOne({
87
+ fname: 'F0008CrossTenant', lname: 'Sentinel',
88
+ email: `f0008-cross-${ts}@tellescope.com`,
89
+ } as any)
90
+
91
+ try {
92
+ const crossRes = await post(
93
+ '/v1/journeys/handle-incoming-communication',
94
+ { enduserId: crossEnduser.id },
95
+ { Authorization: `Bearer ${sdk.authToken}` },
96
+ )
97
+
98
+ await async_test(
99
+ "F-0008: handle_incoming_communication with cross-tenant enduserId returns 404 (no side effect)",
100
+ async () => ({
101
+ status: crossRes.status,
102
+ message: crossRes.data?.message ?? null,
103
+ }),
104
+ { onResult: r => r.status === 404 },
105
+ )
106
+ } finally {
107
+ try { await sdkCrossOrg.api.endusers.deleteOne(crossEnduser.id) } catch {}
108
+ }
109
+ }
110
+
111
+ if (require.main === module) {
112
+ console.log(`🌐 Using API URL: ${host}`)
113
+ const sdk = new Session({ host })
114
+ const sdkNonAdmin = new Session({ host })
115
+
116
+ const runTests = async () => {
117
+ await setup_tests(sdk, sdkNonAdmin)
118
+ await handle_incoming_communication_cross_tenant_tests({ sdk, sdkNonAdmin })
119
+ }
120
+
121
+ runTests()
122
+ .then(() => {
123
+ console.log("✅ F-0008 handle_incoming_communication cross-tenant test suite completed successfully")
124
+ process.exit(0)
125
+ })
126
+ .catch((error) => {
127
+ console.error("❌ F-0008 handle_incoming_communication cross-tenant test suite failed:", error)
128
+ process.exit(1)
129
+ })
130
+ }
@@ -0,0 +1,109 @@
1
+ import { sanitize_user_html } from "@tellescope/utilities"
2
+
3
+ // Regression test for F-0013 / F-0014 (pattern 06 — XSS via dangerouslySetInnerHTML).
4
+ // sanitize_user_html is the canonical render-time sanitizer that replaced remove_script_tags
5
+ // at every dangerouslySetInnerHTML sink. This asserts it neutralizes XSS vectors (incl. encoded /
6
+ // whitespace / mixed-case / iframe-srcdoc bypass variants) while preserving legitimate
7
+ // customization HTML (tables, headings, lists, links, images, inline styles).
8
+ //
9
+ // Pure-function test — no Session needed. Runs as part of the main suite and standalone:
10
+ // ./build_cjs.sh && cd packages/public/sdk && node -r dotenv/config lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html.test.js
11
+
12
+ const fail = (msg: string) => { throw new Error(msg) }
13
+
14
+ const has_no_executable_vector = (out: string) => {
15
+ const o = out.toLowerCase()
16
+ // A handler smuggled into an attribute VALUE (e.g. title="&lt;img onerror=...&gt;") is inert
17
+ // text — strip quoted values before checking for *live* on*= attributes to avoid false positives.
18
+ const withoutValues = o.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''")
19
+ return !/\son[a-z]+\s*=/.test(withoutValues) // no live on*= event-handler attribute
20
+ && !o.includes('javascript:') // dropped schemes never appear in safe output
21
+ && !o.includes('vbscript:')
22
+ && !o.includes('<script') // literal dangerous tags (encoded &lt;script is fine)
23
+ && !o.includes('<iframe')
24
+ && !o.includes('<svg')
25
+ && !o.includes('<math')
26
+ && !o.includes('<object')
27
+ && !o.includes('<embed')
28
+ && !o.includes('<form')
29
+ && !o.includes('<noscript')
30
+ && !o.includes('<template')
31
+ }
32
+
33
+ export const sanitize_user_html_xss_tests = async () => {
34
+ console.log("Running F-0013/F-0014 sanitize_user_html XSS regression tests")
35
+
36
+ const xssPayloads: [string, string][] = [
37
+ ['img onerror', `<img src=x onerror="alert(document.domain)">`],
38
+ ['svg onload', `<svg onload="alert(1)"></svg>`],
39
+ ['svg animate onbegin', `<svg><animate onbegin="alert(1)" attributeName="x" dur="1s"></svg>`],
40
+ ['details ontoggle', `<details open ontoggle="alert(1)"></details>`],
41
+ ['input onfocus autofocus', `<input autofocus onfocus="alert(1)">`],
42
+ ['body onpageshow', `<body onpageshow="alert(1)">`],
43
+ ['a javascript scheme', `<a href="javascript:alert(1)">x</a>`],
44
+ ['a javascript entity-encoded', `<a href="jav&#x09;ascript:alert(1)">x</a>`],
45
+ ['iframe javascript src', `<iframe src="javascript:alert(1)"></iframe>`],
46
+ ['iframe srcdoc nested', `<iframe srcdoc="<img src=x onerror=alert(1)>"></iframe>`],
47
+ ['script tag', `<script>alert(1)</script>`],
48
+ ['onerror newline before =', `<img src=x onerror\n="alert(1)">`],
49
+ ['onerror mixed case', `<IMG SRC=x OnErRoR="alert(1)">`],
50
+ ['marquee onstart', `<marquee onstart="alert(1)">x</marquee>`],
51
+ // mutation / namespace confusion — svg/math/noscript/template must be stripped
52
+ ['mathml mglyph style mxss', `<math><mtext><table><mglyph><style><!--</style><img src=x onerror=alert(1)>`],
53
+ ['svg foreignObject', `<svg><foreignObject><img src=x onerror=alert(1)></foreignObject></svg>`],
54
+ ['noscript context confusion', `<noscript><p title="</noscript><img src=x onerror=alert(1)>">`],
55
+ ['template content', `<template><img src=x onerror=alert(1)></template>`],
56
+ // comment / CDATA confusion
57
+ ['comment confusion', `<!--><img src=x onerror=alert(1)>-->`],
58
+ ['cdata confusion', `<![CDATA[<img src=x onerror=alert(1)>]]>`],
59
+ // markup smuggled inside an attribute value must stay inert
60
+ ['markup inside attr value', `<img src="x" alt="<script>alert(1)</script>">`],
61
+ // protocol obfuscation
62
+ ['vbscript scheme', `<a href="vbscript:msgbox(1)">x</a>`],
63
+ ['data text/html href', `<a href="data:text/html,<script>alert(1)</script>">x</a>`],
64
+ ['javascript decimal entity', `<a href="&#74;avascript:alert(1)">x</a>`],
65
+ ['javascript newline entity', `<a href="jav&#x0A;ascript:alert(1)">x</a>`],
66
+ ]
67
+ for (const [name, payload] of xssPayloads) {
68
+ const out = sanitize_user_html(payload)
69
+ if (!has_no_executable_vector(out)) fail(`XSS not neutralized [${name}] -> ${out}`)
70
+ }
71
+
72
+ // DOM clobbering: caller-controlled id/name must be stripped
73
+ const clobber = sanitize_user_html(`<a id="x" name="getElementById">link</a><img name="y">`)
74
+ if (/\b(id|name)\s*=/.test(clobber)) fail(`id/name not stripped (DOM clobbering): ${clobber}`)
75
+
76
+ // legitimate customization HTML must survive
77
+ const heading = sanitize_user_html(`<h1>Welcome</h1><h3 style="color:#333">Sub</h3>`)
78
+ if (!(heading.includes('<h1>') && heading.includes('<h3') && heading.toLowerCase().includes('color'))) fail(`headings/style stripped: ${heading}`)
79
+
80
+ const table = sanitize_user_html(`<table><thead><tr><th>H</th></tr></thead><tbody><tr><td style="padding:4px" colspan="2">cell</td></tr></tbody></table>`)
81
+ if (!(table.includes('<table') && table.includes('<td') && table.includes('colspan'))) fail(`table stripped: ${table}`)
82
+
83
+ const list = sanitize_user_html(`<ul><li>a</li></ul><ol start="3"><li>c</li></ol>`)
84
+ if (!(list.includes('<ul') && list.includes('<li') && list.includes('<ol'))) fail(`list stripped: ${list}`)
85
+
86
+ const link = sanitize_user_html(`<a href="https://example.com">link</a>`)
87
+ if (!link.includes('href="https://example.com"')) fail(`safe link stripped: ${link}`)
88
+ if (!link.toLowerCase().includes('noopener')) fail(`external link not hardened: ${link}`)
89
+
90
+ const img = sanitize_user_html(`<img src="https://cdn.example.com/a.png" alt="pic" width="200">`)
91
+ if (!(img.includes('src="https://cdn.example.com/a.png"') && img.includes('alt="pic"'))) fail(`http image stripped: ${img}`)
92
+
93
+ const dataimg = sanitize_user_html(`<img src="data:image/png;base64,iVBORw0KGgo=">`)
94
+ if (!dataimg.includes('data:image/png')) fail(`data: image stripped: ${dataimg}`)
95
+
96
+ const fmt = sanitize_user_html(`<p><strong>b</strong> <em>i</em> <span style="font-size:14px">s</span></p><blockquote>q</blockquote>`)
97
+ if (!(fmt.includes('<strong>') && fmt.includes('<span') && fmt.toLowerCase().includes('font-size'))) fail(`formatting stripped: ${fmt}`)
98
+
99
+ const mixed = sanitize_user_html(`<p>Hello <b>name</b></p><img src=x onerror="steal()">`)
100
+ if (!(mixed.includes('<b>name</b>') && !/\son[a-z]+\s*=/.test(mixed.toLowerCase()))) fail(`mixed content not handled: ${mixed}`)
101
+
102
+ console.log("✅ F-0013/F-0014 sanitize_user_html XSS regression tests passed")
103
+ }
104
+
105
+ if (require.main === module) {
106
+ sanitize_user_html_xss_tests()
107
+ .then(() => { console.log("✅ suite completed"); process.exit(0) })
108
+ .catch((err) => { console.error("❌ suite failed:", err); process.exit(1) })
109
+ }
@@ -0,0 +1,50 @@
1
+ import { add_value_for_dotted_key } from "@tellescope/utilities"
2
+
3
+ // Regression test for F-0016 (pattern 17 — prototype pollution).
4
+ // add_value_for_dotted_key must NOT write through __proto__/constructor/prototype path segments
5
+ // (which would pollute Object.prototype process-wide), while still performing legitimate dotted assignment.
6
+ //
7
+ // Pure-function test — no Session needed. Runs in the main suite and standalone:
8
+ // ./build_cjs.sh && cd packages/public/sdk && node -r dotenv/config lib/cjs/tests/api_tests/security/F-0016-prototype-pollution.test.js
9
+
10
+ const fail = (msg: string) => { throw new Error(msg) }
11
+
12
+ export const prototype_pollution_tests = async () => {
13
+ console.log("Running F-0016 prototype-pollution regression tests")
14
+
15
+ // 1. __proto__ path must not pollute Object.prototype
16
+ add_value_for_dotted_key({ insurance: {} } as any, 'insurance.__proto__.__pp_a__', 'polluted')
17
+ const leakedA = ({} as any).__pp_a__
18
+ delete (Object.prototype as any).__pp_a__ // clean up regardless, so a failure here can't contaminate the rest of the suite
19
+ if (leakedA !== undefined) fail('Object.prototype polluted via __proto__ path')
20
+
21
+ // 2. constructor.prototype path must not pollute
22
+ add_value_for_dotted_key({ insurance: {} } as any, 'insurance.constructor.prototype.__pp_b__', 'polluted')
23
+ const leakedB = ({} as any).__pp_b__
24
+ delete (Object.prototype as any).__pp_b__
25
+ if (leakedB !== undefined) fail('Object.prototype polluted via constructor.prototype path')
26
+
27
+ // 3. a leading __proto__ segment must not pollute either
28
+ add_value_for_dotted_key({} as any, '__proto__.__pp_c__', 'polluted')
29
+ const leakedC = ({} as any).__pp_c__
30
+ delete (Object.prototype as any).__pp_c__
31
+ if (leakedC !== undefined) fail('Object.prototype polluted via leading __proto__ segment')
32
+
33
+ // 4. legitimate dotted assignment still works (existing intermediate objects)
34
+ const obj = { a: { b: {} } } as any
35
+ add_value_for_dotted_key(obj, 'a.b.c', 42)
36
+ if (obj.a.b.c !== 42) fail('legitimate dotted assignment broke')
37
+
38
+ // 5. single-key assignment still works
39
+ const flat = {} as any
40
+ add_value_for_dotted_key(flat, 'name', 'ok')
41
+ if (flat.name !== 'ok') fail('single-key assignment broke')
42
+
43
+ console.log("✅ F-0016 prototype-pollution regression tests passed")
44
+ }
45
+
46
+ if (require.main === module) {
47
+ prototype_pollution_tests()
48
+ .then(() => { console.log("✅ suite completed"); process.exit(0) })
49
+ .catch((err) => { console.error("❌ suite failed:", err); process.exit(1) })
50
+ }