@tellescope/sdk 1.250.1 → 1.251.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 (96) hide show
  1. package/lib/cjs/sdk.d.ts +9 -0
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/sdk.js +3 -0
  4. package/lib/cjs/sdk.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/account_switcher.test.d.ts.map +1 -1
  6. package/lib/cjs/tests/api_tests/account_switcher.test.js +1700 -306
  7. package/lib/cjs/tests/api_tests/account_switcher.test.js.map +1 -1
  8. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -1
  9. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js +28 -15
  10. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
  11. package/lib/cjs/tests/api_tests/enduser_login.test.d.ts +6 -0
  12. package/lib/cjs/tests/api_tests/enduser_login.test.d.ts.map +1 -0
  13. package/lib/cjs/tests/api_tests/enduser_login.test.js +315 -0
  14. package/lib/cjs/tests/api_tests/enduser_login.test.js.map +1 -0
  15. package/lib/cjs/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
  16. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js +556 -105
  17. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js.map +1 -1
  18. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.d.ts +7 -0
  19. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.d.ts.map +1 -0
  20. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.js +436 -0
  21. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.js.map +1 -0
  22. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
  23. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
  24. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js +370 -0
  25. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
  26. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
  27. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
  28. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +373 -0
  29. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
  30. package/lib/cjs/tests/setup.d.ts.map +1 -1
  31. package/lib/cjs/tests/setup.js +47 -32
  32. package/lib/cjs/tests/setup.js.map +1 -1
  33. package/lib/cjs/tests/tests.d.ts.map +1 -1
  34. package/lib/cjs/tests/tests.js +190 -161
  35. package/lib/cjs/tests/tests.js.map +1 -1
  36. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.d.ts +3 -0
  37. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.d.ts.map +1 -0
  38. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.js +114 -0
  39. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.js.map +1 -0
  40. package/lib/esm/sdk.d.ts +9 -0
  41. package/lib/esm/sdk.d.ts.map +1 -1
  42. package/lib/esm/sdk.js +3 -0
  43. package/lib/esm/sdk.js.map +1 -1
  44. package/lib/esm/tests/api_tests/account_switcher.test.d.ts.map +1 -1
  45. package/lib/esm/tests/api_tests/account_switcher.test.js +1702 -305
  46. package/lib/esm/tests/api_tests/account_switcher.test.js.map +1 -1
  47. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -1
  48. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js +28 -15
  49. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
  50. package/lib/esm/tests/api_tests/enduser_login.test.d.ts +6 -0
  51. package/lib/esm/tests/api_tests/enduser_login.test.d.ts.map +1 -0
  52. package/lib/esm/tests/api_tests/enduser_login.test.js +308 -0
  53. package/lib/esm/tests/api_tests/enduser_login.test.js.map +1 -0
  54. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts +6 -0
  55. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts.map +1 -0
  56. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js +268 -0
  57. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js.map +1 -0
  58. package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
  59. package/lib/esm/tests/api_tests/medication_added_trigger.test.js +556 -105
  60. package/lib/esm/tests/api_tests/medication_added_trigger.test.js.map +1 -1
  61. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.d.ts +7 -0
  62. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.d.ts.map +1 -0
  63. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.js +432 -0
  64. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.js.map +1 -0
  65. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
  66. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
  67. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js +366 -0
  68. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
  69. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
  70. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
  71. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +369 -0
  72. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
  73. package/lib/esm/tests/setup.d.ts.map +1 -1
  74. package/lib/esm/tests/setup.js +47 -32
  75. package/lib/esm/tests/setup.js.map +1 -1
  76. package/lib/esm/tests/tests.d.ts.map +1 -1
  77. package/lib/esm/tests/tests.js +190 -161
  78. package/lib/esm/tests/tests.js.map +1 -1
  79. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.d.ts +3 -0
  80. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.d.ts.map +1 -0
  81. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.js +111 -0
  82. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.js.map +1 -0
  83. package/lib/tsconfig.tsbuildinfo +1 -1
  84. package/package.json +10 -10
  85. package/src/sdk.ts +12 -0
  86. package/src/tests/api_tests/account_switcher.test.ts +1283 -0
  87. package/src/tests/api_tests/enduser_cross_access_isolation.test.ts +26 -0
  88. package/src/tests/api_tests/enduser_login.test.ts +215 -0
  89. package/src/tests/api_tests/medication_added_trigger.test.ts +345 -4
  90. package/src/tests/api_tests/outbound_chat_sent_trigger.test.ts +339 -0
  91. package/src/tests/api_tests/push_forms_to_portal_group_completion.test.ts +198 -0
  92. package/src/tests/api_tests/set_fields_order_templates.test.ts +258 -0
  93. package/src/tests/setup.ts +8 -1
  94. package/src/tests/tests.ts +23 -6
  95. package/src/tests/unit_tests/conditional_logic_medication.test.ts +133 -0
  96. package/test_generated.pdf +0 -0
@@ -1,4 +1,15 @@
1
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
+ };
2
13
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
14
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
15
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -35,388 +46,1771 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
35
46
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
47
  }
37
48
  };
38
- var __importDefault = (this && this.__importDefault) || function (mod) {
39
- return (mod && mod.__esModule) ? mod : { "default": mod };
49
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
50
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
51
+ if (ar || !(i in from)) {
52
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
53
+ ar[i] = from[i];
54
+ }
55
+ }
56
+ return to.concat(ar || Array.prototype.slice.call(from));
40
57
  };
41
58
  Object.defineProperty(exports, "__esModule", { value: true });
42
59
  exports.account_switcher_tests = void 0;
43
60
  require('source-map-support').install();
44
- var crypto_1 = __importDefault(require("crypto"));
45
61
  var sdk_1 = require("../../sdk");
46
62
  var testing_1 = require("@tellescope/testing");
47
- var utilities_1 = require("@tellescope/utilities");
48
63
  var setup_1 = require("../setup");
49
64
  var host = process.env.API_URL || 'http://localhost:8080';
50
- var TEST_EMAIL = process.env.TEST_EMAIL;
51
- var TEST_PASSWORD = process.env.TEST_PASSWORD;
52
- var NON_ADMIN_EMAIL = process.env.NON_ADMIN_EMAIL;
53
- var NON_ADMIN_PASSWORD = process.env.NON_ADMIN_PASSWORD;
54
- // matches the API key used in tests.ts for the secondary org
55
- var SDK_OTHER_API_KEY = "ba745e25162bb95a795c5fa1af70df188d93c4d3aac9c48b34a5c8c9dd7b80f7";
65
+ var RAND = function () { return Math.random().toString(36).slice(2, 10); };
66
+ var decode_jwt = function (token) {
67
+ try {
68
+ var part = token.split('.')[1];
69
+ var json = Buffer.from(part.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
70
+ return JSON.parse(json);
71
+ }
72
+ catch (_a) {
73
+ return null;
74
+ }
75
+ };
76
+ var passOnAnyResult = { shouldError: false, onResult: function () { return true; } };
56
77
  var account_switcher_tests = function (_a) {
57
78
  var sdk = _a.sdk, sdkNonAdmin = _a.sdkNonAdmin;
58
79
  return __awaiter(void 0, void 0, void 0, function () {
59
- var sdkOther, adminBaseEmail, _b, adminLocal, adminDomain, naBaseEmail, _c, naLocal, naDomain, suffix, emailPlusA, emailPlusB, emailPlusLocked, emailPlusUppercase, emailPlusInjection, emailReadOnly, emailOtherBase, emailPrefixSwitcher, emailNonAdminPlus, emailUnverifTarget, emailCrossOrg, userPlusA, userPlusB, userPlusLocked, userPlusUppercase, userPlusInjection, userReadOnly, userOtherBase, userPrefixSwitcher, userNonAdminPlus, userCrossOrg, userUnverifTarget, restrictiveRole, linkedFromAdmin, linkedIds, sample, preSwitchTokenSession_1, switchResult, switchedSession_1, meAfterSwitch, newLinkedFromSwitched, newLinkedIds, xorgSwitch, xorgSession_1, xorgMe, downgradeSwitch, downgradedSession_1, noAuthSession_1, allLogs, logsArr, switchLog, beforeUnverif, afterUnverif, rateLimitTripped, lastError, i, e_1, msg, cleanup;
60
- var _d, _e, _f, _g, _h, _j, _k;
61
- return __generator(this, function (_l) {
62
- switch (_l.label) {
80
+ var adminId, nonAdminId, adminEmail, nonAdminEmail, nonAdminBusinessId, adminBusinessId, NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD, get_user, __lac_resetCounter, sanitize_marker_tags, set_linkedAccountAccess, clear_linkedAccountAccess, cleanup_marker_tags, ensureOrgToggleEnabled, emailRejectMatcher, seedEmail, createdSeedUserId, _b, adminAfterRequest, pendingFromNonAdmin, fakeEntry, validatorRejectMatcher, mutations, _loop_1, _i, mutations_1, _c, label, mutated, adminWithPending, seededPending, unauthedSdk, is401Rejection, unverifiedEmail, unverifiedUserId, created, _d, _e, adminPre, preLen, adminBeforeAccept, pendingEntry, adminWithReq, pendingForNonAdmin, nonAdminAfterReq, pendingFromAdmin, i, _f, adminForF, pendingForF, _g, adminPendingState, pendingNA, switchedToken, switchedUser, decoded, switchedSdk, originalAdminFname, g6EnduserId, ce, e_1, _h, state, newPending, oSeedState, oPending, oBefore, oBeforeLen, i4State, i4Pending, i4Accepted, i4Entry, kSeedState, kPending, kSwitchedToken, kSwitchedUser, kSwitchedSdk, kBackToken, kBackDecoded, kBackSdk, userCEmail, userCRecord, userCId, userCToken, sdkC, userCState, pendingForC, aAsBResp, aAsBSdk, aAsBDecoded, chainedToken, chainedDecoded, chainedSdk, _j, lSeedState, lPending, lSwitchResp, lSwitchedSdk, lBackResp, lBackSdk, enduserEmail, enduserRec, enduserAuthToken, sdkAsEnduser, isEnduserRejection, _k, i, _l, _m;
81
+ var _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
82
+ return __generator(this, function (_5) {
83
+ switch (_5.label) {
63
84
  case 0:
64
85
  (0, testing_1.log_header)("Account Switcher Tests");
65
- sdkOther = new sdk_1.Session({ host: host, apiKey: SDK_OTHER_API_KEY });
66
- adminBaseEmail = (0, utilities_1.getBaseEmail)(sdk.userInfo.email);
67
- _b = adminBaseEmail.split('@'), adminLocal = _b[0], adminDomain = _b[1];
68
- naBaseEmail = (0, utilities_1.getBaseEmail)(sdkNonAdmin.userInfo.email);
69
- _c = naBaseEmail.split('@'), naLocal = _c[0], naDomain = _c[1];
70
- suffix = crypto_1.default.randomBytes(4).toString('hex');
71
- emailPlusA = "".concat(adminLocal, "+acctsw_a_").concat(suffix, "@").concat(adminDomain);
72
- emailPlusB = "".concat(adminLocal, "+acctsw_b_").concat(suffix, "@").concat(adminDomain);
73
- emailPlusLocked = "".concat(adminLocal, "+acctsw_locked_").concat(suffix, "@").concat(adminDomain);
74
- emailPlusUppercase = "".concat(adminLocal.toUpperCase(), "+AcctSwCaps_").concat(suffix, "@").concat(adminDomain);
75
- emailPlusInjection = "".concat(adminLocal, "+acctsw.x.y_").concat(suffix, "@").concat(adminDomain);
76
- emailReadOnly = "".concat(adminLocal, "+acctsw_ro_").concat(suffix, "@").concat(adminDomain);
77
- emailOtherBase = "acctsw_unrelated_".concat(suffix, "@").concat(adminDomain);
78
- emailPrefixSwitcher = "prefix".concat(adminLocal, "+acctsw_pfx_").concat(suffix, "@").concat(adminDomain);
79
- emailNonAdminPlus = "".concat(naLocal, "+acctsw_na_").concat(suffix, "@").concat(naDomain);
80
- emailUnverifTarget = "".concat(adminLocal, "+acctsw_unvrf_").concat(suffix, "@").concat(adminDomain);
81
- emailCrossOrg = "".concat(adminLocal, "+acctsw_xorg_").concat(suffix, "@").concat(adminDomain);
82
- _l.label = 1;
86
+ adminId = sdk.userInfo.id;
87
+ nonAdminId = sdkNonAdmin.userInfo.id;
88
+ adminEmail = sdk.userInfo.email;
89
+ nonAdminEmail = sdkNonAdmin.userInfo.email;
90
+ nonAdminBusinessId = sdkNonAdmin.userInfo.businessId;
91
+ adminBusinessId = sdk.userInfo.businessId;
92
+ NON_ADMIN_EMAIL = process.env.NON_ADMIN_EMAIL;
93
+ NON_ADMIN_PASSWORD = process.env.NON_ADMIN_PASSWORD;
94
+ if (!(NON_ADMIN_EMAIL && NON_ADMIN_PASSWORD)) {
95
+ throw new Error("NON_ADMIN_EMAIL and NON_ADMIN_PASSWORD must be set to run account_switcher_tests");
96
+ }
97
+ get_user = function (s, id) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
98
+ return [2 /*return*/, s.api.users.getOne(id)
99
+ // The standard CRUD update path rate-limits identical updates to the same record at
100
+ // 3/30s (routing.ts:2631). Cleanup PATCHes between sections all carry the same
101
+ // { linkedAccountAccess: [] } payload and would trip it. Workaround: include a rotating
102
+ // marker tag so every write has a unique JSON payload. Markers are stripped at end of run.
103
+ ];
104
+ }); }); };
105
+ __lac_resetCounter = 0;
106
+ sanitize_marker_tags = function (tags) {
107
+ if (tags === void 0) { tags = []; }
108
+ return tags.filter(function (t) { return typeof t === 'string' && !t.startsWith('__lac_'); });
109
+ };
110
+ set_linkedAccountAccess = function (s, ownerId, entries) { return __awaiter(void 0, void 0, void 0, function () {
111
+ var me, newTags;
112
+ var _a;
113
+ return __generator(this, function (_b) {
114
+ switch (_b.label) {
115
+ case 0:
116
+ __lac_resetCounter++;
117
+ return [4 /*yield*/, s.api.users.getOne(ownerId)];
118
+ case 1:
119
+ me = _b.sent();
120
+ newTags = __spreadArray(__spreadArray([], sanitize_marker_tags((_a = me === null || me === void 0 ? void 0 : me.tags) !== null && _a !== void 0 ? _a : []), true), ["__lac_".concat(__lac_resetCounter)], false);
121
+ return [4 /*yield*/, s.api.users.updateOne(ownerId, {
122
+ linkedAccountAccess: entries,
123
+ tags: newTags,
124
+ }, { replaceObjectFields: true })];
125
+ case 2:
126
+ _b.sent();
127
+ return [2 /*return*/];
128
+ }
129
+ });
130
+ }); };
131
+ clear_linkedAccountAccess = function (s, ownerId) { return __awaiter(void 0, void 0, void 0, function () {
132
+ var me;
133
+ var _a;
134
+ return __generator(this, function (_b) {
135
+ switch (_b.label) {
136
+ case 0: return [4 /*yield*/, s.api.users.getOne(ownerId)];
137
+ case 1:
138
+ me = _b.sent();
139
+ if (!(((_a = me === null || me === void 0 ? void 0 : me.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length))
140
+ return [2 /*return*/];
141
+ return [4 /*yield*/, set_linkedAccountAccess(s, ownerId, [])];
142
+ case 2:
143
+ _b.sent();
144
+ return [2 /*return*/];
145
+ }
146
+ });
147
+ }); };
148
+ cleanup_marker_tags = function (s, ownerId) { return __awaiter(void 0, void 0, void 0, function () {
149
+ var me, cleaned, _a;
150
+ var _b, _c;
151
+ return __generator(this, function (_d) {
152
+ switch (_d.label) {
153
+ case 0:
154
+ _d.trys.push([0, 4, , 5]);
155
+ return [4 /*yield*/, s.api.users.getOne(ownerId)];
156
+ case 1:
157
+ me = _d.sent();
158
+ cleaned = sanitize_marker_tags((_b = me === null || me === void 0 ? void 0 : me.tags) !== null && _b !== void 0 ? _b : []);
159
+ if (!(((_c = me === null || me === void 0 ? void 0 : me.tags) !== null && _c !== void 0 ? _c : []).length !== cleaned.length)) return [3 /*break*/, 3];
160
+ return [4 /*yield*/, s.api.users.updateOne(ownerId, { tags: cleaned }, { replaceObjectFields: true })];
161
+ case 2:
162
+ _d.sent();
163
+ _d.label = 3;
164
+ case 3: return [3 /*break*/, 5];
165
+ case 4:
166
+ _a = _d.sent();
167
+ return [3 /*break*/, 5];
168
+ case 5: return [2 /*return*/];
169
+ }
170
+ });
171
+ }); };
172
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
83
173
  case 1:
84
- _l.trys.push([1, , 56, 58]);
85
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailPlusA, fname: 'A', verifiedEmail: true })];
174
+ _5.sent();
175
+ return [4 /*yield*/, clear_linkedAccountAccess(sdkNonAdmin, nonAdminId)
176
+ // The feature is opt-in per org. Enable it on the test business so the rest of the suite
177
+ // exercises the feature (the O section below explicitly toggles it off to verify gating).
178
+ ];
86
179
  case 2:
87
- // ------------------------------ Fixtures ------------------------------
88
- userPlusA = _l.sent();
89
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailPlusB, fname: 'B', verifiedEmail: true })];
180
+ _5.sent();
181
+ ensureOrgToggleEnabled = function () { return __awaiter(void 0, void 0, void 0, function () {
182
+ var org;
183
+ return __generator(this, function (_a) {
184
+ switch (_a.label) {
185
+ case 0: return [4 /*yield*/, sdk.api.organizations.getOne(adminBusinessId)];
186
+ case 1:
187
+ org = _a.sent();
188
+ if (!((org === null || org === void 0 ? void 0 : org.accountSwitchingEnabled) !== true)) return [3 /*break*/, 4];
189
+ return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { accountSwitchingEnabled: true })];
190
+ case 2:
191
+ _a.sent();
192
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
193
+ case 3:
194
+ _a.sent();
195
+ _a.label = 4;
196
+ case 4: return [2 /*return*/];
197
+ }
198
+ });
199
+ }); };
200
+ return [4 /*yield*/, ensureOrgToggleEnabled()
201
+ // ============================================================
202
+ // A. Email immutability
203
+ // ============================================================
204
+ ];
90
205
  case 3:
