@tellescope/sdk 1.246.2 → 1.248.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 (83) hide show
  1. package/lib/cjs/sdk.d.ts +7 -1
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/sdk.js +2 -0
  4. package/lib/cjs/sdk.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/date_string_validation.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/date_string_validation.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/date_string_validation.test.js +142 -0
  8. package/lib/cjs/tests/api_tests/date_string_validation.test.js.map +1 -0
  9. package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.d.ts +6 -0
  10. package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -0
  11. package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js +243 -0
  12. package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js.map +1 -0
  13. package/lib/cjs/tests/api_tests/field_redaction.test.d.ts +13 -0
  14. package/lib/cjs/tests/api_tests/field_redaction.test.d.ts.map +1 -0
  15. package/lib/cjs/tests/api_tests/field_redaction.test.js +818 -0
  16. package/lib/cjs/tests/api_tests/field_redaction.test.js.map +1 -0
  17. package/lib/cjs/tests/api_tests/form_submitted_trigger.test.d.ts +6 -0
  18. package/lib/cjs/tests/api_tests/form_submitted_trigger.test.d.ts.map +1 -0
  19. package/lib/cjs/tests/api_tests/form_submitted_trigger.test.js +429 -0
  20. package/lib/cjs/tests/api_tests/form_submitted_trigger.test.js.map +1 -0
  21. package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts +6 -0
  22. package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts.map +1 -0
  23. package/lib/cjs/tests/api_tests/integrations_redacted.test.js +273 -0
  24. package/lib/cjs/tests/api_tests/integrations_redacted.test.js.map +1 -0
  25. package/lib/cjs/tests/api_tests/mdb_sort.test.d.ts +6 -0
  26. package/lib/cjs/tests/api_tests/mdb_sort.test.d.ts.map +1 -0
  27. package/lib/cjs/tests/api_tests/mdb_sort.test.js +370 -0
  28. package/lib/cjs/tests/api_tests/mdb_sort.test.js.map +1 -0
  29. package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
  30. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +108 -24
  31. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  32. package/lib/cjs/tests/tests.d.ts.map +1 -1
  33. package/lib/cjs/tests/tests.js +303 -180
  34. package/lib/cjs/tests/tests.js.map +1 -1
  35. package/lib/esm/sdk.d.ts +7 -1
  36. package/lib/esm/sdk.d.ts.map +1 -1
  37. package/lib/esm/sdk.js +2 -0
  38. package/lib/esm/sdk.js.map +1 -1
  39. package/lib/esm/tests/api_tests/date_string_validation.test.d.ts +6 -0
  40. package/lib/esm/tests/api_tests/date_string_validation.test.d.ts.map +1 -0
  41. package/lib/esm/tests/api_tests/date_string_validation.test.js +138 -0
  42. package/lib/esm/tests/api_tests/date_string_validation.test.js.map +1 -0
  43. package/lib/esm/tests/api_tests/enduser_session_invalidation.test.d.ts +6 -0
  44. package/lib/esm/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -0
  45. package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js +239 -0
  46. package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js.map +1 -0
  47. package/lib/esm/tests/api_tests/field_redaction.test.d.ts +13 -0
  48. package/lib/esm/tests/api_tests/field_redaction.test.d.ts.map +1 -0
  49. package/lib/esm/tests/api_tests/field_redaction.test.js +814 -0
  50. package/lib/esm/tests/api_tests/field_redaction.test.js.map +1 -0
  51. package/lib/esm/tests/api_tests/form_submitted_trigger.test.d.ts +6 -0
  52. package/lib/esm/tests/api_tests/form_submitted_trigger.test.d.ts.map +1 -0
  53. package/lib/esm/tests/api_tests/form_submitted_trigger.test.js +425 -0
  54. package/lib/esm/tests/api_tests/form_submitted_trigger.test.js.map +1 -0
  55. package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts +6 -0
  56. package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts.map +1 -0
  57. package/lib/esm/tests/api_tests/integrations_redacted.test.js +269 -0
  58. package/lib/esm/tests/api_tests/integrations_redacted.test.js.map +1 -0
  59. package/lib/esm/tests/api_tests/mdb_sort.test.d.ts +6 -0
  60. package/lib/esm/tests/api_tests/mdb_sort.test.d.ts.map +1 -0
  61. package/lib/esm/tests/api_tests/mdb_sort.test.js +366 -0
  62. package/lib/esm/tests/api_tests/mdb_sort.test.js.map +1 -0
  63. package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
  64. package/lib/esm/tests/api_tests/openloop_webhooks.test.js +108 -24
  65. package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  66. package/lib/esm/tests/tests.d.ts.map +1 -1
  67. package/lib/esm/tests/tests.js +303 -180
  68. package/lib/esm/tests/tests.js.map +1 -1
  69. package/lib/tsconfig.tsbuildinfo +1 -1
  70. package/package.json +10 -10
  71. package/src/sdk.ts +11 -0
  72. package/src/tests/api_tests/calendar_events_bulk_update.test.ts +418 -0
  73. package/src/tests/api_tests/date_string_validation.test.ts +107 -0
  74. package/src/tests/api_tests/enduser_session_invalidation.test.ts +138 -0
  75. package/src/tests/api_tests/field_redaction.test.ts +669 -0
  76. package/src/tests/api_tests/form_started_trigger.test.ts +1 -1
  77. package/src/tests/api_tests/form_submitted_trigger.test.ts +281 -0
  78. package/src/tests/api_tests/integrations_redacted.test.ts +245 -0
  79. package/src/tests/api_tests/mdb_sort.test.ts +259 -0
  80. package/src/tests/api_tests/openloop_webhooks.test.ts +64 -0
  81. package/src/tests/api_tests/organization_settings_duplicates.test.ts +201 -0
  82. package/src/tests/tests.ts +92 -6
  83. package/test_generated.pdf +0 -0
