@tellescope/sdk 1.248.0 → 1.249.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/.env +3 -0
- package/lib/cjs/sdk.d.ts +1 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +1 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/chats_analytics.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/chats_analytics.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/chats_analytics.test.js +256 -0
- package/lib/cjs/tests/api_tests/chats_analytics.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/cross_org_api_key.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/cross_org_api_key.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/cross_org_api_key.test.js +748 -0
- package/lib/cjs/tests/api_tests/cross_org_api_key.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js +426 -2
- package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/eom_billing_codes.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/eom_billing_codes.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/eom_billing_codes.test.js +162 -0
- package/lib/cjs/tests/api_tests/eom_billing_codes.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/eom_procedure_codes.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/eom_procedure_codes.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/eom_procedure_codes.test.js +339 -0
- package/lib/cjs/tests/api_tests/eom_procedure_codes.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/managed_content_file_access.test.d.ts +13 -0
- package/lib/cjs/tests/api_tests/managed_content_file_access.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/managed_content_file_access.test.js +385 -0
- package/lib/cjs/tests/api_tests/managed_content_file_access.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.js +25 -2
- package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +148 -132
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +3 -2
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +1 -0
- package/lib/esm/sdk.js.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/chats_analytics.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/chats_analytics.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/chats_analytics.test.js +252 -0
- package/lib/esm/tests/api_tests/chats_analytics.test.js.map +1 -0
- package/lib/esm/tests/api_tests/cross_org_api_key.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/cross_org_api_key.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/cross_org_api_key.test.js +744 -0
- package/lib/esm/tests/api_tests/cross_org_api_key.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js +426 -2
- package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js.map +1 -1
- package/lib/esm/tests/api_tests/eom_billing_codes.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/eom_billing_codes.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/eom_billing_codes.test.js +158 -0
- package/lib/esm/tests/api_tests/eom_billing_codes.test.js.map +1 -0
- package/lib/esm/tests/api_tests/eom_procedure_codes.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/eom_procedure_codes.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/eom_procedure_codes.test.js +335 -0
- package/lib/esm/tests/api_tests/eom_procedure_codes.test.js.map +1 -0
- package/lib/esm/tests/api_tests/managed_content_file_access.test.d.ts +13 -0
- package/lib/esm/tests/api_tests/managed_content_file_access.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/managed_content_file_access.test.js +358 -0
- package/lib/esm/tests/api_tests/managed_content_file_access.test.js.map +1 -0
- package/lib/esm/tests/api_tests/organization_settings_duplicates.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/organization_settings_duplicates.test.js +25 -2
- package/lib/esm/tests/api_tests/organization_settings_duplicates.test.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +148 -132
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +4 -0
- package/src/tests/api_tests/chats_analytics.test.ts +182 -0
- package/src/tests/api_tests/cross_org_api_key.test.ts +665 -0
- package/src/tests/api_tests/enduser_session_invalidation.test.ts +223 -0
- package/src/tests/api_tests/eom_procedure_codes.test.ts +296 -0
- package/src/tests/api_tests/managed_content_file_access.test.ts +214 -0
- package/src/tests/api_tests/organization_settings_duplicates.test.ts +14 -0
- package/src/tests/tests.ts +10 -2
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session, EnduserSession } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
async_test,
|
|
6
|
+
handleAnyError,
|
|
7
|
+
log_header,
|
|
8
|
+
wait,
|
|
9
|
+
} from "@tellescope/testing"
|
|
10
|
+
import { setup_tests } from "../setup"
|
|
11
|
+
|
|
12
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
|
+
|
|
14
|
+
const CROSS_ORG_API_KEY = process.env.CROSS_ORG_API_KEY
|
|
15
|
+
const CROSS_ORG_TARGET_BUSINESS_ID = process.env.CROSS_ORG_TARGET_BUSINESS_ID
|
|
16
|
+
const CROSS_ORG_UNAPPROVED_BUSINESS_ID = process.env.CROSS_ORG_UNAPPROVED_BUSINESS_ID
|
|
17
|
+
const NON_ADMIN_EMAIL = process.env.NON_ADMIN_EMAIL
|
|
18
|
+
const NON_ADMIN_PASSWORD = process.env.NON_ADMIN_PASSWORD
|
|
19
|
+
|
|
20
|
+
export const cross_org_api_key_tests = async (
|
|
21
|
+
{ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }
|
|
22
|
+
) => {
|
|
23
|
+
log_header("Cross-Organization API Key Tests")
|
|
24
|
+
|
|
25
|
+
if (!(CROSS_ORG_API_KEY && CROSS_ORG_TARGET_BUSINESS_ID && CROSS_ORG_UNAPPROVED_BUSINESS_ID)) {
|
|
26
|
+
console.log("Skipping cross-org API key tests — env vars not set")
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Session Setup ---
|
|
31
|
+
// Session using the cross-org API key WITHOUT the org header (default behavior)
|
|
32
|
+
const sdkDefault = new Session({ host, apiKey: CROSS_ORG_API_KEY })
|
|
33
|
+
|
|
34
|
+
// Session using the cross-org API key WITH approved target org header
|
|
35
|
+
const sdkCrossOrg = new Session({
|
|
36
|
+
host,
|
|
37
|
+
apiKey: CROSS_ORG_API_KEY,
|
|
38
|
+
headers: { 'x-tellescope-organization': CROSS_ORG_TARGET_BUSINESS_ID },
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Session using the cross-org API key WITH UNAPPROVED org header
|
|
42
|
+
const sdkUnapproved = new Session({
|
|
43
|
+
host,
|
|
44
|
+
apiKey: CROSS_ORG_API_KEY,
|
|
45
|
+
headers: { 'x-tellescope-organization': CROSS_ORG_UNAPPROVED_BUSINESS_ID },
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Session with a completely invalid/nonexistent org ID
|
|
49
|
+
const sdkInvalidOrg = new Session({
|
|
50
|
+
host,
|
|
51
|
+
apiKey: CROSS_ORG_API_KEY,
|
|
52
|
+
headers: { 'x-tellescope-organization': '000000000000000000000000' },
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const homeBusinessId = sdk.userInfo.businessId
|
|
56
|
+
|
|
57
|
+
// Create a real regular API key (no approvedBusinessIds) — used across multiple test sections
|
|
58
|
+
const regularApiKeyRecord = await sdk.api.api_keys.createOne({})
|
|
59
|
+
const regularApiKey = (regularApiKeyRecord as any).key as string
|
|
60
|
+
const sdkRegularApiKey = new Session({ host, apiKey: regularApiKey })
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// =============================================
|
|
64
|
+
// AUTHORIZED ACCESS TESTS
|
|
65
|
+
// =============================================
|
|
66
|
+
|
|
67
|
+
// 1. Default behavior (no header) still works — backward compatibility
|
|
68
|
+
await async_test(
|
|
69
|
+
"API key without org header authenticates to home org",
|
|
70
|
+
() => sdkDefault.test_authenticated(),
|
|
71
|
+
{ expectedResult: 'Authenticated!' }
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// 2. Cross-org header with approved org works
|
|
75
|
+
await async_test(
|
|
76
|
+
"API key with approved org header authenticates successfully",
|
|
77
|
+
() => sdkCrossOrg.test_authenticated(),
|
|
78
|
+
{ expectedResult: 'Authenticated!' }
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// 3. Targeting own org explicitly via header (should work as a no-op)
|
|
82
|
+
const sdkOwnOrgExplicit = new Session({
|
|
83
|
+
host,
|
|
84
|
+
apiKey: CROSS_ORG_API_KEY,
|
|
85
|
+
headers: { 'x-tellescope-organization': homeBusinessId },
|
|
86
|
+
})
|
|
87
|
+
await async_test(
|
|
88
|
+
"API key targeting its own org explicitly via header still works",
|
|
89
|
+
() => sdkOwnOrgExplicit.test_authenticated(),
|
|
90
|
+
{ expectedResult: 'Authenticated!' }
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// 4. Can read data from target org
|
|
94
|
+
await async_test(
|
|
95
|
+
"Can read endusers from target org",
|
|
96
|
+
() => sdkCrossOrg.api.endusers.getSome(),
|
|
97
|
+
{ onResult: r => Array.isArray(r) }
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// 5-9. Full CRUD in target org + cross-direction isolation
|
|
101
|
+
let crossOrgEnduserId: string | undefined
|
|
102
|
+
let homeOrgEnduserId: string | undefined
|
|
103
|
+
try {
|
|
104
|
+
// 5. Create in target org — verify record gets target org's businessId
|
|
105
|
+
await async_test(
|
|
106
|
+
"Can create enduser in target org with correct businessId",
|
|
107
|
+
() => sdkCrossOrg.api.endusers.createOne({ fname: 'CrossOrgTest', lname: 'Enduser' }),
|
|
108
|
+
{ onResult: e => {
|
|
109
|
+
crossOrgEnduserId = e.id
|
|
110
|
+
return e.fname === 'CrossOrgTest' && e.businessId === CROSS_ORG_TARGET_BUSINESS_ID
|
|
111
|
+
}}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// 6. Record created in target org is NOT visible from home org (data isolation)
|
|
115
|
+
if (crossOrgEnduserId) {
|
|
116
|
+
await async_test(
|
|
117
|
+
"Cross-org record is NOT visible from home org session",
|
|
118
|
+
() => sdkDefault.api.endusers.getOne(crossOrgEnduserId!),
|
|
119
|
+
handleAnyError
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 7. Create a record in home org and verify it's NOT visible from cross-org session
|
|
124
|
+
await async_test(
|
|
125
|
+
"Home org record is NOT visible from cross-org session",
|
|
126
|
+
async () => {
|
|
127
|
+
const homeEnduser = await sdkDefault.api.endusers.createOne({ fname: 'HomeOrgTest', lname: 'Enduser' })
|
|
128
|
+
homeOrgEnduserId = homeEnduser.id
|
|
129
|
+
return sdkCrossOrg.api.endusers.getOne(homeEnduser.id)
|
|
130
|
+
},
|
|
131
|
+
handleAnyError
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// 8. Update record in target org (full CRUD — update)
|
|
135
|
+
if (crossOrgEnduserId) {
|
|
136
|
+
await async_test(
|
|
137
|
+
"Can update enduser in target org",
|
|
138
|
+
() => sdkCrossOrg.api.endusers.updateOne(crossOrgEnduserId!, { fname: 'CrossOrgUpdated' }),
|
|
139
|
+
{ onResult: () => true }
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 9. Delete record in target org (full CRUD — delete)
|
|
144
|
+
if (crossOrgEnduserId) {
|
|
145
|
+
await async_test(
|
|
146
|
+
"Can delete enduser in target org",
|
|
147
|
+
() => sdkCrossOrg.api.endusers.deleteOne(crossOrgEnduserId!),
|
|
148
|
+
{ onResult: () => true }
|
|
149
|
+
)
|
|
150
|
+
crossOrgEnduserId = undefined // already deleted
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
if (crossOrgEnduserId) {
|
|
154
|
+
await sdkCrossOrg.api.endusers.deleteOne(crossOrgEnduserId).catch(console.error)
|
|
155
|
+
}
|
|
156
|
+
if (homeOrgEnduserId) {
|
|
157
|
+
await sdkDefault.api.endusers.deleteOne(homeOrgEnduserId).catch(console.error)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// =============================================
|
|
162
|
+
// UNAUTHORIZED ACCESS TESTS
|
|
163
|
+
// =============================================
|
|
164
|
+
|
|
165
|
+
// 10. Unapproved org ID is rejected
|
|
166
|
+
await async_test(
|
|
167
|
+
"API key with unapproved org header is rejected",
|
|
168
|
+
() => sdkUnapproved.test_authenticated(),
|
|
169
|
+
handleAnyError
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// 11. Nonexistent org ID is rejected
|
|
173
|
+
await async_test(
|
|
174
|
+
"API key with nonexistent org header is rejected",
|
|
175
|
+
() => sdkInvalidOrg.test_authenticated(),
|
|
176
|
+
handleAnyError
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
// 12. Malformed (non-ObjectId) org header is rejected
|
|
180
|
+
const sdkMalformedOrg = new Session({
|
|
181
|
+
host,
|
|
182
|
+
apiKey: CROSS_ORG_API_KEY,
|
|
183
|
+
headers: { 'x-tellescope-organization': 'not-a-valid-id' },
|
|
184
|
+
})
|
|
185
|
+
await async_test(
|
|
186
|
+
"API key with malformed org header is rejected",
|
|
187
|
+
() => sdkMalformedOrg.test_authenticated(),
|
|
188
|
+
handleAnyError
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
// 13. Empty string org header falls back to home org
|
|
192
|
+
const sdkEmptyHeader = new Session({
|
|
193
|
+
host,
|
|
194
|
+
apiKey: CROSS_ORG_API_KEY,
|
|
195
|
+
headers: { 'x-tellescope-organization': '' },
|
|
196
|
+
})
|
|
197
|
+
await async_test(
|
|
198
|
+
"API key with empty org header falls back to home org",
|
|
199
|
+
() => sdkEmptyHeader.test_authenticated(),
|
|
200
|
+
{ expectedResult: 'Authenticated!' }
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// 14. Regular API key (no approvedBusinessIds) with org header is rejected
|
|
204
|
+
const sdkRegularKeyWithHeader = new Session({
|
|
205
|
+
host,
|
|
206
|
+
apiKey: regularApiKey,
|
|
207
|
+
headers: { 'x-tellescope-organization': CROSS_ORG_TARGET_BUSINESS_ID },
|
|
208
|
+
})
|
|
209
|
+
await async_test(
|
|
210
|
+
"Regular API key (no approvedBusinessIds) with org header is rejected",
|
|
211
|
+
() => sdkRegularKeyWithHeader.test_authenticated(),
|
|
212
|
+
handleAnyError
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
// 15. Password-auth session with org header is rejected
|
|
216
|
+
await async_test(
|
|
217
|
+
"Password-auth session with org header is rejected",
|
|
218
|
+
async () => {
|
|
219
|
+
const sdkPasswordWithOrgHeader = new Session({
|
|
220
|
+
host,
|
|
221
|
+
headers: { 'x-tellescope-organization': CROSS_ORG_TARGET_BUSINESS_ID },
|
|
222
|
+
})
|
|
223
|
+
await sdkPasswordWithOrgHeader.authenticate(process.env.TEST_EMAIL!, process.env.TEST_PASSWORD!)
|
|
224
|
+
return sdkPasswordWithOrgHeader.test_authenticated()
|
|
225
|
+
},
|
|
226
|
+
handleAnyError
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
// =============================================
|
|
230
|
+
// READONLY ENFORCEMENT TESTS
|
|
231
|
+
// =============================================
|
|
232
|
+
|
|
233
|
+
// 16. approvedBusinessIds cannot be set when creating a new API key
|
|
234
|
+
await async_test(
|
|
235
|
+
"Cannot set approvedBusinessIds on API key creation",
|
|
236
|
+
() => sdk.api.api_keys.createOne({ approvedBusinessIds: [CROSS_ORG_TARGET_BUSINESS_ID] } as any),
|
|
237
|
+
handleAnyError
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
// 18. approvedBusinessIds cannot be set via bulk create
|
|
241
|
+
await async_test(
|
|
242
|
+
"Cannot set approvedBusinessIds via bulk create (createSome)",
|
|
243
|
+
() => sdk.api.api_keys.createSome([{ approvedBusinessIds: [CROSS_ORG_TARGET_BUSINESS_ID] }] as any),
|
|
244
|
+
handleAnyError
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
// 17a-c. approvedBusinessIds cannot be updated via API — all replaceObjectFields variants
|
|
248
|
+
let testKeyId: string | undefined
|
|
249
|
+
try {
|
|
250
|
+
const newKey = await sdk.api.api_keys.createOne({})
|
|
251
|
+
testKeyId = newKey.id
|
|
252
|
+
|
|
253
|
+
await async_test(
|
|
254
|
+
"Cannot update approvedBusinessIds on existing API key (no replaceObjectFields)",
|
|
255
|
+
() => sdk.api.api_keys.updateOne(testKeyId!, { approvedBusinessIds: [CROSS_ORG_TARGET_BUSINESS_ID] } as any),
|
|
256
|
+
handleAnyError
|
|
257
|
+
)
|
|
258
|
+
await async_test(
|
|
259
|
+
"Cannot update approvedBusinessIds on existing API key (replaceObjectFields: false)",
|
|
260
|
+
() => sdk.api.api_keys.updateOne(testKeyId!, { approvedBusinessIds: [CROSS_ORG_TARGET_BUSINESS_ID] } as any, { replaceObjectFields: false }),
|
|
261
|
+
handleAnyError
|
|
262
|
+
)
|
|
263
|
+
await async_test(
|
|
264
|
+
"Cannot update approvedBusinessIds on existing API key (replaceObjectFields: true)",
|
|
265
|
+
() => sdk.api.api_keys.updateOne(testKeyId!, { approvedBusinessIds: [CROSS_ORG_TARGET_BUSINESS_ID] } as any, { replaceObjectFields: true }),
|
|
266
|
+
handleAnyError
|
|
267
|
+
)
|
|
268
|
+
} finally {
|
|
269
|
+
if (testKeyId) {
|
|
270
|
+
await sdk.api.api_keys.deleteOne(testKeyId).catch(console.error)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// API key listing isolation — cross-org API key must not be visible to target org sessions,
|
|
275
|
+
// and target org's keys must not be visible to home org sessions
|
|
276
|
+
await async_test(
|
|
277
|
+
"Cross-org session cannot list home org API keys",
|
|
278
|
+
() => sdkCrossOrg.api.api_keys.getSome(),
|
|
279
|
+
{ onResult: keys => keys.every(k => k.businessId !== homeBusinessId) }
|
|
280
|
+
)
|
|
281
|
+
await async_test(
|
|
282
|
+
"Home org session cannot list target org API keys",
|
|
283
|
+
() => sdkDefault.api.api_keys.getSome(),
|
|
284
|
+
{ onResult: keys => keys.every(k => k.businessId !== CROSS_ORG_TARGET_BUSINESS_ID) }
|
|
285
|
+
)
|
|
286
|
+
await async_test(
|
|
287
|
+
"Regular API key session cannot list target org API keys",
|
|
288
|
+
() => sdkRegularApiKey.api.api_keys.getSome(),
|
|
289
|
+
{ onResult: keys => keys.every(k => k.businessId !== CROSS_ORG_TARGET_BUSINESS_ID) }
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
// Direct ID lookup isolation — cross-org session cannot access home org API keys by specific ID.
|
|
293
|
+
// Distinct code path from getSome: a creator-only check firing before org-scope could expose home org keys.
|
|
294
|
+
await async_test(
|
|
295
|
+
"Cross-org session cannot getOne a home org API key by ID",
|
|
296
|
+
() => sdkCrossOrg.api.api_keys.getOne(regularApiKeyRecord.id),
|
|
297
|
+
handleAnyError
|
|
298
|
+
)
|
|
299
|
+
await async_test(
|
|
300
|
+
"Cross-org session getByIds returns no matches for a home org API key",
|
|
301
|
+
() => sdkCrossOrg.api.api_keys.getByIds({ ids: [regularApiKeyRecord.id] }),
|
|
302
|
+
{ onResult: (r: any) => r.matches.length === 0 }
|
|
303
|
+
)
|
|
304
|
+
await async_test(
|
|
305
|
+
"Cross-org session bulk_load contains no home org API keys",
|
|
306
|
+
() => sdkCrossOrg.bulk_load({ load: [{ model: 'api_keys' }] }),
|
|
307
|
+
{ onResult: (r: any) => (r.results[0]?.records ?? []).every((k: any) => k.id !== regularApiKeyRecord.id) }
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
// Creator-only access: only the user who created an API key can read it (intra-org isolation).
|
|
311
|
+
// Elevate sdkNonAdmin to Admin (full read permissions) so any test failure is clearly due to
|
|
312
|
+
// creator-only enforcement, not missing role permissions.
|
|
313
|
+
await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: ['Admin'] }, { replaceObjectFields: true })
|
|
314
|
+
await wait(undefined, 2000) // wait for role change to propagate
|
|
315
|
+
await sdkNonAdmin.authenticate(NON_ADMIN_EMAIL!, NON_ADMIN_PASSWORD!)
|
|
316
|
+
const nonAdminKeyRecord = await sdkNonAdmin.api.api_keys.createOne({})
|
|
317
|
+
try {
|
|
318
|
+
// sdkNonAdmin (Admin) cannot read sdk's key
|
|
319
|
+
await async_test(
|
|
320
|
+
"Admin user cannot getOne another admin's API key",
|
|
321
|
+
() => sdkNonAdmin.api.api_keys.getOne(regularApiKeyRecord.id),
|
|
322
|
+
handleAnyError
|
|
323
|
+
)
|
|
324
|
+
await async_test(
|
|
325
|
+
"Admin user getSome excludes another admin's API key",
|
|
326
|
+
() => sdkNonAdmin.api.api_keys.getSome(),
|
|
327
|
+
{ onResult: keys => keys.every(k => k.id !== regularApiKeyRecord.id) }
|
|
328
|
+
)
|
|
329
|
+
await async_test(
|
|
330
|
+
"Admin user getByIds returns no matches for another admin's API key",
|
|
331
|
+
() => sdkNonAdmin.api.api_keys.getByIds({ ids: [regularApiKeyRecord.id] }),
|
|
332
|
+
{ onResult: (r: any) => r.matches.length === 0 }
|
|
333
|
+
)
|
|
334
|
+
await async_test(
|
|
335
|
+
"Admin user bulk_load excludes another admin's API key",
|
|
336
|
+
() => sdkNonAdmin.bulk_load({ load: [{ model: 'api_keys' }] }),
|
|
337
|
+
{ onResult: (r: any) => (r.results[0]?.records ?? []).every((k: any) => k.id !== regularApiKeyRecord.id) }
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
// sdk (Admin) cannot read sdkNonAdmin's key
|
|
341
|
+
await async_test(
|
|
342
|
+
"Admin cannot getOne a key created by a different admin user",
|
|
343
|
+
() => sdk.api.api_keys.getOne(nonAdminKeyRecord.id),
|
|
344
|
+
handleAnyError
|
|
345
|
+
)
|
|
346
|
+
await async_test(
|
|
347
|
+
"Admin getSome excludes keys created by a different admin user",
|
|
348
|
+
() => sdk.api.api_keys.getSome(),
|
|
349
|
+
{ onResult: keys => keys.every(k => k.id !== nonAdminKeyRecord.id) }
|
|
350
|
+
)
|
|
351
|
+
await async_test(
|
|
352
|
+
"Admin getByIds returns no matches for a key created by a different admin user",
|
|
353
|
+
() => sdk.api.api_keys.getByIds({ ids: [nonAdminKeyRecord.id] }),
|
|
354
|
+
{ onResult: (r: any) => r.matches.length === 0 }
|
|
355
|
+
)
|
|
356
|
+
await async_test(
|
|
357
|
+
"Admin bulk_load excludes keys created by a different admin user",
|
|
358
|
+
() => sdk.bulk_load({ load: [{ model: 'api_keys' }] }),
|
|
359
|
+
{ onResult: (r: any) => (r.results[0]?.records ?? []).every((k: any) => k.id !== nonAdminKeyRecord.id) }
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
// Write isolation: neither admin can mutate the other's key.
|
|
363
|
+
// Use a non-empty body with a readonly field (hashedKey) so the test exercises a real payload,
|
|
364
|
+
// not just an empty-object rejection.
|
|
365
|
+
await async_test(
|
|
366
|
+
"Admin user cannot updateOne another admin's API key",
|
|
367
|
+
() => sdkNonAdmin.api.api_keys.updateOne(regularApiKeyRecord.id, { hashedKey: 'attack' } as any),
|
|
368
|
+
handleAnyError
|
|
369
|
+
)
|
|
370
|
+
await async_test(
|
|
371
|
+
"Admin user cannot deleteOne another admin's API key",
|
|
372
|
+
() => sdkNonAdmin.api.api_keys.deleteOne(regularApiKeyRecord.id),
|
|
373
|
+
handleAnyError
|
|
374
|
+
)
|
|
375
|
+
await async_test(
|
|
376
|
+
"Admin cannot updateOne a key created by a different admin user",
|
|
377
|
+
() => sdk.api.api_keys.updateOne(nonAdminKeyRecord.id, { hashedKey: 'attack' } as any),
|
|
378
|
+
handleAnyError
|
|
379
|
+
)
|
|
380
|
+
await async_test(
|
|
381
|
+
"Admin cannot deleteOne a key created by a different admin user",
|
|
382
|
+
() => sdk.api.api_keys.deleteOne(nonAdminKeyRecord.id),
|
|
383
|
+
handleAnyError
|
|
384
|
+
)
|
|
385
|
+
} finally {
|
|
386
|
+
await sdkNonAdmin.api.api_keys.deleteOne(nonAdminKeyRecord.id).catch(console.error)
|
|
387
|
+
// Restore sdkNonAdmin to Non-Admin and reauthenticate so subsequent tests are unaffected
|
|
388
|
+
await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: ['Non-Admin'] }, { replaceObjectFields: true })
|
|
389
|
+
await wait(undefined, 1000)
|
|
390
|
+
await sdkNonAdmin.authenticate(NON_ADMIN_EMAIL!, NON_ADMIN_PASSWORD!)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// =============================================
|
|
394
|
+
// MULTI-TENANT DATA ISOLATION TESTS
|
|
395
|
+
// =============================================
|
|
396
|
+
|
|
397
|
+
const isolationIds: { id: string, inTargetOrg: boolean }[] = []
|
|
398
|
+
try {
|
|
399
|
+
// Create one enduser in each org to test isolation against
|
|
400
|
+
const targetOrgEnduser = await sdkCrossOrg.api.endusers.createOne({ fname: 'IsolationTest', lname: 'TargetOrg' })
|
|
401
|
+
isolationIds.push({ id: targetOrgEnduser.id, inTargetOrg: true })
|
|
402
|
+
|
|
403
|
+
const homeOrgEnduser = await sdkDefault.api.endusers.createOne({ fname: 'IsolationTest', lname: 'HomeOrg' })
|
|
404
|
+
isolationIds.push({ id: homeOrgEnduser.id, inTargetOrg: false })
|
|
405
|
+
|
|
406
|
+
// --- getOne isolation ---
|
|
407
|
+
|
|
408
|
+
// 18. Password-auth session cannot getOne a record from another org by ID
|
|
409
|
+
await async_test(
|
|
410
|
+
"Password-auth session cannot getOne a target org record by ID",
|
|
411
|
+
() => sdk.api.endusers.getOne(targetOrgEnduser.id),
|
|
412
|
+
handleAnyError
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
// 19. Regular API key session cannot getOne a record from another org by ID
|
|
416
|
+
await async_test(
|
|
417
|
+
"Regular API key session cannot getOne a target org record by ID",
|
|
418
|
+
() => sdkRegularApiKey.api.endusers.getOne(targetOrgEnduser.id),
|
|
419
|
+
handleAnyError
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
// 20. Cross-org session cannot getOne a home org record by ID (reverse direction)
|
|
423
|
+
await async_test(
|
|
424
|
+
"Cross-org session cannot getOne a home org record by ID",
|
|
425
|
+
() => sdkCrossOrg.api.endusers.getOne(homeOrgEnduser.id),
|
|
426
|
+
handleAnyError
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
// --- getSome isolation ---
|
|
430
|
+
|
|
431
|
+
// 21. getSome from home org (password-auth) never returns records belonging to the target org
|
|
432
|
+
await async_test(
|
|
433
|
+
"Password-auth session getSome contains no target org records",
|
|
434
|
+
() => sdk.api.endusers.getSome(),
|
|
435
|
+
{ onResult: endusers =>
|
|
436
|
+
endusers.every(e => e.businessId !== CROSS_ORG_TARGET_BUSINESS_ID && e.id !== targetOrgEnduser.id)
|
|
437
|
+
}
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
// 22. getSome via regular API key never returns records belonging to the target org
|
|
441
|
+
await async_test(
|
|
442
|
+
"Regular API key getSome contains no target org records",
|
|
443
|
+
() => sdkRegularApiKey.api.endusers.getSome(),
|
|
444
|
+
{ onResult: endusers =>
|
|
445
|
+
endusers.every(e => e.businessId !== CROSS_ORG_TARGET_BUSINESS_ID && e.id !== targetOrgEnduser.id)
|
|
446
|
+
}
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
// 23. getSome from target org session never returns records belonging to the home org
|
|
450
|
+
await async_test(
|
|
451
|
+
"Cross-org session getSome contains no home org records",
|
|
452
|
+
() => sdkCrossOrg.api.endusers.getSome(),
|
|
453
|
+
{ onResult: endusers =>
|
|
454
|
+
endusers.every(e => e.businessId !== homeBusinessId && e.id !== homeOrgEnduser.id)
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
// --- getSome filter injection ---
|
|
459
|
+
|
|
460
|
+
// 24. Passing an explicit businessId filter cannot expose cross-tenant records
|
|
461
|
+
await async_test(
|
|
462
|
+
"Regular API key getSome with explicit businessId filter cannot expose target org records",
|
|
463
|
+
() => sdkRegularApiKey.api.endusers.getSome({ filter: { businessId: CROSS_ORG_TARGET_BUSINESS_ID } } as any),
|
|
464
|
+
{ onResult: endusers =>
|
|
465
|
+
endusers.every(e => e.businessId !== CROSS_ORG_TARGET_BUSINESS_ID && e.id !== targetOrgEnduser.id)
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
// 25. Cross-org session cannot use businessId filter to expose home org records
|
|
470
|
+
await async_test(
|
|
471
|
+
"Cross-org session getSome with explicit businessId filter cannot expose home org records",
|
|
472
|
+
() => sdkCrossOrg.api.endusers.getSome({ filter: { businessId: homeBusinessId } } as any),
|
|
473
|
+
{ onResult: endusers =>
|
|
474
|
+
endusers.every(e => e.businessId !== homeBusinessId && e.id !== homeOrgEnduser.id)
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
// --- getByIds isolation ---
|
|
479
|
+
await async_test(
|
|
480
|
+
"Password-auth session getByIds returns no matches for target org record",
|
|
481
|
+
() => sdk.api.endusers.getByIds({ ids: [targetOrgEnduser.id] }),
|
|
482
|
+
{ onResult: (r: any) => r.matches.length === 0 }
|
|
483
|
+
)
|
|
484
|
+
await async_test(
|
|
485
|
+
"Regular API key session getByIds returns no matches for target org record",
|
|
486
|
+
() => sdkRegularApiKey.api.endusers.getByIds({ ids: [targetOrgEnduser.id] }),
|
|
487
|
+
{ onResult: (r: any) => r.matches.length === 0 }
|
|
488
|
+
)
|
|
489
|
+
await async_test(
|
|
490
|
+
"Cross-org session getByIds returns no matches for home org record",
|
|
491
|
+
() => sdkCrossOrg.api.endusers.getByIds({ ids: [homeOrgEnduser.id] }),
|
|
492
|
+
{ onResult: (r: any) => r.matches.length === 0 }
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
// --- bulk_load isolation ---
|
|
496
|
+
await async_test(
|
|
497
|
+
"Password-auth session bulk_load contains no target org endusers",
|
|
498
|
+
() => sdk.bulk_load({ load: [{ model: 'endusers' }] }),
|
|
499
|
+
{ onResult: (r: any) => (r.results[0]?.records ?? []).every((e: any) => e.id !== targetOrgEnduser.id) }
|
|
500
|
+
)
|
|
501
|
+
await async_test(
|
|
502
|
+
"Regular API key session bulk_load contains no target org endusers",
|
|
503
|
+
() => sdkRegularApiKey.bulk_load({ load: [{ model: 'endusers' }] }),
|
|
504
|
+
{ onResult: (r: any) => (r.results[0]?.records ?? []).every((e: any) => e.id !== targetOrgEnduser.id) }
|
|
505
|
+
)
|
|
506
|
+
await async_test(
|
|
507
|
+
"Cross-org session bulk_load contains no home org endusers",
|
|
508
|
+
() => sdkCrossOrg.bulk_load({ load: [{ model: 'endusers' }] }),
|
|
509
|
+
{ onResult: (r: any) => (r.results[0]?.records ?? []).every((e: any) => e.id !== homeOrgEnduser.id) }
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
// --- updateOne isolation ---
|
|
513
|
+
|
|
514
|
+
// 26. Password-auth session cannot updateOne a record from another org
|
|
515
|
+
await async_test(
|
|
516
|
+
"Password-auth session cannot updateOne a target org record",
|
|
517
|
+
() => sdk.api.endusers.updateOne(targetOrgEnduser.id, { fname: 'ShouldNotUpdate' }),
|
|
518
|
+
handleAnyError
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
// 27. Regular API key session cannot updateOne a record from another org
|
|
522
|
+
await async_test(
|
|
523
|
+
"Regular API key session cannot updateOne a target org record",
|
|
524
|
+
() => sdkRegularApiKey.api.endusers.updateOne(targetOrgEnduser.id, { fname: 'ShouldNotUpdate' }),
|
|
525
|
+
handleAnyError
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
// 28. Cross-org session cannot updateOne a home org record
|
|
529
|
+
await async_test(
|
|
530
|
+
"Cross-org session cannot updateOne a home org record",
|
|
531
|
+
() => sdkCrossOrg.api.endusers.updateOne(homeOrgEnduser.id, { fname: 'ShouldNotUpdate' }),
|
|
532
|
+
handleAnyError
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
// --- deleteOne isolation ---
|
|
536
|
+
// Records still exist at this point (prior deletes should have failed);
|
|
537
|
+
// cleanup in finally handles actual deletion via the correct sessions
|
|
538
|
+
|
|
539
|
+
// 29. Password-auth session cannot deleteOne a record from another org
|
|
540
|
+
await async_test(
|
|
541
|
+
"Password-auth session cannot deleteOne a target org record",
|
|
542
|
+
() => sdk.api.endusers.deleteOne(targetOrgEnduser.id),
|
|
543
|
+
handleAnyError
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
// 30. Regular API key session cannot deleteOne a record from another org
|
|
547
|
+
await async_test(
|
|
548
|
+
"Regular API key session cannot deleteOne a target org record",
|
|
549
|
+
() => sdkRegularApiKey.api.endusers.deleteOne(targetOrgEnduser.id),
|
|
550
|
+
handleAnyError
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
// 31. Cross-org session cannot deleteOne a home org record
|
|
554
|
+
await async_test(
|
|
555
|
+
"Cross-org session cannot deleteOne a home org record",
|
|
556
|
+
() => sdkCrossOrg.api.endusers.deleteOne(homeOrgEnduser.id),
|
|
557
|
+
handleAnyError
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
// --- businessId spoofing on create ---
|
|
561
|
+
|
|
562
|
+
// 32. Cross-org session cannot plant a record in the home org by passing an explicit businessId
|
|
563
|
+
await async_test(
|
|
564
|
+
"Cross-org create with explicit home org businessId is rejected",
|
|
565
|
+
() => sdkCrossOrg.api.endusers.createOne({ fname: 'SpoofTest', lname: 'CrossOrg', businessId: homeBusinessId } as any),
|
|
566
|
+
handleAnyError
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
// 33. Home org session cannot plant a record in the target org by passing an explicit businessId
|
|
570
|
+
await async_test(
|
|
571
|
+
"Home org create with explicit target org businessId is rejected",
|
|
572
|
+
() => sdkDefault.api.endusers.createOne({ fname: 'SpoofTest', lname: 'HomeOrg', businessId: CROSS_ORG_TARGET_BUSINESS_ID } as any),
|
|
573
|
+
handleAnyError
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
} finally {
|
|
577
|
+
for (const { id, inTargetOrg } of isolationIds) {
|
|
578
|
+
const session = inTargetOrg ? sdkCrossOrg : sdkDefault
|
|
579
|
+
await session.api.endusers.deleteOne(id).catch(console.error)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// =============================================
|
|
584
|
+
// ENDUSER SESSION TESTS
|
|
585
|
+
// =============================================
|
|
586
|
+
|
|
587
|
+
// 34. EnduserSession with org header is rejected
|
|
588
|
+
await async_test(
|
|
589
|
+
"EnduserSession with org header is rejected",
|
|
590
|
+
() => (new EnduserSession({
|
|
591
|
+
host,
|
|
592
|
+
businessId: homeBusinessId,
|
|
593
|
+
cacheKey: 'cross_org_test_enduser', // unique key — avoids stomping on main suite's enduser session cache
|
|
594
|
+
headers: { 'x-tellescope-organization': CROSS_ORG_TARGET_BUSINESS_ID },
|
|
595
|
+
})).test_authenticated(),
|
|
596
|
+
handleAnyError
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
// =============================================
|
|
600
|
+
// USERS (STAFF) RESOURCE ISOLATION TESTS
|
|
601
|
+
// =============================================
|
|
602
|
+
|
|
603
|
+
// 35. Cross-org session getSome users contains no home org users
|
|
604
|
+
await async_test(
|
|
605
|
+
"Cross-org session getSome users contains no home org users",
|
|
606
|
+
() => sdkCrossOrg.api.users.getSome(),
|
|
607
|
+
{ onResult: users => users.every(u => u.businessId !== homeBusinessId) }
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
// 36. Regular API key getSome users contains no target org users
|
|
611
|
+
await async_test(
|
|
612
|
+
"Regular API key getSome users contains no target org users",
|
|
613
|
+
() => sdkRegularApiKey.api.users.getSome(),
|
|
614
|
+
{ onResult: users => users.every(u => u.businessId !== CROSS_ORG_TARGET_BUSINESS_ID) }
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
// =============================================
|
|
618
|
+
// CREATOR FIELD BEHAVIOR (CROSS-ORG)
|
|
619
|
+
// =============================================
|
|
620
|
+
|
|
621
|
+
// 37. Record created via cross-org session has creator set to home org user, not a target org user
|
|
622
|
+
let creatorTestId: string | undefined
|
|
623
|
+
try {
|
|
624
|
+
await async_test(
|
|
625
|
+
"Cross-org created record has a creator from the home org, not the target org",
|
|
626
|
+
async () => {
|
|
627
|
+
const created = await sdkCrossOrg.api.endusers.createOne({ fname: 'CreatorTest', lname: 'CrossOrg' })
|
|
628
|
+
creatorTestId = created.id
|
|
629
|
+
const targetOrgUserIds = new Set((await sdkCrossOrg.api.users.getSome()).map(u => u.id))
|
|
630
|
+
return created.creator !== undefined && !targetOrgUserIds.has(created.creator)
|
|
631
|
+
},
|
|
632
|
+
{ onResult: r => r === true }
|
|
633
|
+
)
|
|
634
|
+
} finally {
|
|
635
|
+
if (creatorTestId) {
|
|
636
|
+
await sdkCrossOrg.api.endusers.deleteOne(creatorTestId).catch(console.error)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
} finally {
|
|
641
|
+
await sdk.api.api_keys.deleteOne(regularApiKeyRecord.id).catch(console.error)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Allow running independently
|
|
646
|
+
if (require.main === module) {
|
|
647
|
+
console.log(`Using API URL: ${host}`)
|
|
648
|
+
const sdk = new Session({ host })
|
|
649
|
+
const sdkNonAdmin = new Session({ host })
|
|
650
|
+
|
|
651
|
+
const runTests = async () => {
|
|
652
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
653
|
+
await cross_org_api_key_tests({ sdk, sdkNonAdmin })
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
runTests()
|
|
657
|
+
.then(() => {
|
|
658
|
+
console.log("Cross-org API key test suite completed successfully")
|
|
659
|
+
process.exit(0)
|
|
660
|
+
})
|
|
661
|
+
.catch((error) => {
|
|
662
|
+
console.error("Cross-org API key test suite failed:", error)
|
|
663
|
+
process.exit(1)
|
|
664
|
+
})
|
|
665
|
+
}
|