@tellescope/sdk 1.254.0 → 1.255.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 (98) hide show
  1. package/lib/cjs/sdk.d.ts +4 -0
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/sdk.js +5 -0
  4. package/lib/cjs/sdk.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.d.ts +16 -0
  6. package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.js +187 -0
  8. package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.js.map +1 -0
  9. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts +6 -0
  10. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -0
  11. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js +443 -0
  12. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js.map +1 -0
  13. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.d.ts +6 -0
  14. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.d.ts.map +1 -0
  15. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.js +254 -0
  16. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.js.map +1 -0
  17. package/lib/cjs/tests/api_tests/organization_api_keys.test.d.ts +6 -0
  18. package/lib/cjs/tests/api_tests/organization_api_keys.test.d.ts.map +1 -0
  19. package/lib/cjs/tests/api_tests/organization_api_keys.test.js +226 -0
  20. package/lib/cjs/tests/api_tests/organization_api_keys.test.js.map +1 -0
  21. package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts +21 -0
  22. package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts.map +1 -0
  23. package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js +362 -0
  24. package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js.map +1 -0
  25. package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts +20 -0
  26. package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts.map +1 -0
  27. package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.js +139 -0
  28. package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.js.map +1 -0
  29. package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts +18 -0
  30. package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts.map +1 -0
  31. package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.js +156 -0
  32. package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.js.map +1 -0
  33. package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts +20 -0
  34. package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts.map +1 -0
  35. package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.js +157 -0
  36. package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.js.map +1 -0
  37. package/lib/cjs/tests/setup.d.ts.map +1 -1
  38. package/lib/cjs/tests/setup.js +20 -1
  39. package/lib/cjs/tests/setup.js.map +1 -1
  40. package/lib/cjs/tests/tests.d.ts.map +1 -1
  41. package/lib/cjs/tests/tests.js +543 -282
  42. package/lib/cjs/tests/tests.js.map +1 -1
  43. package/lib/esm/sdk.d.ts +6 -2
  44. package/lib/esm/sdk.d.ts.map +1 -1
  45. package/lib/esm/sdk.js +5 -0
  46. package/lib/esm/sdk.js.map +1 -1
  47. package/lib/esm/session.d.ts +1 -0
  48. package/lib/esm/session.d.ts.map +1 -1
  49. package/lib/esm/tests/api_tests/ai_decision_summary_config.test.d.ts +16 -0
  50. package/lib/esm/tests/api_tests/ai_decision_summary_config.test.d.ts.map +1 -0
  51. package/lib/esm/tests/api_tests/ai_decision_summary_config.test.js +183 -0
  52. package/lib/esm/tests/api_tests/ai_decision_summary_config.test.js.map +1 -0
  53. package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts +6 -0
  54. package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -0
  55. package/lib/esm/tests/api_tests/mdi_case_offerings.test.js +439 -0
  56. package/lib/esm/tests/api_tests/mdi_case_offerings.test.js.map +1 -0
  57. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.d.ts +6 -0
  58. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.d.ts.map +1 -0
  59. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.js +250 -0
  60. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.js.map +1 -0
  61. package/lib/esm/tests/api_tests/organization_api_keys.test.d.ts +6 -0
  62. package/lib/esm/tests/api_tests/organization_api_keys.test.d.ts.map +1 -0
  63. package/lib/esm/tests/api_tests/organization_api_keys.test.js +222 -0
  64. package/lib/esm/tests/api_tests/organization_api_keys.test.js.map +1 -0
  65. package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts +21 -0
  66. package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts.map +1 -0
  67. package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js +358 -0
  68. package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js.map +1 -0
  69. package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts +20 -0
  70. package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts.map +1 -0
  71. package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.js +135 -0
  72. package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.js.map +1 -0
  73. package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts +18 -0
  74. package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts.map +1 -0
  75. package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.js +152 -0
  76. package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.js.map +1 -0
  77. package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts +20 -0
  78. package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts.map +1 -0
  79. package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.js +150 -0
  80. package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.js.map +1 -0
  81. package/lib/esm/tests/setup.d.ts.map +1 -1
  82. package/lib/esm/tests/setup.js +20 -1
  83. package/lib/esm/tests/setup.js.map +1 -1
  84. package/lib/esm/tests/tests.d.ts.map +1 -1
  85. package/lib/esm/tests/tests.js +543 -282
  86. package/lib/esm/tests/tests.js.map +1 -1
  87. package/lib/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +10 -10
  89. package/src/sdk.ts +11 -0
  90. package/src/tests/api_tests/mdi_case_offerings.test.ts +414 -0
  91. package/src/tests/api_tests/organization_api_keys.test.ts +207 -0
  92. package/src/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.ts +221 -0
  93. package/src/tests/api_tests/security/F-0145-globalunique-leak.test.ts +78 -0
  94. package/src/tests/api_tests/security/F-0151-load-inbox-redaction.test.ts +81 -0
  95. package/src/tests/api_tests/security/F-0155-webhook-timeout.test.ts +85 -0
  96. package/src/tests/setup.ts +9 -0
  97. package/src/tests/tests.ts +199 -2
  98. package/test_generated.pdf +0 -0
