@tellescope/sdk 1.256.7 → 1.256.8
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/group_event_attendee_self_update.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.js +293 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +132 -47
- package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +26 -22
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/enduser.d.ts +1 -0
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/sdk.d.ts +3 -2
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/session.d.ts +1 -0
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.js +289 -0
- package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +132 -47
- package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +26 -22
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/tests/api_tests/group_event_attendee_self_update.test.ts +161 -0
- package/src/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.ts +66 -6
- package/src/tests/tests.ts +2 -0
- package/test_generated.pdf +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/sdk",
|
|
3
|
-
"version": "1.256.
|
|
3
|
+
"version": "1.256.8",
|
|
4
4
|
"description": "Code for interacting with the Tellescope API",
|
|
5
5
|
"main": "./lib/cjs/sdk.js",
|
|
6
6
|
"module": "./lib/esm/sdk.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"homepage": "https://github.com/tellescope-os/tellescope#readme",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@tellescope/constants": "1.256.7",
|
|
36
|
-
"@tellescope/schema": "1.256.
|
|
36
|
+
"@tellescope/schema": "1.256.8",
|
|
37
37
|
"@tellescope/testing": "1.256.7",
|
|
38
38
|
"@tellescope/types-client": "1.256.7",
|
|
39
39
|
"@tellescope/types-models": "1.256.7",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "194f5c52adb83f8950ad515f37a9c638b8233c90"
|
|
59
59
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session, EnduserSession } from "../../sdk"
|
|
4
|
+
import { async_test, handleAnyError, passOnAnyResult, log_header } from "@tellescope/testing"
|
|
5
|
+
import { setup_tests } from "../setup"
|
|
6
|
+
|
|
7
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
8
|
+
|
|
9
|
+
// An enduser attendee can PATCH a calendar_event (needed to self-cancel out of a group event), and
|
|
10
|
+
// attendeeStatuses / cancelledGroupAttendees are attendee-keyed arrays that drive per-attendee
|
|
11
|
+
// automation triggers (handle_attendee_status_change_triggers + event-wide completion). The
|
|
12
|
+
// calendar_events relationship constraint must let an enduser touch ONLY their own id's entry, so a
|
|
13
|
+
// patient cannot fire or suppress a co-attendee's Appointment triggers by patching the shared event.
|
|
14
|
+
export const group_event_attendee_self_update_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
15
|
+
log_header("Group Event Attendee Self-Update Constraint")
|
|
16
|
+
|
|
17
|
+
const createdEnduserIds: string[] = []
|
|
18
|
+
const createdEventIds: string[] = []
|
|
19
|
+
|
|
20
|
+
const createEnduser = async (label: string) => {
|
|
21
|
+
const e = await sdk.api.endusers.createOne({ email: `attendee_self_update_${label}_${Date.now()}@test.tellescope.com` })
|
|
22
|
+
createdEnduserIds.push(e.id)
|
|
23
|
+
return e
|
|
24
|
+
}
|
|
25
|
+
const enduserSession = async (id: string) => {
|
|
26
|
+
const { authToken, enduser } = await sdk.api.endusers.generate_auth_token({ id })
|
|
27
|
+
return new EnduserSession({ host, authToken, enduser, businessId: sdk.userInfo.businessId })
|
|
28
|
+
}
|
|
29
|
+
const createGroupEvent = async (attendeeIds: string[]) => {
|
|
30
|
+
const event = await sdk.api.calendar_events.createOne({
|
|
31
|
+
title: 'Attendee Self-Update Test',
|
|
32
|
+
durationInMinutes: 30,
|
|
33
|
+
startTimeInMS: Date.now() + Math.floor(Math.random() * 100000),
|
|
34
|
+
attendees: attendeeIds.map(id => ({ type: 'enduser' as const, id })),
|
|
35
|
+
})
|
|
36
|
+
createdEventIds.push(event.id)
|
|
37
|
+
return event
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const attacker = await createEnduser('self')
|
|
42
|
+
const victim = await createEnduser('other')
|
|
43
|
+
const attackerSDK = await enduserSession(attacker.id)
|
|
44
|
+
|
|
45
|
+
// ---- ALLOWED: self-cancel out of a fresh group event (the portal self-cancel path) ----
|
|
46
|
+
const e1 = await createGroupEvent([attacker.id, victim.id])
|
|
47
|
+
await async_test(
|
|
48
|
+
"enduser may add their OWN cancelledGroupAttendees entry (self-cancel)",
|
|
49
|
+
() => attackerSDK.api.calendar_events.updateOne(e1.id, {
|
|
50
|
+
cancelledGroupAttendees: [{ id: attacker.id, at: new Date() }],
|
|
51
|
+
}, { replaceObjectFields: true }),
|
|
52
|
+
passOnAnyResult,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// ---- ALLOWED: set own attendeeStatuses entry ----
|
|
56
|
+
const e2 = await createGroupEvent([attacker.id, victim.id])
|
|
57
|
+
await async_test(
|
|
58
|
+
"enduser may set their OWN attendeeStatuses entry",
|
|
59
|
+
() => attackerSDK.api.calendar_events.updateOne(e2.id, {
|
|
60
|
+
attendeeStatuses: [{ id: attacker.id, at: new Date(), status: 'Cancelled' }],
|
|
61
|
+
}, { replaceObjectFields: true }),
|
|
62
|
+
passOnAnyResult,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// ---- REJECTED: set a co-attendee's attendeeStatuses (would fire that patient's triggers) ----
|
|
66
|
+
const e3 = await createGroupEvent([attacker.id, victim.id])
|
|
67
|
+
await async_test(
|
|
68
|
+
"enduser may NOT set a co-attendee's attendeeStatuses entry",
|
|
69
|
+
() => attackerSDK.api.calendar_events.updateOne(e3.id, {
|
|
70
|
+
attendeeStatuses: [{ id: victim.id, at: new Date(), status: 'No-showed' }],
|
|
71
|
+
}, { replaceObjectFields: true }),
|
|
72
|
+
handleAnyError,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// ---- REJECTED: set a co-attendee's cancelledGroupAttendees entry ----
|
|
76
|
+
const e4 = await createGroupEvent([attacker.id, victim.id])
|
|
77
|
+
await async_test(
|
|
78
|
+
"enduser may NOT set a co-attendee's cancelledGroupAttendees entry",
|
|
79
|
+
() => attackerSDK.api.calendar_events.updateOne(e4.id, {
|
|
80
|
+
cancelledGroupAttendees: [{ id: victim.id, at: new Date() }],
|
|
81
|
+
}, { replaceObjectFields: true }),
|
|
82
|
+
handleAnyError,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// ---- REJECTED: drop a co-attendee's existing entry (would suppress their completion trigger) ----
|
|
86
|
+
const e5 = await createGroupEvent([attacker.id, victim.id])
|
|
87
|
+
await sdk.api.calendar_events.updateOne(e5.id, {
|
|
88
|
+
attendeeStatuses: [{ id: victim.id, at: new Date(), status: 'Completed' }],
|
|
89
|
+
}, { replaceObjectFields: true })
|
|
90
|
+
await async_test(
|
|
91
|
+
"enduser may NOT remove a co-attendee's existing attendeeStatuses entry",
|
|
92
|
+
() => attackerSDK.api.calendar_events.updateOne(e5.id, {
|
|
93
|
+
attendeeStatuses: [{ id: attacker.id, at: new Date(), status: 'Cancelled' }], // silently drops victim's entry
|
|
94
|
+
}, { replaceObjectFields: true }),
|
|
95
|
+
handleAnyError,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// ---- REJECTED: modify a co-attendee's existing entry ----
|
|
99
|
+
const e6 = await createGroupEvent([attacker.id, victim.id])
|
|
100
|
+
await sdk.api.calendar_events.updateOne(e6.id, {
|
|
101
|
+
attendeeStatuses: [{ id: victim.id, at: new Date(), status: 'Completed' }],
|
|
102
|
+
}, { replaceObjectFields: true })
|
|
103
|
+
await async_test(
|
|
104
|
+
"enduser may NOT modify a co-attendee's existing attendeeStatuses entry",
|
|
105
|
+
() => attackerSDK.api.calendar_events.updateOne(e6.id, {
|
|
106
|
+
attendeeStatuses: [{ id: victim.id, at: new Date(), status: 'No-showed' }],
|
|
107
|
+
}, { replaceObjectFields: true }),
|
|
108
|
+
handleAnyError,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// ---- ALLOWED: self-cancel while PRESERVING a co-attendee's existing entry (realistic group self-cancel) ----
|
|
112
|
+
const e7 = await createGroupEvent([attacker.id, victim.id])
|
|
113
|
+
await sdk.api.calendar_events.updateOne(e7.id, {
|
|
114
|
+
cancelledGroupAttendees: [{ id: victim.id, at: new Date() }],
|
|
115
|
+
}, { replaceObjectFields: true })
|
|
116
|
+
const e7ForAttacker = await attackerSDK.api.calendar_events.getOne(e7.id)
|
|
117
|
+
await async_test(
|
|
118
|
+
"enduser may self-cancel while preserving a co-attendee's existing entry",
|
|
119
|
+
() => attackerSDK.api.calendar_events.updateOne(e7.id, {
|
|
120
|
+
cancelledGroupAttendees: [...(e7ForAttacker.cancelledGroupAttendees || []), { id: attacker.id, at: new Date() }],
|
|
121
|
+
}, { replaceObjectFields: true }),
|
|
122
|
+
passOnAnyResult,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// ---- ALLOWED (baseline): staff may set any attendee's status (constraint is enduser-only) ----
|
|
126
|
+
const e8 = await createGroupEvent([attacker.id, victim.id])
|
|
127
|
+
await async_test(
|
|
128
|
+
"staff may set any attendee's status",
|
|
129
|
+
() => sdk.api.calendar_events.updateOne(e8.id, {
|
|
130
|
+
attendeeStatuses: [{ id: victim.id, at: new Date(), status: 'No-showed' }],
|
|
131
|
+
}, { replaceObjectFields: true }),
|
|
132
|
+
passOnAnyResult,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
} finally {
|
|
136
|
+
for (const id of createdEventIds) { try { await sdk.api.calendar_events.deleteOne(id) } catch (_) {} }
|
|
137
|
+
for (const id of createdEnduserIds) { try { await sdk.api.endusers.deleteOne(id) } catch (_) {} }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Allow running this test file independently
|
|
142
|
+
if (require.main === module) {
|
|
143
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
144
|
+
const sdk = new Session({ host })
|
|
145
|
+
const sdkNonAdmin = new Session({ host })
|
|
146
|
+
|
|
147
|
+
const runTests = async () => {
|
|
148
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
149
|
+
await group_event_attendee_self_update_tests({ sdk, sdkNonAdmin })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
runTests()
|
|
153
|
+
.then(() => {
|
|
154
|
+
console.log("✅ Group event attendee self-update constraint tests completed successfully")
|
|
155
|
+
process.exit(0)
|
|
156
|
+
})
|
|
157
|
+
.catch((error) => {
|
|
158
|
+
console.error("❌ Group event attendee self-update constraint tests failed:", error)
|
|
159
|
+
process.exit(1)
|
|
160
|
+
})
|
|
161
|
+
}
|
package/src/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.ts
CHANGED
|
@@ -88,20 +88,80 @@ export const develop_health_benefit_verification_rbac_tests = async ({ sdk, sdkN
|
|
|
88
88
|
{
|
|
89
89
|
shouldError: true,
|
|
90
90
|
onError: (e: any) => {
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
91
|
+
// Match the gate's exact message. The SDK error surfaces { message, info } (no top-level
|
|
92
|
+
// status/code), so we key on the message rather than the HTTP status. This is deliberately
|
|
93
|
+
// specific: a lapsed subscription ("Your subscription has expired") or an expired token
|
|
94
|
+
// ("Unauthenticated") also errors BEFORE the handler runs, but neither contains this string,
|
|
95
|
+
// so the test can't false-pass without the gate executing. Also reject the pre-fix
|
|
96
|
+
// downstream messages, which would indicate the access check never fired.
|
|
94
97
|
const msg = (e?.message ?? '').toLowerCase()
|
|
95
|
-
const status = e?.status ?? e?.code
|
|
96
98
|
if (msg.includes('integration not found') || msg.includes('enduser is required')
|
|
97
99
|
|| msg.includes('organization') || msg.includes('provider')) {
|
|
98
100
|
return false // reached past the access gate — regression
|
|
99
101
|
}
|
|
100
|
-
return
|
|
101
|
-
|| msg.includes('access') || msg.includes('permission') || msg.includes('forbidden')
|
|
102
|
+
return msg.includes('access to this resource')
|
|
102
103
|
},
|
|
103
104
|
},
|
|
104
105
|
)
|
|
106
|
+
|
|
107
|
+
// 4. POSITIVE case: a provider role WITH enduser access must PASS the gate and reach the
|
|
108
|
+
// integration stage (this test org has no Develop Health integration, so it 400s with
|
|
109
|
+
// "Integration not found"). This guards against the gate over-blocking legitimate users —
|
|
110
|
+
// the failure mode a future tightening (e.g. gating on 'All') would introduce.
|
|
111
|
+
const providerRoleName = `f0006-develop-health-provider-${Date.now()}`
|
|
112
|
+
let providerRbapId: string | undefined
|
|
113
|
+
try {
|
|
114
|
+
const providerRbap = await sdk.api.role_based_access_permissions.createOne({
|
|
115
|
+
role: providerRoleName,
|
|
116
|
+
permissions: { ...PROVIDER_PERMISSIONS }, // endusers read/create non-null, eligibility create non-null
|
|
117
|
+
})
|
|
118
|
+
providerRbapId = providerRbap.id
|
|
119
|
+
|
|
120
|
+
await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: [providerRoleName] }, { replaceObjectFields: true })
|
|
121
|
+
await wait(undefined, 1500)
|
|
122
|
+
await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
|
|
123
|
+
|
|
124
|
+
await async_test(
|
|
125
|
+
"F-0006: a provider role with enduser access passes the gate (reaches 'Integration not found', not 403)",
|
|
126
|
+
() => sdkNonAdmin.api.enduser_eligibility_results.develop_health_run_benefit_verification({
|
|
127
|
+
enduserId: new ObjectId().toHexString(),
|
|
128
|
+
providerUserId: new ObjectId().toHexString(),
|
|
129
|
+
insuranceType: 'primary',
|
|
130
|
+
diagnoses: [],
|
|
131
|
+
drugs: [{ name: 'Test Drug', dosage: '10mg', quantity: 1 }],
|
|
132
|
+
drug_history: { currently_taking_drugs: [], previously_taken_drugs: [] },
|
|
133
|
+
} as any),
|
|
134
|
+
{
|
|
135
|
+
shouldError: true,
|
|
136
|
+
onError: (e: any) => {
|
|
137
|
+
const msg = (e?.message ?? '').toLowerCase()
|
|
138
|
+
// Must NOT be the access-denial message (that would mean the gate wrongly blocked a
|
|
139
|
+
// provider). Reaching "Integration not found" (or org/provider validation) proves the
|
|
140
|
+
// gate passed and the read-scoped enduser load did not over-restrict a legitimate role.
|
|
141
|
+
if (msg.includes('access to this resource')) return false
|
|
142
|
+
return msg.includes('integration not found') || msg.includes('organization') || msg.includes('provider')
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
} finally {
|
|
147
|
+
if (providerRbapId) {
|
|
148
|
+
try {
|
|
149
|
+
await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: originalRoles ?? [] }, { replaceObjectFields: true })
|
|
150
|
+
} catch {}
|
|
151
|
+
try { await sdk.api.role_based_access_permissions.deleteOne(providerRbapId) } catch {}
|
|
152
|
+
await wait(undefined, 1500)
|
|
153
|
+
try { await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!) } catch {}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// NOTE — the create>read bypass (endusers.create broader than endusers.read) that the 2026-08-19
|
|
158
|
+
// read-scoping closes is NOT live-testable here: this org has no Develop Health integration, so
|
|
159
|
+
// the handler throws "Integration not found" BEFORE reaching the (now read-scoped) enduser load,
|
|
160
|
+
// and the enduser load is what the create>read fix governs. Exercising it would need either a real
|
|
161
|
+
// Develop Health integration or reordering the handler purely for the test. It is covered by code
|
|
162
|
+
// review + F-0166 (systemic follow-up). If a Develop Health sandbox integration is ever available
|
|
163
|
+
// in the test org, add: role { endusers: create 'All', read 'Assigned' } → call with a real
|
|
164
|
+
// enduser NOT assigned to the caller → expect the enduser-not-found path, not a successful forward.
|
|
105
165
|
} finally {
|
|
106
166
|
// Cleanup: restore original roles, delete the test role.
|
|
107
167
|
try {
|
package/src/tests/tests.ts
CHANGED
|
@@ -58,6 +58,7 @@ import { purchase_made_trigger_tests } from "./api_tests/purchase_made_trigger.t
|
|
|
58
58
|
import { appointment_rescheduled_trigger_tests } from "./api_tests/appointment_rescheduled_trigger.test"
|
|
59
59
|
import { appointment_no_showed_trigger_tests } from "./api_tests/appointment_no_showed_trigger.test"
|
|
60
60
|
import { group_event_attendee_status_triggers_tests } from "./api_tests/group_event_attendee_status_triggers.test"
|
|
61
|
+
import { group_event_attendee_self_update_tests } from "./api_tests/group_event_attendee_self_update.test"
|
|
61
62
|
import { calendar_event_create_and_bulk_triggers_tests } from "./api_tests/calendar_event_create_and_bulk_triggers.test"
|
|
62
63
|
import { journey_error_branching_tests } from "./api_tests/journey_error_branching.test"
|
|
63
64
|
import { journey_delete_cancels_actions_tests } from "./api_tests/journey_delete_cancels_actions.test"
|
|
@@ -5757,6 +5758,7 @@ const automation_trigger_tests = async () => {
|
|
|
5757
5758
|
|
|
5758
5759
|
await appointment_no_showed_trigger_tests({ sdk, sdkNonAdmin })
|
|
5759
5760
|
await group_event_attendee_status_triggers_tests({ sdk, sdkNonAdmin })
|
|
5761
|
+
await group_event_attendee_self_update_tests({ sdk, sdkNonAdmin })
|
|
5760
5762
|
await calendar_event_create_and_bulk_triggers_tests({ sdk, sdkNonAdmin })
|
|
5761
5763
|
await assign_care_team_tests()
|
|
5762
5764
|
await push_forms_to_portal_group_completion_tests({ sdk, sdkNonAdmin })
|
package/test_generated.pdf
CHANGED
|
Binary file
|