91
- userPlusB = _l.sent();
92
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailPlusUppercase, verifiedEmail: true })];
206
+ _5.sent();
207
+ // ============================================================
208
+ // A. Email immutability
209
+ // ============================================================
210
+ (0, testing_1.log_header)("A. Email immutability");
211
+ emailRejectMatcher = function (e) { return (e.statusCode === 400
212
+ || /(updates|disabled|readonly|cannot)/i.test(e.message || '')); };
213
+ return [4 /*yield*/, (0, testing_1.async_test)('A1. Self PATCH of own email rejected', function () { return sdkNonAdmin.api.users.updateOne(nonAdminId, { email: "evil-".concat(RAND(), "@tellescope.com") }); }, { shouldError: true, onError: emailRejectMatcher })];
93
214
  case 4:
94
- userPlusUppercase = _l.sent();
95
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailPlusInjection, verifiedEmail: true })];
215
+ _5.sent();
216
+ return [4 /*yield*/, (0, testing_1.async_test)('A2. Admin PATCH of another user email rejected', function () { return sdk.api.users.updateOne(nonAdminId, { email: "admin-rename-".concat(RAND(), "@tellescope.com") }); }, { shouldError: true, onError: emailRejectMatcher })];
96
217
  case 5:
97
- userPlusInjection = _l.sent();
98
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailOtherBase, verifiedEmail: true })];
218
+ _5.sent();
219
+ return [4 /*yield*/, (0, testing_1.async_test)('A3. Admin PATCH of own email rejected', function () { return sdk.api.users.updateOne(adminId, { email: "admin-self-".concat(RAND(), "@tellescope.com") }); }, { shouldError: true, onError: emailRejectMatcher })];
99
220
  case 6:
100
- userOtherBase = _l.sent();
101
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailPrefixSwitcher, verifiedEmail: true })];
102
- case 7:
103
- userPrefixSwitcher = _l.sent();
104
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailNonAdminPlus, verifiedEmail: true })
105
- // Created with verifiedEmail: false so we can observe propagation from a verified source
221
+ _5.sent();
222
+ return [4 /*yield*/, (0, testing_1.async_test)('A4. verifiedEmail/email unchanged after rejected updates', function () { return get_user(sdk, nonAdminId); }, { shouldError: false, onResult: function (u) { return u.verifiedEmail === true && u.email === nonAdminEmail; } })
223
+ // A5: email IS settable on user creation
106
224
  ];
225
+ case 7:
226
+ _5.sent();
227
+ seedEmail = "seed-".concat(RAND(), "@tellescope.com");
228
+ createdSeedUserId = '';
229
+ return [4 /*yield*/, (0, testing_1.async_test)('A5. Admin can set email on user creation', function () { return sdk.api.users.createOne({ email: seedEmail, fname: 'Seed', lname: 'User' }); }, { shouldError: false, onResult: function (u) { createdSeedUserId = u.id; return u.email === seedEmail; } })];
107
230
  case 8:
108
- userNonAdminPlus = _l.sent();
109
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailUnverifTarget, verifiedEmail: false })
110
- // Locked sibling (lockedOutUntil = 0 => indefinite lockout per types-models)
111
- ];
231
+ _5.sent();
232
+ if (!createdSeedUserId) return [3 /*break*/, 12];
233
+ _5.label = 9;
112
234
  case 9:
113
- // Created with verifiedEmail: false so we can observe propagation from a verified source
114
- userUnverifTarget = _l.sent();
115
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailPlusLocked, verifiedEmail: true })];
235
+ _5.trys.push([9, 11, , 12]);
236
+ return [4 /*yield*/, sdk.api.users.deleteOne(createdSeedUserId)];
116
237
  case 10:
117
- // Locked sibling (lockedOutUntil = 0 => indefinite lockout per types-models)
118
- userPlusLocked = _l.sent();
119
- return [4 /*yield*/, sdk.api.users.updateOne(userPlusLocked.id, { lockedOutUntil: 0 })
120
- // Restrictive role + user assigned to that role (used to verify no privilege escalation)
121
- ];
238
+ _5.sent();
239
+ return [3 /*break*/, 12];
122
240
  case 11:
123
- _l.sent();
124
- return [4 /*yield*/, sdk.api.role_based_access_permissions.createOne({
125
- role: "acctsw_restrictive_".concat(suffix),
126
- permissions: {
127
- users: { read: null, create: null, update: null, delete: null },
128
- endusers: { read: null, create: null, update: null, delete: null },
129
- },
130
- })];
241
+ _b = _5.sent();
242
+ return [3 /*break*/, 12];
131
243
  case 12:
132
- // Restrictive role + user assigned to that role (used to verify no privilege escalation)
133
- restrictiveRole = _l.sent();
134
- return [4 /*yield*/, sdk.api.users.createOne({ email: emailReadOnly, verifiedEmail: true })];
244
+ // ============================================================
245
+ // B. linkedAccountAccess PATCH-self validator
246
+ // ============================================================
247
+ (0, testing_1.log_header)("B. linkedAccountAccess PATCH-self validator");
248
+ // Seed: nonAdmin requests access to admin
249
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
135
250
  case 13:
136
- userReadOnly = _l.sent();
137
- return [4 /*yield*/, sdk.api.users.updateOne(userReadOnly.id, { roles: [restrictiveRole.role] }, { replaceObjectFields: true })
138
- // Cross-org sibling (different org via API key)
139
- ];
251
+ // Seed: nonAdmin requests access to admin
252
+ _5.sent();
253
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
140
254
  case 14:
141
- _l.sent();
142
- return [4 /*yield*/, sdkOther.api.users.createOne({ email: emailCrossOrg, verifiedEmail: true })];
255
+ _5.sent();
256
+ return [4 /*yield*/, get_user(sdk, adminId)];
143
257
  case 15:
144
- // Cross-org sibling (different org via API key)
145
- userCrossOrg = _l.sent();
146
- return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
147
- // ====================== A. get_linked_accounts ======================
148
- ]; // small settle for created records
258
+ adminAfterRequest = _5.sent();
259
+ pendingFromNonAdmin = ((_o = adminAfterRequest.linkedAccountAccess) !== null && _o !== void 0 ? _o : []).find(function (e) { return e.userId === nonAdminId; });
260
+ (0, testing_1.assert)(!!pendingFromNonAdmin && pendingFromNonAdmin.status === 'pending', 'no pending entry seeded for B tests', 'B-seed pending entry present');
261
+ // B14: PATCH self with array unchanged is a no-op
262
+ return [4 /*yield*/, (0, testing_1.async_test)('B14. PATCH self with linkedAccountAccess unchanged succeeds', function () { return sdk.api.users.updateOne(adminId, { linkedAccountAccess: adminAfterRequest.linkedAccountAccess }, { replaceObjectFields: true }); }, passOnAnyResult)
263
+ // B5/B6: cannot add entries
264
+ ];
149
265
  case 16:
150
- _l.sent(); // small settle for created records
151
- return [4 /*yield*/, sdk.api.users.get_linked_accounts()];
266
+ // B14: PATCH self with array unchanged is a no-op
267
+ _5.sent();
268
+ fakeEntry = {
269
+ userId: '000000000000000000000099',
270
+ email: 'fake@tellescope.com',
271
+ fname: 'Fake', lname: 'User', orgName: 'Fake Org',
272
+ status: 'accepted',
273
+ createdAt: new Date(),
274
+ requestExpiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
275
+ };
276
+ validatorRejectMatcher = function (e) { return (e.statusCode === 400
277
+ || e.statusCode === 404
278
+ // "No updates provided" fires when unknown fields (e.g. the removed accountAccessGrantedTo) get
279
+ // stripped by the schema and nothing valid remains — that's a legitimate rejection mechanism.
280
+ || /(linkedAccountAccess|owner|add entries|mutate|immutable|status can only|legacy|accountAccessGrantedTo|replaced|Could not find|No updates provided)/i.test(e.message || '')); };
281
+ return [4 /*yield*/, (0, testing_1.async_test)('B5. Cannot add accepted entry via PATCH', function () { var _a; return sdk.api.users.updateOne(adminId, { linkedAccountAccess: __spreadArray(__spreadArray([], ((_a = adminAfterRequest.linkedAccountAccess) !== null && _a !== void 0 ? _a : []), true), [fakeEntry], false) }, { replaceObjectFields: true }); }, { shouldError: true, onError: validatorRejectMatcher })];
152
282
  case 17:
153
- linkedFromAdmin = _l.sent();
154
- linkedIds = (linkedFromAdmin.linkedAccounts || []).map(function (a) { return a.id; });
155
- (0, testing_1.assert)(linkedIds.includes(userPlusA.id), 'userPlusA missing from linked accounts', 'get_linked_accounts: includes plus sibling A');
156
- (0, testing_1.assert)(linkedIds.includes(userPlusB.id), 'userPlusB missing from linked accounts', 'get_linked_accounts: includes plus sibling B');
157
- (0, testing_1.assert)(linkedIds.includes(userPlusUppercase.id), 'uppercase plus variant missing from linked accounts', 'get_linked_accounts: case-insensitive base match');
158
- (0, testing_1.assert)(linkedIds.includes(userPlusInjection.id), 'regex-special plus variant missing from linked accounts', 'get_linked_accounts: regex special chars do not break matching');
159
- (0, testing_1.assert)(linkedIds.includes(userCrossOrg.id), 'cross-org sibling missing from linked accounts', 'get_linked_accounts: includes cross-org plus sibling');
160
- (0, testing_1.assert)(!linkedIds.includes(sdk.userInfo.id), 'caller present in linked accounts', 'get_linked_accounts: excludes caller');
161
- (0, testing_1.assert)(!linkedIds.includes(userOtherBase.id), 'unrelated-base user present in linked accounts', 'get_linked_accounts: excludes unrelated base email');
162
- (0, testing_1.assert)(!linkedIds.includes(userPrefixSwitcher.id), 'prefix-extended user present in linked accounts (anchored regex broken?)', 'get_linked_accounts: anchored regex excludes prefix-extended local part');
163
- (0, testing_1.assert)(!linkedIds.includes(userPlusLocked.id), 'locked user present in linked accounts', 'get_linked_accounts: excludes locked target');
164
- sample = (linkedFromAdmin.linkedAccounts || []).find(function (a) { return a.id === userPlusB.id; });
165
- (0, testing_1.assert)(!!sample
166
- && typeof sample.email === 'string'
167
- && typeof sample.orgName === 'string'
168
- && typeof sample.requiresMFA === 'boolean', 'linked account entry missing required fields', 'get_linked_accounts: entries have id, email, orgName, requiresMFA');
169
- // noAccessPermissions: non-admin can call without 403
170
- return [4 /*yield*/, (0, testing_1.async_test)('get_linked_accounts callable by non-admin (noAccessPermissions)', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { onResult: function (r) { return Array.isArray(r.linkedAccounts); } })
171
- // ====================== B. switch_account ======================
172
- // 1. Same-org happy path: sdk (admin) → userPlusB
173
- // Capture the source token in a separate Session BEFORE the switch so we can verify
174
- // the original JWT is invalidated (Step 13) without losing access via `sdk` itself.
283
+ _5.sent();
284
+ return [4 /*yield*/, (0, testing_1.async_test)('B6. Cannot add pending entry via PATCH', function () { var _a; return sdk.api.users.updateOne(adminId, { linkedAccountAccess: __spreadArray(__spreadArray([], ((_a = adminAfterRequest.linkedAccountAccess) !== null && _a !== void 0 ? _a : []), true), [__assign(__assign({}, fakeEntry), { status: 'pending' })], false) }, { replaceObjectFields: true }); }, { shouldError: true, onError: validatorRejectMatcher })
285
+ // B7-B13: cannot mutate immutable fields
175
286
  ];
176
287
  case 18:
177
- // noAccessPermissions: non-admin can call without 403
178
- _l.sent();
179
- preSwitchTokenSession_1 = new sdk_1.Session({ host: host, authToken: sdk.authToken });
180
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: pre-switch source token is valid', function () { return preSwitchTokenSession_1.test_authenticated(); }, { expectedResult: 'Authenticated!' })];
288
+ _5.sent();
289
+ mutations = [
290
+ ['userId', __assign(__assign({}, pendingFromNonAdmin), { userId: '000000000000000000000099' })],
291
+ ['email', __assign(__assign({}, pendingFromNonAdmin), { email: 'mutated@tellescope.com' })],
292
+ ['fname', __assign(__assign({}, pendingFromNonAdmin), { fname: 'MutatedF' })],
293
+ ['lname', __assign(__assign({}, pendingFromNonAdmin), { lname: 'MutatedL' })],
294
+ ['orgName', __assign(__assign({}, pendingFromNonAdmin), { orgName: 'MutatedOrg' })],
295
+ ['createdAt', __assign(__assign({}, pendingFromNonAdmin), { createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24) })],
296
+ ['requestExpiresAt', __assign(__assign({}, pendingFromNonAdmin), { requestExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365) })],
297
+ ];
298
+ _loop_1 = function (label, mutated) {
299
+ return __generator(this, function (_6) {
300
+ switch (_6.label) {
301
+ case 0: return [4 /*yield*/, (0, testing_1.async_test)("B7-13. Cannot mutate linkedAccountAccess entry ".concat(label), function () { return sdk.api.users.updateOne(adminId, { linkedAccountAccess: [mutated] }, { replaceObjectFields: true }); }, { shouldError: true, onError: validatorRejectMatcher })];
302
+ case 1:
303
+ _6.sent();
304
+ return [2 /*return*/];
305
+ }
306
+ });
307
+ };
308
+ _i = 0, mutations_1 = mutations;
309
+ _5.label = 19;
181
310
  case 19:
182
- _l.sent();
183
- return [4 /*yield*/, sdk.api.users.switch_account({ targetUserId: userPlusB.id })];
311
+ if (!(_i < mutations_1.length)) return [3 /*break*/, 22];
312
+ _c = mutations_1[_i], label = _c[0], mutated = _c[1];
313
+ return [5 /*yield**/, _loop_1(label, mutated)];
184
314
  case 20:
185
- switchResult = _l.sent();
186
- (0, testing_1.assert)(!!switchResult.authToken, 'no authToken returned from switch_account', 'switch_account: returns authToken');
187
- (0, testing_1.assert)(((_d = switchResult.user) === null || _d === void 0 ? void 0 : _d.id) === userPlusB.id, 'switch_account returned wrong user id', 'switch_account: returns target user record');
188
- (0, testing_1.assert)((((_e = switchResult.user) === null || _e === void 0 ? void 0 : _e.email) || '').toLowerCase() === emailPlusB.toLowerCase(), 'switch_account returned wrong email', 'switch_account: returns target email');
189
- // User model stores password hash in `hashedPass` (not `hashedPassword` — that's Enduser).
190
- // Both are asserted as belt-and-suspenders in case field names drift.
191
- (0, testing_1.assert)(!((_f = switchResult.user) === null || _f === void 0 ? void 0 : _f.hashedPass), "switch_account response leaked hashedPass: ".concat(JSON.stringify(switchResult.user.hashedPass)), 'switch_account: response redacts hashedPass');
192
- (0, testing_1.assert)(!((_g = switchResult.user) === null || _g === void 0 ? void 0 : _g.hashedPassword), "switch_account response leaked hashedPassword: ".concat(JSON.stringify(switchResult.user.hashedPassword)), 'switch_account: response has no hashedPassword (defense-in-depth)');
193
- switchedSession_1 = new sdk_1.Session({ host: host, authToken: switchResult.authToken });
194
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: new token authenticates', function () { return switchedSession_1.test_authenticated(); }, { expectedResult: 'Authenticated!' })];
315
+ _5.sent();
316
+ _5.label = 21;
195
317
  case 21:
196
- _l.sent();
197
- return [4 /*yield*/, switchedSession_1.api.users.getOne(userPlusB.id)];
198
- case 22:
199
- meAfterSwitch = _l.sent();
200
- (0, testing_1.assert)((meAfterSwitch.email || '').toLowerCase() === emailPlusB.toLowerCase(), 'switched session does not resolve to target user', 'switch_account: new session authenticates as target user');
201
- return [4 /*yield*/, switchedSession_1.api.users.get_linked_accounts()];
318
+ _i++;
319
+ return [3 /*break*/, 19];
320
+ case 22:
321
+ // B3: pending -> accepted allowed
322
+ return [4 /*yield*/, (0, testing_1.async_test)('B3. Owner can flip pending -> accepted', function () { return set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingFromNonAdmin), { status: 'accepted' })]); }, passOnAnyResult)
323
+ // B4: accepted -> pending rejected
324
+ ];
202
325
  case 23:
203
- newLinkedFromSwitched = _l.sent();
204
- newLinkedIds = (newLinkedFromSwitched.linkedAccounts || []).map(function (a) { return a.id; });
205
- (0, testing_1.assert)(!newLinkedIds.includes(userPlusB.id), 'post-switch linked accounts include the new caller', 'post-switch get_linked_accounts: excludes new caller');
206
- (0, testing_1.assert)(newLinkedIds.includes(userPlusA.id), 'post-switch linked accounts missing other sibling', 'post-switch get_linked_accounts: includes other plus siblings');
207
- (0, testing_1.assert)(newLinkedIds.includes(sdk.userInfo.id), 'post-switch linked accounts missing original caller', 'post-switch get_linked_accounts: includes original caller');
208
- // JWT invalidation (Step 13): the source token captured pre-switch must now be rejected.
209
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: source JWT invalidated after switch', function () { return preSwitchTokenSession_1.test_authenticated(); }, { shouldError: true, onError: function (e) { var _a; return /unauth|expired|invalid/i.test((e === null || e === void 0 ? void 0 : e.message) || ((_a = e === null || e === void 0 ? void 0 : e.toString) === null || _a === void 0 ? void 0 : _a.call(e)) || String(e)); } })
210
- // Re-authenticate sdk so subsequent tests in this suite work (its prior token is now expired).
326
+ // B3: pending -> accepted allowed
327
+ _5.sent();
328
+ // B4: accepted -> pending rejected
329
+ return [4 /*yield*/, (0, testing_1.async_test)('B4. Cannot flip accepted -> pending', function () { return set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingFromNonAdmin), { status: 'pending' })]); }, { shouldError: true, onError: validatorRejectMatcher })
330
+ // B11/B12: non-owner PATCH of another user's linkedAccountAccess. Use a NON-empty
331
+ // payload so the rate-limit key is unique from later admin-owned `[]` clears.
211
332
  ];
212
333
  case 24:
213
- // JWT invalidation (Step 13): the source token captured pre-switch must now be rejected.
214
- _l.sent();
215
- // Re-authenticate sdk so subsequent tests in this suite work (its prior token is now expired).
216
- return [4 /*yield*/, sdk.authenticate(TEST_EMAIL, TEST_PASSWORD)
217
- // 2. Cross-org happy path: sdk (admin) userCrossOrg (in sdkOther's org)
334
+ // B4: accepted -> pending rejected
335
+ _5.sent();
336
+ // B11/B12: non-owner PATCH of another user's linkedAccountAccess. Use a NON-empty
337
+ // payload so the rate-limit key is unique from later admin-owned `[]` clears.
338
+ return [4 /*yield*/, (0, testing_1.async_test)('B11/B12. Non-owner PATCH of another user linkedAccountAccess rejected', function () { return sdkNonAdmin.api.users.updateOne(adminId, { linkedAccountAccess: [__assign(__assign({}, pendingFromNonAdmin), { status: 'accepted' })] }, { replaceObjectFields: true }); }, { shouldError: true, onError: validatorRejectMatcher })
339
+ // B15: legacy field no longer accepted
218
340
  ];
219
341
  case 25:
220
- // Re-authenticate sdk so subsequent tests in this suite work (its prior token is now expired).
221
- _l.sent();
222
- return [4 /*yield*/, sdk.api.users.switch_account({ targetUserId: userCrossOrg.id })];
342
+ // B11/B12: non-owner PATCH of another user's linkedAccountAccess. Use a NON-empty
343
+ // payload so the rate-limit key is unique from later admin-owned `[]` clears.
344
+ _5.sent();
345
+ // B15: legacy field no longer accepted
346
+ return [4 /*yield*/, (0, testing_1.async_test)('B15. Legacy accountAccessGrantedTo PATCH is rejected', function () { return sdk.api.users.updateOne(adminId, { accountAccessGrantedTo: [nonAdminId] }); }, { shouldError: true, onError: validatorRejectMatcher })
347
+ // B1: Owner can remove a pending entry (re-seed first)
348
+ ];
223
349
  case 26:
224
- xorgSwitch = _l.sent();
225
- (0, testing_1.assert)(((_h = xorgSwitch.user) === null || _h === void 0 ? void 0 : _h.id) === userCrossOrg.id, 'cross-org switch returned wrong user', 'switch_account: cross-org switch returns target');
226
- xorgSession_1 = new sdk_1.Session({ host: host, authToken: xorgSwitch.authToken });
227
- return [4 /*yield*/, xorgSession_1.api.users.getOne(userCrossOrg.id)];
350
+ // B15: legacy field no longer accepted
351
+ _5.sent();
352
+ // B1: Owner can remove a pending entry (re-seed first)
353
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
228
354
  case 27:
229
- xorgMe = _l.sent();
230
- (0, testing_1.assert)((xorgMe.email || '').toLowerCase() === emailCrossOrg.toLowerCase(), 'cross-org session not scoped correctly', 'switch_account: cross-org session resolves to target user');
231
- // Multi-tenant isolation: cross-org session must NOT see source-org users
232
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: cross-org session cannot see source-org user (tenant isolation)', function () { return xorgSession_1.api.users.getOne(sdk.userInfo.id); }, testing_1.handleAnyError)
233
- // Re-auth sdk: previous switch invalidated its token (Step 13)
234
- ];
355
+ // B1: Owner can remove a pending entry (re-seed first)
356
+ _5.sent();
357
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
235
358
  case 28:
236
- // Multi-tenant isolation: cross-org session must NOT see source-org users
237
- _l.sent();
238
- // Re-auth sdk: previous switch invalidated its token (Step 13)
239
- return [4 /*yield*/, sdk.authenticate(TEST_EMAIL, TEST_PASSWORD)
240
- // 3. Permission scoping (no privilege escalation): sdk (admin) → userReadOnly,
241
- // then attempt an admin-only operation with the new token. Must fail.
242
- ];
359
+ _5.sent();
360
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
243
361
  case 29:
244
- // Re-auth sdk: previous switch invalidated its token (Step 13)
245
- _l.sent();
246
- return [4 /*yield*/, sdk.api.users.switch_account({ targetUserId: userReadOnly.id })];
247
- case 30:
248
- downgradeSwitch = _l.sent();
249
- downgradedSession_1 = new sdk_1.Session({ host: host, authToken: downgradeSwitch.authToken });
250
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: downgraded session cannot perform admin op (no privilege escalation)', function () { return downgradedSession_1.api.users.updateOne(userPlusA.id, { roles: ['Admin'] }, { replaceObjectFields: true }); }, testing_1.handleAnyError)
251
- // Re-auth sdk: previous switch invalidated its token (Step 13)
362
+ _5.sent();
363
+ return [4 /*yield*/, (0, testing_1.async_test)('B1. Owner can remove a pending entry', function () { return set_linkedAccountAccess(sdk, adminId, []); }, passOnAnyResult)
364
+ // B2: Owner can remove an accepted entry
252
365
  ];
366
+ case 30:
367
+ _5.sent();
368
+ // B2: Owner can remove an accepted entry
369
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
253
370
  case 31:
254
- _l.sent();
255
- // Re-auth sdk: previous switch invalidated its token (Step 13)
256
- return [4 /*yield*/, sdk.authenticate(TEST_EMAIL, TEST_PASSWORD)
257
- // 3b. Self-switch attempt MUST be rejected (Step 12): no-op that wastes rate-limit budget
258
- // and creates spurious audit logs.
259
- ];
371
+ // B2: Owner can remove an accepted entry
372
+ _5.sent();
373
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
260
374
  case 32:
261
- // Re-auth sdk: previous switch invalidated its token (Step 13)
262
- _l.sent();
263
- // 3b. Self-switch attempt MUST be rejected (Step 12): no-op that wastes rate-limit budget
264
- // and creates spurious audit logs.
265
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects self-switch', function () { return sdk.api.users.switch_account({ targetUserId: sdk.userInfo.id }); }, { shouldError: true, onError: function (e) { var _a; return /own account/i.test((e === null || e === void 0 ? void 0 : e.message) || ((_a = e === null || e === void 0 ? void 0 : e.toString) === null || _a === void 0 ? void 0 : _a.call(e)) || String(e)); } })
266
- // 4. Cross-base attack: sdk (admin) → userOtherBase (different base) MUST be rejected.
267
- ];
375
+ _5.sent();
376
+ return [4 /*yield*/, get_user(sdk, adminId)];
268
377
  case 33:
269
- // 3b. Self-switch attempt MUST be rejected (Step 12): no-op that wastes rate-limit budget
270
- // and creates spurious audit logs.
271
- _l.sent();
272
- // 4. Cross-base attack: sdk (admin) → userOtherBase (different base) MUST be rejected.
273
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects cross-base target', function () { return sdk.api.users.switch_account({ targetUserId: userOtherBase.id }); }, testing_1.handleAnyError)];
378
+ adminWithPending = _5.sent();
379
+ seededPending = ((_p = adminWithPending.linkedAccountAccess) !== null && _p !== void 0 ? _p : []).find(function (e) { return e.userId === nonAdminId; });
380
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, seededPending), { status: 'accepted' })])];
274
381
  case 34:
275
- // 4. Cross-base attack: sdk (admin) → userOtherBase (different base) MUST be rejected.
276
- _l.sent();
277
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: source session unaffected after rejected cross-base attempt', function () { return sdk.test_authenticated(); }, { expectedResult: 'Authenticated!' })
278
- // 5. Locked target: must reject
382
+ _5.sent();
383
+ return [4 /*yield*/, (0, testing_1.async_test)('B2. Owner can remove an accepted entry', function () { return set_linkedAccountAccess(sdk, adminId, []); }, passOnAnyResult)
384
+ // ============================================================
385
+ // C. request_linked_account_access
386
+ // ============================================================
279
387
  ];
280
388
  case 35:
281
- _l.sent();
282
- // 5. Locked target: must reject
283
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects locked target', function () { return sdk.api.users.switch_account({ targetUserId: userPlusLocked.id }); }, testing_1.handleAnyError)
284
- // 6. Nonexistent target: must reject without leaking existence
285
- ];
389
+ _5.sent();
390
+ // ============================================================
391
+ // C. request_linked_account_access
392
+ // ============================================================
393
+ (0, testing_1.log_header)("C. request_linked_account_access");
394
+ unauthedSdk = new sdk_1.Session({ host: host });
395
+ is401Rejection = function (e) { return ((e === null || e === void 0 ? void 0 : e.statusCode) === 401
396
+ || (typeof e === 'string' && /^unauthenticated$/i.test(e))
397
+ || /^unauthenticated$/i.test((e === null || e === void 0 ? void 0 : e.message) || '')); };
398
+ return [4 /*yield*/, (0, testing_1.async_test)('C1. Unauthenticated request returns 401', function () { return unauthedSdk.api.users.request_linked_account_access({ targetEmail: adminEmail }); }, { shouldError: true, onError: is401Rejection })];
286
399
  case 36:
287
- // 5. Locked target: must reject
288
- _l.sent();
289
- // 6. Nonexistent target: must reject without leaking existence
290
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects nonexistent target', function () { return sdk.api.users.switch_account({ targetUserId: '000000000000000000000000' }); }, testing_1.handleAnyError)
291
- // 7. Malformed targetUserId: validation error (does not reach handler / rate limit)
292
- ];
400
+ _5.sent();
401
+ return [4 /*yield*/, (0, testing_1.async_test)('C2. Non-existent email returns {} (no error)', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: "nobody-".concat(RAND(), "@tellescope.example") }); }, passOnAnyResult)];
293
402
  case 37:
294
- // 6. Nonexistent target: must reject without leaking existence
295
- _l.sent();
296
- // 7. Malformed targetUserId: validation error (does not reach handler / rate limit)
297
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects malformed targetUserId', function () { return sdk.api.users.switch_account({ targetUserId: 'not-an-objectid' }); }, testing_1.handleAnyError)
298
- // 8. noAccessPermissions on switch: non-admin can switch to its own plus sibling
403
+ _5.sent();
404
+ return [4 /*yield*/, (0, testing_1.async_test)('C2. No record written for non-existent email', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === 0; } })
405
+ // C3: unverified email -> treated as no-match. Verify via admin-created user.
299
406
  ];
300
407
  case 38:
301
- // 7. Malformed targetUserId: validation error (does not reach handler / rate limit)
302
- _l.sent();
303
- // 8. noAccessPermissions on switch: non-admin can switch to its own plus sibling
304
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: callable by non-admin (noAccessPermissions)', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: userNonAdminPlus.id }); }, { onResult: function (r) { var _a; return ((_a = r.user) === null || _a === void 0 ? void 0 : _a.id) === userNonAdminPlus.id && !!r.authToken; } })
305
- // Re-auth sdkNonAdmin: previous switch invalidated its token (Step 13)
306
- ];
408
+ _5.sent();
409
+ unverifiedEmail = "unverified-".concat(RAND(), "@tellescope.com");
410
+ unverifiedUserId = '';
411
+ _5.label = 39;
307
412
  case 39:
308
- // 8. noAccessPermissions on switch: non-admin can switch to its own plus sibling
309
- _l.sent();
310
- // Re-auth sdkNonAdmin: previous switch invalidated its token (Step 13)
311
- return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
312
- // 9. Unauthenticated request: must reject before reaching handler
313
- ];
413
+ _5.trys.push([39, 41, , 42]);
414
+ return [4 /*yield*/, sdk.api.users.createOne({ email: unverifiedEmail, fname: 'Unv', lname: 'User' })];
314
415
  case 40:
315
- // Re-auth sdkNonAdmin: previous switch invalidated its token (Step 13)
316
- _l.sent();
317
- noAuthSession_1 = new sdk_1.Session({ host: host });
318
- return [4 /*yield*/, (0, testing_1.async_test)('switch_account: rejects unauthenticated caller', function () { return noAuthSession_1.api.users.switch_account({ targetUserId: userPlusA.id }); }, testing_1.handleAnyError)
319
- // 10. Audit log written for the same-org switch performed in step 1
320
- ];
416
+ created = _5.sent();
417
+ unverifiedUserId = created.id;
418
+ return [3 /*break*/, 42];
321
419
  case 41:
322
- _l.sent();
323
- // 10. Audit log written for the same-org switch performed in step 1
324
- return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
325
- case 42:
326
- // 10. Audit log written for the same-org switch performed in step 1
327
- _l.sent();
328
- return [4 /*yield*/, sdk.api.user_logs.getSome({ limit: 1000 })];
420
+ _d = _5.sent();
421
+ return [3 /*break*/, 42];
422
+ case 42: return [4 /*yield*/, (0, testing_1.async_test)('C3. Unverified email treated as no-match', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: unverifiedEmail }); }, passOnAnyResult)];
329
423
  case 43:
330
- allLogs = _l.sent();
331
- logsArr = Array.isArray(allLogs) ? allLogs : ((allLogs === null || allLogs === void 0 ? void 0 : allLogs.user_logs) || []);
332
- switchLog = logsArr.find(function (l) {
333
- var _a, _b, _c;
334
- return ((_a = l === null || l === void 0 ? void 0 : l.info) === null || _a === void 0 ? void 0 : _a.event) === 'account_switch'
335
- && ((_b = l === null || l === void 0 ? void 0 : l.info) === null || _b === void 0 ? void 0 : _b.targetUserId) === userPlusB.id
336
- && ((_c = l === null || l === void 0 ? void 0 : l.info) === null || _c === void 0 ? void 0 : _c.sourceUserId) === sdk.userInfo.id;
337
- });
338
- (0, testing_1.assert)(!!switchLog, 'no account_switch audit log found for sdk → userPlusB', 'switch_account: HIPAA audit log written');
339
- if (switchLog) {
340
- (0, testing_1.assert)(!!switchLog.info.sourceBusinessId && !!switchLog.info.targetBusinessId, 'audit log missing businessIds', 'switch_account: audit log captures both businessIds');
341
- (0, testing_1.assert)((switchLog.info.targetEmail || '').toLowerCase() === emailPlusB.toLowerCase(), 'audit log targetEmail mismatch', 'switch_account: audit log captures targetEmail');
342
- }
343
- return [4 /*yield*/, sdk.api.users.getOne(userUnverifTarget.id)];
424
+ _5.sent();
425
+ if (!unverifiedUserId) return [3 /*break*/, 48];
426
+ return [4 /*yield*/, (0, testing_1.async_test)('C3. No record written on unverified target', function () { return get_user(sdk, unverifiedUserId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === 0; } })];
344
427
  case 44:
345
- beforeUnverif = _l.sent();
346
- (0, testing_1.assert)(beforeUnverif.verifiedEmail !== true, "test fixture userUnverifTarget was unexpectedly already verified", 'verifiedEmail propagation fixture: target starts unverified');
347
- return [4 /*yield*/, sdk.api.users.switch_account({ targetUserId: userUnverifTarget.id })];
428
+ _5.sent();
429
+ _5.label = 45;
348
430
  case 45:
349
- _l.sent();
350
- return [4 /*yield*/, sdk.authenticate(TEST_EMAIL, TEST_PASSWORD)];
431
+ _5.trys.push([45, 47, , 48]);
432
+ return [4 /*yield*/, sdk.api.users.deleteOne(unverifiedUserId)];
351
433
  case 46:
352
- _l.sent();
353
- return [4 /*yield*/, sdk.api.users.getOne(userUnverifTarget.id)];
434
+ _5.sent();
435
+ return [3 /*break*/, 48];
354
436
  case 47:
355
- afterUnverif = _l.sent();
356
- (0, testing_1.assert)(afterUnverif.verifiedEmail === true, "target verifiedEmail did not propagate from verified source", 'switch_account: verified source propagates verifiedEmail to target');
357
- rateLimitTripped = false;
358
- lastError = null;
359
- i = 0;
360
- _l.label = 48;
361
- case 48:
362
- if (!(i < 15)) return [3 /*break*/, 54];
363
- _l.label = 49;
437
+ _e = _5.sent();
438
+ return [3 /*break*/, 48];
439
+ case 48:
440
+ // C5: self-request
441
+ return [4 /*yield*/, (0, testing_1.async_test)('C5. Self-request returns {} and writes nothing', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: nonAdminEmail }); }, passOnAnyResult)];
364
442
  case 49:
365
- _l.trys.push([49, 52, , 53]);
366
- return [4 /*yield*/, sdk.authenticate(TEST_EMAIL, TEST_PASSWORD)];
443
+ // C5: self-request
444
+ _5.sent();
445
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
367
446
  case 50:
368
- _l.sent();
369
- return [4 /*yield*/, sdk.api.users.switch_account({ targetUserId: userPlusA.id })];
447
+ _5.sent();
448
+ return [4 /*yield*/, (0, testing_1.async_test)('C5. No record written on self-request', function () { return get_user(sdkNonAdmin, nonAdminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === 0; } })
449
+ // C4: valid match
450
+ ];
370
451
  case 51:
371
- _l.sent();
372
- return [3 /*break*/, 53];
452
+ _5.sent();
453
+ // C4: valid match
454
+ return [4 /*yield*/, (0, testing_1.async_test)('C4. Valid email request returns {}', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail }); }, passOnAnyResult)];
373
455
  case 52:
374
- e_1 = _l.sent();
375
- lastError = e_1;
376
- msg = (e_1 === null || e_1 === void 0 ? void 0 : e_1.message) || ((_j = e_1 === null || e_1 === void 0 ? void 0 : e_1.toString) === null || _j === void 0 ? void 0 : _j.call(e_1)) || String(e_1);
377
- if (/too many|rate/i.test(msg)) {
378
- rateLimitTripped = true;
379
- return [3 /*break*/, 54];
380
- }
381
- return [3 /*break*/, 53];
456
+ // C4: valid match
457
+ _5.sent();
458
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
382
459
  case 53:
383
- i++;
384
- return [3 /*break*/, 48];
460
+ _5.sent();
461
+ return [4 /*yield*/, (0, testing_1.async_test)('C4. Pending entry written with requester snapshot', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) {
462
+ var _a;
463
+ var e = ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).find(function (x) { return x.userId === nonAdminId; });
464
+ if (!e)
465
+ return false;
466
+ var expiresOk = (new Date(e.requestExpiresAt).getTime() - Date.now()) > 6 * 24 * 60 * 60 * 1000;
467
+ return e.status === 'pending' && e.email === nonAdminEmail && !!e.createdAt && expiresOk;
468
+ } })
469
+ // C6: idempotent
470
+ ];
385
471
  case 54:
386
- (0, testing_1.assert)(rateLimitTripped, "rate limit never tripped after 15 attempts (last error: ".concat((_k = lastError === null || lastError === void 0 ? void 0 : lastError.message) !== null && _k !== void 0 ? _k : 'none', ")"), 'switch_account: rate limit enforced');
387
- // Best-effort re-auth so cleanup below can use sdk
388
- return [4 /*yield*/, sdk.authenticate(TEST_EMAIL, TEST_PASSWORD).catch(function () { return null; })];
472
+ _5.sent();
473
+ return [4 /*yield*/, get_user(sdk, adminId)];
389
474
  case 55:
390
- // Best-effort re-auth so cleanup below can use sdk
391
- _l.sent();
392
- return [3 /*break*/, 58];
475
+ adminPre = _5.sent();
476
+ preLen = ((_q = adminPre.linkedAccountAccess) !== null && _q !== void 0 ? _q : []).length;
477
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
393
478
  case 56:
394
- cleanup = function (fn) { return fn().catch(function () { return null; }); };
395
- return [4 /*yield*/, Promise.all([
396
- userPlusA && cleanup(function () { return sdk.api.users.deleteOne(userPlusA.id); }),
397
- userPlusB && cleanup(function () { return sdk.api.users.deleteOne(userPlusB.id); }),
398
- userPlusLocked && cleanup(function () { return sdk.api.users.deleteOne(userPlusLocked.id); }),
399
- userPlusUppercase && cleanup(function () { return sdk.api.users.deleteOne(userPlusUppercase.id); }),
400
- userPlusInjection && cleanup(function () { return sdk.api.users.deleteOne(userPlusInjection.id); }),
401
- userReadOnly && cleanup(function () { return sdk.api.users.deleteOne(userReadOnly.id); }),
402
- userOtherBase && cleanup(function () { return sdk.api.users.deleteOne(userOtherBase.id); }),
403
- userPrefixSwitcher && cleanup(function () { return sdk.api.users.deleteOne(userPrefixSwitcher.id); }),
404
- userNonAdminPlus && cleanup(function () { return sdk.api.users.deleteOne(userNonAdminPlus.id); }),
405
- userUnverifTarget && cleanup(function () { return sdk.api.users.deleteOne(userUnverifTarget.id); }),
406
- userCrossOrg && cleanup(function () { return sdkOther.api.users.deleteOne(userCrossOrg.id); }),
407
- restrictiveRole && cleanup(function () { return sdk.api.role_based_access_permissions.deleteOne(restrictiveRole.id); }),
408
- ])];
479
+ _5.sent();
480
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
409
481
  case 57:
410
- _l.sent();
411
- return [7 /*endfinally*/];
412
- case 58: return [2 /*return*/];
482
+ _5.sent();
483
+ return [4 /*yield*/, (0, testing_1.async_test)('C6. Duplicate request inside window does not duplicate the entry', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === preLen; } })
484
+ // C7: existing accepted -> no-op
485
+ ];
486
+ case 58:
487
+ _5.sent();
488
+ return [4 /*yield*/, get_user(sdk, adminId)];
489
+ case 59:
490
+ adminBeforeAccept = _5.sent();
491
+ pendingEntry = ((_r = adminBeforeAccept.linkedAccountAccess) !== null && _r !== void 0 ? _r : []).find(function (e) { return e.userId === nonAdminId; });
492
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingEntry), { status: 'accepted' })])];
493
+ case 60:
494
+ _5.sent();
495
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
496
+ case 61:
497
+ _5.sent();
498
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
499
+ case 62:
500
+ _5.sent();
501
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
502
+ case 63:
503
+ _5.sent();
504
+ return [4 /*yield*/, (0, testing_1.async_test)('C7. Re-requesting on accepted entry is a no-op', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) {
505
+ var _a;
506
+ var entries = ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []);
507
+ var e = entries.find(function (x) { return x.userId === nonAdminId; });
508
+ return entries.length === 1 && (e === null || e === void 0 ? void 0 : e.status) === 'accepted';
509
+ } })
510
+ // Reset
511
+ ];
512
+ case 64:
513
+ _5.sent();
514
+ // Reset
515
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)
516
+ // C9: email case-insensitivity (whitespace is rejected at the schema validator;
517
+ // case-insensitive matching happens after emailValidator lowercases the input).
518
+ ];
519
+ case 65:
520
+ // Reset
521
+ _5.sent();
522
+ // C9: email case-insensitivity (whitespace is rejected at the schema validator;
523
+ // case-insensitive matching happens after emailValidator lowercases the input).
524
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail.toUpperCase() })];
525
+ case 66:
526
+ // C9: email case-insensitivity (whitespace is rejected at the schema validator;
527
+ // case-insensitive matching happens after emailValidator lowercases the input).
528
+ _5.sent();
529
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
530
+ case 67:
531
+ _5.sent();
532
+ return [4 /*yield*/, (0, testing_1.async_test)('C9. Email case-insensitive matching', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).some(function (e) { return e.userId === nonAdminId; }); } })
533
+ // C10 is the request_linked_account_access rate-limit test. Because it exhausts admin's
534
+ // `request-linked-${adminId}` counter for 60s, and E5/E6/E7 setup needs admin to send a
535
+ // single request_linked_account_access (to be the source-side in the role-flipped lockout
536
+ // tests), C10 is intentionally relocated to the bottom of the suite (after I4) so no
537
+ // downstream test depends on admin's request_linked quota.
538
+ // ============================================================
539
+ // D. get_linked_accounts
540
+ // ============================================================
541
+ ];
542
+ case 68:
543
+ _5.sent();
544
+ // C10 is the request_linked_account_access rate-limit test. Because it exhausts admin's
545
+ // `request-linked-${adminId}` counter for 60s, and E5/E6/E7 setup needs admin to send a
546
+ // single request_linked_account_access (to be the source-side in the role-flipped lockout
547
+ // tests), C10 is intentionally relocated to the bottom of the suite (after I4) so no
548
+ // downstream test depends on admin's request_linked quota.
549
+ // ============================================================
550
+ // D. get_linked_accounts
551
+ // ============================================================
552
+ (0, testing_1.log_header)("D. get_linked_accounts");
553
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
554
+ case 69:
555
+ _5.sent();
556
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
557
+ case 70:
558
+ _5.sent();
559
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
560
+ case 71:
561
+ _5.sent();
562
+ return [4 /*yield*/, get_user(sdk, adminId)];
563
+ case 72:
564
+ adminWithReq = _5.sent();
565
+ pendingForNonAdmin = ((_s = adminWithReq.linkedAccountAccess) !== null && _s !== void 0 ? _s : []).find(function (e) { return e.userId === nonAdminId; });
566
+ return [4 /*yield*/, (0, testing_1.async_test)('D2. Pending entry not returned', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) { var _a; return !((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).some(function (a) { return a.id === adminId; }); } })];
567
+ case 73:
568
+ _5.sent();
569
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingForNonAdmin), { status: 'accepted' })])];
570
+ case 74:
571
+ _5.sent();
572
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
573
+ case 75:
574
+ _5.sent();
575
+ return [4 /*yield*/, (0, testing_1.async_test)('D1. Accepted entry is returned', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) { var _a; return ((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).some(function (a) { return a.id === adminId; }); } })];
576
+ case 76:
577
+ _5.sent();
578
+ return [4 /*yield*/, (0, testing_1.async_test)('D5. Returned row has expected identity fields', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) {
579
+ var _a;
580
+ var row = ((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).find(function (a) { return a.id === adminId; });
581
+ return !!row && typeof row.email === 'string' && row.email.length > 0 && typeof row.orgName === 'string' && typeof row.requiresMFA === 'boolean';
582
+ } })];
583
+ case 77:
584
+ _5.sent();
585
+ return [4 /*yield*/, (0, testing_1.async_test)('D3. Self is excluded', function () { return sdkNonAdmin.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) { var _a; return !((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).some(function (a) { return a.id === nonAdminId; }); } })];
586
+ case 78:
587
+ _5.sent();
588
+ return [4 /*yield*/, (0, testing_1.async_test)('D6. Empty result for caller with no grants directed at them', function () { return sdk.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) { return Array.isArray(r.linkedAccounts) && r.linkedAccounts.length === 0; } })
589
+ // ============================================================
590
+ // E. switch_account — grant + accessibility
591
+ // ============================================================
592
+ ];
593
+ case 79:
594
+ _5.sent();
595
+ // ============================================================
596
+ // E. switch_account — grant + accessibility
597
+ // ============================================================
598
+ (0, testing_1.log_header)("E. switch_account — grant + accessibility");
599
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
600
+ case 80:
601
+ _5.sent();
602
+ return [4 /*yield*/, (0, testing_1.async_test)('E1. No entry -> 403', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || (e.message || '').includes('not granted'); } })];
603
+ case 81:
604
+ _5.sent();
605
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
606
+ case 82:
607
+ _5.sent();
608
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
609
+ case 83:
610
+ _5.sent();
611
+ return [4 /*yield*/, (0, testing_1.async_test)('E2. Only pending -> 403', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || (e.message || '').includes('not granted'); } })];
612
+ case 84:
613
+ _5.sent();
614
+ return [4 /*yield*/, (0, testing_1.async_test)('E3. Self-switch -> 400', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 400 || (e.message || '').includes('own account'); } })];
615
+ case 85:
616
+ _5.sent();
617
+ return [4 /*yield*/, (0, testing_1.async_test)('E4. Nonexistent target -> 404', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: '000000000000000000000000' }); }, { shouldError: true, onError: function (e) { return e.statusCode === 404 || (e.message || '').includes('not found'); } })
618
+ // E9. Malformed targetUserId -> 400 (mongoIdStringRequired schema validator)
619
+ ];
620
+ case 86:
621
+ _5.sent();
622
+ // E9. Malformed targetUserId -> 400 (mongoIdStringRequired schema validator)
623
+ return [4 /*yield*/, (0, testing_1.async_test)('E9. Malformed targetUserId -> 400', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: 'not-a-mongo-id' }); }, { shouldError: true, onError: function (e) { return e.statusCode === 400 || /(invalid|mongoId|parsing|format)/i.test(e.message || ''); } })
624
+ // E5/E6/E7. Locked target -> 401. Flip roles: admin (only user who can write locked* fields) is the
625
+ // SOURCE, nonAdmin is the TARGET. nonAdmin grants admin access first.
626
+ // Run BEFORE E10's rate-limit exhaustion so admin's switch counter is still fresh here.
627
+ ];
628
+ case 87:
629
+ // E9. Malformed targetUserId -> 400 (mongoIdStringRequired schema validator)
630
+ _5.sent();
631
+ // E5/E6/E7. Locked target -> 401. Flip roles: admin (only user who can write locked* fields) is the
632
+ // SOURCE, nonAdmin is the TARGET. nonAdmin grants admin access first.
633
+ // Run BEFORE E10's rate-limit exhaustion so admin's switch counter is still fresh here.
634
+ return [4 /*yield*/, clear_linkedAccountAccess(sdkNonAdmin, nonAdminId)];
635
+ case 88:
636
+ // E5/E6/E7. Locked target -> 401. Flip roles: admin (only user who can write locked* fields) is the
637
+ // SOURCE, nonAdmin is the TARGET. nonAdmin grants admin access first.
638
+ // Run BEFORE E10's rate-limit exhaustion so admin's switch counter is still fresh here.
639
+ _5.sent();
640
+ return [4 /*yield*/, sdk.api.users.request_linked_account_access({ targetEmail: nonAdminEmail })];
641
+ case 89:
642
+ _5.sent();
643
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
644
+ case 90:
645
+ _5.sent();
646
+ return [4 /*yield*/, get_user(sdkNonAdmin, nonAdminId)];
647
+ case 91:
648
+ nonAdminAfterReq = _5.sent();
649
+ pendingFromAdmin = ((_t = nonAdminAfterReq.linkedAccountAccess) !== null && _t !== void 0 ? _t : []).find(function (e) { return e.userId === adminId; });
650
+ if (!pendingFromAdmin) return [3 /*break*/, 94];
651
+ return [4 /*yield*/, set_linkedAccountAccess(sdkNonAdmin, nonAdminId, [__assign(__assign({}, pendingFromAdmin), { status: 'accepted' })])];
652
+ case 92:
653
+ _5.sent();
654
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
655
+ case 93:
656
+ _5.sent();
657
+ _5.label = 94;
658
+ case 94:
659
+ // E5: lockedOutUntil in the future
660
+ return [4 /*yield*/, sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: Date.now() + 60000 })];
661
+ case 95:
662
+ // E5: lockedOutUntil in the future
663
+ _5.sent();
664
+ return [4 /*yield*/, (0, testing_1.async_test)('E5. Target with lockedOutUntil > now -> 401', function () { return sdk.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 401 || /(locked|not accessible)/i.test(e.message || ''); } })
665
+ // E6: lockedOutUntil === 0 (indefinite lock)
666
+ ];
667
+ case 96:
668
+ _5.sent();
669
+ // E6: lockedOutUntil === 0 (indefinite lock)
670
+ return [4 /*yield*/, sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: 0 })];
671
+ case 97:
672
+ // E6: lockedOutUntil === 0 (indefinite lock)
673
+ _5.sent();
674
+ return [4 /*yield*/, (0, testing_1.async_test)('E6. Target with lockedOutUntil === 0 -> 401', function () { return sdk.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 401 || /(locked|not accessible)/i.test(e.message || ''); } })
675
+ // E7: failedLoginAttempts >= 10
676
+ ];
677
+ case 98:
678
+ _5.sent();
679
+ // E7: failedLoginAttempts >= 10
680
+ return [4 /*yield*/, sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1, failedLoginAttempts: 10 })];
681
+ case 99:
682
+ // E7: failedLoginAttempts >= 10
683
+ _5.sent();
684
+ return [4 /*yield*/, (0, testing_1.async_test)('E7. Target with failedLoginAttempts >= 10 -> 401', function () { return sdk.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 401 || /(locked|not accessible|failed login)/i.test(e.message || ''); } })
685
+ // Restore nonAdmin to a healthy state. Setting lockedOutUntil to 0/future in E5/E6 triggered
686
+ // deauthenticate_user(nonAdminId) via routing.ts:2742-2752, writing `deauthenticated-${id}`
687
+ // to cache with the current timestamp. is_logged_in rejects any token whose iat falls within
688
+ // the 1s slack window after that timestamp — so a re-auth too quickly afterward produces a
689
+ // token that gets immediately invalidated. Wait > 1s past E6's deauth before re-authing.
690
+ ];
691
+ case 100:
692
+ _5.sent();
693
+ // Restore nonAdmin to a healthy state. Setting lockedOutUntil to 0/future in E5/E6 triggered
694
+ // deauthenticate_user(nonAdminId) via routing.ts:2742-2752, writing `deauthenticated-${id}`
695
+ // to cache with the current timestamp. is_logged_in rejects any token whose iat falls within
696
+ // the 1s slack window after that timestamp — so a re-auth too quickly afterward produces a
697
+ // token that gets immediately invalidated. Wait > 1s past E6's deauth before re-authing.
698
+ return [4 /*yield*/, sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1, failedLoginAttempts: 0 })];
699
+ case 101:
700
+ // Restore nonAdmin to a healthy state. Setting lockedOutUntil to 0/future in E5/E6 triggered
701
+ // deauthenticate_user(nonAdminId) via routing.ts:2742-2752, writing `deauthenticated-${id}`
702
+ // to cache with the current timestamp. is_logged_in rejects any token whose iat falls within
703
+ // the 1s slack window after that timestamp — so a re-auth too quickly afterward produces a
704
+ // token that gets immediately invalidated. Wait > 1s past E6's deauth before re-authing.
705
+ _5.sent();
706
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
707
+ case 102:
708
+ _5.sent();
709
+ return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)];
710
+ case 103:
711
+ _5.sent();
712
+ return [4 /*yield*/, clear_linkedAccountAccess(sdkNonAdmin, nonAdminId)
713
+ // E10. switch_account rate limit (20/min). Each failed switch consumes a quota slot
714
+ // (rate-limit check runs first; source token not invalidated). E5-E7 above already burned
715
+ // 3 slots from admin's switch-account counter, so prime 17 more to reach the limit, then
716
+ // assert the next call is 429. Keep this LAST in E — exhausts admin's switch counter.
717
+ ];
718
+ case 104:
719
+ _5.sent();
720
+ i = 0;
721
+ _5.label = 105;
722
+ case 105:
723
+ if (!(i < 17)) return [3 /*break*/, 110];
724
+ _5.label = 106;
725
+ case 106:
726
+ _5.trys.push([106, 108, , 109]);
727
+ return [4 /*yield*/, sdk.api.users.switch_account({ targetUserId: '000000000000000000000000' })];
728
+ case 107:
729
+ _5.sent();
730
+ return [3 /*break*/, 109];
731
+ case 108:
732
+ _f = _5.sent();
733
+ return [3 /*break*/, 109];
734
+ case 109:
735
+ i++;
736
+ return [3 /*break*/, 105];
737
+ case 110: return [4 /*yield*/, (0, testing_1.async_test)('E10. Switch beyond 20/min is rate-limited (429)', function () { return sdk.api.users.switch_account({ targetUserId: '000000000000000000000000' }); }, { shouldError: true, onError: function (e) { return e.statusCode === 429 || (e.message || '').toLowerCase().includes('rate'); } })
738
+ // ============================================================
739
+ // F. switch_account — enforceMFA gap
740
+ // ============================================================
741
+ ];
742
+ case 111:
743
+ _5.sent();
744
+ // ============================================================
745
+ // F. switch_account — enforceMFA gap
746
+ // ============================================================
747
+ (0, testing_1.log_header)("F. switch_account — enforceMFA gap");
748
+ // Set up accepted grant: nonAdmin requests, admin accepts. (E section cleared admin's array.)
749
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
750
+ case 112:
751
+ // Set up accepted grant: nonAdmin requests, admin accepts. (E section cleared admin's array.)
752
+ _5.sent();
753
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
754
+ case 113:
755
+ _5.sent();
756
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
757
+ case 114:
758
+ _5.sent();
759
+ return [4 /*yield*/, get_user(sdk, adminId)];
760
+ case 115:
761
+ adminForF = _5.sent();
762
+ pendingForF = ((_u = adminForF.linkedAccountAccess) !== null && _u !== void 0 ? _u : []).find(function (e) { return e.userId === nonAdminId; });
763
+ if (!pendingForF) return [3 /*break*/, 118];
764
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingForF), { status: 'accepted' })])];
765
+ case 116:
766
+ _5.sent();
767
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
768
+ case 117:
769
+ _5.sent();
770
+ _5.label = 118;
771
+ case 118:
772
+ // Enable enforceMFA on the test business; admin (target) currently has no MFA configured.
773
+ return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { enforceMFA: true })];
774
+ case 119:
775
+ // Enable enforceMFA on the test business; admin (target) currently has no MFA configured.
776
+ _5.sent();
777
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
778
+ // F1: target with no MFA but org enforces -> 403
779
+ ];
780
+ case 120:
781
+ _5.sent();
782
+ // F1: target with no MFA but org enforces -> 403
783
+ return [4 /*yield*/, (0, testing_1.async_test)('F1. enforceMFA on org + target MFA not configured -> 403', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || /(MFA configuration|enforceMFA)/i.test(e.message || ''); } })
784
+ // F2: admin configures MFA, switch now succeeds — but the switched JWT has requiresMFA: true.
785
+ ];
786
+ case 121:
787
+ // F1: target with no MFA but org enforces -> 403
788
+ _5.sent();
789
+ // F2: admin configures MFA, switch now succeeds — but the switched JWT has requiresMFA: true.
790
+ return [4 /*yield*/, sdk.api.users.configure_MFA({ disable: false })];
791
+ case 122:
792
+ // F2: admin configures MFA, switch now succeeds — but the switched JWT has requiresMFA: true.
793
+ _5.sent();
794
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
795
+ case 123:
796
+ _5.sent();
797
+ return [4 /*yield*/, (0, testing_1.async_test)('F2. After target configures MFA, switch succeeds and switched JWT has requiresMFA: true', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) {
798
+ var decoded = decode_jwt(r.authToken);
799
+ return !!r.authToken && (decoded === null || decoded === void 0 ? void 0 : decoded.requiresMFA) === true;
800
+ } })
801
+ // Teardown F: revert enforceMFA first (configure_MFA(disable=true) refuses while enforced),
802
+ // then disable MFA on admin. Re-auth nonAdmin since its source token was invalidated by F2's switch.
803
+ ];
804
+ case 124:
805
+ _5.sent();
806
+ // Teardown F: revert enforceMFA first (configure_MFA(disable=true) refuses while enforced),
807
+ // then disable MFA on admin. Re-auth nonAdmin since its source token was invalidated by F2's switch.
808
+ return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { enforceMFA: false })];
809
+ case 125:
810
+ // Teardown F: revert enforceMFA first (configure_MFA(disable=true) refuses while enforced),
811
+ // then disable MFA on admin. Re-auth nonAdmin since its source token was invalidated by F2's switch.
812
+ _5.sent();
813
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
814
+ case 126:
815
+ _5.sent();
816
+ _5.label = 127;
817
+ case 127:
818
+ _5.trys.push([127, 129, , 130]);
819
+ return [4 /*yield*/, sdk.api.users.configure_MFA({ disable: true })];
820
+ case 128:
821
+ _5.sent();
822
+ return [3 /*break*/, 130];
823
+ case 129:
824
+ _g = _5.sent();
825
+ return [3 /*break*/, 130];
826
+ case 130: return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
827
+ // ============================================================
828
+ // G. switch_account — JWT + audit
829
+ // ============================================================
830
+ ];
831
+ case 131:
832
+ _5.sent();
833
+ // ============================================================
834
+ // G. switch_account — JWT + audit
835
+ // ============================================================
836
+ (0, testing_1.log_header)("G. switch_account — JWT + audit");
837
+ // Set up an accepted grant. clear_linkedAccountAccess removes F's accepted entry, which
838
+ // writes the `grant-revoked-${adminId}-${nonAdminId}` cache key. is_logged_in compares that
839
+ // key against the new JWT's iat with a 1s slack (accounts for JWT iat second-rounding), so
840
+ // we must ensure the new grant is minted well past that window or the freshly-switched
841
+ // token gets rejected as if it were a pre-revocation token. Wait > 1s between the cleanup
842
+ // and the new switch.
843
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
844
+ case 132:
845
+ // Set up an accepted grant. clear_linkedAccountAccess removes F's accepted entry, which
846
+ // writes the `grant-revoked-${adminId}-${nonAdminId}` cache key. is_logged_in compares that
847
+ // key against the new JWT's iat with a 1s slack (accounts for JWT iat second-rounding), so
848
+ // we must ensure the new grant is minted well past that window or the freshly-switched
849
+ // token gets rejected as if it were a pre-revocation token. Wait > 1s between the cleanup
850
+ // and the new switch.
851
+ _5.sent();
852
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
853
+ case 133:
854
+ _5.sent();
855
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
856
+ case 134:
857
+ _5.sent();
858
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
859
+ case 135:
860
+ _5.sent();
861
+ return [4 /*yield*/, get_user(sdk, adminId)];
862
+ case 136:
863
+ adminPendingState = _5.sent();
864
+ pendingNA = ((_v = adminPendingState.linkedAccountAccess) !== null && _v !== void 0 ? _v : []).find(function (e) { return e.userId === nonAdminId; });
865
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, pendingNA), { status: 'accepted' })])];
866
+ case 137:
867
+ _5.sent();
868
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
869
+ case 138:
870
+ _5.sent();
871
+ switchedToken = '';
872
+ switchedUser = null;
873
+ return [4 /*yield*/, (0, testing_1.async_test)('G0. Switch succeeds when accepted grant exists', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) {
874
+ var _a;
875
+ switchedToken = r.authToken;
876
+ switchedUser = r.user;
877
+ return typeof r.authToken === 'string' && r.authToken.length > 0 && ((_a = r.user) === null || _a === void 0 ? void 0 : _a.id) === adminId;
878
+ } })];
879
+ case 139:
880
+ _5.sent();
881
+ decoded = decode_jwt(switchedToken);
882
+ (0, testing_1.assert)(!!decoded, 'JWT decode failed', 'G1. JWT decoded');
883
+ (0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.id) === adminId, "JWT.id ".concat(decoded === null || decoded === void 0 ? void 0 : decoded.id, " != expected ").concat(adminId), 'G1. JWT.id == target');
884
+ (0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.actorUserId) === nonAdminId, "JWT.actorUserId mismatch", 'G1. JWT.actorUserId == source');
885
+ (0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.actorEmail) === nonAdminEmail, "JWT.actorEmail mismatch", 'G1. JWT.actorEmail');
886
+ (0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.actorBusinessId) === nonAdminBusinessId, "JWT.actorBusinessId mismatch", 'G1. JWT.actorBusinessId');
887
+ return [4 /*yield*/, (0, testing_1.async_test)('G2. Pre-switch nonAdmin token is invalidated', function () { return sdkNonAdmin.test_authenticated(); }, { shouldError: true, onError: function () { return true; } })];
888
+ case 140:
889
+ _5.sent();
890
+ switchedSdk = new sdk_1.Session({ host: host });
891
+ switchedSdk.setAuthToken(switchedToken);
892
+ switchedSdk.setUserInfo(switchedUser);
893
+ return [4 /*yield*/, (0, testing_1.async_test)('G3. Switched token authenticates', function () { return switchedSdk.test_authenticated(); }, { shouldError: false, onResult: function (r) { return r === 'Authenticated!'; } })];
894
+ case 141:
895
+ _5.sent();
896
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
897
+ case 142:
898
+ _5.sent();
899
+ return [4 /*yield*/, (0, testing_1.async_test)('G4. user_logs has account_switch event with full info', function () { return sdk.api.user_logs.getOne({ resourceId: adminId, resource: 'users', action: 'update' }); }, { shouldError: false, onResult: function (log) {
900
+ var _a;
901
+ var info = (_a = log === null || log === void 0 ? void 0 : log.info) !== null && _a !== void 0 ? _a : {};
902
+ return (log === null || log === void 0 ? void 0 : log.userId) === nonAdminId
903
+ && info.event === 'account_switch'
904
+ && info.sourceUserId === nonAdminId
905
+ && info.sourceEmail === nonAdminEmail
906
+ && info.sourceBusinessId === nonAdminBusinessId
907
+ && info.targetUserId === adminId
908
+ && info.targetEmail === adminEmail
909
+ && info.targetBusinessId === adminBusinessId;
910
+ } })
911
+ // G5: downstream user_log under switched session carries actorUserId.
912
+ // Capture original fname so we can restore it during cleanup — downstream tests
913
+ // (e.g. Calendar RSVPs) compare userInfo.fname against server-side values.
914
+ ];
915
+ case 143:
916
+ _5.sent();
917
+ originalAdminFname = sdk.userInfo.fname;
918
+ return [4 /*yield*/, switchedSdk.api.users.updateOne(adminId, { fname: "Switched-".concat(RAND()) })];
919
+ case 144:
920
+ _5.sent();
921
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
922
+ case 145:
923
+ _5.sent();
924
+ return [4 /*yield*/, (0, testing_1.async_test)('G5. Downstream user_log under switched session has actorUserId', function () { return sdk.api.user_logs.getSome({ filter: { resourceId: adminId, resource: 'users', action: 'update' } }); }, { shouldError: false, onResult: function (logs) { return (logs !== null && logs !== void 0 ? logs : []).some(function (l) { return l.actorUserId === nonAdminId && l.userId === adminId; }); } })
925
+ // G6. PHI-adjacent collection: create an enduser through the switched session and assert
926
+ // the auto-emitted CRUD user_log carries actorUserId (exercises routing.ts inline insert
927
+ // path, not just storeUserLog or the users-collection update path).
928
+ ];
929
+ case 146:
930
+ _5.sent();
931
+ g6EnduserId = '';
932
+ _5.label = 147;
933
+ case 147:
934
+ _5.trys.push([147, 149, , 150]);
935
+ return [4 /*yield*/, switchedSdk.api.endusers.createOne({ fname: 'Switch', lname: 'Test', email: "switch-test-".concat(RAND(), "@tellescope.example") })];
936
+ case 148:
937
+ ce = _5.sent();
938
+ g6EnduserId = ce.id;
939
+ return [3 /*break*/, 150];
940
+ case 149:
941
+ e_1 = _5.sent();
942
+ return [3 /*break*/, 150];
943
+ case 150: return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
944
+ case 151:
945
+ _5.sent();
946
+ return [4 /*yield*/, (0, testing_1.async_test)('G6. Downstream enduser create under switched session has actorUserId', function () { return sdk.api.user_logs.getSome({ filter: { resource: 'endusers', action: 'create' } }); }, { shouldError: false, onResult: function (logs) { return (logs !== null && logs !== void 0 ? logs : []).some(function (l) { return l.actorUserId === nonAdminId && l.userId === adminId && (g6EnduserId ? l.resourceId === g6EnduserId : true); }); } })];
947
+ case 152:
948
+ _5.sent();
949
+ if (!g6EnduserId) return [3 /*break*/, 156];
950
+ _5.label = 153;
951
+ case 153:
952
+ _5.trys.push([153, 155, , 156]);
953
+ return [4 /*yield*/, sdk.api.endusers.deleteOne(g6EnduserId)];
954
+ case 154:
955
+ _5.sent();
956
+ return [3 /*break*/, 156];
957
+ case 155:
958
+ _h = _5.sent();
959
+ return [3 /*break*/, 156];
960
+ case 156:
961
+ // G7. Cross-org boundary assertion: the switched JWT operates in the TARGET's business.
962
+ // If a future change ever gates cross-org switching, this assertion will fire.
963
+ (0, testing_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.businessId) === adminBusinessId, "JWT.businessId mismatch (got ".concat(decoded === null || decoded === void 0 ? void 0 : decoded.businessId, ", expected ").concat(adminBusinessId, ")"), 'G7. JWT.businessId == target businessId');
964
+ // ============================================================
965
+ // H. Real-time revocation
966
+ // ============================================================
967
+ (0, testing_1.log_header)("H. Real-time revocation");
968
+ return [4 /*yield*/, (0, testing_1.async_test)('H1. Baseline: switched session reads OK', function () { return switchedSdk.test_authenticated(); }, { shouldError: false, onResult: function (r) { return r === 'Authenticated!'; } })
969
+ // Revoke
970
+ ];
971
+ case 157:
972
+ _5.sent();
973
+ // Revoke
974
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
975
+ case 158:
976
+ // Revoke
977
+ _5.sent();
978
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 750)];
979
+ case 159:
980
+ _5.sent();
981
+ return [4 /*yield*/, (0, testing_1.async_test)('H2. Switched session 401 after revoke', function () { return switchedSdk.test_authenticated(); }, { shouldError: true, onError: is401Rejection })];
982
+ case 160:
983
+ _5.sent();
984
+ return [4 /*yield*/, (0, testing_1.async_test)('H3. Owner own session still works (no over-broad invalidation)', function () { return sdk.test_authenticated(); }, { shouldError: false, onResult: function (r) { return r === 'Authenticated!'; } })
985
+ // H6. After revoke, a brand-new switch_account attempt is also rejected (covers the
986
+ // net-new path, complementing H2 which covers the already-minted session path).
987
+ // nonAdmin's source token from G0 was invalidated by that successful switch; re-auth first.
988
+ ];
989
+ case 161:
990
+ _5.sent();
991
+ // H6. After revoke, a brand-new switch_account attempt is also rejected (covers the
992
+ // net-new path, complementing H2 which covers the already-minted session path).
993
+ // nonAdmin's source token from G0 was invalidated by that successful switch; re-auth first.
994
+ return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)];
995
+ case 162:
996
+ // H6. After revoke, a brand-new switch_account attempt is also rejected (covers the
997
+ // net-new path, complementing H2 which covers the already-minted session path).
998
+ // nonAdmin's source token from G0 was invalidated by that successful switch; re-auth first.
999
+ _5.sent();
1000
+ return [4 /*yield*/, (0, testing_1.async_test)('H6. New switch_account after revoke -> 403', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || (e.message || '').includes('not granted'); } })
1001
+ // H5: reject of pending entry does not write a stale revocation key
1002
+ ];
1003
+ case 163:
1004
+ _5.sent();
1005
+ // H5: reject of pending entry does not write a stale revocation key
1006
+ return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)];
1007
+ case 164:
1008
+ // H5: reject of pending entry does not write a stale revocation key
1009
+ _5.sent();
1010
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
1011
+ case 165:
1012
+ _5.sent();
1013
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1014
+ case 166:
1015
+ _5.sent();
1016
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)]; // reject pending
1017
+ case 167:
1018
+ _5.sent(); // reject pending
1019
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1020
+ case 168:
1021
+ _5.sent();
1022
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
1023
+ case 169:
1024
+ _5.sent();
1025
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1026
+ case 170:
1027
+ _5.sent();
1028
+ return [4 /*yield*/, get_user(sdk, adminId)];
1029
+ case 171:
1030
+ state = _5.sent();
1031
+ newPending = ((_w = state.linkedAccountAccess) !== null && _w !== void 0 ? _w : []).find(function (e) { return e.userId === nonAdminId; });
1032
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, newPending), { status: 'accepted' })])];
1033
+ case 172:
1034
+ _5.sent();
1035
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
1036
+ case 173:
1037
+ _5.sent();
1038
+ return [4 /*yield*/, (0, testing_1.async_test)('H5. New switched session works after a prior reject (no stale revocation)', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) { return typeof r.authToken === 'string' && r.authToken.length > 0; } })];
1039
+ case 174:
1040
+ _5.sent();
1041
+ return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
1042
+ // ============================================================
1043
+ // O. Org-toggle gating (accountSwitchingEnabled)
1044
+ // ============================================================
1045
+ ];
1046
+ case 175:
1047
+ _5.sent();
1048
+ // ============================================================
1049
+ // O. Org-toggle gating (accountSwitchingEnabled)
1050
+ // ============================================================
1051
+ (0, testing_1.log_header)("O. Org-toggle gating");
1052
+ // Pre-stage an accepted entry while the toggle is ON (it currently is).
1053
+ // Wait > 1s after the cleanup so the (adminId, nonAdminId) revocation key from H sits
1054
+ // clearly before the new switch's iat — is_logged_in's 1s slack would otherwise reject
1055
+ // freshly minted tokens as if they were pre-revocation.
1056
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
1057
+ case 176:
1058
+ // Pre-stage an accepted entry while the toggle is ON (it currently is).
1059
+ // Wait > 1s after the cleanup so the (adminId, nonAdminId) revocation key from H sits
1060
+ // clearly before the new switch's iat — is_logged_in's 1s slack would otherwise reject
1061
+ // freshly minted tokens as if they were pre-revocation.
1062
+ _5.sent();
1063
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
1064
+ case 177:
1065
+ _5.sent();
1066
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
1067
+ case 178:
1068
+ _5.sent();
1069
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1070
+ case 179:
1071
+ _5.sent();
1072
+ return [4 /*yield*/, get_user(sdk, adminId)];
1073
+ case 180:
1074
+ oSeedState = _5.sent();
1075
+ oPending = ((_x = oSeedState.linkedAccountAccess) !== null && _x !== void 0 ? _x : []).find(function (e) { return e.userId === nonAdminId; });
1076
+ if (!oPending) return [3 /*break*/, 183];
1077
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, oPending), { status: 'accepted' })])];
1078
+ case 181:
1079
+ _5.sent();
1080
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1081
+ case 182:
1082
+ _5.sent();
1083
+ _5.label = 183;
1084
+ case 183:
1085
+ // Toggle OFF.
1086
+ return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { accountSwitchingEnabled: false })];
1087
+ case 184:
1088
+ // Toggle OFF.
1089
+ _5.sent();
1090
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
1091
+ // O1. request_linked_account_access silently no-ops while toggle is off.
1092
+ ];
1093
+ case 185:
1094
+ _5.sent();
1095
+ return [4 /*yield*/, get_user(sdk, adminId)];
1096
+ case 186:
1097
+ oBefore = _5.sent();
1098
+ oBeforeLen = ((_y = oBefore.linkedAccountAccess) !== null && _y !== void 0 ? _y : []).length;
1099
+ return [4 /*yield*/, (0, testing_1.async_test)('O1. request_linked_account_access returns {} while toggle is off', function () { return sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail }); }, { shouldError: false, onResult: function () { return true; } })];
1100
+ case 187:
1101
+ _5.sent();
1102
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1103
+ case 188:
1104
+ _5.sent();
1105
+ return [4 /*yield*/, (0, testing_1.async_test)('O1. No new record written while toggle is off', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length === oBeforeLen; } })
1106
+ // O2. switch_account on a pre-existing accepted grant -> 403 while toggle is off.
1107
+ ];
1108
+ case 189:
1109
+ _5.sent();
1110
+ // O2. switch_account on a pre-existing accepted grant -> 403 while toggle is off.
1111
+ return [4 /*yield*/, (0, testing_1.async_test)('O2. switch_account -> 403 while target org has toggle off', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return e.statusCode === 403 || /(organization has not enabled|switching)/i.test(e.message || ''); } })
1112
+ // O5. With the toggle off, the SOURCE-org check fires first (before the target check),
1113
+ // so switch_account responds with "Your organization has not enabled..." rather than
1114
+ // "Target organization has not enabled...". Single-org fixture means source==target here;
1115
+ // the error-message assertion is what proves the source-side gate is actually firing
1116
+ // (without it, O2 would have passed under the old target-only implementation too).
1117
+ ];
1118
+ case 190:
1119
+ // O2. switch_account on a pre-existing accepted grant -> 403 while toggle is off.
1120
+ _5.sent();
1121
+ // O5. With the toggle off, the SOURCE-org check fires first (before the target check),
1122
+ // so switch_account responds with "Your organization has not enabled..." rather than
1123
+ // "Target organization has not enabled...". Single-org fixture means source==target here;
1124
+ // the error-message assertion is what proves the source-side gate is actually firing
1125
+ // (without it, O2 would have passed under the old target-only implementation too).
1126
+ return [4 /*yield*/, (0, testing_1.async_test)('O5. switch_account error message names the actor org (source-side check fires)', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: function (e) { return /your organization/i.test(e.message || ''); } })
1127
+ // Toggle ON again.
1128
+ ];
1129
+ case 191:
1130
+ // O5. With the toggle off, the SOURCE-org check fires first (before the target check),
1131
+ // so switch_account responds with "Your organization has not enabled..." rather than
1132
+ // "Target organization has not enabled...". Single-org fixture means source==target here;
1133
+ // the error-message assertion is what proves the source-side gate is actually firing
1134
+ // (without it, O2 would have passed under the old target-only implementation too).
1135
+ _5.sent();
1136
+ // Toggle ON again.
1137
+ return [4 /*yield*/, sdk.api.organizations.updateOne(adminBusinessId, { accountSwitchingEnabled: true })];
1138
+ case 192:
1139
+ // Toggle ON again.
1140
+ _5.sent();
1141
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
1142
+ // O3. switch_account now succeeds (same accepted grant as before).
1143
+ ];
1144
+ case 193:
1145
+ _5.sent();
1146
+ // O3. switch_account now succeeds (same accepted grant as before).
1147
+ return [4 /*yield*/, (0, testing_1.async_test)('O3. switch_account succeeds once toggle is re-enabled', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) { return typeof r.authToken === 'string' && r.authToken.length > 0; } })
1148
+ // nonAdmin's source token was invalidated by the switch; re-auth for subsequent tests.
1149
+ ];
1150
+ case 194:
1151
+ // O3. switch_account now succeeds (same accepted grant as before).
1152
+ _5.sent();
1153
+ // nonAdmin's source token was invalidated by the switch; re-auth for subsequent tests.
1154
+ return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
1155
+ // O4. request_linked_account_access writes a record once toggle is re-enabled.
1156
+ ];
1157
+ case 195:
1158
+ // nonAdmin's source token was invalidated by the switch; re-auth for subsequent tests.
1159
+ _5.sent();
1160
+ // O4. request_linked_account_access writes a record once toggle is re-enabled.
1161
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
1162
+ case 196:
1163
+ // O4. request_linked_account_access writes a record once toggle is re-enabled.
1164
+ _5.sent();
1165
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
1166
+ case 197:
1167
+ _5.sent();
1168
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1169
+ case 198:
1170
+ _5.sent();
1171
+ return [4 /*yield*/, (0, testing_1.async_test)('O4. request_linked_account_access writes a pending entry once toggle is on', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { var _a; return ((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).some(function (e) { return e.userId === nonAdminId && e.status === 'pending'; }); } })
1172
+ // ============================================================
1173
+ // I. Cross-cutting / regressions
1174
+ // ============================================================
1175
+ ];
1176
+ case 199:
1177
+ _5.sent();
1178
+ // ============================================================
1179
+ // I. Cross-cutting / regressions
1180
+ // ============================================================
1181
+ (0, testing_1.log_header)("I. Cross-cutting / regressions");
1182
+ return [4 /*yield*/, (0, testing_1.async_test)('I1. Legacy users.updateOne({ accountAccessGrantedTo: [...] }) rejected', function () { return sdk.api.users.updateOne(adminId, { accountAccessGrantedTo: [nonAdminId] }); }, { shouldError: true, onError: function (e) { return e.statusCode === 400 || /(accountAccessGrantedTo|legacy|replaced|No updates provided)/i.test(e.message || ''); } })];
1183
+ case 200:
1184
+ _5.sent();
1185
+ return [4 /*yield*/, (0, testing_1.async_test)('I2. After full revoke, get_linked_accounts no longer shows A', function () { return __awaiter(void 0, void 0, void 0, function () {
1186
+ var r;
1187
+ var _a;
1188
+ return __generator(this, function (_b) {
1189
+ switch (_b.label) {
1190
+ case 0: return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
1191
+ case 1:
1192
+ _b.sent();
1193
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1194
+ case 2:
1195
+ _b.sent();
1196
+ return [4 /*yield*/, sdkNonAdmin.api.users.get_linked_accounts()];
1197
+ case 3:
1198
+ r = _b.sent();
1199
+ return [2 /*return*/, ((_a = r.linkedAccounts) !== null && _a !== void 0 ? _a : []).find(function (a) { return a.id === adminId; })];
1200
+ }
1201
+ });
1202
+ }); }, { shouldError: false, onResult: function (r) { return r === undefined; } })
1203
+ // I4. Accepted-grant expiration semantics — lock in the chosen behavior.
1204
+ // The switch handler does NOT check requestExpiresAt for accepted entries: that field
1205
+ // only governs the *pending* TTL. If this changes, this test will fail; that's the
1206
+ // signal to make the behavior choice deliberately. (Simulating an actually-expired
1207
+ // accepted grant requires either waiting 7 days or mutating requestExpiresAt — the
1208
+ // schema validator blocks the latter, so we lock in the behavior by inspection.)
1209
+ // I2 above did clear_linkedAccountAccess → wait > 1s past the resulting revocation key
1210
+ // before re-granting; otherwise is_logged_in's 1s slack rejects the new switched token.
1211
+ ];
1212
+ case 201:
1213
+ _5.sent();
1214
+ // I4. Accepted-grant expiration semantics — lock in the chosen behavior.
1215
+ // The switch handler does NOT check requestExpiresAt for accepted entries: that field
1216
+ // only governs the *pending* TTL. If this changes, this test will fail; that's the
1217
+ // signal to make the behavior choice deliberately. (Simulating an actually-expired
1218
+ // accepted grant requires either waiting 7 days or mutating requestExpiresAt — the
1219
+ // schema validator blocks the latter, so we lock in the behavior by inspection.)
1220
+ // I2 above did clear_linkedAccountAccess → wait > 1s past the resulting revocation key
1221
+ // before re-granting; otherwise is_logged_in's 1s slack rejects the new switched token.
1222
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
1223
+ case 202:
1224
+ // I4. Accepted-grant expiration semantics — lock in the chosen behavior.
1225
+ // The switch handler does NOT check requestExpiresAt for accepted entries: that field
1226
+ // only governs the *pending* TTL. If this changes, this test will fail; that's the
1227
+ // signal to make the behavior choice deliberately. (Simulating an actually-expired
1228
+ // accepted grant requires either waiting 7 days or mutating requestExpiresAt — the
1229
+ // schema validator blocks the latter, so we lock in the behavior by inspection.)
1230
+ // I2 above did clear_linkedAccountAccess → wait > 1s past the resulting revocation key
1231
+ // before re-granting; otherwise is_logged_in's 1s slack rejects the new switched token.
1232
+ _5.sent();
1233
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
1234
+ case 203:
1235
+ _5.sent();
1236
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1237
+ case 204:
1238
+ _5.sent();
1239
+ return [4 /*yield*/, get_user(sdk, adminId)];
1240
+ case 205:
1241
+ i4State = _5.sent();
1242
+ i4Pending = ((_z = i4State.linkedAccountAccess) !== null && _z !== void 0 ? _z : []).find(function (e) { return e.userId === nonAdminId; });
1243
+ if (!i4Pending) return [3 /*break*/, 208];
1244
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, i4Pending), { status: 'accepted' })])];
1245
+ case 206:
1246
+ _5.sent();
1247
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1248
+ case 207:
1249
+ _5.sent();
1250
+ _5.label = 208;
1251
+ case 208: return [4 /*yield*/, get_user(sdk, adminId)];
1252
+ case 209:
1253
+ i4Accepted = _5.sent();
1254
+ i4Entry = ((_0 = i4Accepted.linkedAccountAccess) !== null && _0 !== void 0 ? _0 : []).find(function (e) { return e.userId === nonAdminId; });
1255
+ return [4 /*yield*/, (0, testing_1.async_test)('I4. Switch succeeds on a future-dated accepted grant (expiration-ignored semantics locked in by inspection)', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) { return (typeof r.authToken === 'string'
1256
+ && r.authToken.length > 0
1257
+ && !!i4Entry
1258
+ && new Date(i4Entry.requestExpiresAt).getTime() > Date.now()); } })
1259
+ // Re-auth nonAdmin since the switch invalidated its source token
1260
+ ];
1261
+ case 210:
1262
+ _5.sent();
1263
+ // Re-auth nonAdmin since the switch invalidated its source token
1264
+ return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
1265
+ // ============================================================
1266
+ // K. Actor-identity model (chained-switch + switch-back semantics)
1267
+ // ============================================================
1268
+ // Premise: the real operator is always the actor (session.actorUserId || session.id).
1269
+ // Grant checks, audit attribution, and request authorship all derive from realActor —
1270
+ // never from the proxy identity. Verified here via:
1271
+ // - get_linked_accounts returns the actor's grants + the actor's own account (switch-back).
1272
+ // - Validator rejects ALL linkedAccountAccess writes from switched sessions.
1273
+ // - request_linked_account_access uses actor's email for self-check (proxy-email no-op
1274
+ // would otherwise create a self-targeted pending entry).
1275
+ // - switch_account back to the actor mints a JWT with actor* claims cleared.
1276
+ // - Audit log for switch-back records event=account_switch_back with proxySessionId.
1277
+ ];
1278
+ case 211:
1279
+ // Re-auth nonAdmin since the switch invalidated its source token
1280
+ _5.sent();
1281
+ // ============================================================
1282
+ // K. Actor-identity model (chained-switch + switch-back semantics)
1283
+ // ============================================================
1284
+ // Premise: the real operator is always the actor (session.actorUserId || session.id).
1285
+ // Grant checks, audit attribution, and request authorship all derive from realActor —
1286
+ // never from the proxy identity. Verified here via:
1287
+ // - get_linked_accounts returns the actor's grants + the actor's own account (switch-back).
1288
+ // - Validator rejects ALL linkedAccountAccess writes from switched sessions.
1289
+ // - request_linked_account_access uses actor's email for self-check (proxy-email no-op
1290
+ // would otherwise create a self-targeted pending entry).
1291
+ // - switch_account back to the actor mints a JWT with actor* claims cleared.
1292
+ // - Audit log for switch-back records event=account_switch_back with proxySessionId.
1293
+ (0, testing_1.log_header)("K. Actor-identity model");
1294
+ // Seed an accepted grant so nonAdmin can switch into admin. Wait > 1s past the prior
1295
+ // clear_linkedAccountAccess so is_logged_in's iat-vs-revoked-key slack doesn't trip.
1296
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
1297
+ case 212:
1298
+ // Seed an accepted grant so nonAdmin can switch into admin. Wait > 1s past the prior
1299
+ // clear_linkedAccountAccess so is_logged_in's iat-vs-revoked-key slack doesn't trip.
1300
+ _5.sent();
1301
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
1302
+ case 213:
1303
+ _5.sent();
1304
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
1305
+ case 214:
1306
+ _5.sent();
1307
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1308
+ case 215:
1309
+ _5.sent();
1310
+ return [4 /*yield*/, get_user(sdk, adminId)];
1311
+ case 216:
1312
+ kSeedState = _5.sent();
1313
+ kPending = ((_1 = kSeedState.linkedAccountAccess) !== null && _1 !== void 0 ? _1 : []).find(function (e) { return e.userId === nonAdminId; });
1314
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, kPending), { status: 'accepted' })])];
1315
+ case 217:
1316
+ _5.sent();
1317
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
1318
+ // K1. Establish a switched session: nonAdmin → admin.
1319
+ ];
1320
+ case 218:
1321
+ _5.sent();
1322
+ kSwitchedToken = '';
1323
+ kSwitchedUser = null;
1324
+ return [4 /*yield*/, (0, testing_1.async_test)('K1. nonAdmin switches into admin (sets up actor-identity scenario)', function () { return sdkNonAdmin.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) {
1325
+ var _a;
1326
+ kSwitchedToken = r.authToken;
1327
+ kSwitchedUser = r.user;
1328
+ return typeof r.authToken === 'string' && r.authToken.length > 0 && ((_a = r.user) === null || _a === void 0 ? void 0 : _a.id) === adminId;
1329
+ } })];
1330
+ case 219:
1331
+ _5.sent();
1332
+ kSwitchedSdk = new sdk_1.Session({ host: host });
1333
+ kSwitchedSdk.setAuthToken(kSwitchedToken);
1334
+ kSwitchedSdk.setUserInfo(kSwitchedUser);
1335
+ // K2. get_linked_accounts from a switched session returns the actor's own account first
1336
+ // (the switch-back entry) and excludes the current proxy identity (admin) — querying
1337
+ // against realActor=nonAdmin would normally surface admin (it has nonAdmin: accepted),
1338
+ // but the caller IS already admin so switching there would no-op. K8b separately covers
1339
+ // the case where the list DOES include actor-grants that aren't the current proxy.
1340
+ return [4 /*yield*/, (0, testing_1.async_test)('K2. get_linked_accounts from switched session: actor first, current proxy excluded', function () { return kSwitchedSdk.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) {
1341
+ var _a;
1342
+ var accounts = ((_a = r === null || r === void 0 ? void 0 : r.linkedAccounts) !== null && _a !== void 0 ? _a : []);
1343
+ if (accounts.length === 0)
1344
+ return false;
1345
+ if (accounts[0].id !== nonAdminId)
1346
+ return false;
1347
+ // Current proxy (admin) must NOT appear, even though admin granted nonAdmin.
1348
+ var hasSelfProxy = accounts.some(function (a) { return a.id === adminId; });
1349
+ return !hasSelfProxy;
1350
+ } })
1351
+ // K3. Validator rejects ALL linkedAccountAccess PATCHes from a switched session — even
1352
+ // ones the proxy identity (admin = ownerId) would normally be authorized to make. Closes
1353
+ // the self-approval / silent-revoke hole. Use the marker-tag helper to ensure a unique
1354
+ // payload (avoid the 3/30s identical-update rate limit colliding with this case).
1355
+ ];
1356
+ case 220:
1357
+ // K2. get_linked_accounts from a switched session returns the actor's own account first
1358
+ // (the switch-back entry) and excludes the current proxy identity (admin) — querying
1359
+ // against realActor=nonAdmin would normally surface admin (it has nonAdmin: accepted),
1360
+ // but the caller IS already admin so switching there would no-op. K8b separately covers
1361
+ // the case where the list DOES include actor-grants that aren't the current proxy.
1362
+ _5.sent();
1363
+ // K3. Validator rejects ALL linkedAccountAccess PATCHes from a switched session — even
1364
+ // ones the proxy identity (admin = ownerId) would normally be authorized to make. Closes
1365
+ // the self-approval / silent-revoke hole. Use the marker-tag helper to ensure a unique
1366
+ // payload (avoid the 3/30s identical-update rate limit colliding with this case).
1367
+ return [4 /*yield*/, (0, testing_1.async_test)('K3. Validator rejects linkedAccountAccess PATCH from switched session', function () { return set_linkedAccountAccess(kSwitchedSdk, adminId, [__assign(__assign({}, kPending), { status: 'accepted' })]); }, { shouldError: true, onError: function (e) { return (e.statusCode === 400
1368
+ || /(switched session|actorUserId|while acting)/i.test(e.message || '')); } })
1369
+ // K4. request_linked_account_access from switched session uses the actor's email for the
1370
+ // self-check. Calling with targetEmail = nonAdminEmail (the actor's own email) → silent
1371
+ // {} and NO write. Old behavior would have used session.email=adminEmail for the self-
1372
+ // check, mismatched, looked up nonAdmin, and created a pending entry on nonAdmin.
1373
+ ];
1374
+ case 221:
1375
+ // K3. Validator rejects ALL linkedAccountAccess PATCHes from a switched session — even
1376
+ // ones the proxy identity (admin = ownerId) would normally be authorized to make. Closes
1377
+ // the self-approval / silent-revoke hole. Use the marker-tag helper to ensure a unique
1378
+ // payload (avoid the 3/30s identical-update rate limit colliding with this case).
1379
+ _5.sent();
1380
+ // K4. request_linked_account_access from switched session uses the actor's email for the
1381
+ // self-check. Calling with targetEmail = nonAdminEmail (the actor's own email) → silent
1382
+ // {} and NO write. Old behavior would have used session.email=adminEmail for the self-
1383
+ // check, mismatched, looked up nonAdmin, and created a pending entry on nonAdmin.
1384
+ return [4 /*yield*/, (0, testing_1.async_test)('K4. request_linked_account_access from switched session uses actor email for self-check', function () { return kSwitchedSdk.api.users.request_linked_account_access({ targetEmail: nonAdminEmail }); }, passOnAnyResult)];
1385
+ case 222:
1386
+ // K4. request_linked_account_access from switched session uses the actor's email for the
1387
+ // self-check. Calling with targetEmail = nonAdminEmail (the actor's own email) → silent
1388
+ // {} and NO write. Old behavior would have used session.email=adminEmail for the self-
1389
+ // check, mismatched, looked up nonAdmin, and created a pending entry on nonAdmin.
1390
+ _5.sent();
1391
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1392
+ case 223:
1393
+ _5.sent();
1394
+ return [4 /*yield*/, (0, testing_1.async_test)('K4. No pending entry created on actor record (actor identity is the requester)', function () { return get_user(sdk, nonAdminId); }, { shouldError: false, onResult: function (u) { var _a; return !((_a = u.linkedAccountAccess) !== null && _a !== void 0 ? _a : []).length; } })
1395
+ // K5. switch-back: nonAdmin (acting as admin) returns to nonAdmin. No grant lookup
1396
+ // (target === realActor); resulting JWT has all actor* claims cleared.
1397
+ ];
1398
+ case 224:
1399
+ _5.sent();
1400
+ kBackToken = '';
1401
+ return [4 /*yield*/, (0, testing_1.async_test)('K5. Switch-back to actor succeeds (no grant lookup required)', function () { return kSwitchedSdk.api.users.switch_account({ targetUserId: nonAdminId }); }, { shouldError: false, onResult: function (r) {
1402
+ var _a;
1403
+ kBackToken = r.authToken;
1404
+ return typeof r.authToken === 'string' && r.authToken.length > 0 && ((_a = r.user) === null || _a === void 0 ? void 0 : _a.id) === nonAdminId;
1405
+ } })];
1406
+ case 225:
1407
+ _5.sent();
1408
+ kBackDecoded = decode_jwt(kBackToken);
1409
+ (0, testing_1.assert)((kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.id) === nonAdminId, "JWT.id ".concat(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.id, " != ").concat(nonAdminId), 'K5. JWT.id == actor');
1410
+ (0, testing_1.assert)(!(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.actorUserId), "JWT still carries actorUserId=".concat(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.actorUserId, " after switch-back"), 'K5. JWT.actorUserId cleared');
1411
+ (0, testing_1.assert)(!(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.actorEmail), "JWT still carries actorEmail after switch-back", 'K5. JWT.actorEmail cleared');
1412
+ (0, testing_1.assert)(!(kBackDecoded === null || kBackDecoded === void 0 ? void 0 : kBackDecoded.actorBusinessId), "JWT still carries actorBusinessId after switch-back", 'K5. JWT.actorBusinessId cleared');
1413
+ kBackSdk = new sdk_1.Session({ host: host });
1414
+ kBackSdk.setAuthToken(kBackToken);
1415
+ return [4 /*yield*/, (0, testing_1.async_test)('K5b. Switched-back token authenticates', function () { return kBackSdk.test_authenticated(); }, { shouldError: false, onResult: function (r) { return r === 'Authenticated!'; } })
1416
+ // K6. Audit log for the switch-back: event=account_switch_back, userId=realActor (nonAdmin),
1417
+ // proxySessionId=admin (the proxy identity that issued the request).
1418
+ ];
1419
+ case 226:
1420
+ _5.sent();
1421
+ // K6. Audit log for the switch-back: event=account_switch_back, userId=realActor (nonAdmin),
1422
+ // proxySessionId=admin (the proxy identity that issued the request).
1423
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
1424
+ case 227:
1425
+ // K6. Audit log for the switch-back: event=account_switch_back, userId=realActor (nonAdmin),
1426
+ // proxySessionId=admin (the proxy identity that issued the request).
1427
+ _5.sent();
1428
+ return [4 /*yield*/, (0, testing_1.async_test)('K6. user_logs has account_switch_back event with realActor as userId + proxySessionId', function () { return sdk.api.user_logs.getSome({ filter: { resourceId: nonAdminId, resource: 'users', action: 'update' } }); }, { shouldError: false, onResult: function (logs) { return (logs !== null && logs !== void 0 ? logs : []).some(function (l) {
1429
+ var _a;
1430
+ var info = (_a = l === null || l === void 0 ? void 0 : l.info) !== null && _a !== void 0 ? _a : {};
1431
+ return (l === null || l === void 0 ? void 0 : l.userId) === nonAdminId
1432
+ && info.event === 'account_switch_back'
1433
+ && info.sourceUserId === nonAdminId
1434
+ && info.proxySessionId === adminId
1435
+ && info.targetUserId === nonAdminId;
1436
+ }); } })
1437
+ // K7. From the now-clean nonAdmin session (kBackSdk, minted by the K5 switch-back),
1438
+ // switching back INTO admin works normally — grant unchanged, chain restarted with no
1439
+ // leftover actor* state. Use kBackSdk because sdkNonAdmin's original token was invalidated
1440
+ // by the K1 switch and we haven't re-authed it yet.
1441
+ ];
1442
+ case 228:
1443
+ _5.sent();
1444
+ // K7. From the now-clean nonAdmin session (kBackSdk, minted by the K5 switch-back),
1445
+ // switching back INTO admin works normally — grant unchanged, chain restarted with no
1446
+ // leftover actor* state. Use kBackSdk because sdkNonAdmin's original token was invalidated
1447
+ // by the K1 switch and we haven't re-authed it yet.
1448
+ return [4 /*yield*/, (0, testing_1.async_test)('K7. Clean nonAdmin session can mint a fresh switch into admin (chain restarted)', function () { return kBackSdk.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: false, onResult: function (r) { return typeof r.authToken === 'string' && r.authToken.length > 0; } })
1449
+ // Re-auth nonAdmin: its original token died in K1, and kBackSdk's token died in K7.
1450
+ ];
1451
+ case 229:
1452
+ // K7. From the now-clean nonAdmin session (kBackSdk, minted by the K5 switch-back),
1453
+ // switching back INTO admin works normally — grant unchanged, chain restarted with no
1454
+ // leftover actor* state. Use kBackSdk because sdkNonAdmin's original token was invalidated
1455
+ // by the K1 switch and we haven't re-authed it yet.
1456
+ _5.sent();
1457
+ // Re-auth nonAdmin: its original token died in K1, and kBackSdk's token died in K7.
1458
+ return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
1459
+ // ============================================================
1460
+ // K8. Chained switch A→B→C preserves the original actor (not the proxy)
1461
+ // ============================================================
1462
+ // Spec: from a switched A-as-B session, switching to C must mint a JWT with
1463
+ // actorUserId=A — never actorUserId=B. The B hop is a UI affordance and must not become
1464
+ // the new "actor" on subsequent switches. Requires a third user C with a direct grant
1465
+ // from C to A; admin creates C and uses generate_auth_token to bootstrap a C-session
1466
+ // for accepting the grant.
1467
+ ];
1468
+ case 230:
1469
+ // Re-auth nonAdmin: its original token died in K1, and kBackSdk's token died in K7.
1470
+ _5.sent();
1471
+ // ============================================================
1472
+ // K8. Chained switch A→B→C preserves the original actor (not the proxy)
1473
+ // ============================================================
1474
+ // Spec: from a switched A-as-B session, switching to C must mint a JWT with
1475
+ // actorUserId=A — never actorUserId=B. The B hop is a UI affordance and must not become
1476
+ // the new "actor" on subsequent switches. Requires a third user C with a direct grant
1477
+ // from C to A; admin creates C and uses generate_auth_token to bootstrap a C-session
1478
+ // for accepting the grant.
1479
+ (0, testing_1.log_header)("K8. Chained switching preserves original actor");
1480
+ userCEmail = "switch-c-".concat(RAND(), "@tellescope.example");
1481
+ return [4 /*yield*/, sdk.api.users.createOne({
1482
+ email: userCEmail,
1483
+ fname: 'Chained',
1484
+ lname: 'Target',
1485
+ notificationEmailsDisabled: true,
1486
+ verifiedEmail: true,
1487
+ })];
1488
+ case 231:
1489
+ userCRecord = _5.sent();
1490
+ userCId = userCRecord.id;
1491
+ return [4 /*yield*/, sdk.api.users.generate_auth_token({ id: userCId })];
1492
+ case 232:
1493
+ userCToken = (_5.sent()).authToken;
1494
+ sdkC = new sdk_1.Session({ host: host });
1495
+ sdkC.setAuthToken(userCToken);
1496
+ // A (nonAdmin) requests access to C; C accepts (validator requires session.id === ownerId).
1497
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: userCEmail })];
1498
+ case 233:
1499
+ // A (nonAdmin) requests access to C; C accepts (validator requires session.id === ownerId).
1500
+ _5.sent();
1501
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1502
+ case 234:
1503
+ _5.sent();
1504
+ return [4 /*yield*/, get_user(sdkC, userCId)];
1505
+ case 235:
1506
+ userCState = _5.sent();
1507
+ pendingForC = ((_2 = userCState.linkedAccountAccess) !== null && _2 !== void 0 ? _2 : []).find(function (e) { return e.userId === nonAdminId; });
1508
+ (0, testing_1.assert)(!!pendingForC, 'K8 setup: pending entry should exist on C from nonAdmin', 'K8 setup: pending on C');
1509
+ return [4 /*yield*/, set_linkedAccountAccess(sdkC, userCId, [__assign(__assign({}, pendingForC), { status: 'accepted' })])];
1510
+ case 236:
1511
+ _5.sent();
1512
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
1513
+ // Establish the A-as-B switched session. The admin→nonAdmin accepted grant from K1 is
1514
+ // still in place (K7's switch consumed a slot but didn't remove the grant).
1515
+ ];
1516
+ case 237:
1517
+ _5.sent();
1518
+ return [4 /*yield*/, sdkNonAdmin.api.users.switch_account({ targetUserId: adminId })];
1519
+ case 238:
1520
+ aAsBResp = _5.sent();
1521
+ aAsBSdk = new sdk_1.Session({ host: host });
1522
+ aAsBSdk.setAuthToken(aAsBResp.authToken);
1523
+ aAsBSdk.setUserInfo(aAsBResp.user);
1524
+ aAsBDecoded = decode_jwt(aAsBResp.authToken);
1525
+ (0, testing_1.assert)((aAsBDecoded === null || aAsBDecoded === void 0 ? void 0 : aAsBDecoded.id) === adminId && (aAsBDecoded === null || aAsBDecoded === void 0 ? void 0 : aAsBDecoded.actorUserId) === nonAdminId, "K8 setup: A-as-B token should carry id=admin, actorUserId=nonAdmin (got id=".concat(aAsBDecoded === null || aAsBDecoded === void 0 ? void 0 : aAsBDecoded.id, ", actorUserId=").concat(aAsBDecoded === null || aAsBDecoded === void 0 ? void 0 : aAsBDecoded.actorUserId, ")"), 'K8 setup: A-as-B JWT confirmed');
1526
+ chainedToken = '';
1527
+ return [4 /*yield*/, (0, testing_1.async_test)('K8. Chained switch A→B→C succeeds when C granted access to the actor', function () { return aAsBSdk.api.users.switch_account({ targetUserId: userCId }); }, { shouldError: false, onResult: function (r) {
1528
+ var _a;
1529
+ chainedToken = r.authToken;
1530
+ return typeof r.authToken === 'string' && r.authToken.length > 0 && ((_a = r.user) === null || _a === void 0 ? void 0 : _a.id) === userCId;
1531
+ } })];
1532
+ case 239:
1533
+ _5.sent();
1534
+ chainedDecoded = decode_jwt(chainedToken);
1535
+ (0, testing_1.assert)((chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.id) === userCId, "chained JWT.id ".concat(chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.id, " != ").concat(userCId), 'K8. chained JWT.id == C');
1536
+ (0, testing_1.assert)((chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorUserId) === nonAdminId, "chained JWT.actorUserId is ".concat(chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorUserId, " \u2014 must remain nonAdmin (the original actor), NOT admin (the proxy hop)"), 'K8. chained JWT.actorUserId == A (original actor preserved, NOT B)');
1537
+ (0, testing_1.assert)((chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorEmail) === nonAdminEmail, "chained JWT.actorEmail ".concat(chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorEmail, " != ").concat(nonAdminEmail), 'K8. chained JWT.actorEmail preserved');
1538
+ (0, testing_1.assert)((chainedDecoded === null || chainedDecoded === void 0 ? void 0 : chainedDecoded.actorBusinessId) === nonAdminBusinessId, "chained JWT.actorBusinessId mismatch", 'K8. chained JWT.actorBusinessId preserved');
1539
+ // Audit log: the chained switch's user_log must attribute to the original actor (nonAdmin),
1540
+ // and record proxySessionId=adminId so the B-hop is reconstructable.
1541
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 500)];
1542
+ case 240:
1543
+ // Audit log: the chained switch's user_log must attribute to the original actor (nonAdmin),
1544
+ // and record proxySessionId=adminId so the B-hop is reconstructable.
1545
+ _5.sent();
1546
+ return [4 /*yield*/, (0, testing_1.async_test)('K8. Chained switch audit log attributes to actor with proxySessionId=B', function () { return sdk.api.user_logs.getSome({ filter: { resourceId: userCId, resource: 'users', action: 'update' } }); }, { shouldError: false, onResult: function (logs) { return (logs !== null && logs !== void 0 ? logs : []).some(function (l) {
1547
+ var _a;
1548
+ var info = (_a = l === null || l === void 0 ? void 0 : l.info) !== null && _a !== void 0 ? _a : {};
1549
+ return (l === null || l === void 0 ? void 0 : l.userId) === nonAdminId
1550
+ && info.event === 'account_switch'
1551
+ && info.sourceUserId === nonAdminId
1552
+ && info.proxySessionId === adminId
1553
+ && info.targetUserId === userCId;
1554
+ }); } })
1555
+ // K8b. From the chained C-session, get_linked_accounts must reflect the ACTOR's
1556
+ // perspective: actor's own account first (switch-back), plus all accounts that have
1557
+ // granted access to the actor (B/admin). The current proxy identity (C) must NOT appear
1558
+ // — caller is already in that session.
1559
+ ];
1560
+ case 241:
1561
+ _5.sent();
1562
+ chainedSdk = new sdk_1.Session({ host: host });
1563
+ chainedSdk.setAuthToken(chainedToken);
1564
+ return [4 /*yield*/, (0, testing_1.async_test)('K8b. get_linked_accounts from chained C-session reflects actor + actor-grants, excludes current proxy', function () { return chainedSdk.api.users.get_linked_accounts(); }, { shouldError: false, onResult: function (r) {
1565
+ var _a;
1566
+ var accounts = ((_a = r === null || r === void 0 ? void 0 : r.linkedAccounts) !== null && _a !== void 0 ? _a : []);
1567
+ if (accounts.length === 0)
1568
+ return false;
1569
+ // Switch-back entry (actor's own account) is first.
1570
+ if (accounts[0].id !== nonAdminId)
1571
+ return false;
1572
+ // Admin appears because admin has nonAdmin: accepted in linkedAccountAccess.
1573
+ var hasAdmin = accounts.some(function (a) { return a.id === adminId; });
1574
+ // The current proxy identity (userC) must NOT appear — it's not switchable from itself.
1575
+ var hasSelfProxy = accounts.some(function (a) { return a.id === userCId; });
1576
+ return hasAdmin && !hasSelfProxy;
1577
+ } })
1578
+ // Cleanup K8: delete C (also clears the linkedAccountAccess that the chained switch
1579
+ // consumed), and re-auth nonAdmin since the K8 setup switch invalidated its token.
1580
+ ];
1581
+ case 242:
1582
+ _5.sent();
1583
+ _5.label = 243;
1584
+ case 243:
1585
+ _5.trys.push([243, 245, , 246]);
1586
+ return [4 /*yield*/, sdk.api.users.deleteOne(userCId)];
1587
+ case 244:
1588
+ _5.sent();
1589
+ return [3 /*break*/, 246];
1590
+ case 245:
1591
+ _j = _5.sent();
1592
+ return [3 /*break*/, 246];
1593
+ case 246: return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
1594
+ // ============================================================
1595
+ // L. linkedAccountAccess read-side redaction (owner-only metadata)
1596
+ // ============================================================
1597
+ // The grant list reveals who's been requesting access to whom. Only the owner needs to
1598
+ // see it (to act on pending requests / inspect accepted grants). Cross-user reads,
1599
+ // switched-session reads, and the switch_account response.user must all redact the field.
1600
+ ];
1601
+ case 247:
1602
+ _5.sent();
1603
+ // ============================================================
1604
+ // L. linkedAccountAccess read-side redaction (owner-only metadata)
1605
+ // ============================================================
1606
+ // The grant list reveals who's been requesting access to whom. Only the owner needs to
1607
+ // see it (to act on pending requests / inspect accepted grants). Cross-user reads,
1608
+ // switched-session reads, and the switch_account response.user must all redact the field.
1609
+ (0, testing_1.log_header)("L. linkedAccountAccess read-side redaction");
1610
+ // Seed an accepted grant so admin's record has a non-empty linkedAccountAccess for the
1611
+ // redaction assertions to be meaningful (a missing field is indistinguishable from a
1612
+ // redacted-empty field otherwise).
1613
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
1614
+ case 248:
1615
+ // Seed an accepted grant so admin's record has a non-empty linkedAccountAccess for the
1616
+ // redaction assertions to be meaningful (a missing field is indistinguishable from a
1617
+ // redacted-empty field otherwise).
1618
+ _5.sent();
1619
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 1500)];
1620
+ case 249:
1621
+ _5.sent();
1622
+ return [4 /*yield*/, sdkNonAdmin.api.users.request_linked_account_access({ targetEmail: adminEmail })];
1623
+ case 250:
1624
+ _5.sent();
1625
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)];
1626
+ case 251:
1627
+ _5.sent();
1628
+ return [4 /*yield*/, get_user(sdk, adminId)];
1629
+ case 252:
1630
+ lSeedState = _5.sent();
1631
+ lPending = ((_3 = lSeedState.linkedAccountAccess) !== null && _3 !== void 0 ? _3 : []).find(function (e) { return e.userId === nonAdminId; });
1632
+ return [4 /*yield*/, set_linkedAccountAccess(sdk, adminId, [__assign(__assign({}, lPending), { status: 'accepted' })])];
1633
+ case 253:
1634
+ _5.sent();
1635
+ return [4 /*yield*/, (0, testing_1.wait)(undefined, 250)
1636
+ // L1. Owner reading own record from a non-switched session: field IS visible.
1637
+ ];
1638
+ case 254:
1639
+ _5.sent();
1640
+ // L1. Owner reading own record from a non-switched session: field IS visible.
1641
+ return [4 /*yield*/, (0, testing_1.async_test)('L1. Owner reading own record sees linkedAccountAccess', function () { return get_user(sdk, adminId); }, { shouldError: false, onResult: function (u) { return Array.isArray(u === null || u === void 0 ? void 0 : u.linkedAccountAccess) && u.linkedAccountAccess.length > 0; } })
1642
+ // L2. Cross-user read (non-admin reads admin): field is redacted.
1643
+ ];
1644
+ case 255:
1645
+ // L1. Owner reading own record from a non-switched session: field IS visible.
1646
+ _5.sent();
1647
+ // L2. Cross-user read (non-admin reads admin): field is redacted.
1648
+ return [4 /*yield*/, (0, testing_1.async_test)('L2. Cross-user read (nonAdmin reads admin) redacts linkedAccountAccess', function () { return get_user(sdkNonAdmin, adminId); }, { shouldError: false, onResult: function (u) { return (u === null || u === void 0 ? void 0 : u.linkedAccountAccess) === undefined; } })
1649
+ // L3. Switched session reading the proxy's own record: still redacted (session.actorUserId
1650
+ // is set, so callerIsRealOwner is false even though value.id === session.id).
1651
+ ];
1652
+ case 256:
1653
+ // L2. Cross-user read (non-admin reads admin): field is redacted.
1654
+ _5.sent();
1655
+ return [4 /*yield*/, sdkNonAdmin.api.users.switch_account({ targetUserId: adminId })];
1656
+ case 257:
1657
+ lSwitchResp = _5.sent();
1658
+ lSwitchedSdk = new sdk_1.Session({ host: host });
1659
+ lSwitchedSdk.setAuthToken(lSwitchResp.authToken);
1660
+ return [4 /*yield*/, (0, testing_1.async_test)('L3. Switched session reading proxy record (admin) redacts linkedAccountAccess', function () { return get_user(lSwitchedSdk, adminId); }, { shouldError: false, onResult: function (u) { return (u === null || u === void 0 ? void 0 : u.linkedAccountAccess) === undefined; } })
1661
+ // L4. switch_account response.user does NOT include linkedAccountAccess (the response
1662
+ // bypasses applyRedactions, so the handler strips the field explicitly).
1663
+ ];
1664
+ case 258:
1665
+ _5.sent();
1666
+ // L4. switch_account response.user does NOT include linkedAccountAccess (the response
1667
+ // bypasses applyRedactions, so the handler strips the field explicitly).
1668
+ (0, testing_1.assert)((lSwitchResp === null || lSwitchResp === void 0 ? void 0 : lSwitchResp.user) && lSwitchResp.user.id === adminId && lSwitchResp.user.linkedAccountAccess === undefined, "switch_account response.user.linkedAccountAccess should be undefined; got ".concat(JSON.stringify((_4 = lSwitchResp === null || lSwitchResp === void 0 ? void 0 : lSwitchResp.user) === null || _4 === void 0 ? void 0 : _4.linkedAccountAccess)), 'L4. switch_account response.user has linkedAccountAccess redacted');
1669
+ return [4 /*yield*/, lSwitchedSdk.api.users.switch_account({ targetUserId: nonAdminId })];
1670
+ case 259:
1671
+ lBackResp = _5.sent();
1672
+ lBackSdk = new sdk_1.Session({ host: host });
1673
+ lBackSdk.setAuthToken(lBackResp.authToken);
1674
+ return [4 /*yield*/, (0, testing_1.async_test)('L5. Owner-in-real-session can read their own linkedAccountAccess after switch-back', function () { return get_user(lBackSdk, nonAdminId); }, { shouldError: false, onResult: function (u) { return Array.isArray(u === null || u === void 0 ? void 0 : u.linkedAccountAccess); } })
1675
+ // L cleanup: re-auth nonAdmin (lBackSdk's token wasn't invalidated, but we want a clean
1676
+ // sdkNonAdmin for the remaining sections).
1677
+ ];
1678
+ case 260:
1679
+ _5.sent();
1680
+ // L cleanup: re-auth nonAdmin (lBackSdk's token wasn't invalidated, but we want a clean
1681
+ // sdkNonAdmin for the remaining sections).
1682
+ return [4 /*yield*/, sdkNonAdmin.authenticate(NON_ADMIN_EMAIL, NON_ADMIN_PASSWORD)
1683
+ // ============================================================
1684
+ // M. Enduser sessions are rejected on user-only endpoints
1685
+ // ============================================================
1686
+ // The three new endpoints are registered as customActions on the `users` model with no
1687
+ // `allowEnduser` and no `enduserAction` declared. With only the user-type auth path active,
1688
+ // is_logged_in's type check (authentication.ts:587 — `if (userInfo.type !== type) return false`)
1689
+ // rejects the enduser JWT and checkAccess returns 401 "Unauthenticated" before businessOnly
1690
+ // even runs. Tests here are negative assertions guarding against future drift — e.g. if
1691
+ // someone adds `allowEnduser` to a custom action by accident.
1692
+ ];
1693
+ case 261:
1694
+ // L cleanup: re-auth nonAdmin (lBackSdk's token wasn't invalidated, but we want a clean
1695
+ // sdkNonAdmin for the remaining sections).
1696
+ _5.sent();
1697
+ // ============================================================
1698
+ // M. Enduser sessions are rejected on user-only endpoints
1699
+ // ============================================================
1700
+ // The three new endpoints are registered as customActions on the `users` model with no
1701
+ // `allowEnduser` and no `enduserAction` declared. With only the user-type auth path active,
1702
+ // is_logged_in's type check (authentication.ts:587 — `if (userInfo.type !== type) return false`)
1703
+ // rejects the enduser JWT and checkAccess returns 401 "Unauthenticated" before businessOnly
1704
+ // even runs. Tests here are negative assertions guarding against future drift — e.g. if
1705
+ // someone adds `allowEnduser` to a custom action by accident.
1706
+ (0, testing_1.log_header)("M. Enduser sessions rejected on user endpoints");
1707
+ enduserEmail = "switch-enduser-".concat(RAND(), "@tellescope.example");
1708
+ return [4 /*yield*/, sdk.api.endusers.createOne({
1709
+ email: enduserEmail,
1710
+ fname: 'Switch',
1711
+ lname: 'Enduser',
1712
+ })];
1713
+ case 262:
1714
+ enduserRec = _5.sent();
1715
+ return [4 /*yield*/, sdk.api.endusers.generate_auth_token({ id: enduserRec.id })
1716
+ // Use a plain Session with the enduser token so we can hit the user-only routes. The
1717
+ // EnduserSession's .api shape doesn't include these methods, but the underlying HTTP
1718
+ // routes do exist server-side — we want to confirm the server-side rejection fires
1719
+ // regardless of what client SDK is used.
1720
+ ];
1721
+ case 263:
1722
+ enduserAuthToken = (_5.sent()).authToken;
1723
+ sdkAsEnduser = new sdk_1.Session({ host: host });
1724
+ sdkAsEnduser.setAuthToken(enduserAuthToken);
1725
+ isEnduserRejection = is401Rejection;
1726
+ return [4 /*yield*/, (0, testing_1.async_test)('M1. Enduser session is rejected on get_linked_accounts', function () { return sdkAsEnduser.api.users.get_linked_accounts(); }, { shouldError: true, onError: isEnduserRejection })];
1727
+ case 264:
1728
+ _5.sent();
1729
+ return [4 /*yield*/, (0, testing_1.async_test)('M2. Enduser session is rejected on switch_account', function () { return sdkAsEnduser.api.users.switch_account({ targetUserId: adminId }); }, { shouldError: true, onError: isEnduserRejection })];
1730
+ case 265:
1731
+ _5.sent();
1732
+ return [4 /*yield*/, (0, testing_1.async_test)('M3. Enduser session is rejected on request_linked_account_access', function () { return sdkAsEnduser.api.users.request_linked_account_access({ targetEmail: adminEmail }); }, { shouldError: true, onError: isEnduserRejection })
1733
+ // Cleanup the inline enduser.
1734
+ ];
1735
+ case 266:
1736
+ _5.sent();
1737
+ _5.label = 267;
1738
+ case 267:
1739
+ _5.trys.push([267, 269, , 270]);
1740
+ return [4 /*yield*/, sdk.api.endusers.deleteOne(enduserRec.id)];
1741
+ case 268:
1742
+ _5.sent();
1743
+ return [3 /*break*/, 270];
1744
+ case 269:
1745
+ _k = _5.sent();
1746
+ return [3 /*break*/, 270];
1747
+ case 270:
1748
+ // ============================================================
1749
+ // C10. request_linked_account_access rate limit (placed last — exhausts admin's quota for 60s)
1750
+ // ============================================================
1751
+ (0, testing_1.log_header)("C10. request_linked_account_access rate limit (placed last)");
1752
+ i = 0;
1753
+ _5.label = 271;
1754
+ case 271:
1755
+ if (!(i < 30)) return [3 /*break*/, 276];
1756
+ _5.label = 272;
1757
+ case 272:
1758
+ _5.trys.push([272, 274, , 275]);
1759
+ return [4 /*yield*/, sdk.api.users.request_linked_account_access({ targetEmail: "rl-".concat(RAND(), "@tellescope.example") })];
1760
+ case 273:
1761
+ _5.sent();
1762
+ return [3 /*break*/, 275];
1763
+ case 274:
1764
+ _l = _5.sent();
1765
+ return [3 /*break*/, 275];
1766
+ case 275:
1767
+ i++;
1768
+ return [3 /*break*/, 271];
1769
+ case 276: return [4 /*yield*/, (0, testing_1.async_test)('C10. 31st request inside one minute is rate-limited (429)', function () { return sdk.api.users.request_linked_account_access({ targetEmail: "rl-".concat(RAND(), "@tellescope.example") }); }, { shouldError: true, onError: function (e) { return e.statusCode === 429 || (e.message || '').toLowerCase().includes('rate'); } })
1770
+ // ============================================================
1771
+ // J. Cleanup
1772
+ // ============================================================
1773
+ ];
1774
+ case 277:
1775
+ _5.sent();
1776
+ // ============================================================
1777
+ // J. Cleanup
1778
+ // ============================================================
1779
+ (0, testing_1.log_header)("J. Cleanup");
1780
+ return [4 /*yield*/, clear_linkedAccountAccess(sdk, adminId)];
1781
+ case 278:
1782
+ _5.sent();
1783
+ return [4 /*yield*/, clear_linkedAccountAccess(sdkNonAdmin, nonAdminId)];
1784
+ case 279:
1785
+ _5.sent();
1786
+ return [4 /*yield*/, cleanup_marker_tags(sdk, adminId)];
1787
+ case 280:
1788
+ _5.sent();
1789
+ return [4 /*yield*/, cleanup_marker_tags(sdkNonAdmin, nonAdminId)
1790
+ // Restore admin's fname (G5 mutated it server-side; downstream tests compare userInfo.fname to the server value).
1791
+ ];
1792
+ case 281:
1793
+ _5.sent();
1794
+ if (!originalAdminFname) return [3 /*break*/, 285];
1795
+ _5.label = 282;
1796
+ case 282:
1797
+ _5.trys.push([282, 284, , 285]);
1798
+ return [4 /*yield*/, sdk.api.users.updateOne(adminId, { fname: originalAdminFname })];
1799
+ case 283:
1800
+ _5.sent();
1801
+ return [3 /*break*/, 285];
1802
+ case 284:
1803
+ _m = _5.sent();
1804
+ return [3 /*break*/, 285];
1805
+ case 285: return [2 /*return*/];
413
1806
  }
414
1807
  });
415
1808
  });
416
1809
  };
417
1810
  exports.account_switcher_tests = account_switcher_tests;
1811
+ // Allow running this test file independently
418
1812
  if (require.main === module) {
419
- console.log("\uD83C\uDF10 Using API URL: ".concat(host));
1813
+ console.log("Using API URL: ".concat(host));
420
1814
  var sdk_2 = new sdk_1.Session({ host: host });
421
1815
  var sdkNonAdmin_1 = new sdk_1.Session({ host: host });
422
1816
  var runTests = function () { return __awaiter(void 0, void 0, void 0, function () {
@@ -434,11 +1828,11 @@ if (require.main === module) {
434
1828
  }); };
435
1829
  runTests()
436
1830
  .then(function () {
437
- console.log("Account switcher test suite completed successfully");
1831
+ console.log("Account switcher test suite completed successfully");
438
1832
  process.exit(0);
439
1833
  })
440
1834
  .catch(function (error) {
441
- console.error("Account switcher test suite failed:", error);
1835
+ console.error("Account switcher test suite failed:", error);
442
1836
  process.exit(1);
443
1837
  });
444
1838
  }