@@ -0,0 +1,669 @@
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
+ import { PROVIDER_PERMISSIONS } from "@tellescope/constants"
11
+
12
+ const host = process.env.API_URL || 'http://localhost:8080' as const
13
+ const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.env.NON_ADMIN_PASSWORD]
14
+
15
+ const RECORDING_FIELDS = ['recordingURI', 'recordingId', 'recordingDurationInSeconds'] as const
16
+ const ALL_REDACTABLE_FIELDS = [...RECORDING_FIELDS, 'transcription', 'recordingTranscriptionData', 'aiSummary'] as const
17
+
18
+ const hasFields = (record: any, fields: readonly string[]) =>
19
+ fields.every(f => f in record && record[f] !== undefined)
20
+
21
+ const lacksFields = (record: any, fields: readonly string[]) =>
22
+ fields.every(f => !(f in record) || record[f] === undefined)
23
+
24
+ /**
25
+ * Tests for role-based field redactions on phone_calls.
26
+ *
27
+ * Verifies that fieldRedactions configured on a RoleBasedAccessPermission
28
+ * properly hide specified fields from API responses across all read paths
29
+ * (getOne, getSome) and write responses (updateOne).
30
+ */
31
+ export const field_redaction_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
32
+ log_header("Field Redaction Tests")
33
+
34
+ // Create test data
35
+ const testEnduser = await sdk.api.endusers.createOne({
36
+ fname: 'FieldRedactionTest',
37
+ lname: 'User',
38
+ email: 'field-redaction-test@example.com',
39
+ })
40
+
41
+ const testPhoneCall = await sdk.api.phone_calls.createOne({
42
+ enduserId: testEnduser.id,
43
+ inbound: true,
44
+ from: '+15551234567',
45
+ to: '+15559876543',
46
+ isVoicemail: true,
47
+ recordingURI: 'https://example.com/recording.wav',
48
+ recordingId: 'rec_test_123',
49
+ recordingDurationInSeconds: 45,
50
+ transcription: 'Hello, this is a voicemail transcription.',
51
+ recordingTranscriptionData: '{"results":{"transcripts":[{"transcript":"full call transcription data"}]}}',
52
+ aiSummary: 'Patient called about prescription refill.',
53
+ })
54
+
55
+ // Create role with full field redactions for phone_calls
56
+ const FULL_ACCESS = { create: 'All' as const, read: 'All' as const, update: 'All' as const, delete: 'All' as const }
57
+
58
+ const fullRedactionRole = 'full-redaction-test-role'
59
+ const rbapFull = await sdk.api.role_based_access_permissions.createOne({
60
+ role: fullRedactionRole,
61
+ permissions: { ...PROVIDER_PERMISSIONS, phone_calls: FULL_ACCESS, endusers: FULL_ACCESS },
62
+ fieldRedactions: {
63
+ phone_calls: [...ALL_REDACTABLE_FIELDS],
64
+ },
65
+ })
66
+
67
+ const originalRoles = sdkNonAdmin.userInfo.roles
68
+
69
+ try {
70
+ // Assign full-redaction role to non-admin
71
+ await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: [fullRedactionRole] }, { replaceObjectFields: true })
72
+ await wait(undefined, 1500)
73
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
74
+
75
+ // ========================================
76
+ // Test 1: Full redaction on getOne
77
+ // ========================================
78
+ log_header("Test 1: Full redaction on getOne")
79
+
80
+ await async_test(
81
+ "getOne - all redactable fields should be absent for redacted role",
82
+ () => sdkNonAdmin.api.phone_calls.getOne(testPhoneCall.id),
83
+ {
84
+ onResult: (r: any) => {
85
+ const redacted = lacksFields(r, [...ALL_REDACTABLE_FIELDS])
86
+ const corePresent = hasFields(r, ['enduserId', 'inbound', 'from', 'to'])
87
+ if (!redacted) {
88
+ const leaked = ALL_REDACTABLE_FIELDS.filter(f => f in r && r[f] !== undefined)
89
+ console.log(` ❌ VULNERABILITY: getOne leaked redacted fields: ${leaked.join(', ')}`)
90
+ } else {
91
+ console.log(" ✅ SAFE: all redactable fields properly redacted on getOne")
92
+ }
93
+ if (!corePresent) {
94
+ console.log(" ❌ ERROR: core fields (enduserId, inbound, from, to) are missing")
95
+ }
96
+ return redacted && corePresent
97
+ }
98
+ }
99
+ )
100
+
101
+ // ========================================
102
+ // Test 2: Admin sees all fields
103
+ // ========================================
104
+ log_header("Test 2: Admin sees all fields")
105
+
106
+ await async_test(
107
+ "getOne (admin) - all fields should be visible",
108
+ () => sdk.api.phone_calls.getOne(testPhoneCall.id),
109
+ {
110
+ onResult: (r: any) => {
111
+ const allPresent = hasFields(r, [...ALL_REDACTABLE_FIELDS])
112
+ if (!allPresent) {
113
+ const missing = ALL_REDACTABLE_FIELDS.filter(f => !(f in r) || r[f] === undefined)
114
+ console.log(` ❌ ERROR: admin is missing fields: ${missing.join(', ')}`)
115
+ } else {
116
+ console.log(" ✅ Admin can see all fields")
117
+ }
118
+ return allPresent
119
+ }
120
+ }
121
+ )
122
+
123
+ // ========================================
124
+ // Test 3: Partial redaction (recordings only)
125
+ // ========================================
126
+ log_header("Test 3: Partial redaction (recordings only)")
127
+
128
+ const partialRedactionRole = 'partial-redaction-test-role'
129
+ const rbapPartial = await sdk.api.role_based_access_permissions.createOne({
130
+ role: partialRedactionRole,
131
+ permissions: { ...PROVIDER_PERMISSIONS, phone_calls: FULL_ACCESS, endusers: FULL_ACCESS },
132
+ fieldRedactions: {
133
+ phone_calls: [...RECORDING_FIELDS],
134
+ },
135
+ })
136
+
137
+ try {
138
+ await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: [partialRedactionRole] }, { replaceObjectFields: true })
139
+ await wait(undefined, 1500)
140
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
141
+
142
+ await async_test(
143
+ "getOne - only recording fields should be redacted, transcription/summary visible",
144
+ () => sdkNonAdmin.api.phone_calls.getOne(testPhoneCall.id),
145
+ {
146
+ onResult: (r: any) => {
147
+ const recordingRedacted = lacksFields(r, [...RECORDING_FIELDS])
148
+ const nonRecordingPresent = hasFields(r, ['transcription', 'recordingTranscriptionData', 'aiSummary'])
149
+ if (!recordingRedacted) {
150
+ const leaked = RECORDING_FIELDS.filter(f => f in r && r[f] !== undefined)
151
+ console.log(` ❌ VULNERABILITY: recording fields leaked: ${leaked.join(', ')}`)
152
+ }
153
+ if (!nonRecordingPresent) {
154
+ const missing = ['transcription', 'recordingTranscriptionData', 'aiSummary'].filter(f => !(f in r) || r[f] === undefined)
155
+ console.log(` ❌ ERROR: non-redacted fields missing: ${missing.join(', ')}`)
156
+ }
157
+ if (recordingRedacted && nonRecordingPresent) {
158
+ console.log(" ✅ SAFE: partial redaction works correctly")
159
+ }
160
+ return recordingRedacted && nonRecordingPresent
161
+ }
162
+ }
163
+ )
164
+ } finally {
165
+ // Restore full-redaction role and clean up partial role
166
+ await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: [fullRedactionRole] }, { replaceObjectFields: true })
167
+ await wait(undefined, 1500)
168
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
169
+ await sdk.api.role_based_access_permissions.deleteOne(rbapPartial.id)
170
+ }
171
+
172
+ // ========================================
173
+ // Test 4: getSome/readMany consistency
174
+ // ========================================
175
+ log_header("Test 4: getSome/readMany consistency")
176
+
177
+ await async_test(
178
+ "getSome - redacted fields should be absent on readMany results",
179
+ () => sdkNonAdmin.api.phone_calls.getSome(),
180
+ {
181
+ onResult: (r: any) => {
182
+ const matches = r.filter((pc: any) => pc.id === testPhoneCall.id)
183
+ if (matches.length === 0) {
184
+ console.log(" ⚠️ SKIPPED: test phone call not found in getSome results")
185
+ return true
186
+ }
187
+ const record = matches[0]
188
+ const redacted = lacksFields(record, [...ALL_REDACTABLE_FIELDS])
189
+ if (!redacted) {
190
+ const leaked = ALL_REDACTABLE_FIELDS.filter(f => f in record && record[f] !== undefined)
191
+ console.log(` ❌ VULNERABILITY: getSome leaked redacted fields: ${leaked.join(', ')}`)
192
+ } else {
193
+ console.log(" ✅ SAFE: getSome properly redacts fields")
194
+ }
195
+ return redacted
196
+ }
197
+ }
198
+ )
199
+
200
+ await async_test(
201
+ "getSome (admin) - all fields should be visible",
202
+ () => sdk.api.phone_calls.getSome(),
203
+ {
204
+ onResult: (r: any) => {
205
+ const match = r.find((pc: any) => pc.id === testPhoneCall.id)
206
+ if (!match) {
207
+ console.log(" ⚠️ SKIPPED: test phone call not found in admin getSome results")
208
+ return true
209
+ }
210
+ const allPresent = hasFields(match, [...ALL_REDACTABLE_FIELDS])
211
+ if (!allPresent) {
212
+ const missing = ALL_REDACTABLE_FIELDS.filter(f => !(f in match) || match[f] === undefined)
213
+ console.log(` ❌ FALSE POSITIVE: admin getSome missing fields: ${missing.join(', ')}`)
214
+ } else {
215
+ console.log(" ✅ Admin getSome sees all fields")
216
+ }
217
+ return allPresent
218
+ }
219
+ }
220
+ )
221
+
222
+ // ========================================
223
+ // Test 5: Update response doesn't leak
224
+ // ========================================
225
+ log_header("Test 5: Update response doesn't leak redacted fields")
226
+
227
+ await async_test(
228
+ "updateOne - response should not contain redacted fields",
229
+ () => sdkNonAdmin.api.phone_calls.updateOne(testPhoneCall.id, { note: 'updated by redaction test' }),
230
+ {
231
+ onResult: (r: any) => {
232
+ const redacted = lacksFields(r, [...ALL_REDACTABLE_FIELDS])
233
+ if (!redacted) {
234
+ const leaked = ALL_REDACTABLE_FIELDS.filter(f => f in r && r[f] !== undefined)
235
+ console.log(` ❌ VULNERABILITY: updateOne response leaked redacted fields: ${leaked.join(', ')}`)
236
+ } else {
237
+ console.log(" ✅ SAFE: updateOne response does not contain redacted fields")
238
+ }
239
+ return redacted
240
+ }
241
+ }
242
+ )
243
+
244
+ await async_test(
245
+ "updateOne (admin) - all fields should be visible in response",
246
+ () => sdk.api.phone_calls.updateOne(testPhoneCall.id, { note: 'admin update test' }),
247
+ {
248
+ onResult: (r: any) => {
249
+ const allPresent = hasFields(r, [...ALL_REDACTABLE_FIELDS])
250
+ if (!allPresent) {
251
+ const missing = ALL_REDACTABLE_FIELDS.filter(f => !(f in r) || r[f] === undefined)
252
+ console.log(` ❌ FALSE POSITIVE: admin updateOne missing fields: ${missing.join(', ')}`)
253
+ } else {
254
+ console.log(" ✅ Admin updateOne sees all fields")
255
+ }
256
+ return allPresent
257
+ }
258
+ }
259
+ )
260
+
261
+ // ========================================
262
+ // Test 6: Create response doesn't leak
263
+ // ========================================
264
+ log_header("Test 6: Create response doesn't leak redacted fields")
265
+
266
+ let createdPhoneCallId: string | undefined
267
+ await async_test(
268
+ "createOne - response should not contain redacted fields",
269
+ () => sdkNonAdmin.api.phone_calls.createOne({
270
+ enduserId: testEnduser.id,
271
+ inbound: false,
272
+ from: '+15551111111',
273
+ to: '+15552222222',
274
+ recordingURI: 'https://example.com/leak-test.wav',
275
+ recordingId: 'rec_leak_test',
276
+ recordingDurationInSeconds: 10,
277
+ transcription: 'Leak test transcription.',
278
+ recordingTranscriptionData: '{"test":"data"}',
279
+ aiSummary: 'Leak test summary.',
280
+ }),
281
+ {
282
+ onResult: (r: any) => {
283
+ createdPhoneCallId = r.id
284
+ const redacted = lacksFields(r, [...ALL_REDACTABLE_FIELDS])
285
+ if (!redacted) {
286
+ const leaked = ALL_REDACTABLE_FIELDS.filter(f => f in r && r[f] !== undefined)
287
+ console.log(` ❌ VULNERABILITY: createOne response leaked redacted fields: ${leaked.join(', ')}`)
288
+ } else {
289
+ console.log(" ✅ SAFE: createOne response does not contain redacted fields")
290
+ }
291
+ return redacted
292
+ }
293
+ }
294
+ )
295
+ // Cleanup the created phone call
296
+ if (createdPhoneCallId) {
297
+ try { await sdk.api.phone_calls.deleteOne(createdPhoneCallId) } catch(e) {}
298
+ }
299
+
300
+ let adminCreatedPhoneCallId: string | undefined
301
+ await async_test(
302
+ "createOne (admin) - all fields should be visible in response",
303
+ () => sdk.api.phone_calls.createOne({
304
+ enduserId: testEnduser.id,
305
+ inbound: false,
306
+ from: '+15553333333',
307
+ to: '+15554444444',
308
+ recordingURI: 'https://example.com/admin-test.wav',
309
+ recordingId: 'rec_admin_test',
310
+ recordingDurationInSeconds: 20,
311
+ transcription: 'Admin create test transcription.',
312
+ recordingTranscriptionData: '{"admin":"test"}',
313
+ aiSummary: 'Admin create test summary.',
314
+ }),
315
+ {
316
+ onResult: (r: any) => {
317
+ adminCreatedPhoneCallId = r.id
318
+ const allPresent = hasFields(r, [...ALL_REDACTABLE_FIELDS])
319
+ if (!allPresent) {
320
+ const missing = ALL_REDACTABLE_FIELDS.filter(f => !(f in r) || r[f] === undefined)
321
+ console.log(` ❌ FALSE POSITIVE: admin createOne missing fields: ${missing.join(', ')}`)
322
+ } else {
323
+ console.log(" ✅ Admin createOne sees all fields")
324
+ }
325
+ return allPresent
326
+ }
327
+ }
328
+ )
329
+ if (adminCreatedPhoneCallId) {
330
+ try { await sdk.api.phone_calls.deleteOne(adminCreatedPhoneCallId) } catch(e) {}
331
+ }
332
+
333
+ // ========================================
334
+ // Test 7: bulk_load redaction
335
+ // ========================================
336
+ log_header("Test 7: bulk_load redaction")
337
+
338
+ await async_test(
339
+ "bulk_load - redacted fields should be absent",
340
+ () => sdkNonAdmin.bulk_load({ load: [{ model: 'phone_calls' as any, options: { limit: 100 } }] }),
341
+ {
342
+ onResult: (r: any) => {
343
+ const phoneCallResult = r.results[0]
344
+ if (!phoneCallResult || phoneCallResult.records.length === 0) {
345
+ console.log(" ⚠️ SKIPPED: no phone_calls returned from bulk_load")
346
+ return true
347
+ }
348
+ const match = phoneCallResult.records.find((pc: any) => pc.id === testPhoneCall.id)
349
+ if (!match) {
350
+ console.log(" ⚠️ SKIPPED: test phone call not found in bulk_load results")
351
+ return true
352
+ }
353
+ const redacted = lacksFields(match, [...ALL_REDACTABLE_FIELDS])
354
+ if (!redacted) {
355
+ const leaked = ALL_REDACTABLE_FIELDS.filter(f => f in match && match[f] !== undefined)
356
+ console.log(` ❌ VULNERABILITY: bulk_load leaked redacted fields: ${leaked.join(', ')}`)
357
+ } else {
358
+ console.log(" ✅ SAFE: bulk_load properly redacts fields")
359
+ }
360
+ return redacted
361
+ }
362
+ }
363
+ )
364
+
365
+ await async_test(
366
+ "bulk_load (admin) - all fields should be visible",
367
+ () => sdk.bulk_load({ load: [{ model: 'phone_calls' as any, options: { limit: 100 } }] }),
368
+ {
369
+ onResult: (r: any) => {
370
+ const phoneCallResult = r.results[0]
371
+ if (!phoneCallResult || phoneCallResult.records.length === 0) {
372
+ console.log(" ⚠️ SKIPPED: no phone_calls returned from admin bulk_load")
373
+ return true
374
+ }
375
+ const match = phoneCallResult.records.find((pc: any) => pc.id === testPhoneCall.id)
376
+ if (!match) {
377
+ console.log(" ⚠️ SKIPPED: test phone call not found in admin bulk_load results")
378
+ return true
379
+ }
380
+ const allPresent = hasFields(match, [...ALL_REDACTABLE_FIELDS])
381
+ if (!allPresent) {
382
+ const missing = ALL_REDACTABLE_FIELDS.filter(f => !(f in match) || match[f] === undefined)
383
+ console.log(` ❌ FALSE POSITIVE: admin bulk_load missing fields: ${missing.join(', ')}`)
384
+ } else {
385
+ console.log(" ✅ Admin bulk_load sees all fields")
386
+ }
387
+ return allPresent
388
+ }
389
+ }
390
+ )
391
+
392
+ // ========================================
393
+ // Test 7b: bulk-read (getByIds) redaction
394
+ // ========================================
395
+ log_header("Test 7b: bulk-read (getByIds) redaction")
396
+
397
+ await async_test(
398
+ "getByIds - redacted fields should be absent",
399
+ () => sdkNonAdmin.api.phone_calls.getByIds({ ids: [testPhoneCall.id] }),
400
+ {
401
+ onResult: (r: any) => {
402
+ if (!r.matches || r.matches.length === 0) {
403
+ console.log(" ⚠️ SKIPPED: no phone_calls returned from getByIds")
404
+ return true
405
+ }
406
+ const match = r.matches.find((pc: any) => pc.id === testPhoneCall.id)
407
+ if (!match) {
408
+ console.log(" ⚠️ SKIPPED: test phone call not found in getByIds results")
409
+ return true
410
+ }
411
+ const redacted = lacksFields(match, [...ALL_REDACTABLE_FIELDS])
412
+ if (!redacted) {
413
+ const leaked = ALL_REDACTABLE_FIELDS.filter(f => f in match && match[f] !== undefined)
414
+ console.log(` ❌ VULNERABILITY: getByIds leaked redacted fields: ${leaked.join(', ')}`)
415
+ } else {
416
+ console.log(" ✅ SAFE: getByIds properly redacts fields")
417
+ }
418
+ return redacted
419
+ }
420
+ }
421
+ )
422
+
423
+ await async_test(
424
+ "getByIds (admin) - all fields should be visible",
425
+ () => sdk.api.phone_calls.getByIds({ ids: [testPhoneCall.id] }),
426
+ {
427
+ onResult: (r: any) => {
428
+ if (!r.matches || r.matches.length === 0) {
429
+ console.log(" ⚠️ SKIPPED: no phone_calls returned from admin getByIds")
430
+ return true
431
+ }
432
+ const match = r.matches.find((pc: any) => pc.id === testPhoneCall.id)
433
+ if (!match) {
434
+ console.log(" ⚠️ SKIPPED: test phone call not found in admin getByIds results")
435
+ return true
436
+ }
437
+ const allPresent = hasFields(match, [...ALL_REDACTABLE_FIELDS])
438
+ if (!allPresent) {
439
+ const missing = ALL_REDACTABLE_FIELDS.filter(f => !(f in match) || match[f] === undefined)
440
+ console.log(` ❌ FALSE POSITIVE: admin getByIds missing fields: ${missing.join(', ')}`)
441
+ } else {
442
+ console.log(" ✅ Admin getByIds sees all fields")
443
+ }
444
+ return allPresent
445
+ }
446
+ }
447
+ )
448
+
449
+ // ========================================
450
+ // Test 8: load_inbox_data redaction
451
+ // ========================================
452
+ log_header("Test 8: load_inbox_data redaction")
453
+
454
+ await async_test(
455
+ "load_inbox_data - phone_calls should have redacted fields absent",
456
+ () => sdkNonAdmin.api.endusers.load_inbox_data({ enduserIds: [testEnduser.id] }),
457
+ {
458
+ onResult: (r: any) => {
459
+ if (!r.phone_calls || r.phone_calls.length === 0) {
460
+ console.log(" ⚠️ SKIPPED: no phone_calls returned from load_inbox_data")
461
+ return true
462
+ }
463
+ const match = r.phone_calls.find((pc: any) => pc.id === testPhoneCall.id)
464
+ if (!match) {
465
+ console.log(" ⚠️ SKIPPED: test phone call not found in load_inbox_data results")
466
+ return true
467
+ }
468
+ const redacted = lacksFields(match, [...ALL_REDACTABLE_FIELDS])
469
+ if (!redacted) {
470
+ const leaked = ALL_REDACTABLE_FIELDS.filter(f => f in match && match[f] !== undefined)
471
+ console.log(` ❌ VULNERABILITY: load_inbox_data leaked redacted fields: ${leaked.join(', ')}`)
472
+ } else {
473
+ console.log(" ✅ SAFE: load_inbox_data properly redacts phone_call fields")
474
+ }
475
+ return redacted
476
+ }
477
+ }
478
+ )
479
+
480
+ await async_test(
481
+ "load_inbox_data (admin) - all phone_call fields should be visible",
482
+ () => sdk.api.endusers.load_inbox_data({ enduserIds: [testEnduser.id] }),
483
+ {
484
+ onResult: (r: any) => {
485
+ if (!r.phone_calls || r.phone_calls.length === 0) {
486
+ console.log(" ⚠️ SKIPPED: no phone_calls returned from admin load_inbox_data")
487
+ return true
488
+ }
489
+ const match = r.phone_calls.find((pc: any) => pc.id === testPhoneCall.id)
490
+ if (!match) {
491
+ console.log(" ⚠️ SKIPPED: test phone call not found in admin load_inbox_data results")
492
+ return true
493
+ }
494
+ const allPresent = hasFields(match, [...ALL_REDACTABLE_FIELDS])
495
+ if (!allPresent) {
496
+ const missing = ALL_REDACTABLE_FIELDS.filter(f => !(f in match) || match[f] === undefined)
497
+ console.log(` ❌ FALSE POSITIVE: admin load_inbox_data missing fields: ${missing.join(', ')}`)
498
+ } else {
499
+ console.log(" ✅ Admin load_inbox_data sees all phone_call fields")
500
+ }
501
+ return allPresent
502
+ }
503
+ }
504
+ )
505
+
506
+ // ========================================
507
+ // Test 9: No-redaction role sees all fields
508
+ // ========================================
509
+ log_header("Test 9: Role without fieldRedactions sees all fields")
510
+
511
+ const noRedactionRole = 'no-redaction-test-role'
512
+ const rbapNoRedaction = await sdk.api.role_based_access_permissions.createOne({
513
+ role: noRedactionRole,
514
+ permissions: { ...PROVIDER_PERMISSIONS, phone_calls: FULL_ACCESS, endusers: FULL_ACCESS },
515
+ // No fieldRedactions
516
+ })
517
+
518
+ try {
519
+ await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: [noRedactionRole] }, { replaceObjectFields: true })
520
+ await wait(undefined, 1500)
521
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
522
+
523
+ await async_test(
524
+ "getOne - role without fieldRedactions should see all fields",
525
+ () => sdkNonAdmin.api.phone_calls.getOne(testPhoneCall.id),
526
+ {
527
+ onResult: (r: any) => {
528
+ const allPresent = hasFields(r, [...ALL_REDACTABLE_FIELDS])
529
+ if (!allPresent) {
530
+ const missing = ALL_REDACTABLE_FIELDS.filter(f => !(f in r) || r[f] === undefined)
531
+ console.log(` ❌ ERROR: no-redaction role is missing fields: ${missing.join(', ')}`)
532
+ } else {
533
+ console.log(" ✅ No-redaction role can see all fields")
534
+ }
535
+ return allPresent
536
+ }
537
+ }
538
+ )
539
+ } finally {
540
+ await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: [fullRedactionRole] }, { replaceObjectFields: true })
541
+ await wait(undefined, 1500)
542
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
543
+ await sdk.api.role_based_access_permissions.deleteOne(rbapNoRedaction.id)
544
+ }
545
+
546
+ // ========================================
547
+ // Test 10: Redaction scoped to model
548
+ // ========================================
549
+ log_header("Test 10: phone_calls redaction doesn't affect other models")
550
+
551
+ await async_test(
552
+ "enduser read - phone_calls fieldRedactions should not affect enduser fields",
553
+ () => sdkNonAdmin.api.endusers.getOne(testEnduser.id),
554
+ {
555
+ onResult: (r: any) => {
556
+ const corePresent = hasFields(r, ['fname', 'lname', 'email'])
557
+ if (!corePresent) {
558
+ console.log(" ❌ ERROR: enduser fields missing — phone_calls redaction may be leaking across models")
559
+ } else {
560
+ console.log(" ✅ SAFE: phone_calls redaction does not affect enduser fields")
561
+ }
562
+ return corePresent
563
+ }
564
+ }
565
+ )
566
+
567
+ // ========================================
568
+ // Test 11: data-sync redaction
569
+ // ========================================
570
+ log_header("Test 11: data-sync redaction")
571
+
572
+ const syncFrom = new Date(0) // far enough back to capture the test phone call
573
+
574
+ await async_test(
575
+ "data-sync - redacted fields should be absent in parsed data",
576
+ () => sdkNonAdmin.sync({ from: syncFrom }),
577
+ {
578
+ onResult: (r: any) => {
579
+ const match = r.results.find((rec: any) => rec.recordId === testPhoneCall.id && rec.modelName === 'phone_calls')
580
+ if (!match) {
581
+ console.log(" ⚠️ SKIPPED: test phone call not found in data-sync results")
582
+ return true
583
+ }
584
+ if (match.data === 'deleted') {
585
+ console.log(" ⚠️ SKIPPED: test phone call marked as deleted in data-sync")
586
+ return true
587
+ }
588
+ const parsed = JSON.parse(match.data)
589
+ const redacted = lacksFields(parsed, [...ALL_REDACTABLE_FIELDS])
590
+ const corePresent = hasFields(parsed, ['enduserId', 'inbound', 'from', 'to'])
591
+ if (!redacted) {
592
+ const leaked = ALL_REDACTABLE_FIELDS.filter(f => f in parsed && parsed[f] !== undefined)
593
+ console.log(` ❌ VULNERABILITY: data-sync leaked redacted fields: ${leaked.join(', ')}`)
594
+ } else {
595
+ console.log(" ✅ SAFE: data-sync properly redacts fields in parsed data")
596
+ }
597
+ if (!corePresent) {
598
+ console.log(" ❌ ERROR: core fields (enduserId, inbound, from, to) are missing from data-sync record")
599
+ }
600
+ return redacted && corePresent
601
+ }
602
+ }
603
+ )
604
+
605
+ await async_test(
606
+ "data-sync (admin) - all fields should be visible in parsed data",
607
+ () => sdk.sync({ from: syncFrom }),
608
+ {
609
+ onResult: (r: any) => {
610
+ const match = r.results.find((rec: any) => rec.recordId === testPhoneCall.id && rec.modelName === 'phone_calls')
611
+ if (!match) {
612
+ console.log(" ⚠️ SKIPPED: test phone call not found in admin data-sync results")
613
+ return true
614
+ }
615
+ if (match.data === 'deleted') {
616
+ console.log(" ⚠️ SKIPPED: test phone call marked as deleted in admin data-sync")
617
+ return true
618
+ }
619
+ const parsed = JSON.parse(match.data)
620
+ const allPresent = hasFields(parsed, [...ALL_REDACTABLE_FIELDS])
621
+ if (!allPresent) {
622
+ const missing = ALL_REDACTABLE_FIELDS.filter(f => !(f in parsed) || parsed[f] === undefined)
623
+ console.log(` ❌ FALSE POSITIVE: admin data-sync missing fields: ${missing.join(', ')}`)
624
+ } else {
625
+ console.log(" ✅ Admin data-sync sees all fields")
626
+ }
627
+ return allPresent
628
+ }
629
+ }
630
+ )
631
+
632
+ console.log("\n" + "=".repeat(60))
633
+ console.log("Field Redaction Tests Complete")
634
+ console.log("=".repeat(60))
635
+
636
+ } finally {
637
+ // Restore original roles
638
+ await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: originalRoles }, { replaceObjectFields: true })
639
+ await wait(undefined, 1000)
640
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
641
+
642
+ // Cleanup test data
643
+ try { await sdk.api.role_based_access_permissions.deleteOne(rbapFull.id) } catch(e) { console.error('Cleanup error (rbap):', e) }
644
+ try { await sdk.api.phone_calls.deleteOne(testPhoneCall.id) } catch(e) { console.error('Cleanup error (phone_call):', e) }
645
+ try { await sdk.api.endusers.deleteOne(testEnduser.id) } catch(e) { console.error('Cleanup error (enduser):', e) }
646
+ }
647
+ }
648
+
649
+ // Allow running this test file independently
650
+ if (require.main === module) {
651
+ console.log(`Using API URL: ${host}`)
652
+ const sdk = new Session({ host })
653
+ const sdkNonAdmin = new Session({ host })
654
+
655
+ const runTests = async () => {
656
+ await setup_tests(sdk, sdkNonAdmin)
657
+ await field_redaction_tests({ sdk, sdkNonAdmin })
658
+ }
659
+
660
+ runTests()
661
+ .then(() => {
662
+ console.log("✅ Field redaction test suite completed successfully")
663
+ process.exit(0)
664
+ })
665
+ .catch((error) => {
666
+ console.error("❌ Field redaction test suite failed:", error)
667
+ process.exit(1)
668
+ })
669
+ }
@@ -20,7 +20,7 @@ export const form_started_trigger_tests = async ({ sdk, sdkNonAdmin } : { sdk: S
20
20
  })
21
21
 
22
22
  const postToFormsort = async (o: {
23
- answers: { key: string, value: any }[],
23
+ answers: { key: string, value: any, label?: string }[],
24
24
  responder_uuid: string,
25
25
  finalized: boolean,
26
26
  }) => {