@tellescope/sdk 1.249.1 → 1.250.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 (44) hide show
  1. package/lib/cjs/enduser.d.ts +14 -2
  2. package/lib/cjs/enduser.d.ts.map +1 -1
  3. package/lib/cjs/enduser.js +14 -3
  4. package/lib/cjs/enduser.js.map +1 -1
  5. package/lib/cjs/sdk.d.ts +1 -0
  6. package/lib/cjs/sdk.d.ts.map +1 -1
  7. package/lib/cjs/sdk.js +3 -3
  8. package/lib/cjs/sdk.js.map +1 -1
  9. package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.d.ts.map +1 -1
  10. package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.js +46 -11
  11. package/lib/cjs/tests/api_tests/beluga_pharmacy_mappings.test.js.map +1 -1
  12. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.d.ts +6 -0
  13. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -0
  14. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js +782 -0
  15. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -0
  16. package/lib/cjs/tests/tests.d.ts.map +1 -1
  17. package/lib/cjs/tests/tests.js +173 -156
  18. package/lib/cjs/tests/tests.js.map +1 -1
  19. package/lib/esm/enduser.d.ts +14 -2
  20. package/lib/esm/enduser.d.ts.map +1 -1
  21. package/lib/esm/enduser.js +14 -3
  22. package/lib/esm/enduser.js.map +1 -1
  23. package/lib/esm/sdk.d.ts +3 -2
  24. package/lib/esm/sdk.d.ts.map +1 -1
  25. package/lib/esm/sdk.js +3 -3
  26. package/lib/esm/sdk.js.map +1 -1
  27. package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.d.ts.map +1 -1
  28. package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.js +46 -11
  29. package/lib/esm/tests/api_tests/beluga_pharmacy_mappings.test.js.map +1 -1
  30. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.d.ts +6 -0
  31. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -0
  32. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js +778 -0
  33. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -0
  34. package/lib/esm/tests/tests.d.ts.map +1 -1
  35. package/lib/esm/tests/tests.js +173 -156
  36. package/lib/esm/tests/tests.js.map +1 -1
  37. package/lib/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +10 -10
  39. package/src/enduser.ts +12 -6
  40. package/src/sdk.ts +3 -3
  41. package/src/tests/api_tests/beluga_pharmacy_mappings.test.ts +55 -0
  42. package/src/tests/api_tests/enduser_cross_access_isolation.test.ts +542 -0
  43. package/src/tests/tests.ts +29 -8
  44. package/test_generated.pdf +0 -0
