@tellescope/sdk 1.255.19 → 1.256.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 (82) hide show
  1. package/lib/cjs/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
  2. package/lib/cjs/tests/api_tests/custom_dashboards.test.js +99 -8
  3. package/lib/cjs/tests/api_tests/custom_dashboards.test.js.map +1 -1
  4. package/lib/cjs/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
  5. package/lib/cjs/tests/api_tests/default_summary_journey.test.js.map +1 -0
  6. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
  7. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
  8. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js +278 -0
  9. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
  10. package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
  11. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +212 -33
  12. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  13. package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
  14. package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
  15. package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
  16. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
  17. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
  18. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +242 -0
  19. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
  20. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
  21. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
  22. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +443 -0
  23. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
  24. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
  25. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
  26. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +231 -0
  27. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
  28. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -1
  29. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +524 -294
  30. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -1
  31. package/lib/cjs/tests/tests.d.ts.map +1 -1
  32. package/lib/cjs/tests/tests.js +431 -230
  33. package/lib/cjs/tests/tests.js.map +1 -1
  34. package/lib/esm/enduser.d.ts +0 -1
  35. package/lib/esm/enduser.d.ts.map +1 -1
  36. package/lib/esm/sdk.d.ts +2 -3
  37. package/lib/esm/sdk.d.ts.map +1 -1
  38. package/lib/esm/session.d.ts +0 -1
  39. package/lib/esm/session.d.ts.map +1 -1
  40. package/lib/esm/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
  41. package/lib/esm/tests/api_tests/custom_dashboards.test.js +99 -8
  42. package/lib/esm/tests/api_tests/custom_dashboards.test.js.map +1 -1
  43. package/lib/esm/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
  44. package/lib/esm/tests/api_tests/default_summary_journey.test.js.map +1 -0
  45. package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
  46. package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
  47. package/lib/esm/tests/api_tests/file_download_unicode_names.test.js +248 -0
  48. package/lib/esm/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
  49. package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
  50. package/lib/esm/tests/api_tests/openloop_webhooks.test.js +213 -34
  51. package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  52. package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
  53. package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
  54. package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
  55. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
  56. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
  57. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +238 -0
  58. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
  59. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
  60. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
  61. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +436 -0
  62. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
  63. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
  64. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
  65. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +224 -0
  66. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
  67. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -1
  68. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +524 -294
  69. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -1
  70. package/lib/esm/tests/tests.d.ts.map +1 -1
  71. package/lib/esm/tests/tests.js +431 -230
  72. package/lib/esm/tests/tests.js.map +1 -1
  73. package/lib/tsconfig.tsbuildinfo +1 -1
  74. package/package.json +10 -10
  75. package/src/tests/api_tests/custom_dashboards.test.ts +92 -0
  76. package/src/tests/api_tests/file_download_unicode_names.test.ts +223 -0
  77. package/src/tests/api_tests/openloop_webhooks.test.ts +138 -0
  78. package/src/tests/api_tests/phone_calls_cold_transfer.test.ts +54 -0
  79. package/src/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.ts +215 -0
  80. package/src/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.ts +215 -0
  81. package/src/tests/tests.ts +212 -1
  82. package/test_generated.pdf +0 -0