@@ -0,0 +1,221 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session, EnduserSession } from "../../../sdk"
4
+ import {
5
+ async_test,
6
+ log_header,
7
+ } from "@tellescope/testing"
8
+ import { setup_tests } from "../../setup"
9
+
10
+ const host = process.env.API_URL || 'http://localhost:8080' as const
11
+ const businessId = '60398b1131a295e64f084ff6'
12
+
13
+ const ENDUSER_PASSWORD = 'F0116TestPassword!123'
14
+
15
+ const CROSS_ENDUSER_MESSAGE = "enduserId does not match creator id for enduser session"
16
+
17
+ /**
18
+ * Regression test for the enduser cross-enduser CREATE gating gaps:
19
+ * F-0116 enduser_observations (security-audit/findings/F-0116-*.md)
20
+ * F-0117 enduser_orders (security-audit/findings/F-0117-*.md)
21
+ * F-0119 managed_content_records (security-audit/findings/F-0119-*.md)
22
+ * F-0118 chat_rooms (enduserIds-only gate; staff userIds targeting is by-design)
23
+ *
24
+ * Each model exposed enduserActions.create with an `enduserId`/`enduserIds` field that
25
+ * lacked a write-side gate, letting an authenticated enduser create records bound to a
26
+ * DIFFERENT enduser in the same tenant. The fix adds a `constraints.relationship`
27
+ * evaluator mirroring the canonical `tickets` pattern (schema.ts).
28
+ *
29
+ * Methodology: each section asserts BOTH the exploit-is-blocked case AND that the
30
+ * legitimate path (self-create + staff-create) still works.
31
+ */
32
+ export const enduser_cross_create_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
33
+ log_header("F-0116/F-0117/F-0118/F-0119: enduser cross-enduser create gates")
34
+
35
+ let attackerId: string | undefined
36
+ let victimId: string | undefined
37
+ const createdObservationIds: string[] = []
38
+ const createdOrderIds: string[] = []
39
+ const createdContentIds: string[] = []
40
+ const createdRoomIds: string[] = []
41
+
42
+ try {
43
+ // ---- Setup: attacker enduser (with portal session) + victim enduser ----
44
+ const attackerEmail = `f0116-attacker-${Date.now()}@example.com`
45
+ const victimEmail = `f0116-victim-${Date.now()}@example.com`
46
+ const attacker = await sdk.api.endusers.createOne({ email: attackerEmail, fname: 'F0116', lname: 'Attacker' })
47
+ attackerId = attacker.id
48
+ const victim = await sdk.api.endusers.createOne({ email: victimEmail, fname: 'F0116', lname: 'Victim' })
49
+ victimId = victim.id
50
+ await sdk.api.endusers.set_password({ id: attacker.id, password: ENDUSER_PASSWORD })
51
+
52
+ const attackerSDK = new EnduserSession({ host, businessId })
53
+ await attackerSDK.authenticate(attackerEmail, ENDUSER_PASSWORD)
54
+
55
+ // ============================================================
56
+ // F-0116 — enduser_observations
57
+ // ============================================================
58
+ await async_test(
59
+ `F-0116: enduser cannot create observation on another enduser (createOne)`,
60
+ () => attackerSDK.api.enduser_observations.createOne({
61
+ enduserId: victimId!, category: 'vital-signs', status: 'final',
62
+ measurement: { unit: 'mmHg', value: 220 },
63
+ } as any),
64
+ { shouldError: true, onError: e => e.message === CROSS_ENDUSER_MESSAGE },
65
+ )
66
+ await async_test(
67
+ `F-0116: enduser cannot create observation on another enduser (createMany)`,
68
+ () => attackerSDK.api.enduser_observations.createSome([{
69
+ enduserId: victimId!, category: 'vital-signs', status: 'final',
70
+ measurement: { unit: 'mmHg', value: 220 },
71
+ }] as any),
72
+ { shouldError: true, onError: e => e.message === CROSS_ENDUSER_MESSAGE },
73
+ )
74
+
75
+ // Positive control: enduser CAN create an observation on their OWN record
76
+ await async_test(
77
+ `F-0116: enduser can still create observation on own record`,
78
+ () => attackerSDK.api.enduser_observations.createOne({
79
+ enduserId: attackerId!, category: 'vital-signs', status: 'final',
80
+ measurement: { unit: 'mmHg', value: 120 },
81
+ } as any),
82
+ { onResult: (o: any) => { if (o?.id) createdObservationIds.push(o.id); return o?.enduserId === attackerId } },
83
+ )
84
+ // Positive control: staff CAN create an observation on any tenant enduser
85
+ await async_test(
86
+ `F-0116: staff can still create observation on any enduser`,
87
+ () => sdk.api.enduser_observations.createOne({
88
+ enduserId: victimId!, category: 'vital-signs', status: 'final',
89
+ measurement: { unit: 'mmHg', value: 118 },
90
+ } as any),
91
+ { onResult: (o: any) => { if (o?.id) createdObservationIds.push(o.id); return o?.enduserId === victimId } },
92
+ )
93
+
94
+ // ============================================================
95
+ // F-0117 — enduser_orders
96
+ // ============================================================
97
+ const orderPayload = (enduserId: string) => ({
98
+ enduserId, externalId: `f0117-${Date.now()}`, source: 'f0117-source',
99
+ title: 'f0117 order', status: 'pending',
100
+ })
101
+ await async_test(
102
+ `F-0117: enduser cannot create order on another enduser`,
103
+ () => attackerSDK.api.enduser_orders.createOne(orderPayload(victimId!) as any),
104
+ { shouldError: true, onError: e => e.message === CROSS_ENDUSER_MESSAGE },
105
+ )
106
+ await async_test(
107
+ `F-0117: enduser can still create order on own record`,
108
+ () => attackerSDK.api.enduser_orders.createOne(orderPayload(attackerId!) as any),
109
+ { onResult: (o: any) => { if (o?.id) createdOrderIds.push(o.id); return o?.enduserId === attackerId } },
110
+ )
111
+ await async_test(
112
+ `F-0117: staff can still create order on any enduser`,
113
+ () => sdk.api.enduser_orders.createOne(orderPayload(victimId!) as any),
114
+ { onResult: (o: any) => { if (o?.id) createdOrderIds.push(o.id); return o?.enduserId === victimId } },
115
+ )
116
+
117
+ // ============================================================
118
+ // F-0119 — managed_content_records
119
+ // ============================================================
120
+ await async_test(
121
+ `F-0119: enduser cannot create content with allowUnauthenticatedAccess`,
122
+ () => attackerSDK.api.managed_content_records.createOne({
123
+ title: 'f0119 phishing', textContent: 'x', allowUnauthenticatedAccess: true,
124
+ } as any),
125
+ { shouldError: true, onError: e => e.message === "allowUnauthenticatedAccess cannot be set by endusers" },
126
+ )
127
+ await async_test(
128
+ `F-0119: enduser cannot create content with publicRead`,
129
+ () => attackerSDK.api.managed_content_records.createOne({
130
+ title: 'f0119 broadcast', textContent: 'x', publicRead: true,
131
+ } as any),
132
+ { shouldError: true, onError: e => e.message === "publicRead cannot be set by endusers" },
133
+ )
134
+ await async_test(
135
+ `F-0119: enduser cannot create content bound to another enduser`,
136
+ () => attackerSDK.api.managed_content_records.createOne({
137
+ title: 'f0119 cross', textContent: 'x', enduserId: victimId!,
138
+ } as any),
139
+ { shouldError: true, onError: e => e.message === CROSS_ENDUSER_MESSAGE },
140
+ )
141
+ await async_test(
142
+ `F-0119: enduser can still create ordinary content`,
143
+ () => attackerSDK.api.managed_content_records.createOne({
144
+ title: 'f0119 ok', textContent: 'hello',
145
+ } as any),
146
+ { onResult: (o: any) => { if (o?.id) createdContentIds.push(o.id); return !o?.publicRead && !o?.allowUnauthenticatedAccess } },
147
+ )
148
+ await async_test(
149
+ `F-0119: staff can still create public content`,
150
+ () => sdk.api.managed_content_records.createOne({
151
+ title: 'f0119 staff public', textContent: 'x', publicRead: true,
152
+ } as any),
153
+ { onResult: (o: any) => { if (o?.id) createdContentIds.push(o.id); return o?.publicRead === true } },
154
+ )
155
+
156
+ // ============================================================
157
+ // F-0118 — chat_rooms (enduserIds-only gate; staff userIds targeting is by-design)
158
+ // ============================================================
159
+ const staffId = sdk.userInfo.id
160
+ await async_test(
161
+ `F-0118: enduser cannot add another patient to a chat room (enduserIds)`,
162
+ () => attackerSDK.api.chat_rooms.createOne({
163
+ type: 'internal', enduserIds: [attackerId!, victimId!], userIds: [staffId],
164
+ } as any),
165
+ { shouldError: true, onError: e => e.message === "enduserIds may only contain your own id for enduser session" },
166
+ )
167
+ // Positive control: patient CAN create a room with care-team staff + self (intended feature)
168
+ await async_test(
169
+ `F-0118: enduser can still create a room with staff + self`,
170
+ () => attackerSDK.api.chat_rooms.createOne({
171
+ type: 'internal', enduserIds: [attackerId!], userIds: [staffId],
172
+ } as any),
173
+ { onResult: (o: any) => { if (o?.id) createdRoomIds.push(o.id); return o?.enduserIds?.length === 1 && o?.userIds?.includes(staffId) } },
174
+ )
175
+ // Positive control: staff can still create multi-patient rooms
176
+ await async_test(
177
+ `F-0118: staff can still create a room with multiple endusers`,
178
+ () => sdk.api.chat_rooms.createOne({
179
+ type: 'internal', enduserIds: [attackerId!, victimId!], userIds: [staffId],
180
+ } as any),
181
+ { onResult: (o: any) => { if (o?.id) createdRoomIds.push(o.id); return o?.enduserIds?.length === 2 } },
182
+ )
183
+ } finally {
184
+ for (const id of createdRoomIds) {
185
+ try { await sdk.api.chat_rooms.deleteOne(id) } catch {}
186
+ }
187
+ for (const id of createdContentIds) {
188
+ try { await sdk.api.managed_content_records.deleteOne(id) } catch {}
189
+ }
190
+ for (const id of createdObservationIds) {
191
+ try { await sdk.api.enduser_observations.deleteOne(id) } catch {}
192
+ }
193
+ for (const id of createdOrderIds) {
194
+ try { await sdk.api.enduser_orders.deleteOne(id) } catch {}
195
+ }
196
+ if (attackerId) { try { await sdk.api.endusers.deleteOne(attackerId) } catch {} }
197
+ if (victimId) { try { await sdk.api.endusers.deleteOne(victimId) } catch {} }
198
+ }
199
+ }
200
+
201
+ // Allow running this test file independently
202
+ if (require.main === module) {
203
+ console.log(`🌐 Using API URL: ${host}`)
204
+ const sdk = new Session({ host })
205
+ const sdkNonAdmin = new Session({ host })
206
+
207
+ const runTests = async () => {
208
+ await setup_tests(sdk, sdkNonAdmin)
209
+ await enduser_cross_create_tests({ sdk, sdkNonAdmin })
210
+ }
211
+
212
+ runTests()
213
+ .then(() => {
214
+ console.log("✅ F-0116/F-0117/F-0118/F-0119 enduser cross-create test suite completed successfully")
215
+ process.exit(0)
216
+ })
217
+ .catch((error) => {
218
+ console.error("❌ enduser cross-create test suite failed:", error)
219
+ process.exit(1)
220
+ })
221
+ }
@@ -0,0 +1,78 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../../sdk"
4
+ import {
5
+ async_test,
6
+ log_header,
7
+ } from "@tellescope/testing"
8
+ import { setup_tests } from "../../setup"
9
+
10
+ const host = process.env.API_URL || 'http://localhost:8080' as const
11
+
12
+ /**
13
+ * Regression test for F-0145
14
+ * (security-audit/findings/F-0145-globalunique-conflict-leaks-cross-tenant-record.md).
15
+ *
16
+ * The create endpoint generator's unconditional globalUnique-enforcement block
17
+ * (routing.ts, "globalUnique should be enforced even if _overrideUnique is true")
18
+ * threw uniquenessError([{ existingRecord: matches[0] }]) with the RAW matched record.
19
+ * `findSomeByMatchAny_Global` deletes businessId, so the match is global; for `users`
20
+ * (globalUnique: ['email','phone']) the 409 body leaked the full record incl. hashedPass.
21
+ *
22
+ * Fix: redact existingRecord to { _id, businessId } like the safe validateUniquenessConstraints
23
+ * path. This reproduces the leak with a same-tenant collision (same code path) and asserts the
24
+ * 409 body's existingRecord exposes nothing beyond _id + businessId.
25
+ */
26
+ export const globalunique_leak_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
27
+ log_header("F-0145: globalUnique conflict response redaction")
28
+
29
+ let userId: string | undefined
30
+ try {
31
+ const email = `f0145-victim-${Date.now()}@example.com`
32
+ const user = await sdk.api.users.createOne({ email } as any)
33
+ userId = user.id
34
+
35
+ await async_test(
36
+ 'F-0145: globalUnique 409 existingRecord exposes only _id + businessId (no email/hashedPass)',
37
+ () => sdk.api.users.createOne({ email, _overrideUnique: true } as any),
38
+ {
39
+ shouldError: true,
40
+ onError: (e: any) => {
41
+ const rec = e?.info?.[0]?.existingRecord
42
+ if (!rec || typeof rec !== 'object') return false
43
+ const keys = Object.keys(rec)
44
+ return (
45
+ keys.length > 0
46
+ && keys.every(k => k === '_id' || k === 'businessId' || k === 'id')
47
+ && !('email' in rec)
48
+ && !('hashedPass' in rec)
49
+ && !('hashedInviteCode' in rec)
50
+ )
51
+ },
52
+ },
53
+ )
54
+ } finally {
55
+ if (userId) { try { await sdk.api.users.deleteOne(userId) } catch {} }
56
+ }
57
+ }
58
+
59
+ if (require.main === module) {
60
+ console.log(`🌐 Using API URL: ${host}`)
61
+ const sdk = new Session({ host })
62
+ const sdkNonAdmin = new Session({ host })
63
+
64
+ const runTests = async () => {
65
+ await setup_tests(sdk, sdkNonAdmin)
66
+ await globalunique_leak_tests({ sdk, sdkNonAdmin })
67
+ }
68
+
69
+ runTests()
70
+ .then(() => {
71
+ console.log("✅ F-0145 globalUnique leak test suite completed successfully")
72
+ process.exit(0)
73
+ })
74
+ .catch((error) => {
75
+ console.error("❌ F-0145 globalUnique leak test suite failed:", error)
76
+ process.exit(1)
77
+ })
78
+ }
@@ -0,0 +1,81 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../../sdk"
4
+ import {
5
+ async_test,
6
+ log_header,
7
+ } from "@tellescope/testing"
8
+ import { setup_tests } from "../../setup"
9
+
10
+ const host = process.env.API_URL || 'http://localhost:8080' as const
11
+
12
+ const ENDUSER_PASSWORD = 'F0151TestPassword!123'
13
+
14
+ /**
15
+ * Regression test for F-0151
16
+ * (security-audit/findings/F-0151-load-inbox-data-endusers-unredacted.md).
17
+ *
18
+ * load_inbox_data serialized $lookup'ed endusers via convert_id_name only (NOT applyRedactions),
19
+ * so endusers.hashedPassword (redactions:['all']) leaked to staff sessions — while the sibling
20
+ * phone_calls in the same response correctly went through applyRedactions.
21
+ *
22
+ * Repro: create an enduser, set their portal password (→ hashedPassword), seed an inbound
23
+ * (logOnly) email so the enduser appears in the inbox join, then load_inbox_data as staff and
24
+ * assert the returned enduser entry has NO hashedPassword.
25
+ */
26
+ export const load_inbox_redaction_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
27
+ log_header("F-0151: load_inbox_data enduser redaction")
28
+
29
+ let enduserId: string | undefined
30
+ let emailId: string | undefined
31
+ try {
32
+ const email = `f0151-enduser-${Date.now()}@example.com`
33
+ const enduser = await sdk.api.endusers.createOne({ email, fname: 'F0151', lname: 'Inbox' })
34
+ enduserId = enduser.id
35
+ await sdk.api.endusers.set_password({ id: enduser.id, password: ENDUSER_PASSWORD })
36
+
37
+ // Seed an inbound logOnly email so the enduser is joined into the inbox payload
38
+ const seeded = await sdk.api.emails.createOne({
39
+ enduserId: enduser.id, subject: 'F0151 seed', textContent: 'seed',
40
+ logOnly: true, inbound: true,
41
+ } as any)
42
+ emailId = seeded.id
43
+
44
+ await async_test(
45
+ 'F-0151: load_inbox_data does not leak enduser.hashedPassword to staff',
46
+ () => sdk.api.endusers.load_inbox_data({ enduserIds: [enduser.id] } as any),
47
+ {
48
+ onResult: (r: any) => {
49
+ const returned = (r?.endusers ?? []).find((e: any) => (e.id || e._id)?.toString() === enduserId)
50
+ // require that the enduser actually came back (so the assertion is meaningful),
51
+ // and that hashedPassword is absent
52
+ return !!returned && !('hashedPassword' in returned)
53
+ },
54
+ },
55
+ )
56
+ } finally {
57
+ if (emailId) { try { await sdk.api.emails.deleteOne(emailId) } catch {} }
58
+ if (enduserId) { try { await sdk.api.endusers.deleteOne(enduserId) } catch {} }
59
+ }
60
+ }
61
+
62
+ if (require.main === module) {
63
+ console.log(`🌐 Using API URL: ${host}`)
64
+ const sdk = new Session({ host })
65
+ const sdkNonAdmin = new Session({ host })
66
+
67
+ const runTests = async () => {
68
+ await setup_tests(sdk, sdkNonAdmin)
69
+ await load_inbox_redaction_tests({ sdk, sdkNonAdmin })
70
+ }
71
+
72
+ runTests()
73
+ .then(() => {
74
+ console.log("✅ F-0151 load_inbox_data redaction test suite completed successfully")
75
+ process.exit(0)
76
+ })
77
+ .catch((error) => {
78
+ console.error("❌ F-0151 load_inbox_data redaction test suite failed:", error)
79
+ process.exit(1)
80
+ })
81
+ }
@@ -0,0 +1,85 @@
1
+ require('source-map-support').install();
2
+
3
+ import http from "http"
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
+ // The server-side axios timeout is 15s (F-0155 fix). We bound our own wait above that.
14
+ const CLIENT_WAIT_MS = 20_000
15
+
16
+ /**
17
+ * Regression test for F-0155
18
+ * (security-audit/findings/F-0155-outbound-webhook-no-timeout-slow-loris.md).
19
+ *
20
+ * Outbound webhook delivery used axios.post(url, ...) with no `timeout` (axios default = infinite).
21
+ * A tenant-controlled receiver that accepts the connection and never responds hangs the awaited
22
+ * request indefinitely (slow-loris → worker exhaustion). Fix: timeout: 15_000 on the axios calls.
23
+ *
24
+ * Repro: stand up a local HTTP server that accepts the connection but NEVER responds, point
25
+ * send_automation_webhook (which awaits the axios call) at it, and race the call against a 20s
26
+ * client timer.
27
+ * - green (fixed): server times out at ~15s → handler returns 400 → SDK call rejects < 20s
28
+ * - red (unfixed): the call is still pending at 20s → client timer wins
29
+ */
30
+ export const webhook_timeout_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
31
+ log_header("F-0155: outbound webhook timeout")
32
+
33
+ let enduserId: string | undefined
34
+ const slowServer = http.createServer(() => { /* accept connection, never respond */ })
35
+
36
+ try {
37
+ await new Promise<void>(resolve => slowServer.listen(0, '127.0.0.1', () => resolve()))
38
+ const port = (slowServer.address() as any).port
39
+ const blackholeUrl = `http://127.0.0.1:${port}/hook`
40
+
41
+ const enduser = await sdk.api.endusers.createOne({ email: `f0155-${Date.now()}@example.com`, fname: 'F0155', lname: 'Webhook' })
42
+ enduserId = enduser.id
43
+
44
+ await async_test(
45
+ 'F-0155: outbound webhook to a non-responding receiver times out server-side (does not hang)',
46
+ async () => {
47
+ const start = Date.now()
48
+ const outcome = await Promise.race([
49
+ sdk.api.webhooks.send_automation_webhook({
50
+ enduserId: enduser.id,
51
+ action: { info: { url: blackholeUrl } },
52
+ } as any).then(() => 'completed').catch(() => 'errored'),
53
+ new Promise<string>(r => setTimeout(() => r('client-timeout'), CLIENT_WAIT_MS)),
54
+ ])
55
+ return { outcome, elapsed: Date.now() - start }
56
+ },
57
+ // green: the server-side axios timeout fired (call settled) before our 20s client wait
58
+ { onResult: (r: any) => r.outcome !== 'client-timeout' && r.elapsed < CLIENT_WAIT_MS },
59
+ )
60
+ } finally {
61
+ slowServer.close()
62
+ if (enduserId) { try { await sdk.api.endusers.deleteOne(enduserId) } catch {} }
63
+ }
64
+ }
65
+
66
+ if (require.main === module) {
67
+ console.log(`🌐 Using API URL: ${host}`)
68
+ const sdk = new Session({ host })
69
+ const sdkNonAdmin = new Session({ host })
70
+
71
+ const runTests = async () => {
72
+ await setup_tests(sdk, sdkNonAdmin)
73
+ await webhook_timeout_tests({ sdk, sdkNonAdmin })
74
+ }
75
+
76
+ runTests()
77
+ .then(() => {
78
+ console.log("✅ F-0155 webhook timeout test suite completed successfully")
79
+ process.exit(0)
80
+ })
81
+ .catch((error) => {
82
+ console.error("❌ F-0155 webhook timeout test suite failed:", error)
83
+ process.exit(1)
84
+ })
85
+ }
@@ -150,4 +150,13 @@ export const setup_tests = async (sdk: Session, sdkNonAdmin: Session) => {
150
150
  // may do some stuff in background after returning
151
151
  await async_test('reset_db', () => sdk.reset_db(), passOnVoid)
152
152
  await wait(undefined, 250)
153
+
154
+ // Prevent test-triggered notifications (e.g. timeTrackRejected, inbound-message
155
+ // notifications) from enqueuing real notification emails to SQS for the primary
156
+ // test accounts. On-the-fly test users already set this flag individually.
157
+ // Set after the final reset_db so it isn't wiped by the reset.
158
+ await Promise.all([
159
+ sdk.api.users.updateOne(sdk.userInfo.id, { notificationEmailsDisabled: true }),
160
+ sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { notificationEmailsDisabled: true }),
161
+ ]).catch(console.error)
153
162
  }