@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.
Files changed (80) hide show
  1. package/.env +3 -0
  2. package/lib/cjs/sdk.d.ts +1 -0
  3. package/lib/cjs/sdk.d.ts.map +1 -1
  4. package/lib/cjs/sdk.js +1 -0
  5. package/lib/cjs/sdk.js.map +1 -1
  6. package/lib/cjs/tests/api_tests/chats_analytics.test.d.ts +6 -0
  7. package/lib/cjs/tests/api_tests/chats_analytics.test.d.ts.map +1 -0
  8. package/lib/cjs/tests/api_tests/chats_analytics.test.js +256 -0
  9. package/lib/cjs/tests/api_tests/chats_analytics.test.js.map +1 -0
  10. package/lib/cjs/tests/api_tests/cross_org_api_key.test.d.ts +6 -0
  11. package/lib/cjs/tests/api_tests/cross_org_api_key.test.d.ts.map +1 -0
  12. package/lib/cjs/tests/api_tests/cross_org_api_key.test.js +748 -0
  13. package/lib/cjs/tests/api_tests/cross_org_api_key.test.js.map +1 -0
  14. package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -1
  15. package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js +426 -2
  16. package/lib/cjs/tests/api_tests/enduser_session_invalidation.test.js.map +1 -1
  17. package/lib/cjs/tests/api_tests/eom_billing_codes.test.d.ts +6 -0
  18. package/lib/cjs/tests/api_tests/eom_billing_codes.test.d.ts.map +1 -0
  19. package/lib/cjs/tests/api_tests/eom_billing_codes.test.js +162 -0
  20. package/lib/cjs/tests/api_tests/eom_billing_codes.test.js.map +1 -0
  21. package/lib/cjs/tests/api_tests/eom_procedure_codes.test.d.ts +6 -0
  22. package/lib/cjs/tests/api_tests/eom_procedure_codes.test.d.ts.map +1 -0
  23. package/lib/cjs/tests/api_tests/eom_procedure_codes.test.js +339 -0
  24. package/lib/cjs/tests/api_tests/eom_procedure_codes.test.js.map +1 -0
  25. package/lib/cjs/tests/api_tests/managed_content_file_access.test.d.ts +13 -0
  26. package/lib/cjs/tests/api_tests/managed_content_file_access.test.d.ts.map +1 -0
  27. package/lib/cjs/tests/api_tests/managed_content_file_access.test.js +385 -0
  28. package/lib/cjs/tests/api_tests/managed_content_file_access.test.js.map +1 -0
  29. package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.d.ts.map +1 -1
  30. package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.js +25 -2
  31. package/lib/cjs/tests/api_tests/organization_settings_duplicates.test.js.map +1 -1
  32. package/lib/cjs/tests/tests.d.ts.map +1 -1
  33. package/lib/cjs/tests/tests.js +148 -132
  34. package/lib/cjs/tests/tests.js.map +1 -1
  35. package/lib/esm/sdk.d.ts +3 -2
  36. package/lib/esm/sdk.d.ts.map +1 -1
  37. package/lib/esm/sdk.js +1 -0
  38. package/lib/esm/sdk.js.map +1 -1
  39. package/lib/esm/session.d.ts +1 -0
  40. package/lib/esm/session.d.ts.map +1 -1
  41. package/lib/esm/tests/api_tests/chats_analytics.test.d.ts +6 -0
  42. package/lib/esm/tests/api_tests/chats_analytics.test.d.ts.map +1 -0
  43. package/lib/esm/tests/api_tests/chats_analytics.test.js +252 -0
  44. package/lib/esm/tests/api_tests/chats_analytics.test.js.map +1 -0
  45. package/lib/esm/tests/api_tests/cross_org_api_key.test.d.ts +6 -0
  46. package/lib/esm/tests/api_tests/cross_org_api_key.test.d.ts.map +1 -0
  47. package/lib/esm/tests/api_tests/cross_org_api_key.test.js +744 -0
  48. package/lib/esm/tests/api_tests/cross_org_api_key.test.js.map +1 -0
  49. package/lib/esm/tests/api_tests/enduser_session_invalidation.test.d.ts.map +1 -1
  50. package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js +426 -2
  51. package/lib/esm/tests/api_tests/enduser_session_invalidation.test.js.map +1 -1
  52. package/lib/esm/tests/api_tests/eom_billing_codes.test.d.ts +6 -0
  53. package/lib/esm/tests/api_tests/eom_billing_codes.test.d.ts.map +1 -0
  54. package/lib/esm/tests/api_tests/eom_billing_codes.test.js +158 -0
  55. package/lib/esm/tests/api_tests/eom_billing_codes.test.js.map +1 -0
  56. package/lib/esm/tests/api_tests/eom_procedure_codes.test.d.ts +6 -0
  57. package/lib/esm/tests/api_tests/eom_procedure_codes.test.d.ts.map +1 -0
  58. package/lib/esm/tests/api_tests/eom_procedure_codes.test.js +335 -0
  59. package/lib/esm/tests/api_tests/eom_procedure_codes.test.js.map +1 -0
  60. package/lib/esm/tests/api_tests/managed_content_file_access.test.d.ts +13 -0
  61. package/lib/esm/tests/api_tests/managed_content_file_access.test.d.ts.map +1 -0
  62. package/lib/esm/tests/api_tests/managed_content_file_access.test.js +358 -0
  63. package/lib/esm/tests/api_tests/managed_content_file_access.test.js.map +1 -0
  64. package/lib/esm/tests/api_tests/organization_settings_duplicates.test.d.ts.map +1 -1
  65. package/lib/esm/tests/api_tests/organization_settings_duplicates.test.js +25 -2
  66. package/lib/esm/tests/api_tests/organization_settings_duplicates.test.js.map +1 -1
  67. package/lib/esm/tests/tests.d.ts.map +1 -1
  68. package/lib/esm/tests/tests.js +148 -132
  69. package/lib/esm/tests/tests.js.map +1 -1
  70. package/lib/tsconfig.tsbuildinfo +1 -1
  71. package/package.json +10 -10
  72. package/src/sdk.ts +4 -0
  73. package/src/tests/api_tests/chats_analytics.test.ts +182 -0
  74. package/src/tests/api_tests/cross_org_api_key.test.ts +665 -0
  75. package/src/tests/api_tests/enduser_session_invalidation.test.ts +223 -0
  76. package/src/tests/api_tests/eom_procedure_codes.test.ts +296 -0
  77. package/src/tests/api_tests/managed_content_file_access.test.ts +214 -0
  78. package/src/tests/api_tests/organization_settings_duplicates.test.ts +14 -0
  79. package/src/tests/tests.ts +10 -2
  80. 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
+ }