@@ -102,10 +102,11 @@ var OBJECT_ID = '68a1b2c3d4e5f60718293a4b';
102
102
  var allowed_paths_confinement_tests = function (_a) {
103
103
  var sdk = _a.sdk;
104
104
  return __awaiter(void 0, void 0, void 0, function () {
105
- var probes, request, isPathDenial, expectDenied, expectAllowed, formId, productId, joinEnduserId, formEnduserEmail, hostUserId, eventIds, businessId, form, publicFormSession, product, joinEnduser, joinLinkToken, joinEvent, unrelatedEvent, joinSession, hostUser, startLinkToken, startEvent, startSession, baselineEnduserToken, baselineUserToken, S1, S2, refreshedJoin, refreshedJoinToken, S3, refreshedStart, refreshedStartToken, S4, prefixGranted, _i, prefixGranted_1, _b, method, path, r, S6, ABSENT_ID_A, ABSENT_ID_B, INVALID_EMAIL, consentAttempt, refreshedForScopes, rescopedToken, S5, _c, _d, path, _e, _f, path, adminAuthenticated, apiKeySDK, _g, _h, path, r, failures, scenarios, _loop_1, _j, scenarios_1, scenario, _k, probes_1, p, _l, eventIds_1, id, _m, _o, e, _p, _q, _r, _s;
106
- var _t, _u, _v, _w;
107
- return __generator(this, function (_x) {
108
- switch (_x.label) {
105
+ var probes, request, isPathDenial, expectDenied, expectAllowed, formId, productId, joinEnduserId, formEnduserEmail, hostUserId, templateId, bookingEnduserId, eventIds, businessId, form, publicFormSession, product, joinEnduser, joinLinkToken, joinEvent, unrelatedEvent, joinSession, hostUser, startLinkToken, startEvent, startSession, baselineEnduserToken, baselineUserToken, S1, formEnduserId, publicRefresh, refreshedEnduser_1, withheld, consentEcho, echoedKeys, leakedKeys, fieldsThatMustBeRefused, _i, fieldsThatMustBeRefused_1, _b, field, value, r, mixed, afterProbes, S2, attendanceWrite, echoedEvent, refreshedJoin, refreshedJoinToken, S3, refreshedStart, refreshedStartToken, S4, prefixGranted, _c, prefixGranted_1, _d, method, path, r, S6, ABSENT_ID_A, ABSENT_ID_B, INVALID_EMAIL, consentAttempt, refreshedForScopes, rescopedToken, S7, bookingTemplate, bookingEnduserEmail, bookingEnduser, bookingSession, bookingEcho, bookingLeaked, _e, _f, _g, field, value, r, S5, _h, _j, path, _k, _l, path, adminAuthenticated, apiKeySDK, _m, _o, path, r, failures, scenarios, _loop_1, _p, scenarios_1, scenario, _q, probes_1, p, _r, eventIds_1, id, _s, _t, e, _u, _v, _w, _x, _y, _z;
106
+ var _0, _1;
107
+ var _2, _3, _4, _5, _6, _7, _8, _9, _10;
108
+ return __generator(this, function (_11) {
109
+ switch (_11.label) {
109
110
  case 0:
110
111
  (0, testing_1.log_header)("F-0166: allowedPaths wildcard bypasses session path confinement");
111
112
  probes = [];
@@ -172,16 +173,16 @@ var allowed_paths_confinement_tests = function (_a) {
172
173
  });
173
174
  };
174
175
  eventIds = [];
175
- _x.label = 1;
176
+ _11.label = 1;
176
177
  case 1:
177
- _x.trys.push([1, , 101, 130]);
178
+ _11.trys.push([1, , 123, 160]);
178
179
  businessId = sdk.userInfo.businessId;
179
180
  return [4 /*yield*/, sdk.api.forms.createOne({
180
181
  title: "F-0166 path confinement ".concat(RAND()),
181
182
  allowPublicURL: true,
182
183
  })];
183
184
  case 2:
184
- form = _x.sent();
185
+ form = _11.sent();
185
186
  formId = form.id;
186
187
  return [4 /*yield*/, sdk.api.form_fields.createOne({
187
188
  formId: form.id,
@@ -190,7 +191,7 @@ var allowed_paths_confinement_tests = function (_a) {
190
191
  previousFields: [{ type: 'root', info: {} }],
191
192
  })];
192
193
  case 3:
193
- _x.sent();
194
+ _11.sent();
194
195
  formEnduserEmail = "f0166-form-".concat(RAND(), "@tellescope.example");
195
196
  return [4 /*yield*/, new sdk_1.EnduserSession({ host: host, businessId: businessId })
196
197
  .api.form_responses.session_for_public_form({
@@ -202,20 +203,20 @@ var allowed_paths_confinement_tests = function (_a) {
202
203
  skipMatch: true, // also skips the intake-phone requirement
203
204
  })];
204
205
  case 4:
205
- publicFormSession = _x.sent();
206
+ publicFormSession = _11.sent();
206
207
  return [4 /*yield*/, sdk.api.products.createOne({
207
208
  title: "F-0166 product ".concat(RAND()),
208
209
  cost: { amount: 500, currency: 'USD' },
209
210
  processor: 'Stripe',
210
211
  })];
211
212
  case 5:
212
- product = _x.sent();
213
+ product = _11.sent();
213
214
  productId = product.id;
214
215
  return [4 /*yield*/, sdk.api.endusers.createOne({
215
216
  email: "f0166-join-".concat(RAND(), "@tellescope.example"),
216
217
  })];
217
218
  case 6:
218
- joinEnduser = _x.sent();
219
+ joinEnduser = _11.sent();
219
220
  joinEnduserId = joinEnduser.id;
220
221
  joinLinkToken = "f0166-join-".concat(RAND()).concat(RAND());
221
222
  return [4 /*yield*/, sdk.api.calendar_events.createOne({
@@ -223,14 +224,14 @@ var allowed_paths_confinement_tests = function (_a) {
223
224
  attendees: [{ type: 'enduser', id: joinEnduser.id, joinLinkToken: joinLinkToken }],
224
225
  })];
225
226
  case 7:
226
- joinEvent = _x.sent();
227
+ joinEvent = _11.sent();
227
228
  eventIds.push(joinEvent.id);
228
229
  return [4 /*yield*/, sdk.api.calendar_events.createOne({
229
230
  title: 'F-0166 unrelated', durationInMinutes: 30, startTimeInMS: Date.now() + 3600000,
230
231
  attendees: [{ type: 'enduser', id: joinEnduser.id }],
231
232
  })];
232
233
  case 8:
233
- unrelatedEvent = _x.sent();
234
+ unrelatedEvent = _11.sent();
234
235
  eventIds.push(unrelatedEvent.id);
235
236
  return [4 /*yield*/, new sdk_1.EnduserSession({ host: host, businessId: businessId })
236
237
  .api.calendar_events.session_for_join_link({ token: joinLinkToken })
@@ -238,7 +239,7 @@ var allowed_paths_confinement_tests = function (_a) {
238
239
  // throwaway user; verifiedEmail must be set at create time (it is rejected on update)
239
240
  ];
240
241
  case 9:
241
- joinSession = _x.sent();
242
+ joinSession = _11.sent();
242
243
  return [4 /*yield*/, sdk.api.users.createOne({
243
244
  email: "f0166-host-".concat(RAND(), "@tellescope.example"),
244
245
  fname: 'Path', lname: 'Host',
@@ -246,7 +247,7 @@ var allowed_paths_confinement_tests = function (_a) {
246
247
  notificationEmailsDisabled: true,
247
248
  })];
248
249
  case 10:
249
- hostUser = _x.sent();
250
+ hostUser = _11.sent();
250
251
  hostUserId = hostUser.id;
251
252
  startLinkToken = "f0166-start-".concat(RAND()).concat(RAND());
252
253
  return [4 /*yield*/, sdk.api.calendar_events.createOne({
@@ -255,204 +256,333 @@ var allowed_paths_confinement_tests = function (_a) {
255
256
  startLinkToken: startLinkToken,
256
257
  })];
257
258
  case 11:
258
- startEvent = _x.sent();
259
+ startEvent = _11.sent();
259
260
  eventIds.push(startEvent.id);
260
261
  return [4 /*yield*/, sdk.api.calendar_events.session_for_start_link({ token: startLinkToken })
261
262
  // -- unconfined baselines of each session type (generate_auth_token, never .authenticate)
262
263
  ];
263
264
  case 12:
264
- startSession = _x.sent();
265
+ startSession = _11.sent();
265
266
  return [4 /*yield*/, sdk.api.endusers.generate_auth_token({ id: joinEnduser.id })];
266
267
  case 13:
267
- baselineEnduserToken = (_x.sent()).authToken;
268
+ baselineEnduserToken = (_11.sent()).authToken;
268
269
  return [4 /*yield*/, sdk.api.users.generate_auth_token({ id: hostUser.id })
269
270
  /* ============ S1: public-form session — no wildcard, correctly confined already ============ */
270
271
  ];
271
272
  case 14:
272
- baselineUserToken = (_x.sent()).authToken;
273
+ baselineUserToken = (_11.sent()).authToken;
273
274
  S1 = 'S1 public-form';
274
275
  return [4 /*yield*/, expectAllowed(S1, 'always-allowed path', publicFormSession.authToken, 'get', '/test-authenticated')];
275
276
  case 15:
276
- _x.sent();
277
+ _11.sent();
277
278
  return [4 /*yield*/, expectAllowed(S1, 'listed: form info for access code', publicFormSession.authToken, 'get', '/form-info-for-access-code', { accessCode: publicFormSession.accessCode })];
278
279
  case 16:
279
- _x.sent();
280
+ _11.sent();
280
281
  return [4 /*yield*/, expectAllowed(S1, 'listed: refresh enduser session', publicFormSession.authToken, 'post', '/refresh-enduser-session')];
281
282
  case 17:
282
- _x.sent();
283
+ _11.sent();
283
284
  return [4 /*yield*/, expectAllowed(S1, 'listed: product by id', publicFormSession.authToken, 'get', "/product/".concat(productId))];
284
285
  case 18:
285
- _x.sent();
286
+ _11.sent();
286
287
  return [4 /*yield*/, expectDenied(S1, 'unlisted: calendar events', publicFormSession.authToken, baselineEnduserToken, 'get', '/calendar-events')];
287
288
  case 19:
288
- _x.sent();
289
+ _11.sent();
289
290
  return [4 /*yield*/, expectDenied(S1, 'unlisted: form responses', publicFormSession.authToken, baselineEnduserToken, 'get', '/form-responses')];
290
291
  case 20:
291
- _x.sent();
292
+ _11.sent();
292
293
  return [4 /*yield*/, expectDenied(S1, 'unlisted: files', publicFormSession.authToken, baselineEnduserToken, 'get', '/files')
293
294
  // 'get:/product' names one route, so it no longer leaks the whole /products namespace the way a
294
295
  // bare '/product' prefix entry did — that used to reach /products/prepare-stripe-checkout too
295
296
  ];
296
297
  case 21:
297
- _x.sent();
298
+ _11.sent();
298
299
  // 'get:/product' names one route, so it no longer leaks the whole /products namespace the way a
299
300
  // bare '/product' prefix entry did — that used to reach /products/prepare-stripe-checkout too
300
301
  return [4 /*yield*/, expectDenied(S1, 'exact matching: /products not granted by get:/product', publicFormSession.authToken, baselineEnduserToken, 'get', '/products')
301
- /* ============ S2: join-link session RED before the fix ============ */
302
+ /* ---- field confinement on the granted generic update route ----
303
+ * 'patch:/enduser/:id' is granted for Portal V2's terms gate, so the route check alone no longer
304
+ * protects this session — SESSION_SCOPE_WRITABLE_FIELDS does. These probes are the live control:
305
+ * the consent write lands, and every contactable/identity field is refused. Without them the
306
+ * module test is the only thing standing between a scope-table edit and account takeover.
307
+ */
302
308
  ];
303
309
  case 22:
304
310
  // 'get:/product' names one route, so it no longer leaks the whole /products namespace the way a
305
311
  // bare '/product' prefix entry did — that used to reach /products/prepare-stripe-checkout too
306
- _x.sent();
312
+ _11.sent();
313
+ formEnduserId = publicFormSession.enduserId;
314
+ return [4 /*yield*/, request(publicFormSession.authToken, 'post', '/refresh-enduser-session', {})];
315
+ case 23:
316
+ publicRefresh = _11.sent();
317
+ refreshedEnduser_1 = (_3 = (_2 = publicRefresh.body) === null || _2 === void 0 ? void 0 : _2.enduser) !== null && _3 !== void 0 ? _3 : {};
318
+ probes.push({
319
+ scenario: S1,
320
+ label: 'public refresh exposes the session\'s own id',
321
+ path: '/refresh-enduser-session',
322
+ expected: 'allowed',
323
+ status: refreshedEnduser_1.id === formEnduserId ? 200 : 500,
324
+ pass: refreshedEnduser_1.id === formEnduserId,
325
+ note: "id=".concat(refreshedEnduser_1.id),
326
+ });
327
+ withheld = ['email', 'phone', 'dateOfBirth', 'fname', 'lname', 'note', 'insurance', 'tags']
328
+ .filter(function (f) { return refreshedEnduser_1[f] !== undefined; });
329
+ probes.push({
330
+ scenario: S1,
331
+ label: 'public refresh still withholds the patient record',
332
+ path: '/refresh-enduser-session',
333
+ expected: 'denied',
334
+ status: withheld.length ? 200 : 403,
335
+ pass: withheld.length === 0,
336
+ note: withheld.length ? "LEAKED [".concat(withheld, "]") : "keys=[".concat(Object.keys(refreshedEnduser_1), "]"),
337
+ });
338
+ return [4 /*yield*/, expectAllowed(S1, 'field-restricted: may record terms consent', publicFormSession.authToken, 'patch', "/enduser/".concat(formEnduserId), { updates: { termsSigned: new Date().toISOString(), termsVersion: '1' } })
339
+ // The write being confined is not enough: the update RESPONSE is not redacted for enduser sessions
340
+ // (applyRedactions runs only for user sessions on that path), so an unprojected echo would hand
341
+ // this session the record it can't otherwise read — GET /enduser/:id is not even in this scope.
342
+ // Assert the response carries ONLY what the session was allowed to write.
343
+ ];
344
+ case 24:
345
+ _11.sent();
346
+ return [4 /*yield*/, request(publicFormSession.authToken, 'patch', "/enduser/".concat(formEnduserId), { updates: { termsVersion: '2' } })];
347
+ case 25:
348
+ consentEcho = _11.sent();
349
+ echoedKeys = Object.keys((_4 = consentEcho.body) !== null && _4 !== void 0 ? _4 : {});
350
+ leakedKeys = echoedKeys.filter(function (k) { return !['id', 'termsSigned', 'termsVersion'].includes(k); });
351
+ probes.push({
352
+ scenario: S1,
353
+ label: 'field-restricted: update response echoes only the writable fields',
354
+ path: "/enduser/".concat(formEnduserId),
355
+ expected: 'allowed',
356
+ status: leakedKeys.length === 0 ? consentEcho.status : 500,
357
+ pass: consentEcho.status === 200 && leakedKeys.length === 0,
358
+ note: leakedKeys.length ? "LEAKED [".concat(leakedKeys, "]") : "echoed [".concat(echoedKeys, "]"),
359
+ });
360
+ fieldsThatMustBeRefused = [
361
+ ['email', "f0166-takeover-".concat(RAND(), "@tellescope.example")],
362
+ ['phone', '+15555550123'],
363
+ ['landline', '+15555550124'],
364
+ ['dateOfBirth', '1990-01-01'],
365
+ ['fname', 'F0166Overwritten'],
366
+ ];
367
+ _i = 0, fieldsThatMustBeRefused_1 = fieldsThatMustBeRefused;
368
+ _11.label = 26;
369
+ case 26:
370
+ if (!(_i < fieldsThatMustBeRefused_1.length)) return [3 /*break*/, 29];
371
+ _b = fieldsThatMustBeRefused_1[_i], field = _b[0], value = _b[1];
372
+ return [4 /*yield*/, request(publicFormSession.authToken, 'patch', "/enduser/".concat(formEnduserId), { updates: (_0 = {}, _0[field] = value, _0) })];
373
+ case 27:
374
+ r = _11.sent();
375
+ probes.push({
376
+ scenario: S1,
377
+ label: "field-restricted: cannot write enduser '".concat(field, "'"),
378
+ path: "/enduser/".concat(formEnduserId),
379
+ expected: 'denied',
380
+ status: r.status,
381
+ pass: r.status === 403,
382
+ note: 'route is granted; the field allowlist is what refuses this',
383
+ });
384
+ _11.label = 28;
385
+ case 28:
386
+ _i++;
387
+ return [3 /*break*/, 26];
388
+ case 29: return [4 /*yield*/, request(publicFormSession.authToken, 'patch', "/enduser/".concat(formEnduserId), { updates: { termsVersion: '2', email: "f0166-mixed-".concat(RAND(), "@tellescope.example") } })];
389
+ case 30:
390
+ mixed = _11.sent();
391
+ probes.push({
392
+ scenario: S1,
393
+ label: 'field-restricted: a mixed allowed+denied update is refused whole',
394
+ path: "/enduser/".concat(formEnduserId),
395
+ expected: 'denied',
396
+ status: mixed.status,
397
+ pass: mixed.status === 403,
398
+ note: 'no partial application — email must not ride along with termsVersion',
399
+ });
400
+ return [4 /*yield*/, sdk.api.endusers.getOne(formEnduserId)];
401
+ case 31:
402
+ afterProbes = _11.sent();
403
+ probes.push({
404
+ scenario: S1,
405
+ label: 'field-restricted: stored email survived every attempt',
406
+ path: "/enduser/".concat(formEnduserId),
407
+ expected: 'denied',
408
+ status: afterProbes.email === formEnduserEmail ? 403 : 200,
409
+ pass: afterProbes.email === formEnduserEmail,
410
+ note: "stored=".concat(afterProbes.email),
411
+ });
307
412
  S2 = 'S2 join-link (enduser)';
308
413
  return [4 /*yield*/, expectDenied(S2, 'unlisted: form responses', joinSession.authToken, baselineEnduserToken, 'get', '/form-responses')];
309
- case 23:
310
- _x.sent();
414
+ case 32:
415
+ _11.sent();
311
416
  return [4 /*yield*/, expectDenied(S2, 'unlisted: files', joinSession.authToken, baselineEnduserToken, 'get', '/files')];
312
- case 24:
313
- _x.sent();
417
+ case 33:
418
+ _11.sent();
314
419
  return [4 /*yield*/, expectDenied(S2, 'unlisted: portal customizations', joinSession.authToken, baselineEnduserToken, 'get', '/portal-customizations')];
315
- case 25:
316
- _x.sent();
420
+ case 34:
421
+ _11.sent();
317
422
  return [4 /*yield*/, expectDenied(S2, 'unlisted: UNRELATED calendar event', joinSession.authToken, baselineEnduserToken, 'get', "/calendar-event/".concat(unrelatedEvent.id))];
318
- case 26:
319
- _x.sent();
423
+ case 35:
424
+ _11.sent();
320
425
  return [4 /*yield*/, expectAllowed(S2, 'always-allowed path', joinSession.authToken, 'get', '/test-authenticated')];
321
- case 27:
322
- _x.sent();
426
+ case 36:
427
+ _11.sent();
323
428
  return [4 /*yield*/, expectAllowed(S2, 'listed: my meetings', joinSession.authToken, 'get', '/my-meetings')
324
429
  // enduserActions for organizations is {}, so an enduser 401s here regardless of confinement
325
430
  ];
326
- case 28:
327
- _x.sent();
431
+ case 37:
432
+ _11.sent();
328
433
  // enduserActions for organizations is {}, so an enduser 401s here regardless of confinement
329
434
  return [4 /*yield*/, expectAllowed(S2, 'listed: organizations', joinSession.authToken, 'get', '/organizations', undefined, { allow401: true })];
330
- case 29:
435
+ case 38:
331
436
  // enduserActions for organizations is {}, so an enduser 401s here regardless of confinement
332
- _x.sent();
437
+ _11.sent();
333
438
  return [4 /*yield*/, expectAllowed(S2, 'listed: users', joinSession.authToken, 'get', '/users')];
334
- case 30:
335
- _x.sent();
439
+ case 39:
440
+ _11.sent();
336
441
  return [4 /*yield*/, expectAllowed(S2, 'listed: pinned calendar event', joinSession.authToken, 'get', "/calendar-event/".concat(joinEvent.id))];
337
- case 31:
338
- _x.sent();
442
+ case 40:
443
+ _11.sent();
339
444
  return [4 /*yield*/, expectAllowed(S2, 'wildcard: meeting by id', joinSession.authToken, 'get', "/meeting/".concat(OBJECT_ID))
340
445
  // the two paths widened in this change so the portal video flow keeps working
341
446
  ];
342
- case 32:
343
- _x.sent();
447
+ case 41:
448
+ _11.sent();
344
449
  // the two paths widened in this change so the portal video flow keeps working
345
450
  return [4 /*yield*/, expectAllowed(S2, 'widened: calendar events list', joinSession.authToken, 'get', '/calendar-events')];
346
- case 33:
451
+ case 42:
347
452
  // the two paths widened in this change so the portal video flow keeps working
348
- _x.sent();
453
+ _11.sent();
349
454
  return [4 /*yield*/, expectAllowed(S2, 'widened: join meeting for event', joinSession.authToken, 'post', '/join-meeting-for-event', { calendarEventId: joinEvent.id })
350
- // confinement must survive a refresh rather than widening
455
+ // The WaitingRoom marks attendance here. This write is deliberately NOT field-restricted: the
456
+ // portal reads the event back through useCalendarEvents, whose updateElement REPLACES the local
457
+ // record with the update response (react state.tsx), so a truncated response would drop event
458
+ // fields out of local state mid-visit and break a live Twilio call. Assert both halves of that
459
+ // contract — the write lands, AND the response is still a whole event.
351
460
  ];
352
- case 34:
353
- _x.sent();
461
+ case 43:
462
+ _11.sent();
463
+ return [4 /*yield*/, request(joinSession.authToken, 'patch', "/calendar-event/".concat(joinEvent.id), { updates: { joinedVideoCall: [{ id: joinEnduserId, at: new Date().toISOString() }] } })];
464
+ case 44:
465
+ attendanceWrite = _11.sent();
466
+ probes.push({
467
+ scenario: S2,
468
+ label: 'marks joinedVideoCall on its pinned event',
469
+ path: "/calendar-event/".concat(joinEvent.id),
470
+ expected: 'allowed',
471
+ status: attendanceWrite.status,
472
+ pass: attendanceWrite.status === 200,
473
+ });
474
+ echoedEvent = (_5 = attendanceWrite.body) !== null && _5 !== void 0 ? _5 : {};
475
+ probes.push({
476
+ scenario: S2,
477
+ label: 'attendance write returns the WHOLE event (client replaces local state from it)',
478
+ path: "/calendar-event/".concat(joinEvent.id),
479
+ expected: 'allowed',
480
+ status: (echoedEvent.title && echoedEvent.durationInMinutes) ? 200 : 500,
481
+ pass: !!echoedEvent.title && !!echoedEvent.durationInMinutes && !!echoedEvent.joinedVideoCall,
482
+ note: "echoed ".concat(Object.keys(echoedEvent).length, " keys"),
483
+ });
354
484
  return [4 /*yield*/, request(joinSession.authToken, 'post', '/refresh-enduser-session', {})];
355
- case 35:
356
- refreshedJoin = _x.sent();
357
- refreshedJoinToken = (_t = refreshedJoin.body) === null || _t === void 0 ? void 0 : _t.authToken;
485
+ case 45:
486
+ refreshedJoin = _11.sent();
487
+ refreshedJoinToken = (_6 = refreshedJoin.body) === null || _6 === void 0 ? void 0 : _6.authToken;
358
488
  probes.push({
359
489
  scenario: S2, label: 'refresh returned a new token', path: '/refresh-enduser-session',
360
490
  expected: 'allowed', status: refreshedJoin.status, pass: !!refreshedJoinToken,
361
491
  });
362
- if (!refreshedJoinToken) return [3 /*break*/, 38];
492
+ if (!refreshedJoinToken) return [3 /*break*/, 48];
363
493
  return [4 /*yield*/, expectDenied(S2, 'after refresh: still denied form responses', refreshedJoinToken, baselineEnduserToken, 'get', '/form-responses')];
364
- case 36:
365
- _x.sent();
494
+ case 46:
495
+ _11.sent();
366
496
  return [4 /*yield*/, expectAllowed(S2, 'after refresh: still allowed my meetings', refreshedJoinToken, 'get', '/my-meetings')];
367
- case 37:
368
- _x.sent();
369
- _x.label = 38;
370
- case 38:
497
+ case 47:
498
+ _11.sent();
499
+ _11.label = 48;
500
+ case 48:
371
501
  S3 = 'S3 start-link (STAFF)';
372
502
  return [4 /*yield*/, expectDenied(S3, 'unlisted: form responses', startSession.authToken, baselineUserToken, 'get', '/form-responses')];
373
- case 39:
374
- _x.sent();
503
+ case 49:
504
+ _11.sent();
375
505
  return [4 /*yield*/, expectDenied(S3, 'unlisted: journeys', startSession.authToken, baselineUserToken, 'get', '/journeys')];
376
- case 40:
377
- _x.sent();
506
+ case 50:
507
+ _11.sent();
378
508
  return [4 /*yield*/, expectDenied(S3, 'unlisted: tickets', startSession.authToken, baselineUserToken, 'get', '/tickets')];
379
- case 41:
380
- _x.sent();
509
+ case 51:
510
+ _11.sent();
381
511
  return [4 /*yield*/, expectDenied(S3, 'unlisted: data sync (org-wide change firehose)', startSession.authToken, baselineUserToken, 'get', '/data-sync', { from: new Date(Date.now() - 60000).toISOString() })];
382
- case 42:
383
- _x.sent();
512
+ case 52:
513
+ _11.sent();
384
514
  return [4 /*yield*/, expectDenied(S3, 'unlisted: file download URL', startSession.authToken, baselineUserToken, 'get', '/file-download-URL', { secureName: 'f0166-does-not-exist' })];
385
- case 43:
386
- _x.sent();
515
+ case 53:
516
+ _11.sent();
387
517
  return [4 /*yield*/, expectDenied(S3, 'unlisted: UNRELATED calendar event', startSession.authToken, baselineUserToken, 'get', "/calendar-event/".concat(unrelatedEvent.id))];
388
- case 44:
389
- _x.sent();
518
+ case 54:
519
+ _11.sent();
390
520
  return [4 /*yield*/, expectAllowed(S3, 'always-allowed path', startSession.authToken, 'get', '/test-authenticated')];
391
- case 45:
392
- _x.sent();
521
+ case 55:
522
+ _11.sent();
393
523
  return [4 /*yield*/, expectAllowed(S3, 'listed: my meetings', startSession.authToken, 'get', '/my-meetings')];
394
- case 46:
395
- _x.sent();
524
+ case 56:
525
+ _11.sent();
396
526
  return [4 /*yield*/, expectAllowed(S3, 'listed: calendar events', startSession.authToken, 'get', '/calendar-events')];
397
- case 47:
398
- _x.sent();
527
+ case 57:
528
+ _11.sent();
399
529
  return [4 /*yield*/, expectAllowed(S3, 'listed: organizations', startSession.authToken, 'get', '/organizations')];
400
- case 48:
401
- _x.sent();
530
+ case 58:
531
+ _11.sent();
402
532
  return [4 /*yield*/, expectAllowed(S3, 'listed: users', startSession.authToken, 'get', '/users')];
403
- case 49:
404
- _x.sent();
533
+ case 59:
534
+ _11.sent();
405
535
  return [4 /*yield*/, expectAllowed(S3, 'listed: endusers', startSession.authToken, 'get', '/endusers')];
406
- case 50:
407
- _x.sent();
536
+ case 60:
537
+ _11.sent();
408
538
  return [4 /*yield*/, expectAllowed(S3, 'listed: pinned calendar event', startSession.authToken, 'get', "/calendar-event/".concat(startEvent.id))];
409
- case 51:
410
- _x.sent();
539
+ case 61:
540
+ _11.sent();
411
541
  return [4 /*yield*/, expectAllowed(S3, 'wildcard: meeting by id', startSession.authToken, 'get', "/meeting/".concat(OBJECT_ID))];
412
- case 52:
413
- _x.sent();
542
+ case 62:
543
+ _11.sent();
414
544
  return [4 /*yield*/, expectAllowed(S3, 'prefix descendant: users bulk-read', startSession.authToken, 'post', '/users/bulk-read', { ids: [hostUser.id] })];
415
- case 53:
416
- _x.sent();
545
+ case 63:
546
+ _11.sent();
417
547
  return [4 /*yield*/, expectAllowed(S3, 'prefix descendant: endusers bulk-read', startSession.authToken, 'post', '/endusers/bulk-read', { ids: [joinEnduser.id] })];
418
- case 54:
419
- _x.sent();
548
+ case 64:
549
+ _11.sent();
420
550
  return [4 /*yield*/, expectAllowed(S3, 'widened: join meeting for event', startSession.authToken, 'post', '/join-meeting-for-event', { calendarEventId: startEvent.id })];
421
- case 55:
422
- _x.sent();
551
+ case 65:
552
+ _11.sent();
423
553
  return [4 /*yield*/, expectAllowed(S3, 'widened: add attendees to meeting', startSession.authToken, 'post', '/add-attendees-to-meeting', { id: OBJECT_ID })
424
554
  // login_user propagates allowedPaths implicitly through its ...toSign rest spread — the
425
555
  // "(ASSUMPTION)" comments in the mint sites depend on this, so assert it explicitly
426
556
  ];
427
- case 56:
428
- _x.sent();
557
+ case 66:
558
+ _11.sent();
429
559
  return [4 /*yield*/, request(startSession.authToken, 'post', '/refresh-session', {})];
430
- case 57:
431
- refreshedStart = _x.sent();
432
- refreshedStartToken = (_u = refreshedStart.body) === null || _u === void 0 ? void 0 : _u.authToken;
560
+ case 67:
561
+ refreshedStart = _11.sent();
562
+ refreshedStartToken = (_7 = refreshedStart.body) === null || _7 === void 0 ? void 0 : _7.authToken;
433
563
  probes.push({
434
564
  scenario: S3, label: 'refresh returned a new token', path: '/refresh-session',
435
565
  expected: 'allowed', status: refreshedStart.status, pass: !!refreshedStartToken,
436
566
  });
437
- if (!refreshedStartToken) return [3 /*break*/, 60];
567
+ if (!refreshedStartToken) return [3 /*break*/, 70];
438
568
  return [4 /*yield*/, expectDenied(S3, 'after refresh: still denied journeys', refreshedStartToken, baselineUserToken, 'get', '/journeys')];
439
- case 58:
440
- _x.sent();
569
+ case 68:
570
+ _11.sent();
441
571
  return [4 /*yield*/, expectAllowed(S3, 'after refresh: still allowed my meetings', refreshedStartToken, 'get', '/my-meetings')];
442
- case 59:
443
- _x.sent();
444
- _x.label = 60;
445
- case 60:
572
+ case 69:
573
+ _11.sent();
574
+ _11.label = 70;
575
+ case 70:
446
576
  S4 = 'S4 widening side-effects';
447
577
  prefixGranted = [
448
578
  ['patch', '/calendar-events/bulk-update'],
449
579
  ['post', '/calendar-events/report'],
450
580
  ];
451
- _i = 0, prefixGranted_1 = prefixGranted;
452
- _x.label = 61;
453
- case 61:
454
- if (!(_i < prefixGranted_1.length)) return [3 /*break*/, 64];
455
- _b = prefixGranted_1[_i], method = _b[0], path = _b[1];
581
+ _c = 0, prefixGranted_1 = prefixGranted;
582
+ _11.label = 71;
583
+ case 71:
584
+ if (!(_c < prefixGranted_1.length)) return [3 /*break*/, 74];
585
+ _d = prefixGranted_1[_c], method = _d[0], path = _d[1];
456
586
  return [4 /*yield*/, axios_1.default.request({
457
587
  method: method,
458
588
  url: "".concat(host, "/v1").concat(path),
@@ -460,8 +590,8 @@ var allowed_paths_confinement_tests = function (_a) {
460
590
  headers: { Authorization: "Bearer ".concat(joinSession.authToken) },
461
591
  validateStatus: function () { return true; },
462
592
  })];
463
- case 62:
464
- r = _x.sent();
593
+ case 72:
594
+ r = _11.sent();
465
595
  probes.push({
466
596
  scenario: S4,
467
597
  label: "enduser action gating blocks ".concat(method.toUpperCase(), " ").concat(path),
@@ -471,146 +601,222 @@ var allowed_paths_confinement_tests = function (_a) {
471
601
  pass: r.status === 401 || r.status === 403,
472
602
  note: 'prefix-granted by /calendar-events; blocked by enduserActions instead',
473
603
  });
474
- _x.label = 63;
475
- case 63:
476
- _i++;
477
- return [3 /*break*/, 61];
478
- case 64:
604
+ _11.label = 73;
605
+ case 73:
606
+ _c++;
607
+ return [3 /*break*/, 71];
608
+ case 74:
479
609
  S6 = 'S6 namespace escapes';
480
610
  ABSENT_ID_A = '68a1b2c3d4e5f60718293a4b';
481
611
  ABSENT_ID_B = '68a1b2c3d4e5f60718293a4c';
482
612
  INVALID_EMAIL = 'f0166-not-an-email';
483
613
  return [4 /*yield*/, expectDenied(S6, 'ESCAPE: /users/consent (minted an unconfined token)', startSession.authToken, baselineUserToken, 'patch', '/users/consent', { termsVersion: '1' })];
484
- case 65:
485
- _x.sent();
614
+ case 75:
615
+ _11.sent();
486
616
  return [4 /*yield*/, expectDenied(S6, 'ESCAPE: /users/switch-account', startSession.authToken, baselineUserToken, 'post', '/users/switch-account', { targetUserId: ABSENT_ID_A })];
487
- case 66:
488
- _x.sent();
617
+ case 76:
618
+ _11.sent();
489
619
  return [4 /*yield*/, expectDenied(S6, 'ESCAPE: /users/configure-inbox', startSession.authToken, baselineUserToken, 'post', '/users/configure-inbox', {})];
490
- case 67:
491
- _x.sent();
620
+ case 77:
621
+ _11.sent();
492
622
  return [4 /*yield*/, expectDenied(S6, 'PHI: /endusers/load-inbox-data', startSession.authToken, baselineUserToken, 'get', '/endusers/load-inbox-data')];
493
- case 68:
494
- _x.sent();
623
+ case 78:
624
+ _11.sent();
495
625
  return [4 /*yield*/, expectDenied(S6, 'PHI: /endusers/report', startSession.authToken, baselineUserToken, 'get', '/endusers/report')];
496
- case 69:
497
- _x.sent();
626
+ case 79:
627
+ _11.sent();
498
628
  return [4 /*yield*/, expectDenied(S6, 'PHI: /endusers/engagement', startSession.authToken, baselineUserToken, 'get', '/endusers/engagement')];
499
- case 70:
500
- _x.sent();
629
+ case 80:
630
+ _11.sent();
501
631
  return [4 /*yield*/, expectDenied(S6, 'DESTRUCTIVE: /endusers/merge', startSession.authToken, baselineUserToken, 'post', '/endusers/merge', { sourceEnduserId: ABSENT_ID_A, destinationEnduserId: ABSENT_ID_B })];
502
- case 71:
503
- _x.sent();
632
+ case 81:
633
+ _11.sent();
504
634
  return [4 /*yield*/, expectDenied(S6, 'DESTRUCTIVE: /endusers/bulk-update', startSession.authToken, baselineUserToken, 'patch', '/endusers/bulk-update', { ids: [ABSENT_ID_A], fields: {} })
505
635
  // method scoping, end to end: the same path is readable but not writable.
506
636
  // Baselines use the ADMIN token — the throwaway host user cannot create users or endusers, so it
507
637
  // would 403 on permissions and the probe could not distinguish that from a confinement denial.
508
638
  ];
509
- case 72:
510
- _x.sent();
639
+ case 82:
640
+ _11.sent();
511
641
  // method scoping, end to end: the same path is readable but not writable.
512
642
  // Baselines use the ADMIN token — the throwaway host user cannot create users or endusers, so it
513
643
  // would 403 on permissions and the probe could not distinguish that from a confinement denial.
514
644
  return [4 /*yield*/, expectAllowed(S6, 'method scoping: GET /endusers allowed', startSession.authToken, 'get', '/endusers')];
515
- case 73:
645
+ case 83:
516
646
  // method scoping, end to end: the same path is readable but not writable.
517
647
  // Baselines use the ADMIN token — the throwaway host user cannot create users or endusers, so it
518
648
  // would 403 on permissions and the probe could not distinguish that from a confinement denial.
519
- _x.sent();
649
+ _11.sent();
520
650
  return [4 /*yield*/, expectDenied(S6, 'method scoping: POST /endusers (create) denied', startSession.authToken, sdk.authToken, 'post', '/endusers', { email: INVALID_EMAIL })];
521
- case 74:
522
- _x.sent();
651
+ case 84:
652
+ _11.sent();
523
653
  return [4 /*yield*/, expectAllowed(S6, 'method scoping: GET /users allowed', startSession.authToken, 'get', '/users')];
524
- case 75:
525
- _x.sent();
654
+ case 85:
655
+ _11.sent();
526
656
  return [4 /*yield*/, expectDenied(S6, 'method scoping: POST /users (create) denied', startSession.authToken, sdk.authToken, 'post', '/users', { email: INVALID_EMAIL })
527
657
  // even if consent were reachable, the token it mints must stay confined (defense in depth)
528
658
  ];
529
- case 76:
530
- _x.sent();
659
+ case 86:
660
+ _11.sent();
531
661
  return [4 /*yield*/, request(startSession.authToken, 'patch', '/users/consent', { termsVersion: '1' })];
532
- case 77:
533
- consentAttempt = _x.sent();
534
- if (!(consentAttempt.status === 200 && ((_v = consentAttempt.body) === null || _v === void 0 ? void 0 : _v.authToken))) return [3 /*break*/, 79];
662
+ case 87:
663
+ consentAttempt = _11.sent();
664
+ if (!(consentAttempt.status === 200 && ((_8 = consentAttempt.body) === null || _8 === void 0 ? void 0 : _8.authToken))) return [3 /*break*/, 89];
535
665
  return [4 /*yield*/, expectDenied(S6, 'consent-minted token is still confined', consentAttempt.body.authToken, baselineUserToken, 'get', '/journeys')];
536
- case 78:
537
- _x.sent();
538
- _x.label = 79;
539
- case 79: return [4 /*yield*/, request(startSession.authToken, 'post', '/refresh-session', {})];
540
- case 80:
541
- refreshedForScopes = _x.sent();
542
- rescopedToken = (_w = refreshedForScopes.body) === null || _w === void 0 ? void 0 : _w.authToken;
666
+ case 88:
667
+ _11.sent();
668
+ _11.label = 89;
669
+ case 89: return [4 /*yield*/, request(startSession.authToken, 'post', '/refresh-session', {})];
670
+ case 90:
671
+ refreshedForScopes = _11.sent();
672
+ rescopedToken = (_9 = refreshedForScopes.body) === null || _9 === void 0 ? void 0 : _9.authToken;
543
673
  probes.push({
544
674
  scenario: S6, label: 'refresh returned a new token', path: '/refresh-session',
545
675
  expected: 'allowed', status: refreshedForScopes.status, pass: !!rescopedToken,
546
676
  });
547
- if (!rescopedToken) return [3 /*break*/, 86];
677
+ if (!rescopedToken) return [3 /*break*/, 96];
548
678
  return [4 /*yield*/, expectDenied(S6, 'after refresh: still cannot reach /users/consent', rescopedToken, baselineUserToken, 'patch', '/users/consent', { termsVersion: '1' })];
549
- case 81:
550
- _x.sent();
679
+ case 91:
680
+ _11.sent();
551
681
  return [4 /*yield*/, expectDenied(S6, 'after refresh: still cannot reach /endusers/load-inbox-data', rescopedToken, baselineUserToken, 'get', '/endusers/load-inbox-data')];
552
- case 82:
553
- _x.sent();
682
+ case 92:
683
+ _11.sent();
554
684
  return [4 /*yield*/, expectDenied(S6, 'after refresh: POST /endusers still denied', rescopedToken, baselineUserToken, 'post', '/endusers', { email: "f0166-should-not-exist-".concat(RAND(), "@tellescope.example") })];
555
- case 83:
556
- _x.sent();
685
+ case 93:
686
+ _11.sent();
557
687
  return [4 /*yield*/, expectAllowed(S6, 'after refresh: GET /endusers still allowed', rescopedToken, 'get', '/endusers')];
558
- case 84:
559
- _x.sent();
688
+ case 94:
689
+ _11.sent();
560
690
  return [4 /*yield*/, expectAllowed(S6, 'after refresh: my-meetings still allowed', rescopedToken, 'get', '/my-meetings')];
561
- case 85:
562
- _x.sent();
563
- _x.label = 86;
564
- case 86:
691
+ case 95:
692
+ _11.sent();
693
+ _11.label = 96;
694
+ case 96:
695
+ S7 = 'S7 appointment-booking';
696
+ return [4 /*yield*/, sdk.api.calendar_event_templates.createOne({
697
+ title: "F-0166 booking ".concat(RAND()), durationInMinutes: 30,
698
+ })];
699
+ case 97:
700
+ bookingTemplate = _11.sent();
701
+ templateId = bookingTemplate.id;
702
+ bookingEnduserEmail = "f0166-booking-".concat(RAND(), "@tellescope.example");
703
+ return [4 /*yield*/, sdk.api.endusers.createOne({
704
+ email: bookingEnduserEmail, fname: 'Booking', lname: 'Confinement',
705
+ })];
706
+ case 98:
707
+ bookingEnduser = _11.sent();
708
+ bookingEnduserId = bookingEnduser.id;
709
+ return [4 /*yield*/, new sdk_1.EnduserSession({ host: host, businessId: businessId })
710
+ .api.calendar_events.session_for_public_appointment_booking({
711
+ businessId: businessId,
712
+ calendarEventTemplateId: bookingTemplate.id,
713
+ email: bookingEnduserEmail,
714
+ fname: 'Booking',
715
+ lname: 'Confinement',
716
+ })];
717
+ case 99:
718
+ bookingSession = _11.sent();
719
+ return [4 /*yield*/, expectAllowed(S7, 'field-restricted: may record terms consent', bookingSession.authToken, 'patch', "/enduser/".concat(bookingEnduser.id), { updates: { termsSigned: new Date().toISOString(), termsVersion: '1' } })];
720
+ case 100:
721
+ _11.sent();
722
+ return [4 /*yield*/, request(bookingSession.authToken, 'patch', "/enduser/".concat(bookingEnduser.id), { updates: { termsVersion: '2' } })];
723
+ case 101:
724
+ bookingEcho = _11.sent();
725
+ bookingLeaked = Object.keys((_10 = bookingEcho.body) !== null && _10 !== void 0 ? _10 : {})
726
+ .filter(function (k) { return !['id', 'termsSigned', 'termsVersion'].includes(k); });
727
+ probes.push({
728
+ scenario: S7,
729
+ label: 'field-restricted: update response echoes only the writable fields',
730
+ path: "/enduser/".concat(bookingEnduser.id),
731
+ expected: 'allowed',
732
+ status: bookingLeaked.length === 0 ? bookingEcho.status : 500,
733
+ pass: bookingEcho.status === 200 && bookingLeaked.length === 0,
734
+ note: bookingLeaked.length ? "LEAKED [".concat(bookingLeaked, "]") : undefined,
735
+ });
736
+ _e = 0, _f = [['email', "f0166-bt-".concat(RAND(), "@tellescope.example")], ['phone', '+15555550188']];
737
+ _11.label = 102;
738
+ case 102:
739
+ if (!(_e < _f.length)) return [3 /*break*/, 105];
740
+ _g = _f[_e], field = _g[0], value = _g[1];
741
+ return [4 /*yield*/, request(bookingSession.authToken, 'patch', "/enduser/".concat(bookingEnduser.id), { updates: (_1 = {}, _1[field] = value, _1) })];
742
+ case 103:
743
+ r = _11.sent();
744
+ probes.push({
745
+ scenario: S7,
746
+ label: "field-restricted: cannot write enduser '".concat(field, "'"),
747
+ path: "/enduser/".concat(bookingEnduser.id),
748
+ expected: 'denied',
749
+ status: r.status,
750
+ pass: r.status === 403,
751
+ note: 'route is granted; the field allowlist is what refuses this',
752
+ });
753
+ _11.label = 104;
754
+ case 104:
755
+ _e++;
756
+ return [3 /*break*/, 102];
757
+ case 105:
758
+ // the booking scope must not have gained anything else along with the enduser write
759
+ return [4 /*yield*/, expectDenied(S7, 'unlisted: form responses', bookingSession.authToken, baselineEnduserToken, 'get', '/form-responses')];
760
+ case 106:
761
+ // the booking scope must not have gained anything else along with the enduser write
762
+ _11.sent();
763
+ return [4 /*yield*/, expectDenied(S7, 'unlisted: calendar events', bookingSession.authToken, baselineEnduserToken, 'get', '/calendar-events')];
764
+ case 107:
765
+ _11.sent();
766
+ return [4 /*yield*/, expectAllowed(S7, 'listed: refresh enduser session', bookingSession.authToken, 'post', '/refresh-enduser-session')
767
+ /* ============ S5: ordinary sessions with no allowedPaths claim ============ */
768
+ ];
769
+ case 108:
770
+ _11.sent();
565
771
  S5 = 'S5 unconfined sessions';
566
- _c = 0, _d = ['/journeys', '/form-responses', '/endusers', '/calendar-events', '/tickets'];
567
- _x.label = 87;
568
- case 87:
569
- if (!(_c < _d.length)) return [3 /*break*/, 90];
570
- path = _d[_c];
772
+ _h = 0, _j = ['/journeys', '/form-responses', '/endusers', '/calendar-events', '/tickets'];
773
+ _11.label = 109;
774
+ case 109:
775
+ if (!(_h < _j.length)) return [3 /*break*/, 112];
776
+ path = _j[_h];
571
777
  return [4 /*yield*/, expectAllowed(S5, "staff session reaches ".concat(path), baselineUserToken, 'get', path)];
572
- case 88:
573
- _x.sent();
574
- _x.label = 89;
575
- case 89:
576
- _c++;
577
- return [3 /*break*/, 87];
578
- case 90: return [4 /*yield*/, expectAllowed(S5, 'staff session reaches /data-sync', baselineUserToken, 'get', '/data-sync', { from: new Date(Date.now() - 60000).toISOString() })];
579
- case 91:
580
- _x.sent();
581
- _e = 0, _f = ['/form-responses', '/calendar-events', '/files'];
582
- _x.label = 92;
583
- case 92:
584
- if (!(_e < _f.length)) return [3 /*break*/, 95];
585
- path = _f[_e];
778
+ case 110:
779
+ _11.sent();
780
+ _11.label = 111;
781
+ case 111:
782
+ _h++;
783
+ return [3 /*break*/, 109];
784
+ case 112: return [4 /*yield*/, expectAllowed(S5, 'staff session reaches /data-sync', baselineUserToken, 'get', '/data-sync', { from: new Date(Date.now() - 60000).toISOString() })];
785
+ case 113:
786
+ _11.sent();
787
+ _k = 0, _l = ['/form-responses', '/calendar-events', '/files'];
788
+ _11.label = 114;
789
+ case 114:
790
+ if (!(_k < _l.length)) return [3 /*break*/, 117];
791
+ path = _l[_k];
586
792
  return [4 /*yield*/, expectAllowed(S5, "enduser session reaches ".concat(path), baselineEnduserToken, 'get', path)];
587
- case 93:
588
- _x.sent();
589
- _x.label = 94;
590
- case 94:
591
- _e++;
592
- return [3 /*break*/, 92];
593
- case 95: return [4 /*yield*/, sdk.test_authenticated()];
594
- case 96:
595
- adminAuthenticated = _x.sent();
793
+ case 115:
794
+ _11.sent();
795
+ _11.label = 116;
796
+ case 116:
797
+ _k++;
798
+ return [3 /*break*/, 114];
799
+ case 117: return [4 /*yield*/, sdk.test_authenticated()];
800
+ case 118:
801
+ adminAuthenticated = _11.sent();
596
802
  probes.push({
597
803
  scenario: S5, label: 'admin sdk still authenticated', path: '/test-authenticated',
598
804
  expected: 'allowed', status: 200, pass: adminAuthenticated === 'Authenticated!',
599
805
  });
600
806
  apiKeySDK = new sdk_1.Session({ host: host, apiKey: '3n5q0SCBT_iUvZz-b9BJtX7o7HQUVJ9v132PgHJNJsg.' });
601
- _g = 0, _h = ['/journeys', '/form-responses', '/endusers', '/calendar-events', '/files'];
602
- _x.label = 97;
603
- case 97:
604
- if (!(_g < _h.length)) return [3 /*break*/, 100];
605
- path = _h[_g];
807
+ _m = 0, _o = ['/journeys', '/form-responses', '/endusers', '/calendar-events', '/files'];
808
+ _11.label = 119;
809
+ case 119:
810
+ if (!(_m < _o.length)) return [3 /*break*/, 122];
811
+ path = _o[_m];
606
812
  return [4 /*yield*/, axios_1.default.request({
607
813
  method: 'get',
608
814
  url: "".concat(host, "/v1").concat(path),
609
815
  headers: { Authorization: "API_KEY ".concat(apiKeySDK.apiKey) },
610
816
  validateStatus: function () { return true; },
611
817
  })];
612
- case 98:
613
- r = _x.sent();
818
+ case 120:
819
+ r = _11.sent();
614
820
  probes.push({
615
821
  scenario: S5,
616
822
  label: "API key (headless) reaches ".concat(path),
@@ -619,11 +825,11 @@ var allowed_paths_confinement_tests = function (_a) {
619
825
  status: r.status,
620
826
  pass: r.status !== 403,
621
827
  });
622
- _x.label = 99;
623
- case 99:
624
- _g++;
625
- return [3 /*break*/, 97];
626
- case 100:
828
+ _11.label = 121;
829
+ case 121:
830
+ _m++;
831
+ return [3 /*break*/, 119];
832
+ case 122:
627
833
  failures = probes.filter(function (p) { return !p.pass; });
628
834
  console.log('');
629
835
  console.log(' F-0166 — full probe table');
@@ -633,111 +839,135 @@ var allowed_paths_confinement_tests = function (_a) {
633
839
  var inScenario = probes.filter(function (p) { return p.scenario === scenario; });
634
840
  var failed = inScenario.filter(function (p) { return !p.pass; }).length;
635
841
  console.log(" ".concat(scenario, ": ").concat(inScenario.length - failed, "/").concat(inScenario.length, " passing").concat(failed ? " <-- ".concat(failed, " FAILING") : ''));
636
- for (var _y = 0, inScenario_1 = inScenario; _y < inScenario_1.length; _y++) {
637
- var p = inScenario_1[_y];
842
+ for (var _12 = 0, inScenario_1 = inScenario; _12 < inScenario_1.length; _12++) {
843
+ var p = inScenario_1[_12];
638
844
  var baseline = p.baselineStatus !== undefined ? " baseline=".concat(p.baselineStatus) : '';
639
845
  console.log(" ".concat(p.pass ? ' ok ' : 'FAIL', " want=").concat(p.expected.padEnd(7), " got=").concat(p.status).concat(baseline, " ").concat(p.label).concat(p.note ? " (".concat(p.note, ")") : ''));
640
846
  }
641
847
  };
642
- for (_j = 0, scenarios_1 = scenarios; _j < scenarios_1.length; _j++) {
643
- scenario = scenarios_1[_j];
848
+ for (_p = 0, scenarios_1 = scenarios; _p < scenarios_1.length; _p++) {
849
+ scenario = scenarios_1[_p];
644
850
  _loop_1(scenario);
645
851
  }
646
852
  console.log(' ' + '-'.repeat(112));
647
853
  console.log(" TOTAL: ".concat(probes.length - failures.length, "/").concat(probes.length, " passing, ").concat(failures.length, " failing"));
648
854
  console.log('');
649
- for (_k = 0, probes_1 = probes; _k < probes_1.length; _k++) {
650
- p = probes_1[_k];
855
+ for (_q = 0, probes_1 = probes; _q < probes_1.length; _q++) {
856
+ p = probes_1[_q];
651
857
  (0, testing_1.assert)(p.pass, "".concat(p.scenario, " \u2014 ").concat(p.label, " (").concat(p.path, "): wanted ").concat(p.expected, ", got status ").concat(p.status)
652
858
  + (p.baselineStatus !== undefined ? " (unconfined baseline got ".concat(p.baselineStatus, ")") : ''), "".concat(p.scenario, " \u2014 ").concat(p.label));
653
859
  }
654
- return [3 /*break*/, 130];
655
- case 101:
656
- _l = 0, eventIds_1 = eventIds;
657
- _x.label = 102;
658
- case 102:
659
- if (!(_l < eventIds_1.length)) return [3 /*break*/, 107];
660
- id = eventIds_1[_l];
661
- _x.label = 103;
662
- case 103:
663
- _x.trys.push([103, 105, , 106]);
860
+ return [3 /*break*/, 160];
861
+ case 123:
862
+ _r = 0, eventIds_1 = eventIds;
863
+ _11.label = 124;
864
+ case 124:
865
+ if (!(_r < eventIds_1.length)) return [3 /*break*/, 129];
866
+ id = eventIds_1[_r];
867
+ _11.label = 125;
868
+ case 125:
869
+ _11.trys.push([125, 127, , 128]);
664
870
  return [4 /*yield*/, sdk.api.calendar_events.deleteOne(id)];
665
- case 104:
666
- _x.sent();
667
- return [3 /*break*/, 106];
668
- case 105:
669
- _m = _x.sent();
670
- return [3 /*break*/, 106];
671
- case 106:
672
- _l++;
673
- return [3 /*break*/, 102];
674
- case 107:
675
- if (!joinEnduserId) return [3 /*break*/, 111];
676
- _x.label = 108;
677
- case 108:
678
- _x.trys.push([108, 110, , 111]);
871
+ case 126:
872
+ _11.sent();
873
+ return [3 /*break*/, 128];
874
+ case 127:
875
+ _s = _11.sent();
876
+ return [3 /*break*/, 128];
877
+ case 128:
878
+ _r++;
879
+ return [3 /*break*/, 124];
880
+ case 129:
881
+ if (!joinEnduserId) return [3 /*break*/, 133];
882
+ _11.label = 130;
883
+ case 130:
884
+ _11.trys.push([130, 132, , 133]);
679
885
  return [4 /*yield*/, sdk.api.endusers.deleteOne(joinEnduserId)];
680
- case 109:
681
- _x.sent();
682
- return [3 /*break*/, 111];
683
- case 110:
684
- _o = _x.sent();
685
- return [3 /*break*/, 111];
686
- case 111:
687
- if (!formEnduserEmail) return [3 /*break*/, 117];
688
- _x.label = 112;
689
- case 112:
690
- _x.trys.push([112, 116, , 117]);
886
+ case 131:
887
+ _11.sent();
888
+ return [3 /*break*/, 133];
889
+ case 132:
890
+ _t = _11.sent();
891
+ return [3 /*break*/, 133];
892
+ case 133:
893
+ if (!formEnduserEmail) return [3 /*break*/, 139];
894
+ _11.label = 134;
895
+ case 134:
896
+ _11.trys.push([134, 138, , 139]);
691
897
  return [4 /*yield*/, sdk.api.endusers.getOne({ email: formEnduserEmail })];
692
- case 113:
693
- e = _x.sent();
694
- if (!(e === null || e === void 0 ? void 0 : e.id)) return [3 /*break*/, 115];
898
+ case 135:
899
+ e = _11.sent();
900
+ if (!(e === null || e === void 0 ? void 0 : e.id)) return [3 /*break*/, 137];
695
901
  return [4 /*yield*/, sdk.api.endusers.deleteOne(e.id)];
696
- case 114:
697
- _x.sent();
698
- _x.label = 115;
699
- case 115: return [3 /*break*/, 117];
700
- case 116:
701
- _p = _x.sent();
702
- return [3 /*break*/, 117];
703
- case 117:
704
- if (!hostUserId) return [3 /*break*/, 121];
705
- _x.label = 118;
706
- case 118:
707
- _x.trys.push([118, 120, , 121]);
902
+ case 136:
903
+ _11.sent();
904
+ _11.label = 137;
905
+ case 137: return [3 /*break*/, 139];
906
+ case 138:
907
+ _u = _11.sent();
908
+ return [3 /*break*/, 139];
909
+ case 139:
910
+ if (!hostUserId) return [3 /*break*/, 143];
911
+ _11.label = 140;
912
+ case 140:
913
+ _11.trys.push([140, 142, , 143]);
708
914
  return [4 /*yield*/, sdk.api.users.deleteOne(hostUserId)];
709
- case 119:
710
- _x.sent();
711
- return [3 /*break*/, 121];
712
- case 120:
713
- _q = _x.sent();
714
- return [3 /*break*/, 121];
715
- case 121:
716
- if (!productId) return [3 /*break*/, 125];
717
- _x.label = 122;
718
- case 122:
719
- _x.trys.push([122, 124, , 125]);
915
+ case 141:
916
+ _11.sent();
917
+ return [3 /*break*/, 143];
918
+ case 142:
919
+ _v = _11.sent();
920
+ return [3 /*break*/, 143];
921
+ case 143:
922
+ if (!bookingEnduserId) return [3 /*break*/, 147];
923
+ _11.label = 144;
924
+ case 144:
925
+ _11.trys.push([144, 146, , 147]);
926
+ return [4 /*yield*/, sdk.api.endusers.deleteOne(bookingEnduserId)];
927
+ case 145:
928
+ _11.sent();
929
+ return [3 /*break*/, 147];
930
+ case 146:
931
+ _w = _11.sent();
932
+ return [3 /*break*/, 147];
933
+ case 147:
934
+ if (!templateId) return [3 /*break*/, 151];
935
+ _11.label = 148;
936
+ case 148:
937
+ _11.trys.push([148, 150, , 151]);
938
+ return [4 /*yield*/, sdk.api.calendar_event_templates.deleteOne(templateId)];
939
+ case 149:
940
+ _11.sent();
941
+ return [3 /*break*/, 151];
942
+ case 150:
943
+ _x = _11.sent();
944
+ return [3 /*break*/, 151];
945
+ case 151:
946
+ if (!productId) return [3 /*break*/, 155];
947
+ _11.label = 152;
948
+ case 152:
949
+ _11.trys.push([152, 154, , 155]);
720
950
  return [4 /*yield*/, sdk.api.products.deleteOne(productId)];
721
- case 123:
722
- _x.sent();
723
- return [3 /*break*/, 125];
724
- case 124:
725
- _r = _x.sent();
726
- return [3 /*break*/, 125];
727
- case 125:
728
- if (!formId) return [3 /*break*/, 129];
729
- _x.label = 126;
730
- case 126:
731
- _x.trys.push([126, 128, , 129]);
951
+ case 153:
952
+ _11.sent();
953
+ return [3 /*break*/, 155];
954
+ case 154:
955
+ _y = _11.sent();
956
+ return [3 /*break*/, 155];
957
+ case 155:
958
+ if (!formId) return [3 /*break*/, 159];
959
+ _11.label = 156;
960
+ case 156:
961
+ _11.trys.push([156, 158, , 159]);
732
962
  return [4 /*yield*/, sdk.api.forms.deleteOne(formId)];
733
- case 127:
734
- _x.sent();
735
- return [3 /*break*/, 129];
736
- case 128:
737
- _s = _x.sent();
738
- return [3 /*break*/, 129];
739
- case 129: return [7 /*endfinally*/];
740
- case 130: return [2 /*return*/];
963
+ case 157:
964
+ _11.sent();
965
+ return [3 /*break*/, 159];
966
+ case 158:
967
+ _z = _11.sent();
968
+ return [3 /*break*/, 159];
969
+ case 159: return [7 /*endfinally*/];
970
+ case 160: return [2 /*return*/];
741
971
  }
742
972
  });
743
973
  });