@tellescope/sdk 1.255.18 → 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 (165) hide show
  1. package/lib/cjs/sdk.d.ts +7 -2
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/sdk.js +1 -0
  4. package/lib/cjs/sdk.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -1
  6. package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js +18 -8
  7. package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
  8. package/lib/cjs/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
  9. package/lib/cjs/tests/api_tests/custom_dashboards.test.js +99 -8
  10. package/lib/cjs/tests/api_tests/custom_dashboards.test.js.map +1 -1
  11. package/lib/cjs/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
  12. package/lib/cjs/tests/api_tests/default_summary_journey.test.js.map +1 -0
  13. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
  14. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
  15. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js +278 -0
  16. package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
  17. package/lib/cjs/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
  18. package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js +203 -72
  19. package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
  20. package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
  21. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +212 -33
  22. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  23. package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
  24. package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
  25. package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
  26. package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.d.ts +6 -0
  27. package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.d.ts.map +1 -0
  28. package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.js +102 -0
  29. package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.js.map +1 -0
  30. package/lib/cjs/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
  31. package/lib/cjs/tests/api_tests/scoped_api_keys.test.js +84 -81
  32. package/lib/cjs/tests/api_tests/scoped_api_keys.test.js.map +1 -1
  33. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
  34. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
  35. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +242 -0
  36. package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
  37. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
  38. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
  39. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +443 -0
  40. package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
  41. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
  42. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
  43. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +231 -0
  44. package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
  45. package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -1
  46. package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +121 -78
  47. package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -1
  48. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts +6 -0
  49. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -0
  50. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +1003 -0
  51. package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -0
  52. package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts +17 -0
  53. package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts.map +1 -0
  54. package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +215 -0
  55. package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -0
  56. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts +6 -0
  57. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -0
  58. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +1200 -0
  59. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -0
  60. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts +6 -0
  61. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -0
  62. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +2330 -0
  63. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -0
  64. package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts +6 -0
  65. package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts.map +1 -0
  66. package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.js +204 -0
  67. package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.js.map +1 -0
  68. package/lib/cjs/tests/tests.d.ts.map +1 -1
  69. package/lib/cjs/tests/tests.js +489 -255
  70. package/lib/cjs/tests/tests.js.map +1 -1
  71. package/lib/esm/enduser.d.ts +0 -1
  72. package/lib/esm/enduser.d.ts.map +1 -1
  73. package/lib/esm/sdk.d.ts +9 -5
  74. package/lib/esm/sdk.d.ts.map +1 -1
  75. package/lib/esm/sdk.js +1 -0
  76. package/lib/esm/sdk.js.map +1 -1
  77. package/lib/esm/session.d.ts +0 -1
  78. package/lib/esm/session.d.ts.map +1 -1
  79. package/lib/esm/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -1
  80. package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js +18 -8
  81. package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
  82. package/lib/esm/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
  83. package/lib/esm/tests/api_tests/custom_dashboards.test.js +99 -8
  84. package/lib/esm/tests/api_tests/custom_dashboards.test.js.map +1 -1
  85. package/lib/esm/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
  86. package/lib/esm/tests/api_tests/default_summary_journey.test.js.map +1 -0
  87. package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
  88. package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
  89. package/lib/esm/tests/api_tests/file_download_unicode_names.test.js +248 -0
  90. package/lib/esm/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
  91. package/lib/esm/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
  92. package/lib/esm/tests/api_tests/healthie_multi_integration.test.js +205 -74
  93. package/lib/esm/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
  94. package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
  95. package/lib/esm/tests/api_tests/openloop_webhooks.test.js +213 -34
  96. package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
  97. package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
  98. package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
  99. package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
  100. package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.d.ts +6 -0
  101. package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.d.ts.map +1 -0
  102. package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.js +98 -0
  103. package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.js.map +1 -0
  104. package/lib/esm/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
  105. package/lib/esm/tests/api_tests/scoped_api_keys.test.js +84 -81
  106. package/lib/esm/tests/api_tests/scoped_api_keys.test.js.map +1 -1
  107. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
  108. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
  109. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +238 -0
  110. package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
  111. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
  112. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
  113. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +436 -0
  114. package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
  115. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
  116. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
  117. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +224 -0
  118. package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
  119. package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -1
  120. package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +121 -78
  121. package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -1
  122. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts +6 -0
  123. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -0
  124. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +996 -0
  125. package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -0
  126. package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts +17 -0
  127. package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts.map +1 -0
  128. package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +211 -0
  129. package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -0
  130. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts +6 -0
  131. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -0
  132. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +1193 -0
  133. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -0
  134. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts +6 -0
  135. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -0
  136. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +2323 -0
  137. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -0
  138. package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts +6 -0
  139. package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts.map +1 -0
  140. package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.js +200 -0
  141. package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.js.map +1 -0
  142. package/lib/esm/tests/tests.d.ts.map +1 -1
  143. package/lib/esm/tests/tests.js +489 -255
  144. package/lib/esm/tests/tests.js.map +1 -1
  145. package/lib/tsconfig.tsbuildinfo +1 -1
  146. package/package.json +10 -10
  147. package/src/sdk.ts +4 -0
  148. package/src/tests/api_tests/auto_merge_form_submission.test.ts +13 -4
  149. package/src/tests/api_tests/custom_dashboards.test.ts +92 -0
  150. package/src/tests/api_tests/file_download_unicode_names.test.ts +223 -0
  151. package/src/tests/api_tests/healthie_multi_integration.test.ts +103 -5
  152. package/src/tests/api_tests/openloop_webhooks.test.ts +138 -0
  153. package/src/tests/api_tests/phone_calls_cold_transfer.test.ts +54 -0
  154. package/src/tests/api_tests/phone_calls_conference_hold.test.ts +62 -0
  155. package/src/tests/api_tests/scoped_api_keys.test.ts +5 -0
  156. package/src/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.ts +215 -0
  157. package/src/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.ts +38 -4
  158. package/src/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.ts +721 -0
  159. package/src/tests/api_tests/security/enduser-ai-summary-trust-gate.test.ts +125 -0
  160. package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +753 -0
  161. package/src/tests/api_tests/security/public_endpoint_auth_field_lock.test.ts +1707 -0
  162. package/src/tests/api_tests/tickets_bulk_assign_care_team.test.ts +132 -0
  163. package/src/tests/tests.ts +232 -2
  164. package/test_generated.pdf +0 -0
  165. package/.env +0 -13
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.webhook_outbound_ssrf_tests = void 0;
43
+ require('source-map-support').install();
44
+ var http_1 = __importDefault(require("http"));
45
+ var os_1 = __importDefault(require("os"));
46
+ var sdk_1 = require("../../../sdk");
47
+ var testing_1 = require("@tellescope/testing");
48
+ var setup_1 = require("../../setup");
49
+ var host = process.env.API_URL || 'http://localhost:8080';
50
+ var LOCAL_HOSTNAMES = ['localhost', '127.0.0.1', '::1'];
51
+ var TEST_SECRET = "f0015 outbound ssrf test secret";
52
+ /**
53
+ * The sentinel — the destination that must NEVER be reached — is bound to this machine's own RFC1918
54
+ * address rather than to loopback, and that choice is load-bearing.
55
+ *
56
+ * Loopback is deliberately permitted outside prod/staging (see api/modules/outbound_url.ts), because
57
+ * several test suites bind their receivers with `listen(0, '127.0.0.1')` and an ephemeral port cannot be
58
+ * allowlisted ahead of time. A loopback sentinel would therefore be ALLOWED in exactly the environment
59
+ * where this test runs, and "the sentinel was not contacted" would pass by allowance while proving
60
+ * nothing.
61
+ *
62
+ * RFC1918 is refused in every environment, so binding there keeps the assertion meaningful AND keeps a
63
+ * real listener present — which is what lets us prove non-delivery by observation rather than by
64
+ * reading a log message.
65
+ */
66
+ var first_private_ipv4 = function () {
67
+ for (var _i = 0, _a = Object.values(os_1.default.networkInterfaces()); _i < _a.length; _i++) {
68
+ var addresses = _a[_i];
69
+ for (var _b = 0, _c = addresses !== null && addresses !== void 0 ? addresses : []; _b < _c.length; _b++) {
70
+ var address = _c[_b];
71
+ if (address.family !== 'IPv4' || address.internal)
72
+ continue;
73
+ if (/^10\./.test(address.address))
74
+ return address.address;
75
+ if (/^192\.168\./.test(address.address))
76
+ return address.address;
77
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(address.address))
78
+ return address.address;
79
+ }
80
+ }
81
+ return undefined;
82
+ };
83
+ /**
84
+ * 127.0.0.1 only means "the machine running this test" when the API is also local. Against staging or
85
+ * prod it is the API server itself — the sentinel would never be contacted, every assertion here would
86
+ * pass for the wrong reason, and we would be creating a real webhook in a live tenant. Fail loudly.
87
+ */
88
+ var assert_local_api = function () {
89
+ var hostname = new URL(host).hostname;
90
+ if (!LOCAL_HOSTNAMES.includes(hostname)) {
91
+ throw new Error("F-0015 SSRF tests must run against a local API (got ".concat(host, "). ")
92
+ + "127.0.0.1 would resolve to the API server rather than this machine, and this test configures a webhook.");
93
+ }
94
+ };
95
+ var listen = function (server, address) { return new Promise(function (resolve, reject) {
96
+ server.once('error', reject);
97
+ server.listen(0, address, function () { return resolve(); }); // ephemeral port — nothing here needs a fixed one
98
+ }); };
99
+ var port_of = function (server) { return server.address().port; };
100
+ /** Webhook delivery is fire-and-forget, so absence has to be established by waiting. */
101
+ var settle = function (ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); };
102
+ var wait_until = function (predicate, ms) { return __awaiter(void 0, void 0, void 0, function () {
103
+ var deadline;
104
+ return __generator(this, function (_a) {
105
+ switch (_a.label) {
106
+ case 0:
107
+ deadline = Date.now() + ms;
108
+ _a.label = 1;
109
+ case 1:
110
+ if (!(Date.now() < deadline)) return [3 /*break*/, 3];
111
+ if (predicate())
112
+ return [2 /*return*/, true];
113
+ return [4 /*yield*/, settle(100)];
114
+ case 2:
115
+ _a.sent();
116
+ return [3 /*break*/, 1];
117
+ case 3: return [2 /*return*/, predicate()];
118
+ }
119
+ });
120
+ }); };
121
+ /**
122
+ * Regression tests for F-0015
123
+ * (security-audit/findings/F-0015-webhook-outbound-ssrf.md).
124
+ *
125
+ * Outbound webhooks request customer-configured URLs that are validated only for SYNTAX
126
+ * (urlValidator -> validator.isURL, which happily accepts http://10.0.0.1/). Four call sites are
127
+ * affected: api/modules/webhooks.ts:145, :214, :270, and the five-method block at api/v1/api.ts:9723.
128
+ *
129
+ * Two properties make this worse than a blind SSRF:
130
+ * - A non-2xx response body is stored in log.response, and webhook_logs is surfaced to customers for
131
+ * debugging — so an internal endpoint's error body is readable by the tenant that configured it.
132
+ * - The automation send-webhook action builds its URL at runtime through five nested template
133
+ * replacers over action.info.url, so patient-controlled field values can reach the destination.
134
+ *
135
+ * And critically: axios@0.30.2 follows redirects by default (maxRedirects: 5). A pre-flight check on
136
+ * the configured URL is therefore NOT sufficient — a receiver can simply 302 to an internal address
137
+ * after validation has already passed. T4 exists to prove exactly that, which is why the fix has to
138
+ * validate at connect time (a guarded agent) rather than as a string check.
139
+ *
140
+ * Each test below asserts ABSENCE ("the sentinel was never contacted"), which passes trivially if the
141
+ * request never happened at all — a missing allowlist entry, a webhook that never fired, a broken
142
+ * subscription. So each is paired with a positive control that proves the attempt really was made.
143
+ */
144
+ var webhook_outbound_ssrf_tests = function (_a) {
145
+ var sdk = _a.sdk;
146
+ return __awaiter(void 0, void 0, void 0, function () {
147
+ var SENTINEL, privateIP, sentinelHits, okHits, redirectHits, sentinelURL, sentinel, allowed, enduserId, templateEnduserId, configuredWebhook, sentinelPort, okURL_1, redirectURL_1, enduser_1, send_automation_webhook_to_1, templateEnduser_1, webhooks, _i, _b, w, _c, _d, _e;
148
+ return __generator(this, function (_f) {
149
+ switch (_f.label) {
150
+ case 0:
151
+ (0, testing_1.log_header)("F-0015: outbound webhook SSRF");
152
+ assert_local_api();
153
+ SENTINEL = "f0015-sentinel-".concat(Date.now(), "-").concat(Math.random().toString(36).slice(2));
154
+ privateIP = first_private_ipv4();
155
+ if (!privateIP) {
156
+ // Degrade loudly rather than silently passing. Everything below needs a bindable address that the
157
+ // guard refuses in every environment, and loopback does not qualify (see the note above).
158
+ (0, testing_1.assert)(false, 'F-0015 SKIPPED: no non-loopback RFC1918 interface on this host, so the SSRF sentinel cannot be '
159
+ + 'bound anywhere the guard refuses. Run on a host with a private network interface.', 'F-0015 outbound SSRF');
160
+ return [2 /*return*/];
161
+ }
162
+ sentinelHits = 0 // must NEVER be reached once fixed
163
+ ;
164
+ okHits = 0 // permitted target: must stay reachable
165
+ ;
166
+ redirectHits = 0 // permitted hop 1 of the redirect chain
167
+ ;
168
+ sentinelURL = '';
169
+ sentinel = http_1.default.createServer(function (_, res) {
170
+ sentinelHits++;
171
+ res.writeHead(200, { 'content-type': 'application/json' });
172
+ res.end(JSON.stringify({ sentinel: SENTINEL, credentials: 'internal-only' }));
173
+ });
174
+ allowed = http_1.default.createServer(function (req, res) {
175
+ if ((req.url || '').startsWith('/redirect-to-internal')) {
176
+ redirectHits++;
177
+ // Hop 1 is permitted and genuinely succeeds; hop 2 points somewhere that is not.
178
+ res.writeHead(302, { location: sentinelURL });
179
+ res.end();
180
+ return;
181
+ }
182
+ okHits++;
183
+ res.writeHead(200, { 'content-type': 'application/json' });
184
+ res.end(JSON.stringify({ ok: true }));
185
+ });
186
+ configuredWebhook = false;
187
+ _f.label = 1;
188
+ case 1:
189
+ _f.trys.push([1, , 14, 31]);
190
+ return [4 /*yield*/, listen(sentinel, privateIP)];
191
+ case 2:
192
+ _f.sent();
193
+ sentinelPort = port_of(sentinel);
194
+ sentinelURL = "http://".concat(privateIP, ":").concat(sentinelPort, "/ssrf-sentinel");
195
+ return [4 /*yield*/, listen(allowed, '127.0.0.1')];
196
+ case 3:
197
+ _f.sent();
198
+ okURL_1 = "http://127.0.0.1:".concat(port_of(allowed), "/webhook-ok");
199
+ redirectURL_1 = "http://127.0.0.1:".concat(port_of(allowed), "/redirect-to-internal");
200
+ return [4 /*yield*/, sdk.api.endusers.createOne({
201
+ email: "f0015-".concat(Date.now(), "@example.com"), fname: 'F0015', lname: 'Ssrf',
202
+ })];
203
+ case 4:
204
+ enduser_1 = _f.sent();
205
+ enduserId = enduser_1.id;
206
+ send_automation_webhook_to_1 = function (url, forEnduserId) {
207
+ if (forEnduserId === void 0) { forEnduserId = enduser_1.id; }
208
+ return __awaiter(void 0, void 0, void 0, function () {
209
+ var outcome;
210
+ return __generator(this, function (_a) {
211
+ switch (_a.label) {
212
+ case 0:
213
+ sentinelHits = 0;
214
+ okHits = 0;
215
+ redirectHits = 0;
216
+ return [4 /*yield*/, sdk.api.webhooks.send_automation_webhook({
217
+ enduserId: forEnduserId,
218
+ action: { info: { url: url } },
219
+ }).then(function () { return 'resolved'; }).catch(function () { return 'rejected'; })
220
+ // The action awaits its own axios call, but give redirect chains a moment to land.
221
+ ];
222
+ case 1:
223
+ outcome = _a.sent();
224
+ // The action awaits its own axios call, but give redirect chains a moment to land.
225
+ return [4 /*yield*/, settle(750)];
226
+ case 2:
227
+ // The action awaits its own axios call, but give redirect chains a moment to land.
228
+ _a.sent();
229
+ return [2 /*return*/, { outcome: outcome, sentinelHits: sentinelHits, okHits: okHits, redirectHits: redirectHits }];
230
+ }
231
+ });
232
+ });
233
+ };
234
+ // ---------------------------------------------------------------------------------------------
235
+ // T6 first: the positive control for the whole file. If an allowlisted target does not receive the
236
+ // webhook, every "sentinel not contacted" assertion below is meaningless.
237
+ // ---------------------------------------------------------------------------------------------
238
+ return [4 /*yield*/, (0, testing_1.async_test)('F-0015: automation webhook to a permitted target still delivers (regression baseline)', function () { return send_automation_webhook_to_1(okURL_1); }, { onResult: function (r) { return r.okHits === 1 && r.sentinelHits === 0; } })
239
+ // T3 — direct non-allowlisted private destination.
240
+ ];
241
+ case 5:
242
+ // ---------------------------------------------------------------------------------------------
243
+ // T6 first: the positive control for the whole file. If an allowlisted target does not receive the
244
+ // webhook, every "sentinel not contacted" assertion below is meaningless.
245
+ // ---------------------------------------------------------------------------------------------
246
+ _f.sent();
247
+ // T3 — direct non-allowlisted private destination.
248
+ return [4 /*yield*/, (0, testing_1.async_test)('F-0015: automation webhook does not reach a private destination', function () { return send_automation_webhook_to_1(sentinelURL); }, { onResult: function (r) { return r.sentinelHits === 0; } })
249
+ // T4 — the redirect bypass. THE test that justifies a connect-time guard over a pre-flight check.
250
+ ];
251
+ case 6:
252
+ // T3 — direct non-allowlisted private destination.
253
+ _f.sent();
254
+ // T4 — the redirect bypass. THE test that justifies a connect-time guard over a pre-flight check.
255
+ return [4 /*yield*/, (0, testing_1.async_test)('F-0015: automation webhook does not follow a redirect from a permitted host into a private one', function () { return send_automation_webhook_to_1(redirectURL_1); }, {
256
+ // redirectHits === 1 is the positive control: hop 1 was allowlisted and really was reached, so
257
+ // sentinelHits === 0 can only mean hop 2 was refused — not that the request never happened.
258
+ onResult: function (r) { return r.redirectHits === 1 && r.sentinelHits === 0; },
259
+ })
260
+ // T5 — the destination arrives via template interpolation, so validation must run AFTER the
261
+ // replacers rather than against the stored config. {{enduser.<key>}} resolves from enduser.fields.
262
+ ];
263
+ case 7:
264
+ // T4 — the redirect bypass. THE test that justifies a connect-time guard over a pre-flight check.
265
+ _f.sent();
266
+ return [4 /*yield*/, sdk.api.endusers.createOne({
267
+ email: "f0015-tpl-".concat(Date.now(), "@example.com"),
268
+ fname: 'F0015', lname: 'Template',
269
+ fields: { ssrfTarget: "".concat(privateIP, ":").concat(sentinelPort) },
270
+ })];
271
+ case 8:
272
+ templateEnduser_1 = _f.sent();
273
+ templateEnduserId = templateEnduser_1.id;
274
+ return [4 /*yield*/, (0, testing_1.async_test)('F-0015: automation webhook validates the URL after template interpolation, not before', function () { return send_automation_webhook_to_1('http://{{enduser.ssrfTarget}}/ssrf-sentinel', templateEnduser_1.id); }, { onResult: function (r) { return r.sentinelHits === 0; } })
275
+ // ---------------------------------------------------------------------------------------------
276
+ // The configured-webhook delivery path (api/modules/webhooks.ts:145), which is a different code
277
+ // path from the automation action above. Paired: allowlisted target proves the subscription and
278
+ // trigger plumbing works, then the same plumbing pointed at the sentinel must not reach it.
279
+ // ---------------------------------------------------------------------------------------------
280
+ // CUDSubscription is an object of booleans, not a list of verbs (see webhooks_tests.ts fullSubscription).
281
+ ];
282
+ case 9:
283
+ _f.sent();
284
+ // ---------------------------------------------------------------------------------------------
285
+ // The configured-webhook delivery path (api/modules/webhooks.ts:145), which is a different code
286
+ // path from the automation action above. Paired: allowlisted target proves the subscription and
287
+ // trigger plumbing works, then the same plumbing pointed at the sentinel must not reach it.
288
+ // ---------------------------------------------------------------------------------------------
289
+ // CUDSubscription is an object of booleans, not a list of verbs (see webhooks_tests.ts fullSubscription).
290
+ return [4 /*yield*/, sdk.api.webhooks.configure({
291
+ url: okURL_1, secret: TEST_SECRET, subscriptions: { endusers: { create: true } },
292
+ })];
293
+ case 10:
294
+ // ---------------------------------------------------------------------------------------------
295
+ // The configured-webhook delivery path (api/modules/webhooks.ts:145), which is a different code
296
+ // path from the automation action above. Paired: allowlisted target proves the subscription and
297
+ // trigger plumbing works, then the same plumbing pointed at the sentinel must not reach it.
298
+ // ---------------------------------------------------------------------------------------------
299
+ // CUDSubscription is an object of booleans, not a list of verbs (see webhooks_tests.ts fullSubscription).
300
+ _f.sent();
301
+ configuredWebhook = true;
302
+ return [4 /*yield*/, (0, testing_1.async_test)('F-0015: configured webhook to a permitted target still delivers (positive control)', function () { return __awaiter(void 0, void 0, void 0, function () {
303
+ var e, delivered;
304
+ return __generator(this, function (_a) {
305
+ switch (_a.label) {
306
+ case 0:
307
+ sentinelHits = 0;
308
+ okHits = 0;
309
+ return [4 /*yield*/, sdk.api.endusers.createOne({ email: "f0015-ok-".concat(Date.now(), "@example.com") })];
310
+ case 1:
311
+ e = _a.sent();
312
+ return [4 /*yield*/, wait_until(function () { return okHits > 0; }, 8000)];
313
+ case 2:
314
+ delivered = _a.sent();
315
+ return [4 /*yield*/, sdk.api.endusers.deleteOne(e.id).catch(function () { })];
316
+ case 3:
317
+ _a.sent();
318
+ return [2 /*return*/, { delivered: delivered, okHits: okHits, sentinelHits: sentinelHits }];
319
+ }
320
+ });
321
+ }); }, { onResult: function (r) { return r.delivered === true && r.sentinelHits === 0; } })
322
+ // Repoint the same configuration at the sentinel, keeping the subscription intact.
323
+ ];
324
+ case 11:
325
+ _f.sent();
326
+ // Repoint the same configuration at the sentinel, keeping the subscription intact.
327
+ return [4 /*yield*/, sdk.api.webhooks.update({ url: sentinelURL })];
328
+ case 12:
329
+ // Repoint the same configuration at the sentinel, keeping the subscription intact.
330
+ _f.sent();
331
+ return [4 /*yield*/, (0, testing_1.async_test)('F-0015: configured webhook does not deliver to a private destination', function () { return __awaiter(void 0, void 0, void 0, function () {
332
+ var e;
333
+ return __generator(this, function (_a) {
334
+ switch (_a.label) {
335
+ case 0:
336
+ sentinelHits = 0;
337
+ okHits = 0;
338
+ return [4 /*yield*/, sdk.api.endusers.createOne({ email: "f0015-blocked-".concat(Date.now(), "@example.com") })
339
+ // Wait out the window a real delivery would have landed in, then assert it did not.
340
+ ];
341
+ case 1:
342
+ e = _a.sent();
343
+ // Wait out the window a real delivery would have landed in, then assert it did not.
344
+ return [4 /*yield*/, settle(6000)];
345
+ case 2:
346
+ // Wait out the window a real delivery would have landed in, then assert it did not.
347
+ _a.sent();
348
+ return [4 /*yield*/, sdk.api.endusers.deleteOne(e.id).catch(function () { })];
349
+ case 3:
350
+ _a.sent();
351
+ return [2 /*return*/, { sentinelHits: sentinelHits }];
352
+ }
353
+ });
354
+ }); }, { onResult: function (r) { return r.sentinelHits === 0; } })];
355
+ case 13:
356
+ _f.sent();
357
+ return [3 /*break*/, 31];
358
+ case 14:
359
+ if (!configuredWebhook) return [3 /*break*/, 22];
360
+ _f.label = 15;
361
+ case 15:
362
+ _f.trys.push([15, 21, , 22]);
363
+ return [4 /*yield*/, sdk.api.webhooks.getSome({})];
364
+ case 16:
365
+ webhooks = (_f.sent()).webhooks;
366
+ _i = 0, _b = (webhooks !== null && webhooks !== void 0 ? webhooks : []);
367
+ _f.label = 17;
368
+ case 17:
369
+ if (!(_i < _b.length)) return [3 /*break*/, 20];
370
+ w = _b[_i];
371
+ return [4 /*yield*/, sdk.api.webhooks.deleteOne(w.id).catch(function () { })];
372
+ case 18:
373
+ _f.sent();
374
+ _f.label = 19;
375
+ case 19:
376
+ _i++;
377
+ return [3 /*break*/, 17];
378
+ case 20: return [3 /*break*/, 22];
379
+ case 21:
380
+ _c = _f.sent();
381
+ return [3 /*break*/, 22];
382
+ case 22:
383
+ if (!enduserId) return [3 /*break*/, 26];
384
+ _f.label = 23;
385
+ case 23:
386
+ _f.trys.push([23, 25, , 26]);
387
+ return [4 /*yield*/, sdk.api.endusers.deleteOne(enduserId)];
388
+ case 24:
389
+ _f.sent();
390
+ return [3 /*break*/, 26];
391
+ case 25:
392
+ _d = _f.sent();
393
+ return [3 /*break*/, 26];
394
+ case 26:
395
+ if (!templateEnduserId) return [3 /*break*/, 30];
396
+ _f.label = 27;
397
+ case 27:
398
+ _f.trys.push([27, 29, , 30]);
399
+ return [4 /*yield*/, sdk.api.endusers.deleteOne(templateEnduserId)];
400
+ case 28:
401
+ _f.sent();
402
+ return [3 /*break*/, 30];
403
+ case 29:
404
+ _e = _f.sent();
405
+ return [3 /*break*/, 30];
406
+ case 30:
407
+ sentinel.close();
408
+ allowed.close();
409
+ return [7 /*endfinally*/];
410
+ case 31: return [2 /*return*/];
411
+ }
412
+ });
413
+ });
414
+ };
415
+ exports.webhook_outbound_ssrf_tests = webhook_outbound_ssrf_tests;
416
+ if (require.main === module) {
417
+ console.log("\uD83C\uDF10 Using API URL: ".concat(host));
418
+ var sdk_2 = new sdk_1.Session({ host: host });
419
+ var sdkNonAdmin_1 = new sdk_1.Session({ host: host });
420
+ var runTests = function () { return __awaiter(void 0, void 0, void 0, function () {
421
+ return __generator(this, function (_a) {
422
+ switch (_a.label) {
423
+ case 0: return [4 /*yield*/, (0, setup_1.setup_tests)(sdk_2, sdkNonAdmin_1)];
424
+ case 1:
425
+ _a.sent();
426
+ return [4 /*yield*/, (0, exports.webhook_outbound_ssrf_tests)({ sdk: sdk_2, sdkNonAdmin: sdkNonAdmin_1 })];
427
+ case 2:
428
+ _a.sent();
429
+ return [2 /*return*/];
430
+ }
431
+ });
432
+ }); };
433
+ runTests()
434
+ .then(function () {
435
+ console.log("✅ F-0015 outbound webhook SSRF test suite completed successfully");
436
+ process.exit(0);
437
+ })
438
+ .catch(function (error) {
439
+ console.error("❌ F-0015 outbound webhook SSRF test suite failed:", error);
440
+ process.exit(1);
441
+ });
442
+ }
443
+ //# sourceMappingURL=F-0015-webhook-outbound-ssrf.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"F-0015-webhook-outbound-ssrf.test.js","sourceRoot":"","sources":["../../../../../src/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { Session } from "../../../sdk";
2
+ /**
3
+ * Regression tests for F-0025
4
+ * (security-audit/findings/F-0025-proxy-image-unauth-ssrf.md).
5
+ *
6
+ * GET /v1/proxy-image accepted an arbitrary ?url= and streamed the upstream response body straight
7
+ * back with Access-Control-Allow-Origin: *, with no authentication, no destination validation and no
8
+ * content-type restriction. That reaches http://169.254.169.254/ (EC2 instance credentials under
9
+ * IMDSv1), 127.0.0.1, and anything else inside the VPC.
10
+ *
11
+ * The endpoint's only legitimate job is to re-serve an image that ALREADY renders in the staff
12
+ * browser, with permissive CORS headers, so html2canvas can rasterize a form-response PDF without
13
+ * tainting the canvas (canvas.toDataURL() throws on a tainted canvas, and the form-response view sets
14
+ * crossOrigin="anonymous"). Anything reachable only from inside our network was therefore never a
15
+ * legitimate input — which is why refusing non-public destinations cannot regress the feature.
16
+ *
17
+ * Repro: stand up a local HTTP server that serves a JSON secret, point proxy-image at it with no
18
+ * credentials, and assert the secret does not come back.
19
+ * - red (unfixed): the sentinel body is streamed back to an unauthenticated caller
20
+ * - green (fixed): the request is refused and the sentinel server is never contacted
21
+ *
22
+ * Assertions are deliberately written as "4xx and nothing leaked" rather than pinning an exact status,
23
+ * because the fix both authenticates the endpoint and moves it to POST (the url moves into the body so
24
+ * presigned S3 URLs for patient attachments stop landing in access logs). Either 401 or 404/405 on the
25
+ * old GET shape is an acceptable outcome; streaming the body is not.
26
+ *
27
+ * The authenticated happy-path test lives with the Phase 2 endpoint change — it cannot exist here
28
+ * because the POST route does not yet exist, and a test that fails for "not implemented" would muddy
29
+ * the vulnerability proof.
30
+ */
31
+ export declare const proxy_image_ssrf_tests: ({ sdk }: {
32
+ sdk: Session;
33
+ sdkNonAdmin: Session;
34
+ }) => Promise<void>;
35
+ //# sourceMappingURL=F-0025-proxy-image-unauth-ssrf.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"F-0025-proxy-image-unauth-ssrf.test.d.ts","sourceRoot":"","sources":["../../../../../src/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AA0BtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,sBAAsB;SAA2B,OAAO;iBAAe,OAAO;mBAkG1F,CAAA"}