@tellescope/sdk 1.255.3 → 1.255.4
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/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/medication_added_trigger.test.js +206 -136
- package/lib/cjs/tests/api_tests/medication_added_trigger.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +10 -0
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +184 -41
- package/lib/cjs/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -1
- package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/medication_added_trigger.test.js +206 -136
- package/lib/esm/tests/api_tests/medication_added_trigger.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts +10 -0
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js +184 -41
- package/lib/esm/tests/api_tests/security/F-0005-ai-conversations-rbac.test.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/medication_added_trigger.test.ts +59 -1
- package/src/tests/api_tests/security/F-0005-ai-conversations-rbac.test.ts +123 -0
- package/test_generated.pdf +0 -0
|
@@ -182,7 +182,7 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
|
|
|
182
182
|
title: "Medication - Beluga Protocol Test"
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
-
// Simulate the Beluga RX_WRITTEN webhook with two meds covering category present (typical) and "N/A"
|
|
185
|
+
// Simulate the Beluga RX_WRITTEN webhook with two meds covering category/pharmacyNotes present (typical) and "N/A"/absent
|
|
186
186
|
const webhookResponse = await fetch(`${host}/v1/webhooks/beluga`, {
|
|
187
187
|
method: 'POST',
|
|
188
188
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -198,6 +198,7 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
|
|
|
198
198
|
medId: 'test-ndc-123',
|
|
199
199
|
rxId: 'test-rx-456',
|
|
200
200
|
category: 'weightloss1',
|
|
201
|
+
pharmacyNotes: 'Dispense with syringes',
|
|
201
202
|
}, {
|
|
202
203
|
name: 'Metformin',
|
|
203
204
|
strength: '500mg',
|
|
@@ -245,6 +246,16 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
|
|
|
245
246
|
async () => belugaMedMetformin,
|
|
246
247
|
{ onResult: (m: EnduserMedication) => m?.category === 'N/A' }
|
|
247
248
|
)
|
|
249
|
+
await async_test(
|
|
250
|
+
"Beluga RX_WRITTEN - Medication has notes from webhook pharmacyNotes",
|
|
251
|
+
async () => belugaMedSemaglutide,
|
|
252
|
+
{ onResult: (m: EnduserMedication) => m?.notes === 'Dispense with syringes' }
|
|
253
|
+
)
|
|
254
|
+
await async_test(
|
|
255
|
+
"Beluga RX_WRITTEN - Medication omits notes when pharmacyNotes not provided on med",
|
|
256
|
+
async () => belugaMedMetformin,
|
|
257
|
+
{ onResult: (m: EnduserMedication) => m?.notes === undefined || m?.notes === null }
|
|
258
|
+
)
|
|
248
259
|
|
|
249
260
|
// Backwards-compatibility: a webhook with no category on any med should result in undefined category
|
|
250
261
|
const belugaFormNoCategory = await sdk.api.forms.createOne({
|
|
@@ -294,6 +305,11 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
|
|
|
294
305
|
async () => belugaMedNoCategory,
|
|
295
306
|
{ onResult: (m: EnduserMedication) => m?.category === undefined || m?.category === null }
|
|
296
307
|
)
|
|
308
|
+
await async_test(
|
|
309
|
+
"Beluga RX_WRITTEN - Medication omits notes when pharmacyNotes not provided",
|
|
310
|
+
async () => belugaMedNoCategory,
|
|
311
|
+
{ onResult: (m: EnduserMedication) => m?.notes === undefined || m?.notes === null }
|
|
312
|
+
)
|
|
297
313
|
|
|
298
314
|
try {
|
|
299
315
|
// Clean up Beluga test data
|
|
@@ -389,14 +405,56 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
|
|
|
389
405
|
{ onResult: (e: Enduser) => e.fields?.['Medication Category'] === '' }
|
|
390
406
|
)
|
|
391
407
|
|
|
408
|
+
// ---- Set Fields with {{medication.notes}} Test ----
|
|
409
|
+
log_header("Medication Added - Set Fields with {{medication.notes}}")
|
|
410
|
+
|
|
411
|
+
const setFieldsPharmacyNotesTrigger = await sdk.api.automation_triggers.createOne({
|
|
412
|
+
event: { type: 'Medication Added', info: { titles: [], protocols: [] } },
|
|
413
|
+
action: { type: 'Set Fields', info: { fields: [{ name: 'Medication Notes', type: 'Custom Value' as const, value: '{{medication.notes}}' }] }},
|
|
414
|
+
status: 'Active',
|
|
415
|
+
title: "Medication - Set Fields medication.notes"
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
const setFieldsPharmacyNotesEnduser = await sdk.api.endusers.createOne({})
|
|
419
|
+
|
|
420
|
+
const setFieldsPharmacyNotesMed = await sdk.api.enduser_medications.createOne({
|
|
421
|
+
enduserId: setFieldsPharmacyNotesEnduser.id,
|
|
422
|
+
title: 'Ozempic 1mg',
|
|
423
|
+
notes: 'Dispense with syringes',
|
|
424
|
+
})
|
|
425
|
+
await wait(undefined, 500)
|
|
426
|
+
|
|
427
|
+
await async_test(
|
|
428
|
+
"Medication Added - Set Fields copies medication.notes to enduser field",
|
|
429
|
+
() => sdk.api.endusers.getOne(setFieldsPharmacyNotesEnduser.id),
|
|
430
|
+
{ onResult: (e: Enduser) => e.fields?.['Medication Notes'] === 'Dispense with syringes' }
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
// notes absent on medication → placeholder resolves to empty string
|
|
434
|
+
const setFieldsPharmacyNotesEnduserEmpty = await sdk.api.endusers.createOne({})
|
|
435
|
+
const setFieldsPharmacyNotesMedEmpty = await sdk.api.enduser_medications.createOne({
|
|
436
|
+
enduserId: setFieldsPharmacyNotesEnduserEmpty.id,
|
|
437
|
+
title: 'Lisinopril 40mg',
|
|
438
|
+
})
|
|
439
|
+
await wait(undefined, 500)
|
|
440
|
+
|
|
441
|
+
await async_test(
|
|
442
|
+
"Medication Added - Set Fields with no notes resolves to empty string",
|
|
443
|
+
() => sdk.api.endusers.getOne(setFieldsPharmacyNotesEnduserEmpty.id),
|
|
444
|
+
{ onResult: (e: Enduser) => e.fields?.['Medication Notes'] === '' }
|
|
445
|
+
)
|
|
446
|
+
|
|
392
447
|
try {
|
|
393
448
|
await sdk.api.automation_triggers.deleteOne(setFieldsTrigger.id)
|
|
394
449
|
await sdk.api.automation_triggers.deleteOne(setFieldsTriggerFiltered.id)
|
|
395
450
|
await sdk.api.automation_triggers.deleteOne(setFieldsCategoryTrigger.id)
|
|
451
|
+
await sdk.api.automation_triggers.deleteOne(setFieldsPharmacyNotesTrigger.id)
|
|
396
452
|
await sdk.api.endusers.deleteOne(setFieldsEnduser.id)
|
|
397
453
|
await sdk.api.endusers.deleteOne(setFieldsEnduser2.id)
|
|
398
454
|
await sdk.api.endusers.deleteOne(setFieldsCategoryEnduser.id)
|
|
399
455
|
await sdk.api.endusers.deleteOne(setFieldsCategoryEnduserEmpty.id)
|
|
456
|
+
await sdk.api.endusers.deleteOne(setFieldsPharmacyNotesEnduser.id)
|
|
457
|
+
await sdk.api.endusers.deleteOne(setFieldsPharmacyNotesEnduserEmpty.id)
|
|
400
458
|
} finally {}
|
|
401
459
|
|
|
402
460
|
// ---- titleCondition (compound conditional logic on title) ----
|
|
@@ -28,6 +28,16 @@ const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.
|
|
|
28
28
|
* 2. Assigns the role to the non-admin user.
|
|
29
29
|
* 3. Calls each endpoint as the non-admin.
|
|
30
30
|
* 4. Asserts each endpoint returns a 403-equivalent error (not 200).
|
|
31
|
+
* 5. Positive case: a role granting ai_conversations create (read/update denied) must pass
|
|
32
|
+
* the send_message gate — generating a NEW conversation requires only create access.
|
|
33
|
+
* 6. But the same role must still 403 when passing a conversationId: continuing an existing
|
|
34
|
+
* conversation reads its stored history (returned in the response) and appends to it,
|
|
35
|
+
* so it requires read + update access in addition to create.
|
|
36
|
+
* 7. Default-provider-permissions role (ai_conversations = Assigned): send_message must not
|
|
37
|
+
* 500. Regression: the post-generation $push previously ran through the caller's
|
|
38
|
+
* access-scoped DB, where "Assigned" filters can't match the just-created conversation,
|
|
39
|
+
* returning null and crashing the handler (Cannot read properties of null '_id') after
|
|
40
|
+
* credits were consumed. bedrock.ts now persists via tenant-scoped org-wide queries.
|
|
31
41
|
*
|
|
32
42
|
* Pre-fix:
|
|
33
43
|
* - send_message: 200 (or some downstream error from Bedrock) — NOT 403. Test fails.
|
|
@@ -39,7 +49,11 @@ export const ai_conversations_rbac_tests = async ({ sdk, sdkNonAdmin } : { sdk:
|
|
|
39
49
|
log_header("F-0005: ai_conversations RBAC bypass regression")
|
|
40
50
|
|
|
41
51
|
const roleName = `f0005-ai-conversations-no-access-${Date.now()}`
|
|
52
|
+
const grantRoleName = `f0005-ai-conversations-create-granted-${Date.now()}`
|
|
53
|
+
const assignedRoleName = `f0005-ai-conversations-assigned-default-${Date.now()}`
|
|
42
54
|
let rbapId: string | undefined
|
|
55
|
+
let grantRbapId: string | undefined
|
|
56
|
+
let assignedRbapId: string | undefined
|
|
43
57
|
const originalRoles = sdkNonAdmin.userInfo.roles
|
|
44
58
|
|
|
45
59
|
try {
|
|
@@ -109,6 +123,109 @@ export const ai_conversations_rbac_tests = async ({ sdk, sdkNonAdmin } : { sdk:
|
|
|
109
123
|
},
|
|
110
124
|
},
|
|
111
125
|
)
|
|
126
|
+
|
|
127
|
+
// 4. Positive case: granting only ai_conversations create must pass the RBAC gate.
|
|
128
|
+
// send_message may still fail downstream (e.g. 400 "Organization has not set up credits"),
|
|
129
|
+
// but it must NOT be an access-denial error.
|
|
130
|
+
const grantRbap = await sdk.api.role_based_access_permissions.createOne({
|
|
131
|
+
role: grantRoleName,
|
|
132
|
+
permissions: {
|
|
133
|
+
...PROVIDER_PERMISSIONS,
|
|
134
|
+
ai_conversations: { create: 'All', read: null, update: null, delete: null },
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
grantRbapId = grantRbap.id
|
|
138
|
+
|
|
139
|
+
await sdk.api.users.updateOne(
|
|
140
|
+
sdkNonAdmin.userInfo.id,
|
|
141
|
+
{ roles: [grantRoleName] },
|
|
142
|
+
{ replaceObjectFields: true },
|
|
143
|
+
)
|
|
144
|
+
await wait(undefined, 1500)
|
|
145
|
+
await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
|
|
146
|
+
|
|
147
|
+
await async_test(
|
|
148
|
+
"F-0005: ai_conversations.send_message must NOT be access-denied when role grants create",
|
|
149
|
+
() => sdkNonAdmin.api.ai_conversations.send_message({
|
|
150
|
+
message: 'F-0005 positive-case test',
|
|
151
|
+
type: 'Test',
|
|
152
|
+
maxTokens: 1,
|
|
153
|
+
} as any)
|
|
154
|
+
.then(() => 'allowed' as const)
|
|
155
|
+
.catch((e: any) => {
|
|
156
|
+
const msg = (e?.message ?? '').toLowerCase()
|
|
157
|
+
const status = e?.status ?? e?.code
|
|
158
|
+
if (status === 403 || status === 401
|
|
159
|
+
|| msg.includes('access') || msg.includes('permission') || msg.includes('forbidden')) {
|
|
160
|
+
throw e // access denial — the gate incorrectly blocked a create-granted role
|
|
161
|
+
}
|
|
162
|
+
return 'allowed' as const // downstream (e.g. credits) error is fine — the gate passed
|
|
163
|
+
}),
|
|
164
|
+
{ onResult: r => r === 'allowed' },
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
// 5. The create-granted (read/update-denied) role must still be blocked from CONTINUING an
|
|
168
|
+
// existing conversation — send_message with conversationId returns the full stored history
|
|
169
|
+
// and appends to it. Fake id is fine: the access check must fire before any lookup.
|
|
170
|
+
await async_test(
|
|
171
|
+
"F-0005: ai_conversations.send_message with conversationId must 403 when role denies read/update",
|
|
172
|
+
() => sdkNonAdmin.api.ai_conversations.send_message({
|
|
173
|
+
message: 'F-0005 conversationId bypass test',
|
|
174
|
+
type: 'Test',
|
|
175
|
+
maxTokens: 1,
|
|
176
|
+
conversationId: new ObjectId().toHexString(),
|
|
177
|
+
} as any),
|
|
178
|
+
{
|
|
179
|
+
shouldError: true,
|
|
180
|
+
onError: (e: any) => {
|
|
181
|
+
const msg = (e?.message ?? '').toLowerCase()
|
|
182
|
+
const status = e?.status ?? e?.code
|
|
183
|
+
return status === 403 || status === 401
|
|
184
|
+
|| msg.includes('access') || msg.includes('permission') || msg.includes('forbidden')
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
// 6. Regression: a role with default provider permissions (ai_conversations = Assigned)
|
|
190
|
+
// must be able to generate a new conversation without a 500. Previously the
|
|
191
|
+
// post-generation $push ran through the access-scoped DB, matched nothing under
|
|
192
|
+
// "Assigned", and crashed the handler with "Cannot read properties of null ('_id')".
|
|
193
|
+
const assignedRbap = await sdk.api.role_based_access_permissions.createOne({
|
|
194
|
+
role: assignedRoleName,
|
|
195
|
+
permissions: { ...PROVIDER_PERMISSIONS },
|
|
196
|
+
})
|
|
197
|
+
assignedRbapId = assignedRbap.id
|
|
198
|
+
|
|
199
|
+
await sdk.api.users.updateOne(
|
|
200
|
+
sdkNonAdmin.userInfo.id,
|
|
201
|
+
{ roles: [assignedRoleName] },
|
|
202
|
+
{ replaceObjectFields: true },
|
|
203
|
+
)
|
|
204
|
+
await wait(undefined, 1500)
|
|
205
|
+
await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
|
|
206
|
+
|
|
207
|
+
await async_test(
|
|
208
|
+
"F-0005: ai_conversations.send_message must not 500 for Assigned-access (default provider) role",
|
|
209
|
+
() => sdkNonAdmin.api.ai_conversations.send_message({
|
|
210
|
+
message: 'F-0005 assigned-access regression test',
|
|
211
|
+
type: 'Test',
|
|
212
|
+
maxTokens: 1,
|
|
213
|
+
} as any)
|
|
214
|
+
.then(() => 'ok' as const)
|
|
215
|
+
.catch((e: any) => {
|
|
216
|
+
const msg = (e?.message ?? '').toLowerCase()
|
|
217
|
+
const status = e?.status ?? e?.code
|
|
218
|
+
if (status === 500 || msg.includes('internal error') || msg.includes('cannot read properties')) {
|
|
219
|
+
throw e // the null-update crash — regression
|
|
220
|
+
}
|
|
221
|
+
if (status === 403 || status === 401
|
|
222
|
+
|| msg.includes('access') || msg.includes('permission') || msg.includes('forbidden')) {
|
|
223
|
+
throw e // Assigned access must pass the RBAC gate
|
|
224
|
+
}
|
|
225
|
+
return 'ok' as const // downstream (e.g. credits) error is fine
|
|
226
|
+
}),
|
|
227
|
+
{ onResult: r => r === 'ok' },
|
|
228
|
+
)
|
|
112
229
|
} finally {
|
|
113
230
|
// Cleanup: restore original roles, delete the test role.
|
|
114
231
|
try {
|
|
@@ -121,6 +238,12 @@ export const ai_conversations_rbac_tests = async ({ sdk, sdkNonAdmin } : { sdk:
|
|
|
121
238
|
if (rbapId) {
|
|
122
239
|
try { await sdk.api.role_based_access_permissions.deleteOne(rbapId) } catch {}
|
|
123
240
|
}
|
|
241
|
+
if (grantRbapId) {
|
|
242
|
+
try { await sdk.api.role_based_access_permissions.deleteOne(grantRbapId) } catch {}
|
|
243
|
+
}
|
|
244
|
+
if (assignedRbapId) {
|
|
245
|
+
try { await sdk.api.role_based_access_permissions.deleteOne(assignedRbapId) } catch {}
|
|
246
|
+
}
|
|
124
247
|
// Re-authenticate the non-admin to drop the no-access role from their JWT
|
|
125
248
|
// before subsequent tests run.
|
|
126
249
|
// Role restore above re-triggers deauthenticate_user; wait > 1s so the freshly minted
|
package/test_generated.pdf
CHANGED
|
Binary file
|