@@ -0,0 +1,782 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
24
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.enduser_cross_access_isolation_tests = void 0;
51
+ require('source-map-support').install();
52
+ var sdk_1 = require("../../sdk");
53
+ var testing_1 = require("@tellescope/testing");
54
+ var schema_1 = require("@tellescope/schema");
55
+ var setup_1 = require("../setup");
56
+ var host = process.env.API_URL || 'http://localhost:8080';
57
+ var businessId = '60398b1131a295e64f084ff6';
58
+ var buildAttendees = function (enduserId) { return [{ id: enduserId, type: 'enduser' }]; };
59
+ // Per-model coverage of every enduser-scoped read / ownership-mutation endpoint.
60
+ // New enduser-scoped models added with a FilterAccessConstraint on enduserId,
61
+ // enduserIds, or attendees.id MUST be added here (or to EXEMPT_MODELS) — the
62
+ // schema drift guard below will fail the test until coverage is added.
63
+ var MODEL_CASES = [
64
+ {
65
+ model: 'tickets',
66
+ ownerField: 'enduserId',
67
+ buildPayload: function (enduserBId) { return ({ enduserId: enduserBId, title: 'isolation: ticket' }); },
68
+ },
69
+ {
70
+ model: 'engagement_events',
71
+ ownerField: 'enduserId',
72
+ buildPayload: function (enduserBId) { return ({ enduserId: enduserBId, type: 'isolation', significance: 1 }); },
73
+ },
74
+ {
75
+ model: 'enduser_observations',
76
+ ownerField: 'enduserId',
77
+ buildPayload: function (enduserBId) { return ({
78
+ enduserId: enduserBId,
79
+ status: 'final',
80
+ category: 'vital-signs',
81
+ measurement: { unit: 'mmHg', value: 120 },
82
+ }); },
83
+ },
84
+ {
85
+ model: 'enduser_tasks',
86
+ ownerField: 'enduserId',
87
+ buildPayload: function (enduserBId) { return ({ enduserId: enduserBId, title: 'isolation: task' }); },
88
+ },
89
+ {
90
+ model: 'care_plans',
91
+ ownerField: 'enduserId',
92
+ buildPayload: function (enduserBId) { return ({ enduserId: enduserBId, title: 'isolation: care plan' }); },
93
+ },
94
+ {
95
+ model: 'enduser_medications',
96
+ ownerField: 'enduserId',
97
+ buildPayload: function (enduserBId) { return ({ enduserId: enduserBId, title: 'isolation: medication' }); },
98
+ },
99
+ {
100
+ model: 'enduser_orders',
101
+ ownerField: 'enduserId',
102
+ buildPayload: function (enduserBId) { return ({
103
+ enduserId: enduserBId,
104
+ title: 'isolation: order',
105
+ status: 'pending',
106
+ source: 'isolation-test',
107
+ externalId: "iso-".concat(Date.now()),
108
+ }); },
109
+ },
110
+ {
111
+ model: 'enduser_problems',
112
+ ownerField: 'enduserId',
113
+ buildPayload: function (enduserBId) { return ({
114
+ enduserId: enduserBId,
115
+ title: 'isolation: problem',
116
+ }); },
117
+ },
118
+ {
119
+ model: 'managed_content_record_assignments',
120
+ ownerField: 'enduserId',
121
+ setup: function (sdk, _enduserBId) { return __awaiter(void 0, void 0, void 0, function () {
122
+ var content;
123
+ return __generator(this, function (_a) {
124
+ switch (_a.label) {
125
+ case 0: return [4 /*yield*/, sdk.api.managed_content_records.createOne({
126
+ title: "isolation content ".concat(Date.now()),
127
+ textContent: 'isolation',
128
+ htmlContent: '<p>isolation</p>',
129
+ })];
130
+ case 1:
131
+ content = _a.sent();
132
+ return [2 /*return*/, {
133
+ payloadOverride: { contentId: content.id },
134
+ cleanup: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
135
+ switch (_a.label) {
136
+ case 0: return [4 /*yield*/, sdk.api.managed_content_records.deleteOne(content.id).catch(function () { })];
137
+ case 1:
138
+ _a.sent();
139
+ return [2 /*return*/];
140
+ }
141
+ }); }); },
142
+ }];
143
+ }
144
+ });
145
+ }); },
146
+ buildPayload: function (enduserBId) { return ({ enduserId: enduserBId }); },
147
+ },
148
+ {
149
+ model: 'purchases',
150
+ ownerField: 'enduserId',
151
+ buildPayload: function (enduserBId) { return ({
152
+ enduserId: enduserBId,
153
+ title: 'isolation: purchase',
154
+ cost: { amount: 100, currency: 'USD' },
155
+ processor: 'Stripe',
156
+ }); },
157
+ },
158
+ {
159
+ model: 'purchase_credits',
160
+ ownerField: 'enduserId',
161
+ buildPayload: function (enduserBId) { return ({
162
+ enduserId: enduserBId,
163
+ title: 'isolation: credit',
164
+ value: { type: 'Credit', info: { amount: 50, currency: 'USD' } },
165
+ }); },
166
+ },
167
+ {
168
+ model: 'chat_rooms',
169
+ ownerField: 'enduserIds',
170
+ buildPayload: function (enduserBId) { return ({
171
+ type: 'internal',
172
+ userIds: [],
173
+ enduserIds: [enduserBId],
174
+ title: 'isolation: chat_room',
175
+ }); },
176
+ },
177
+ {
178
+ model: 'chats',
179
+ ownerField: 'enduserId',
180
+ setup: function (sdk, enduserBId) { return __awaiter(void 0, void 0, void 0, function () {
181
+ var room;
182
+ return __generator(this, function (_a) {
183
+ switch (_a.label) {
184
+ case 0: return [4 /*yield*/, sdk.api.chat_rooms.createOne({
185
+ type: 'internal',
186
+ userIds: [],
187
+ enduserIds: [enduserBId],
188
+ title: 'isolation: chats parent',
189
+ })];
190
+ case 1:
191
+ room = _a.sent();
192
+ return [2 /*return*/, {
193
+ payloadOverride: { roomId: room.id, senderId: enduserBId },
194
+ cleanup: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
195
+ switch (_a.label) {
196
+ case 0: return [4 /*yield*/, sdk.api.chat_rooms.deleteOne(room.id).catch(function () { })];
197
+ case 1:
198
+ _a.sent();
199
+ return [2 /*return*/];
200
+ }
201
+ }); }); },
202
+ }];
203
+ }
204
+ });
205
+ }); },
206
+ buildPayload: function (enduserBId) { return ({ message: 'isolation chat', enduserId: enduserBId }); },
207
+ },
208
+ {
209
+ model: 'calendar_events',
210
+ ownerField: 'attendees.id',
211
+ buildPayload: function (enduserBId) { return ({
212
+ title: 'isolation: calendar_event',
213
+ durationInMinutes: 30,
214
+ startTimeInMS: Date.now() + 86400000,
215
+ attendees: buildAttendees(enduserBId),
216
+ }); },
217
+ },
218
+ {
219
+ model: 'ticket_threads',
220
+ ownerField: 'enduserId',
221
+ buildPayload: function (enduserBId) { return ({
222
+ enduserId: enduserBId,
223
+ subject: 'isolation: thread',
224
+ }); },
225
+ },
226
+ {
227
+ model: 'ticket_thread_comments',
228
+ ownerField: 'enduserId',
229
+ setup: function (sdk, enduserBId) { return __awaiter(void 0, void 0, void 0, function () {
230
+ var thread;
231
+ return __generator(this, function (_a) {
232
+ switch (_a.label) {
233
+ case 0: return [4 /*yield*/, sdk.api.ticket_threads.createOne({
234
+ enduserId: enduserBId,
235
+ subject: 'isolation: thread comments parent',
236
+ })];
237
+ case 1:
238
+ thread = _a.sent();
239
+ return [2 /*return*/, {
240
+ payloadOverride: { ticketThreadId: thread.id },
241
+ cleanup: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
242
+ switch (_a.label) {
243
+ case 0: return [4 /*yield*/, sdk.api.ticket_threads.deleteOne(thread.id).catch(function () { })];
244
+ case 1:
245
+ _a.sent();
246
+ return [2 /*return*/];
247
+ }
248
+ }); }); },
249
+ }];
250
+ }
251
+ });
252
+ }); },
253
+ buildPayload: function (enduserBId) { return ({
254
+ enduserId: enduserBId,
255
+ ticketThreadId: '',
256
+ html: '<p>isolation</p>',
257
+ plaintext: 'isolation',
258
+ inbound: true,
259
+ public: false,
260
+ }); },
261
+ },
262
+ {
263
+ model: 'form_responses',
264
+ ownerField: 'enduserId',
265
+ setup: function (sdk, _enduserBId) { return __awaiter(void 0, void 0, void 0, function () {
266
+ var form;
267
+ return __generator(this, function (_a) {
268
+ switch (_a.label) {
269
+ case 0: return [4 /*yield*/, sdk.api.forms.createOne({ title: "isolation form ".concat(Date.now()) })];
270
+ case 1:
271
+ form = _a.sent();
272
+ return [2 /*return*/, {
273
+ payloadOverride: { formId: form.id },
274
+ cleanup: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
275
+ switch (_a.label) {
276
+ case 0: return [4 /*yield*/, sdk.api.forms.deleteOne(form.id).catch(function () { })];
277
+ case 1:
278
+ _a.sent();
279
+ return [2 /*return*/];
280
+ }
281
+ }); }); },
282
+ }];
283
+ }
284
+ });
285
+ }); },
286
+ buildPayload: function (enduserBId) { return ({
287
+ enduserId: enduserBId,
288
+ formId: '',
289
+ formTitle: 'isolation form',
290
+ }); },
291
+ },
292
+ {
293
+ model: 'enduser_eligibility_results',
294
+ ownerField: 'enduserId',
295
+ buildPayload: function (enduserBId) { return ({
296
+ enduserId: enduserBId,
297
+ title: 'isolation: eligibility',
298
+ type: 'Prescription',
299
+ externalId: "iso-".concat(Date.now()),
300
+ source: 'isolation-test',
301
+ status: 'Pending',
302
+ }); },
303
+ },
304
+ ];
305
+ var COVERED_MODELS = new Set(MODEL_CASES.map(function (c) { return c.model; }));
306
+ // Models that have a FilterAccessConstraint on enduserId / enduserIds /
307
+ // attendees.id but are intentionally NOT exercised here. Each entry must
308
+ // include a one-line reason. Empty by default; add only with justification.
309
+ var EXEMPT_MODELS = [
310
+ {
311
+ model: 'meetings',
312
+ reason: 'No default CRUD ops — created via admin-only start_meeting and read by endusers via custom my_meetings/read/join_meeting_for_event actions, which do not match this fixture pattern.',
313
+ },
314
+ ];
315
+ var RELEVANT_OWNER_FIELDS = new Set(['enduserId', 'enduserIds', 'attendees.id']);
316
+ var discoverEnduserScopedModels = function () {
317
+ var _a;
318
+ var found = [];
319
+ for (var _i = 0, _b = Object.keys(schema_1.schema); _i < _b.length; _i++) {
320
+ var name_1 = _b[_i];
321
+ var model = schema_1.schema[name_1];
322
+ var access = (_a = model === null || model === void 0 ? void 0 : model.constraints) === null || _a === void 0 ? void 0 : _a.access;
323
+ if (!access)
324
+ continue;
325
+ var hasFilter = access.some(function (c) { return (c === null || c === void 0 ? void 0 : c.type) === 'filter' && typeof c.field === 'string' && RELEVANT_OWNER_FIELDS.has(c.field); });
326
+ if (hasFilter)
327
+ found.push(name_1);
328
+ }
329
+ return found;
330
+ };
331
+ var assertNotPresent = function (records, id, label) {
332
+ (0, testing_1.assert)(!records.find(function (r) { return r.id === id; }), "".concat(label, " returned record owned by other enduser (id=").concat(id, ")"), label);
333
+ };
334
+ var expectForbidden = function (label, run) { return (0, testing_1.async_test)(label, run, testing_1.handleAnyError); };
335
+ var expectEmptyOrForbidden = function (label, run) { return (0, testing_1.async_test)(label, function () { return __awaiter(void 0, void 0, void 0, function () {
336
+ var r, _e_1;
337
+ return __generator(this, function (_a) {
338
+ switch (_a.label) {
339
+ case 0:
340
+ _a.trys.push([0, 2, , 3]);
341
+ return [4 /*yield*/, run()];
342
+ case 1:
343
+ r = _a.sent();
344
+ return [2 /*return*/, { rejected: false, length: r.length }];
345
+ case 2:
346
+ _e_1 = _a.sent();
347
+ return [2 /*return*/, { rejected: true, length: 0 }];
348
+ case 3: return [2 /*return*/];
349
+ }
350
+ });
351
+ }); }, { onResult: function (r) { return r.rejected || r.length === 0; } }); };
352
+ var expectMatchesEmptyOrForbidden = function (label, run) { return (0, testing_1.async_test)(label, function () { return __awaiter(void 0, void 0, void 0, function () {
353
+ var r, _e_2;
354
+ var _a;
355
+ return __generator(this, function (_b) {
356
+ switch (_b.label) {
357
+ case 0:
358
+ _b.trys.push([0, 2, , 3]);
359
+ return [4 /*yield*/, run()];
360
+ case 1:
361
+ r = _b.sent();
362
+ return [2 /*return*/, { rejected: false, length: ((_a = r === null || r === void 0 ? void 0 : r.matches) !== null && _a !== void 0 ? _a : []).length }];
363
+ case 2:
364
+ _e_2 = _b.sent();
365
+ return [2 /*return*/, { rejected: true, length: 0 }];
366
+ case 3: return [2 /*return*/];
367
+ }
368
+ });
369
+ }); }, { onResult: function (r) { return r.rejected || r.length === 0; } }); };
370
+ var expectGetOneFails = function (label, run) { return (0, testing_1.async_test)(label, function () { return __awaiter(void 0, void 0, void 0, function () {
371
+ var r, _e_3;
372
+ return __generator(this, function (_a) {
373
+ switch (_a.label) {
374
+ case 0:
375
+ _a.trys.push([0, 2, , 3]);
376
+ return [4 /*yield*/, run()
377
+ // some models may return undefined on no-match; that's also acceptable
378
+ ];
379
+ case 1:
380
+ r = _a.sent();
381
+ // some models may return undefined on no-match; that's also acceptable
382
+ return [2 /*return*/, { rejected: false, found: !!r }];
383
+ case 2:
384
+ _e_3 = _a.sent();
385
+ return [2 /*return*/, { rejected: true, found: false }];
386
+ case 3: return [2 /*return*/];
387
+ }
388
+ });
389
+ }); }, { onResult: function (r) { return r.rejected || !r.found; } }); };
390
+ var recordHasOwner = function (record, ownerField, enduserId) {
391
+ if (!record)
392
+ return false;
393
+ if (ownerField === 'enduserId')
394
+ return record.enduserId === enduserId;
395
+ if (ownerField === 'enduserIds')
396
+ return Array.isArray(record.enduserIds) && record.enduserIds.includes(enduserId);
397
+ if (ownerField === 'attendees.id')
398
+ return Array.isArray(record.attendees) && record.attendees.some(function (a) { return (a === null || a === void 0 ? void 0 : a.id) === enduserId; });
399
+ return false;
400
+ };
401
+ var enduser_cross_access_isolation_tests = function (_a) {
402
+ var sdk = _a.sdk, _sdkNonAdmin = _a.sdkNonAdmin;
403
+ return __awaiter(void 0, void 0, void 0, function () {
404
+ var discovered, exemptSet, missing, password, ts, enduserA, enduserB, sdkA, sdkB, _loop_1, _i, MODEL_CASES_1, c;
405
+ var _b;
406
+ return __generator(this, function (_c) {
407
+ switch (_c.label) {
408
+ case 0:
409
+ (0, testing_1.log_header)("Enduser cross-access isolation");
410
+ discovered = discoverEnduserScopedModels();
411
+ exemptSet = new Set(EXEMPT_MODELS.map(function (e) { return e.model; }));
412
+ missing = discovered.filter(function (m) { return !COVERED_MODELS.has(m) && !exemptSet.has(m); });
413
+ (0, testing_1.assert)(missing.length === 0, "Missing isolation coverage for enduser-scoped models: ".concat(missing.join(', '), ". ") +
414
+ "Add to MODEL_CASES or EXEMPT_MODELS in enduser_cross_access_isolation.test.ts.", 'schema drift guard: every FilterAccessConstraint on enduserId/enduserIds/attendees.id is covered');
415
+ password = 'IsolationTestPassword123!';
416
+ ts = Date.now();
417
+ return [4 /*yield*/, sdk.api.endusers.createOne({ email: "iso_a_".concat(ts, "@test.tellescope.com") })];
418
+ case 1:
419
+ enduserA = _c.sent();
420
+ return [4 /*yield*/, sdk.api.endusers.createOne({ email: "iso_b_".concat(ts, "@test.tellescope.com") })];
421
+ case 2:
422
+ enduserB = _c.sent();
423
+ return [4 /*yield*/, sdk.api.endusers.set_password({ id: enduserA.id, password: password })];
424
+ case 3:
425
+ _c.sent();
426
+ return [4 /*yield*/, sdk.api.endusers.set_password({ id: enduserB.id, password: password })];
427
+ case 4:
428
+ _c.sent();
429
+ sdkA = new sdk_1.EnduserSession({ host: host, businessId: businessId });
430
+ sdkB = new sdk_1.EnduserSession({ host: host, businessId: businessId });
431
+ _c.label = 5;
432
+ case 5:
433
+ _c.trys.push([5, , 12, 15]);
434
+ // Sanity check: each enduser session can authenticate. We only use sdkA
435
+ // for negative assertions, but a failed sdkB auth would mean the test
436
+ // data setup itself is malformed.
437
+ return [4 /*yield*/, sdkA.authenticate(enduserA.email, password)];
438
+ case 6:
439
+ // Sanity check: each enduser session can authenticate. We only use sdkA
440
+ // for negative assertions, but a failed sdkB auth would mean the test
441
+ // data setup itself is malformed.
442
+ _c.sent();
443
+ return [4 /*yield*/, sdkB.authenticate(enduserB.email, password)];
444
+ case 7:
445
+ _c.sent();
446
+ _loop_1 = function (c) {
447
+ var sublog, setupResult, e_1, payload, createdId, created, e_2, enduserApi, _d;
448
+ return __generator(this, function (_f) {
449
+ switch (_f.label) {
450
+ case 0:
451
+ sublog = function (variant) { return "".concat(c.model, ": ").concat(variant); };
452
+ setupResult = void 0;
453
+ if (!c.setup) return [3 /*break*/, 4];
454
+ _f.label = 1;
455
+ case 1:
456
+ _f.trys.push([1, 3, , 4]);
457
+ return [4 /*yield*/, c.setup(sdk, enduserB.id)];
458
+ case 2:
459
+ setupResult = _f.sent();
460
+ return [3 /*break*/, 4];
461
+ case 3:
462
+ e_1 = _f.sent();
463
+ (0, testing_1.assert)(false, "".concat(c.model, ": setup hook failed: ").concat(e_1.message), sublog('setup'));
464
+ return [2 /*return*/, "continue"];
465
+ case 4:
466
+ payload = __assign(__assign({}, c.buildPayload(enduserB.id)), ((_b = setupResult === null || setupResult === void 0 ? void 0 : setupResult.payloadOverride) !== null && _b !== void 0 ? _b : {}));
467
+ _f.label = 5;
468
+ case 5:
469
+ _f.trys.push([5, 7, , 10]);
470
+ return [4 /*yield*/, sdk.api[c.model].createOne(payload)];
471
+ case 6:
472
+ created = _f.sent();
473
+ createdId = created === null || created === void 0 ? void 0 : created.id;
474
+ return [3 /*break*/, 10];
475
+ case 7:
476
+ e_2 = _f.sent();
477
+ (0, testing_1.assert)(false, "".concat(c.model, ": admin createOne failed: ").concat(e_2.message), sublog('admin createOne'));
478
+ if (!setupResult) return [3 /*break*/, 9];
479
+ return [4 /*yield*/, setupResult.cleanup().catch(function () { })];
480
+ case 8:
481
+ _f.sent();
482
+ _f.label = 9;
483
+ case 9: return [2 /*return*/, "continue"];
484
+ case 10:
485
+ if (!!createdId) return [3 /*break*/, 13];
486
+ (0, testing_1.assert)(false, "".concat(c.model, ": admin createOne did not return an id"), sublog('admin createOne'));
487
+ if (!setupResult) return [3 /*break*/, 12];
488
+ return [4 /*yield*/, setupResult.cleanup().catch(function () { })];
489
+ case 11:
490
+ _f.sent();
491
+ _f.label = 12;
492
+ case 12: return [2 /*return*/, "continue"];
493
+ case 13:
494
+ enduserApi = sdkA.api[c.model];
495
+ _f.label = 14;
496
+ case 14:
497
+ _f.trys.push([14, , 27, 33]);
498
+ // 0a. Fixture sanity: admin can re-fetch the record AND its owner
499
+ // field is actually set to enduser B. Without this, A's negative
500
+ // assertions could pass trivially (e.g. if createOne silently
501
+ // dropped the ownership field, no enduser would ever match).
502
+ return [4 /*yield*/, (0, testing_1.async_test)(sublog('fixture: admin re-fetch confirms ownership set to enduser B'), function () { return __awaiter(void 0, void 0, void 0, function () {
503
+ var fetched;
504
+ return __generator(this, function (_a) {
505
+ switch (_a.label) {
506
+ case 0: return [4 /*yield*/, sdk.api[c.model].getOne(createdId)];
507
+ case 1:
508
+ fetched = _a.sent();
509
+ return [2 /*return*/, { fetched: fetched, owned: recordHasOwner(fetched, c.ownerField, enduserB.id) }];
510
+ }
511
+ });
512
+ }); }, { onResult: function (r) { return !!r.fetched && r.owned === true; } })
513
+ // 0b. Fixture sanity: enduser B (the legitimate owner) can see the
514
+ // record via getOne. Models that block all enduser reads (empty
515
+ // enduserActions) will reject — that's accepted. The check fails
516
+ // only if B is "found" returns a different record id.
517
+ ];
518
+ case 15:
519
+ // 0a. Fixture sanity: admin can re-fetch the record AND its owner
520
+ // field is actually set to enduser B. Without this, A's negative
521
+ // assertions could pass trivially (e.g. if createOne silently
522
+ // dropped the ownership field, no enduser would ever match).
523
+ _f.sent();
524
+ // 0b. Fixture sanity: enduser B (the legitimate owner) can see the
525
+ // record via getOne. Models that block all enduser reads (empty
526
+ // enduserActions) will reject — that's accepted. The check fails
527
+ // only if B is "found" returns a different record id.
528
+ return [4 /*yield*/, (0, testing_1.async_test)(sublog('fixture: owner enduser B can fetch own record (or model blocks endusers)'), function () { return __awaiter(void 0, void 0, void 0, function () {
529
+ var fetched, _e_4;
530
+ return __generator(this, function (_a) {
531
+ switch (_a.label) {
532
+ case 0:
533
+ _a.trys.push([0, 2, , 3]);
534
+ return [4 /*yield*/, sdkB.api[c.model].getOne(createdId)];
535
+ case 1:
536
+ fetched = _a.sent();
537
+ return [2 /*return*/, { rejected: false, matched: !!fetched && fetched.id === createdId }];
538
+ case 2:
539
+ _e_4 = _a.sent();
540
+ return [2 /*return*/, { rejected: true, matched: false }];
541
+ case 3: return [2 /*return*/];
542
+ }
543
+ });
544
+ }); }, { onResult: function (r) { return r.rejected || r.matched; } })
545
+ // 1. getOne by id — expect throw or undefined
546
+ ];
547
+ case 16:
548
+ // 0b. Fixture sanity: enduser B (the legitimate owner) can see the
549
+ // record via getOne. Models that block all enduser reads (empty
550
+ // enduserActions) will reject — that's accepted. The check fails
551
+ // only if B is "found" returns a different record id.
552
+ _f.sent();
553
+ // 1. getOne by id — expect throw or undefined
554
+ return [4 /*yield*/, expectGetOneFails(sublog('getOne by id rejects or returns nothing'), function () { return enduserApi.getOne(createdId); })
555
+ // 2. getOne by ownership filter — expect throw or undefined
556
+ ];
557
+ case 17:
558
+ // 1. getOne by id — expect throw or undefined
559
+ _f.sent();
560
+ // 2. getOne by ownership filter — expect throw or undefined
561
+ return [4 /*yield*/, expectGetOneFails(sublog('getOne by owner filter rejects or returns nothing'), function () {
562
+ var _a;
563
+ return enduserApi.getOne((_a = {}, _a[c.ownerField] = enduserB.id, _a));
564
+ })
565
+ // 3. getSome (no filter) — record must not appear (or call rejected)
566
+ ];
567
+ case 18:
568
+ // 2. getOne by ownership filter — expect throw or undefined
569
+ _f.sent();
570
+ // 3. getSome (no filter) — record must not appear (or call rejected)
571
+ return [4 /*yield*/, (0, testing_1.async_test)(sublog('getSome (no filter) excludes other-enduser record'), function () { return __awaiter(void 0, void 0, void 0, function () {
572
+ var records, _e_5;
573
+ return __generator(this, function (_a) {
574
+ switch (_a.label) {
575
+ case 0:
576
+ _a.trys.push([0, 2, , 3]);
577
+ return [4 /*yield*/, enduserApi.getSome({})];
578
+ case 1:
579
+ records = _a.sent();
580
+ return [2 /*return*/, { rejected: false, hasRecord: !!records.find(function (r) { return r.id === createdId; }) }];
581
+ case 2:
582
+ _e_5 = _a.sent();
583
+ return [2 /*return*/, { rejected: true, hasRecord: false }];
584
+ case 3: return [2 /*return*/];
585
+ }
586
+ });
587
+ }); }, { onResult: function (r) { return r.rejected || !r.hasRecord; } })
588
+ // 4. getSome (owner filter) — must be empty (or rejected)
589
+ ];
590
+ case 19:
591
+ // 3. getSome (no filter) — record must not appear (or call rejected)
592
+ _f.sent();
593
+ // 4. getSome (owner filter) — must be empty (or rejected)
594
+ return [4 /*yield*/, expectEmptyOrForbidden(sublog('getSome (owner filter) returns empty or rejects'), function () {
595
+ var _a;
596
+ return enduserApi.getSome({ filter: (_a = {}, _a[c.ownerField] = enduserB.id, _a) });
597
+ })
598
+ // 5. getByIds — matches must be empty (or rejected)
599
+ ];
600
+ case 20:
601
+ // 4. getSome (owner filter) — must be empty (or rejected)
602
+ _f.sent();
603
+ // 5. getByIds — matches must be empty (or rejected)
604
+ return [4 /*yield*/, expectMatchesEmptyOrForbidden(sublog('getByIds returns no matches or rejects'), function () { return enduserApi.getByIds({ ids: [createdId] }); })
605
+ // 6. /bulk-actions/read with owner filter
606
+ ];
607
+ case 21:
608
+ // 5. getByIds — matches must be empty (or rejected)
609
+ _f.sent();
610
+ // 6. /bulk-actions/read with owner filter
611
+ return [4 /*yield*/, (0, testing_1.async_test)(sublog('bulk_load (owner filter) returns null or empty for other-enduser data'), function () { return __awaiter(void 0, void 0, void 0, function () {
612
+ var r, result, _e_6;
613
+ var _a;
614
+ return __generator(this, function (_b) {
615
+ switch (_b.label) {
616
+ case 0:
617
+ _b.trys.push([0, 2, , 3]);
618
+ return [4 /*yield*/, sdkA.bulk_load({
619
+ load: [{
620
+ model: c.model,
621
+ options: { filter: (_a = {}, _a[c.ownerField] = enduserB.id, _a) },
622
+ }],
623
+ })];
624
+ case 1:
625
+ r = _b.sent();
626
+ result = r.results[0];
627
+ if (result === null)
628
+ return [2 /*return*/, { ok: true }];
629
+ return [2 /*return*/, { ok: result.records.length === 0, count: result.records.length }];
630
+ case 2:
631
+ _e_6 = _b.sent();
632
+ return [2 /*return*/, { ok: true }]; // rejection is also safe
633
+ case 3: return [2 /*return*/];
634
+ }
635
+ });
636
+ }); }, { onResult: function (r) { return r.ok === true; } })
637
+ // 7. /bulk-actions/read with no filter — record must not appear
638
+ ];
639
+ case 22:
640
+ // 6. /bulk-actions/read with owner filter
641
+ _f.sent();
642
+ // 7. /bulk-actions/read with no filter — record must not appear
643
+ return [4 /*yield*/, (0, testing_1.async_test)(sublog('bulk_load (no filter) excludes other-enduser record'), function () { return __awaiter(void 0, void 0, void 0, function () {
644
+ var r, result, _e_7;
645
+ return __generator(this, function (_a) {
646
+ switch (_a.label) {
647
+ case 0:
648
+ _a.trys.push([0, 2, , 3]);
649
+ return [4 /*yield*/, sdkA.bulk_load({
650
+ load: [{ model: c.model, options: {} }],
651
+ })];
652
+ case 1:
653
+ r = _a.sent();
654
+ result = r.results[0];
655
+ if (result === null)
656
+ return [2 /*return*/, { ok: true }];
657
+ return [2 /*return*/, { ok: !result.records.find(function (rec) { return rec.id === createdId; }) }];
658
+ case 2:
659
+ _e_7 = _a.sent();
660
+ return [2 /*return*/, { ok: true }];
661
+ case 3: return [2 /*return*/];
662
+ }
663
+ });
664
+ }); }, { onResult: function (r) { return r.ok === true; } })
665
+ // 8. updateOne — expect throw
666
+ ];
667
+ case 23:
668
+ // 7. /bulk-actions/read with no filter — record must not appear
669
+ _f.sent();
670
+ // 8. updateOne — expect throw
671
+ return [4 /*yield*/, expectForbidden(sublog('updateOne rejects'), function () { return enduserApi.updateOne(createdId, { /* no-op-ish update */}); })
672
+ // 9. deleteOne — expect throw
673
+ ];
674
+ case 24:
675
+ // 8. updateOne — expect throw
676
+ _f.sent();
677
+ // 9. deleteOne — expect throw
678
+ return [4 /*yield*/, expectForbidden(sublog('deleteOne rejects'), function () { return enduserApi.deleteOne(createdId); })
679
+ // After all attempted writes, confirm the record still exists when
680
+ // fetched as admin — i.e. enduser A's failed update/delete were no-ops.
681
+ ];
682
+ case 25:
683
+ // 9. deleteOne — expect throw
684
+ _f.sent();
685
+ // After all attempted writes, confirm the record still exists when
686
+ // fetched as admin — i.e. enduser A's failed update/delete were no-ops.
687
+ return [4 /*yield*/, (0, testing_1.async_test)(sublog('record still exists after failed enduser writes (admin verify)'), function () { return __awaiter(void 0, void 0, void 0, function () {
688
+ var found, _a;
689
+ return __generator(this, function (_b) {
690
+ switch (_b.label) {
691
+ case 0:
692
+ _b.trys.push([0, 2, , 3]);
693
+ return [4 /*yield*/, sdk.api[c.model].getOne(createdId)];
694
+ case 1:
695
+ found = _b.sent();
696
+ return [2 /*return*/, !!found && found.id === createdId];
697
+ case 2:
698
+ _a = _b.sent();
699
+ return [2 /*return*/, false];
700
+ case 3: return [2 /*return*/];
701
+ }
702
+ });
703
+ }); }, { onResult: function (r) { return r === true; } })];
704
+ case 26:
705
+ // After all attempted writes, confirm the record still exists when
706
+ // fetched as admin — i.e. enduser A's failed update/delete were no-ops.
707
+ _f.sent();
708
+ return [3 /*break*/, 33];
709
+ case 27:
710
+ _f.trys.push([27, 29, , 30]);
711
+ return [4 /*yield*/, sdk.api[c.model].deleteOne(createdId).catch(function () { })];
712
+ case 28:
713
+ _f.sent();
714
+ return [3 /*break*/, 30];
715
+ case 29:
716
+ _d = _f.sent();
717
+ return [3 /*break*/, 30];
718
+ case 30:
719
+ if (!setupResult) return [3 /*break*/, 32];
720
+ return [4 /*yield*/, setupResult.cleanup().catch(function () { })];
721
+ case 31:
722
+ _f.sent();
723
+ _f.label = 32;
724
+ case 32: return [7 /*endfinally*/];
725
+ case 33: return [2 /*return*/];
726
+ }
727
+ });
728
+ };
729
+ _i = 0, MODEL_CASES_1 = MODEL_CASES;
730
+ _c.label = 8;
731
+ case 8:
732
+ if (!(_i < MODEL_CASES_1.length)) return [3 /*break*/, 11];
733
+ c = MODEL_CASES_1[_i];
734
+ return [5 /*yield**/, _loop_1(c)];
735
+ case 9:
736
+ _c.sent();
737
+ _c.label = 10;
738
+ case 10:
739
+ _i++;
740
+ return [3 /*break*/, 8];
741
+ case 11: return [3 /*break*/, 15];
742
+ case 12: return [4 /*yield*/, sdk.api.endusers.deleteOne(enduserA.id).catch(function () { })];
743
+ case 13:
744
+ _c.sent();
745
+ return [4 /*yield*/, sdk.api.endusers.deleteOne(enduserB.id).catch(function () { })];
746
+ case 14:
747
+ _c.sent();
748
+ return [7 /*endfinally*/];
749
+ case 15: return [2 /*return*/];
750
+ }
751
+ });
752
+ });
753
+ };
754
+ exports.enduser_cross_access_isolation_tests = enduser_cross_access_isolation_tests;
755
+ if (require.main === module) {
756
+ console.log("Using API URL: ".concat(host));
757
+ var sdk_2 = new sdk_1.Session({ host: host });
758
+ var sdkNonAdmin_1 = new sdk_1.Session({ host: host });
759
+ var runTests = function () { return __awaiter(void 0, void 0, void 0, function () {
760
+ return __generator(this, function (_a) {
761
+ switch (_a.label) {
762
+ case 0: return [4 /*yield*/, (0, setup_1.setup_tests)(sdk_2, sdkNonAdmin_1)];
763
+ case 1:
764
+ _a.sent();
765
+ return [4 /*yield*/, (0, exports.enduser_cross_access_isolation_tests)({ sdk: sdk_2, sdkNonAdmin: sdkNonAdmin_1 })];
766
+ case 2:
767
+ _a.sent();
768
+ return [2 /*return*/];
769
+ }
770
+ });
771
+ }); };
772
+ runTests()
773
+ .then(function () {
774
+ console.log("Enduser cross-access isolation test suite completed successfully");
775
+ process.exit(0);
776
+ })
777
+ .catch(function (error) {
778
+ console.error("Enduser cross-access isolation test suite failed:", error);
779
+ process.exit(1);
780
+ });
781
+ }
782
+ //# sourceMappingURL=enduser_cross_access_isolation.test.js.map