@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.
- package/lib/cjs/sdk.d.ts +7 -1
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +2 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/date_string_validation.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/date_string_validation.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/date_string_validation.test.js +142 -0
- package/lib/cjs/tests/api_tests/date_string_validation.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js +243 -0
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.d.ts +13 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.js +818 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/form_submitted_trigger.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/form_submitted_trigger.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/form_submitted_trigger.test.js +429 -0
- package/lib/cjs/tests/api_tests/form_submitted_trigger.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.js +273 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.js +370 -0
- package/lib/cjs/tests/api_tests/mdb_sort.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +108 -24
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +303 -180
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +7 -1
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +2 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/date_string_validation.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/date_string_validation.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/date_string_validation.test.js +138 -0
- package/lib/esm/tests/api_tests/date_string_validation.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js +239 -0
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js.map +1 -0
- package/lib/esm/tests/api_tests/field_redaction.test.d.ts +13 -0
- package/lib/esm/tests/api_tests/field_redaction.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/field_redaction.test.js +814 -0
- package/lib/esm/tests/api_tests/field_redaction.test.js.map +1 -0
- package/lib/esm/tests/api_tests/form_submitted_trigger.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/form_submitted_trigger.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/form_submitted_trigger.test.js +425 -0
- package/lib/esm/tests/api_tests/form_submitted_trigger.test.js.map +1 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.js +269 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.js.map +1 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.js +366 -0
- package/lib/esm/tests/api_tests/mdb_sort.test.js.map +1 -0
- package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js +108 -24
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +303 -180
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +11 -0
- package/src/tests/api_tests/calendar_events_bulk_update.test.ts +418 -0
- package/src/tests/api_tests/date_string_validation.test.ts +107 -0
- package/src/tests/api_tests/enduser_session_invalidation.test.ts +138 -0
- package/src/tests/api_tests/field_redaction.test.ts +669 -0
- package/src/tests/api_tests/form_started_trigger.test.ts +1 -1
- package/src/tests/api_tests/form_submitted_trigger.test.ts +281 -0
- package/src/tests/api_tests/integrations_redacted.test.ts +245 -0
- package/src/tests/api_tests/mdb_sort.test.ts +259 -0
- package/src/tests/api_tests/openloop_webhooks.test.ts +64 -0
- package/src/tests/api_tests/organization_settings_duplicates.test.ts +201 -0
- package/src/tests/tests.ts +92 -6
- 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
|
}) => {
|