@oneuptime/common 12.0.11 → 12.0.13

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 (251) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
  2. package/Models/DatabaseModels/NetworkDevice.ts +64 -0
  3. package/Models/DatabaseModels/StatusPage.ts +59 -0
  4. package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
  5. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
  6. package/Server/API/AIChatAPI.ts +55 -56
  7. package/Server/API/CommonAPI.ts +89 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +20 -9
  9. package/Server/API/SlackAPI.ts +13 -10
  10. package/Server/API/StatusPageAPI.ts +2197 -2128
  11. package/Server/API/TelemetryAPI.ts +72 -3
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  15. package/Server/Middleware/UserAuthorization.ts +11 -2
  16. package/Server/Services/ApiKeyPermissionService.ts +116 -2
  17. package/Server/Services/DatabaseService.ts +17 -6
  18. package/Server/Services/LogAggregationService.ts +34 -0
  19. package/Server/Services/LogService.ts +21 -0
  20. package/Server/Services/NetworkSiteService.ts +12 -0
  21. package/Server/Services/SpanService.ts +21 -0
  22. package/Server/Services/TraceAggregationService.ts +15 -0
  23. package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
  24. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  25. package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
  26. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
  27. package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
  28. package/Server/Utils/APIKey/AccessPermission.ts +16 -40
  29. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
  30. package/Server/Utils/LogRedaction.ts +576 -0
  31. package/Server/Utils/Logger.ts +123 -46
  32. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
  33. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
  34. package/Server/Utils/Monitor/MonitorResource.ts +29 -9
  35. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
  36. package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
  37. package/Server/Utils/StartServer.ts +30 -9
  38. package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
  39. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
  40. package/Server/Utils/VM/VMRunner.ts +693 -874
  41. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
  42. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
  43. package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
  44. package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
  45. package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
  46. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
  47. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
  48. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
  49. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
  50. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
  51. package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
  52. package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
  53. package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
  54. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
  55. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
  56. package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
  57. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
  58. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
  59. package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
  60. package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
  61. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
  62. package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
  63. package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
  64. package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
  65. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
  66. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
  67. package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
  68. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
  69. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
  71. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
  72. package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
  73. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
  74. package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
  75. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
  76. package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
  77. package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
  78. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
  79. package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
  80. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
  81. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
  82. package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
  83. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
  84. package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
  85. package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
  86. package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
  87. package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
  88. package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
  89. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
  90. package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
  91. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
  92. package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
  93. package/Tests/UI/Components/MasterPage.test.tsx +36 -1
  94. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
  95. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
  96. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
  97. package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
  98. package/Types/API/Hostname.ts +79 -0
  99. package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
  100. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
  101. package/Types/Database/DatabaseProperty.ts +38 -0
  102. package/Types/Database/TableColumn.ts +34 -7
  103. package/Types/Events/Recurring.ts +16 -1
  104. package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
  105. package/Types/Monitor/MonitorStep.ts +13 -1
  106. package/Types/Monitor/MonitorType.ts +345 -40
  107. package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
  108. package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
  109. package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
  110. package/UI/Components/CardSelect/CardSelect.tsx +714 -105
  111. package/UI/Components/EmptyState/EmptyState.tsx +8 -1
  112. package/UI/Components/Footer/Footer.tsx +32 -6
  113. package/UI/Components/Forms/Fields/FormField.tsx +3 -0
  114. package/UI/Components/Forms/Types/Field.ts +8 -0
  115. package/UI/Components/Loader/PageLoader.tsx +29 -8
  116. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
  117. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
  118. package/UI/Components/MasterPage/MasterPage.tsx +24 -11
  119. package/Utils/Monitor/MonitorMetricType.ts +16 -1
  120. package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
  121. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
  122. package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
  123. package/Utils/Telemetry/CrossSignalScope.ts +75 -4
  124. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
  125. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
  127. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
  129. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
  131. package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  133. package/build/dist/Server/API/AIChatAPI.js +48 -35
  134. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  135. package/build/dist/Server/API/CommonAPI.js +59 -0
  136. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  137. package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
  138. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  139. package/build/dist/Server/API/SlackAPI.js +13 -10
  140. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  141. package/build/dist/Server/API/StatusPageAPI.js +745 -673
  142. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  143. package/build/dist/Server/API/TelemetryAPI.js +34 -3
  144. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js.map +1 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  151. package/build/dist/Server/Middleware/UserAuthorization.js +6 -2
  152. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  153. package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
  154. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  155. package/build/dist/Server/Services/DatabaseService.js +15 -6
  156. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  157. package/build/dist/Server/Services/LogAggregationService.js +2 -0
  158. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  159. package/build/dist/Server/Services/LogService.js +16 -0
  160. package/build/dist/Server/Services/LogService.js.map +1 -1
  161. package/build/dist/Server/Services/NetworkSiteService.js +12 -0
  162. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  163. package/build/dist/Server/Services/SpanService.js +16 -0
  164. package/build/dist/Server/Services/SpanService.js.map +1 -1
  165. package/build/dist/Server/Services/TraceAggregationService.js +2 -0
  166. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  167. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
  168. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  169. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  170. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  171. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
  172. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
  173. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
  174. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  175. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
  176. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  177. package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
  178. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  179. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
  180. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  181. package/build/dist/Server/Utils/LogRedaction.js +449 -0
  182. package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
  183. package/build/dist/Server/Utils/Logger.js +98 -45
  184. package/build/dist/Server/Utils/Logger.js.map +1 -1
  185. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
  186. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  187. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
  188. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  189. package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
  190. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
  192. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  193. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
  194. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
  195. package/build/dist/Server/Utils/StartServer.js +10 -7
  196. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  197. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
  198. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
  199. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
  200. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
  201. package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
  202. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  203. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
  204. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  205. package/build/dist/Types/API/Hostname.js +63 -0
  206. package/build/dist/Types/API/Hostname.js.map +1 -1
  207. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
  208. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  209. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
  210. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  211. package/build/dist/Types/Database/DatabaseProperty.js +38 -0
  212. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  213. package/build/dist/Types/Database/TableColumn.js +24 -6
  214. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +13 -1
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Monitor/MonitorStep.js +9 -1
  218. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  219. package/build/dist/Types/Monitor/MonitorType.js +330 -31
  220. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  221. package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
  222. package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
  223. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
  224. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
  225. package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
  226. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  227. package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
  228. package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
  229. package/build/dist/UI/Components/Footer/Footer.js +7 -4
  230. package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
  231. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  232. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  233. package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
  234. package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
  235. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
  236. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  237. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
  238. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  239. package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
  240. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  241. package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
  242. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  243. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
  244. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  245. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
  246. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
  247. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
  248. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
  249. package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
  250. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  251. package/package.json +1 -1
@@ -1,361 +1,14 @@
1
1
  import CapturedMetric from "../../../Types/Monitor/CustomCodeMonitor/CapturedMetric";
2
2
  import ReturnResult from "../../../Types/IsolatedVM/ReturnResult";
3
- import { JSONObject, JSONValue } from "../../../Types/JSON";
3
+ import { JSONObject } from "../../../Types/JSON";
4
4
  import axios, { AxiosResponse } from "axios";
5
5
  import crypto from "crypto";
6
6
  import http from "http";
7
7
  import https from "https";
8
8
  import ivm from "isolated-vm";
9
9
  import CaptureSpan from "../Telemetry/CaptureSpan";
10
- import Dictionary from "../../../Types/Dictionary";
11
- import GenericObject from "../../../Types/GenericObject";
12
- import vm, { Context } from "vm";
13
10
  import SSRFProtection from "../SSRFProtection";
14
11
 
15
- /**
16
- * Symbol used to retrieve the real (unwrapped) target from a sandbox proxy.
17
- * Hidden from user code via ownKeys / has traps.
18
- */
19
- const PROXY_TARGET_SYMBOL: unique symbol = Symbol("sandboxProxyTarget");
20
-
21
- /**
22
- * Hardening prelude injected before user code in `runCodeInNodeVM`.
23
- *
24
- * Node's `vm` module is not a security boundary. The published PoC for
25
- * GHSA-g9cp-35m2-fjv6 forces a stack-overflow `RangeError`, walks
26
- * `e.__proto__.__proto__.__proto__` to `Object.prototype`, then reads
27
- * `.toString.constructor` to obtain a `Function` constructor that compiles
28
- * code in a realm where `process.binding('spawn_sync')` is reachable.
29
- *
30
- * This prelude closes that path by:
31
- * - severing `Error.prototype`'s link to `Object.prototype` so the 3-level
32
- * walk lands on `null` instead of `Object.prototype`;
33
- * - deleting `.constructor` from every built-in prototype, so even a
34
- * different walk (e.g. `(0).constructor.constructor`) cannot resolve to a
35
- * function constructor;
36
- * - clearing `Function` / `eval` from the sandbox global;
37
- * - freezing the affected prototypes so user code cannot reattach them.
38
- *
39
- * This is a hotfix for the public PoC. The durable fix is to drop
40
- * `runCodeInNodeVM` in favor of running synthetic monitor scripts in an
41
- * out-of-process sandbox (tracked on the `probe-runner` branch).
42
- */
43
- const VM_HARDENING_PRELUDE: string = `(() => {
44
- const _ctors = [
45
- Object, Function, Array, String, Number, Boolean, RegExp,
46
- Error, RangeError, TypeError, SyntaxError, ReferenceError, EvalError, URIError,
47
- Symbol, Date, Map, Set, WeakMap, WeakSet, Promise, Proxy,
48
- ArrayBuffer, DataView,
49
- Int8Array, Uint8Array, Uint8ClampedArray,
50
- Int16Array, Uint16Array, Int32Array, Uint32Array,
51
- Float32Array, Float64Array,
52
- ];
53
- if (typeof BigInt !== 'undefined') _ctors.push(BigInt);
54
-
55
- for (const C of _ctors) {
56
- try { if (C && C.prototype) delete C.prototype.constructor; } catch (_) {}
57
- }
58
-
59
- // Generator / async-function prototypes have no named global — reach via syntax.
60
- try { delete Object.getPrototypeOf(function*(){}).constructor; } catch (_) {}
61
- try { delete Object.getPrototypeOf(async function(){}).constructor; } catch (_) {}
62
- try { delete Object.getPrototypeOf(async function*(){}).constructor; } catch (_) {}
63
-
64
- try { Object.setPrototypeOf(Error.prototype, null); } catch (_) {}
65
-
66
- try {
67
- Object.defineProperty(globalThis, 'Function', {
68
- value: undefined, writable: false, configurable: false,
69
- });
70
- } catch (_) {}
71
- try {
72
- Object.defineProperty(globalThis, 'eval', {
73
- value: undefined, writable: false, configurable: false,
74
- });
75
- } catch (_) {}
76
-
77
- for (const C of _ctors) {
78
- try { if (C && C.prototype) Object.freeze(C.prototype); } catch (_) {}
79
- }
80
- })();`;
81
-
82
- /** Properties blocked on every host-realm object exposed to the sandbox. */
83
- const BLOCKED_SANDBOX_PROPERTIES: ReadonlySet<string> = new Set([
84
- "constructor",
85
- "__proto__",
86
- "prototype",
87
- "mainModule",
88
- /*
89
- * Block Playwright methods that can spawn processes or access internals.
90
- * Prevents RCE via browser.browserType().launch({executablePath:"/bin/sh"})
91
- * and traversal via page.context().browser().browserType().launch(...)
92
- */
93
- "browserType", // Browser → BrowserType (which has launch/connect)
94
- "_browserType", // Internal alias for browserType — same escape vector
95
- "launch", // BrowserType.launch() spawns a child process
96
- "launchServer", // BrowserType.launchServer() spawns a browser server process
97
- "launchPersistentContext", // BrowserType.launchPersistentContext() spawns a child process
98
- "connectOverCDP", // BrowserType.connectOverCDP() connects via Chrome DevTools Protocol
99
- "connect", // BrowserType.connect() connects to a remote browser
100
- "newCDPSession", // BrowserContext/Page.newCDPSession() opens raw CDP sessions
101
- ]);
102
-
103
- /**
104
- * Wraps a host-realm value in a Proxy that blocks prototype-chain traversal.
105
- * Primitives and null/undefined pass through unchanged.
106
- * Object proxies are cached to preserve identity; function proxies are created
107
- * per-access so they bind to the correct `this` (parent object).
108
- */
109
- function createSandboxProxy(
110
- value: unknown,
111
- cache: WeakMap<GenericObject, unknown>,
112
- parentObj?: GenericObject,
113
- ): unknown {
114
- if (value === null || value === undefined) {
115
- return value;
116
- }
117
-
118
- const valueType: string = typeof value;
119
-
120
- if (valueType !== "object" && valueType !== "function") {
121
- return value;
122
- }
123
-
124
- const target: GenericObject = value as GenericObject;
125
-
126
- if (valueType === "function") {
127
- /*
128
- * Function proxies are NOT cached because the same function may be a method
129
- * on different parent objects and needs a different `this` binding each time.
130
- */
131
- const fnProxy: unknown = new Proxy(
132
- target as (...args: unknown[]) => unknown,
133
- {
134
- get(
135
- fnTarget: (...args: unknown[]) => unknown,
136
- prop: string | symbol,
137
- ): unknown {
138
- if (prop === PROXY_TARGET_SYMBOL) {
139
- return fnTarget;
140
- }
141
- if (
142
- typeof prop === "string" &&
143
- BLOCKED_SANDBOX_PROPERTIES.has(prop)
144
- ) {
145
- return undefined;
146
- }
147
- const val: unknown = Reflect.get(
148
- fnTarget,
149
- prop,
150
- fnTarget as GenericObject,
151
- );
152
- return createSandboxProxy(val, cache, fnTarget as GenericObject);
153
- },
154
- getPrototypeOf(): null {
155
- return null;
156
- },
157
- apply(
158
- fnTarget: (...args: unknown[]) => unknown,
159
- _thisArg: unknown,
160
- args: unknown[],
161
- ): unknown {
162
- const thisObj: GenericObject = (parentObj ||
163
- fnTarget) as GenericObject;
164
- try {
165
- const result: unknown = Reflect.apply(fnTarget, thisObj, args);
166
- if (result instanceof Promise) {
167
- return result.then(
168
- (v: unknown) => {
169
- return createSandboxProxy(v, cache);
170
- },
171
- (err: unknown) => {
172
- throw createSandboxProxy(err, cache);
173
- },
174
- );
175
- }
176
- return createSandboxProxy(result, cache);
177
- } catch (err: unknown) {
178
- throw createSandboxProxy(err, cache);
179
- }
180
- },
181
- has(
182
- fnTarget: (...args: unknown[]) => unknown,
183
- prop: string | symbol,
184
- ): boolean {
185
- if (
186
- typeof prop === "string" &&
187
- BLOCKED_SANDBOX_PROPERTIES.has(prop)
188
- ) {
189
- return false;
190
- }
191
- return Reflect.has(fnTarget, prop);
192
- },
193
- ownKeys(
194
- fnTarget: (...args: unknown[]) => unknown,
195
- ): (string | symbol)[] {
196
- return Reflect.ownKeys(fnTarget).filter((k: string | symbol) => {
197
- return !(
198
- typeof k === "string" && BLOCKED_SANDBOX_PROPERTIES.has(k)
199
- );
200
- });
201
- },
202
- getOwnPropertyDescriptor(
203
- fnTarget: (...args: unknown[]) => unknown,
204
- prop: string | symbol,
205
- ): PropertyDescriptor | undefined {
206
- if (
207
- typeof prop === "string" &&
208
- BLOCKED_SANDBOX_PROPERTIES.has(prop)
209
- ) {
210
- return undefined;
211
- }
212
- const desc: PropertyDescriptor | undefined =
213
- Reflect.getOwnPropertyDescriptor(fnTarget, prop);
214
- if (desc && "value" in desc) {
215
- desc.value = createSandboxProxy(
216
- desc.value,
217
- cache,
218
- fnTarget as GenericObject,
219
- );
220
- }
221
- return desc;
222
- },
223
- },
224
- );
225
- return fnProxy;
226
- }
227
-
228
- // Object — use cache to preserve identity and handle circular references
229
- if (cache.has(target)) {
230
- return cache.get(target);
231
- }
232
-
233
- const objProxy: GenericObject = new Proxy(target, {
234
- get(objTarget: GenericObject, prop: string | symbol): unknown {
235
- if (prop === PROXY_TARGET_SYMBOL) {
236
- return objTarget;
237
- }
238
- if (typeof prop === "string" && BLOCKED_SANDBOX_PROPERTIES.has(prop)) {
239
- return undefined;
240
- }
241
- const val: unknown = Reflect.get(objTarget, prop, objTarget);
242
- return createSandboxProxy(val, cache, objTarget);
243
- },
244
- getPrototypeOf(): null {
245
- return null;
246
- },
247
- set(
248
- objTarget: GenericObject,
249
- prop: string | symbol,
250
- newValue: unknown,
251
- ): boolean {
252
- return Reflect.set(objTarget, prop, newValue);
253
- },
254
- has(objTarget: GenericObject, prop: string | symbol): boolean {
255
- if (typeof prop === "string" && BLOCKED_SANDBOX_PROPERTIES.has(prop)) {
256
- return false;
257
- }
258
- return Reflect.has(objTarget, prop);
259
- },
260
- ownKeys(objTarget: GenericObject): (string | symbol)[] {
261
- return Reflect.ownKeys(objTarget).filter((k: string | symbol) => {
262
- return !(typeof k === "string" && BLOCKED_SANDBOX_PROPERTIES.has(k));
263
- });
264
- },
265
- getOwnPropertyDescriptor(
266
- objTarget: GenericObject,
267
- prop: string | symbol,
268
- ): PropertyDescriptor | undefined {
269
- if (typeof prop === "string" && BLOCKED_SANDBOX_PROPERTIES.has(prop)) {
270
- return undefined;
271
- }
272
- const desc: PropertyDescriptor | undefined =
273
- Reflect.getOwnPropertyDescriptor(objTarget, prop);
274
- if (desc && "value" in desc) {
275
- desc.value = createSandboxProxy(desc.value, cache, objTarget);
276
- }
277
- return desc;
278
- },
279
- });
280
-
281
- cache.set(target, objProxy);
282
- return objProxy;
283
- }
284
-
285
- /**
286
- * Recursively unwraps sandbox proxies in a return value so the host code
287
- * receives original objects (e.g. Buffers that pass `instanceof` checks).
288
- */
289
- export function deepUnwrapProxies(
290
- value: unknown,
291
- visited?: WeakSet<GenericObject>,
292
- ): unknown {
293
- if (value === null || value === undefined) {
294
- return value;
295
- }
296
-
297
- const valueType: string = typeof value;
298
-
299
- if (valueType !== "object" && valueType !== "function") {
300
- return value;
301
- }
302
-
303
- const obj: Record<string | symbol, unknown> = value as Record<
304
- string | symbol,
305
- unknown
306
- >;
307
-
308
- // If it's one of our proxies, unwrap to the original target
309
- try {
310
- const underlying: unknown = obj[PROXY_TARGET_SYMBOL];
311
- if (underlying !== undefined) {
312
- return underlying;
313
- }
314
- } catch {
315
- // Not a proxy or symbol access failed — treat as a plain value
316
- }
317
-
318
- if (!visited) {
319
- visited = new WeakSet<GenericObject>();
320
- }
321
-
322
- if (visited.has(obj as GenericObject)) {
323
- return obj;
324
- }
325
-
326
- visited.add(obj as GenericObject);
327
-
328
- if (Array.isArray(obj)) {
329
- for (let i: number = 0; i < obj.length; i++) {
330
- (obj as unknown[])[i] = deepUnwrapProxies((obj as unknown[])[i], visited);
331
- }
332
- } else if (valueType === "object") {
333
- for (const key of Object.keys(obj as Record<string, unknown>)) {
334
- (obj as Record<string, unknown>)[key] = deepUnwrapProxies(
335
- (obj as Record<string, unknown>)[key],
336
- visited,
337
- );
338
- }
339
- }
340
-
341
- return obj;
342
- }
343
-
344
- /**
345
- * Unwraps a single value if it is a sandbox proxy, otherwise returns it as-is.
346
- */
347
- function unwrapProxy<T>(value: T): T {
348
- if (value && typeof value === "object") {
349
- const underlying: unknown = (value as Record<symbol, unknown>)[
350
- PROXY_TARGET_SYMBOL
351
- ];
352
- if (underlying !== undefined) {
353
- return underlying as T;
354
- }
355
- }
356
- return value;
357
- }
358
-
359
12
  export default class VMRunner {
360
13
  /*
361
14
  * Works out which URL the sandbox's axios call will actually dial, so the
@@ -403,250 +56,100 @@ export default class VMRunner {
403
56
  }
404
57
 
405
58
  @CaptureSpan()
406
- public static async runCodeInNodeVM(data: {
59
+ public static async runCodeInSandbox(data: {
407
60
  code: string;
408
61
  options: {
409
62
  timeout?: number;
410
63
  args?: JSONObject | undefined;
411
- context?: Dictionary<GenericObject | string> | undefined;
412
64
  };
413
65
  }): Promise<ReturnResult> {
414
66
  const { code, options } = data;
415
67
  const timeout: number = options.timeout || 5000;
416
68
 
417
69
  const logMessages: string[] = [];
418
- const MAX_LOG_BYTES: number = 1_000_000; // 1MB cap
419
- let totalLogBytes: number = 0;
420
-
421
70
  const capturedMetrics: CapturedMetric[] = [];
422
71
  const MAX_METRICS: number = 100;
423
-
424
- // Track timer handles so we can clean them up after execution
425
- type TimerHandle = ReturnType<typeof setTimeout>;
426
- const pendingTimeouts: TimerHandle[] = [];
427
- const pendingIntervals: TimerHandle[] = [];
428
-
429
- const wrappedSetTimeout: (
430
- fn: (...args: unknown[]) => void,
431
- ms?: number,
432
- ...rest: unknown[]
433
- ) => TimerHandle = (
434
- fn: (...args: unknown[]) => void,
435
- ms?: number,
436
- ...rest: unknown[]
437
- ): TimerHandle => {
438
- const handle: TimerHandle = setTimeout(fn, ms, ...rest);
439
- pendingTimeouts.push(handle);
440
- return handle;
72
+ const MAX_LOG_MESSAGES: number = 1000;
73
+ const MAX_LOG_BYTES: number = 1_000_000;
74
+ const MAX_SCRIPT_ERROR_MESSAGE_LENGTH: number = 10_000;
75
+ let logBytes: number = 0;
76
+
77
+ const pendingHostTimeouts: Set<ReturnType<typeof global.setTimeout>> =
78
+ new Set<ReturnType<typeof global.setTimeout>>();
79
+ const pendingAxiosControllers: Set<AbortController> =
80
+ new Set<AbortController>();
81
+ let acceptingHostOperations: boolean = true;
82
+ type PendingAxiosOperation =
83
+ | { status: "pending" }
84
+ | { status: "fulfilled"; value: string }
85
+ | { status: "rejected"; errorMessage: string };
86
+ const pendingAxiosOperations: Map<string, PendingAxiosOperation> = new Map<
87
+ string,
88
+ PendingAxiosOperation
89
+ >();
90
+ let nextAxiosOperationId: number = 0;
91
+
92
+ type PendingSleepOperation = {
93
+ settled: boolean;
441
94
  };
95
+ const pendingSleepOperations: Map<string, PendingSleepOperation> = new Map<
96
+ string,
97
+ PendingSleepOperation
98
+ >();
99
+ let nextSleepOperationId: number = 0;
442
100
 
443
- const wrappedClearTimeout: (handle: TimerHandle) => void = (
444
- handle: TimerHandle,
445
- ): void => {
446
- const actual: TimerHandle = unwrapProxy(handle);
447
- clearTimeout(actual);
448
- const idx: number = pendingTimeouts.indexOf(actual);
449
- if (idx !== -1) {
450
- pendingTimeouts.splice(idx, 1);
451
- }
452
- };
453
-
454
- const wrappedSetInterval: (
455
- fn: (...args: unknown[]) => void,
456
- ms?: number,
457
- ...rest: unknown[]
458
- ) => TimerHandle = (
459
- fn: (...args: unknown[]) => void,
460
- ms?: number,
461
- ...rest: unknown[]
462
- ): TimerHandle => {
463
- const handle: TimerHandle = setInterval(fn, ms, ...rest);
464
- pendingIntervals.push(handle);
465
- return handle;
466
- };
467
-
468
- const wrappedClearInterval: (handle: TimerHandle) => void = (
469
- handle: TimerHandle,
470
- ): void => {
471
- const actual: TimerHandle = unwrapProxy(handle);
472
- clearInterval(actual);
473
- const idx: number = pendingIntervals.indexOf(actual);
474
- if (idx !== -1) {
475
- pendingIntervals.splice(idx, 1);
476
- }
477
- };
101
+ const sanitizeScriptError: (error: unknown) => Error = (
102
+ error: unknown,
103
+ ): Error => {
104
+ let message: string = "Sandbox script failed";
478
105
 
479
- // Proxy cache shared across all wrapped host objects in this execution
480
- const proxyCache: WeakMap<GenericObject, unknown> = new WeakMap();
481
-
482
- // Use null-prototype object to break this.constructor chain on the global
483
- const sandbox: Context = Object.create(null) as Context;
484
- sandbox["process"] = Object.freeze(Object.create(null));
485
- sandbox["console"] = createSandboxProxy(
486
- {
487
- log: (...args: JSONValue[]) => {
488
- const msg: string = args.join(" ");
489
- totalLogBytes += msg.length;
490
- if (totalLogBytes <= MAX_LOG_BYTES) {
491
- logMessages.push(msg);
492
- }
493
- },
494
- },
495
- proxyCache,
496
- );
497
- sandbox["http"] = createSandboxProxy(http, proxyCache);
498
- sandbox["https"] = createSandboxProxy(https, proxyCache);
499
- sandbox["axios"] = createSandboxProxy(axios, proxyCache);
500
- sandbox["crypto"] = createSandboxProxy(crypto, proxyCache);
501
- sandbox["setTimeout"] = createSandboxProxy(wrappedSetTimeout, proxyCache);
502
- sandbox["clearTimeout"] = createSandboxProxy(
503
- wrappedClearTimeout,
504
- proxyCache,
505
- );
506
- sandbox["setInterval"] = createSandboxProxy(wrappedSetInterval, proxyCache);
507
- sandbox["clearInterval"] = createSandboxProxy(
508
- wrappedClearInterval,
509
- proxyCache,
510
- );
511
-
512
- sandbox["oneuptime"] = createSandboxProxy(
513
- {
514
- captureMetric: (
515
- name: unknown,
516
- value: unknown,
517
- attributes?: unknown,
518
- ): void => {
519
- if (typeof name !== "string" || name.length === 0) {
520
- return;
521
- }
522
- if (typeof value !== "number" || isNaN(value)) {
523
- return;
524
- }
525
- if (capturedMetrics.length >= MAX_METRICS) {
526
- return;
527
- }
528
- const metric: CapturedMetric = {
529
- name: name.substring(0, 200),
530
- value: value,
531
- };
532
- if (attributes && typeof attributes === "object") {
533
- const safeAttrs: JSONObject = {};
534
- for (const [k, v] of Object.entries(
535
- attributes as Record<string, unknown>,
536
- )) {
537
- if (
538
- typeof v === "string" ||
539
- typeof v === "number" ||
540
- typeof v === "boolean"
541
- ) {
542
- safeAttrs[k] = String(v);
543
- }
544
- }
545
- metric.attributes = safeAttrs;
106
+ try {
107
+ if (typeof error === "string") {
108
+ message = error;
109
+ } else if (error && typeof error === "object") {
110
+ const candidateMessage: unknown = (error as { message?: unknown })[
111
+ "message"
112
+ ];
113
+
114
+ if (typeof candidateMessage === "string") {
115
+ message = candidateMessage;
546
116
  }
547
- capturedMetrics.push(metric);
548
- },
549
- },
550
- proxyCache,
551
- );
552
-
553
- // Wrap any additional context (e.g. Playwright browser/page objects)
554
- if (options.context) {
555
- for (const key of Object.keys(options.context)) {
556
- const val: GenericObject | string | undefined = options.context[key];
557
- sandbox[key] =
558
- typeof val === "string" ? val : createSandboxProxy(val, proxyCache);
117
+ }
118
+ } catch {
119
+ // Do not invoke any attacker-controlled coercion while reporting errors.
559
120
  }
560
- }
561
-
562
- if (options.args) {
563
- // args is plain JSON data — no host functions to protect against
564
- sandbox["args"] = options.args;
565
- }
566
-
567
- vm.createContext(sandbox, {
568
- codeGeneration: {
569
- strings: false,
570
- wasm: false,
571
- },
572
- });
573
-
574
- const script: string = `(async()=>{
575
- ${VM_HARDENING_PRELUDE}
576
- ${code}
577
- })()`;
578
121
 
579
- try {
580
- let returnVal: unknown;
581
- let scriptError: Error | undefined;
122
+ let sanitizedMessage: string = "";
582
123
 
583
- try {
584
- /*
585
- * vm timeout only covers synchronous CPU time, so wrap with
586
- * Promise.race to also cover async operations (network, timers, etc.)
587
- */
588
- const vmPromise: Promise<unknown> = vm.runInContext(script, sandbox, {
589
- timeout: timeout,
590
- });
124
+ for (const character of message) {
125
+ const characterCode: number = character.charCodeAt(0);
591
126
 
592
- const overallTimeout: Promise<never> = new Promise(
593
- (
594
- _resolve: (value: never) => void,
595
- reject: (reason: Error) => void,
596
- ) => {
597
- const handle: NodeJS.Timeout = global.setTimeout(() => {
598
- reject(new Error("Script execution timed out"));
599
- }, timeout + 5000);
600
- // Don't let this timer keep the process alive
601
- handle.unref();
602
- },
603
- );
127
+ if (
128
+ characterCode === 9 ||
129
+ characterCode === 10 ||
130
+ characterCode === 13 ||
131
+ (characterCode >= 32 && characterCode !== 127)
132
+ ) {
133
+ sanitizedMessage += character;
134
+ }
604
135
 
605
- returnVal = await Promise.race([vmPromise, overallTimeout]);
606
- } catch (err: unknown) {
607
- /*
608
- * Capture user-thrown errors (including timeouts) so the caller can
609
- * still access side-channel data collected before the throw — e.g.
610
- * screenshots assigned to a host-realm object passed via `context`.
611
- * Rethrowing here would discard those partial results.
612
- */
613
- scriptError =
614
- err instanceof Error
615
- ? err
616
- : new Error(typeof err === "string" ? err : String(err));
136
+ if (sanitizedMessage.length >= MAX_SCRIPT_ERROR_MESSAGE_LENGTH) {
137
+ break;
138
+ }
617
139
  }
618
140
 
619
- return {
620
- returnValue: deepUnwrapProxies(returnVal),
621
- logMessages,
622
- capturedMetrics,
623
- scriptError,
624
- };
625
- } finally {
626
- // Clean up any lingering timers to prevent resource leaks
627
- for (const handle of pendingTimeouts) {
628
- clearTimeout(handle);
629
- }
630
- for (const handle of pendingIntervals) {
631
- clearInterval(handle);
141
+ message = sanitizedMessage.substring(0, MAX_SCRIPT_ERROR_MESSAGE_LENGTH);
142
+
143
+ if (!message) {
144
+ message = "Sandbox script failed";
632
145
  }
633
- }
634
- }
635
146
 
636
- @CaptureSpan()
637
- public static async runCodeInSandbox(data: {
638
- code: string;
639
- options: {
640
- timeout?: number;
641
- args?: JSONObject | undefined;
147
+ /*
148
+ * Deliberately create a fresh host Error so isolate-owned properties and
149
+ * stack frames never escape with the result.
150
+ */
151
+ return new Error(message);
642
152
  };
643
- }): Promise<ReturnResult> {
644
- const { code, options } = data;
645
- const timeout: number = options.timeout || 5000;
646
-
647
- const logMessages: string[] = [];
648
- const capturedMetrics: CapturedMetric[] = [];
649
- const MAX_METRICS: number = 100;
650
153
 
651
154
  const isolate: ivm.Isolate = new ivm.Isolate({ memoryLimit: 128 });
652
155
 
@@ -657,24 +160,73 @@ export default class VMRunner {
657
160
  // Set up global object
658
161
  await jail.set("global", jail.derefInto());
659
162
 
660
- // console.log - fire-and-forget callback
163
+ /*
164
+ * Callback values become ordinary functions in the destination isolate.
165
+ * Never expose ivm.Reference or ivm.ExternalCopy handles to user code:
166
+ * their prototype methods can be used to cross the isolate boundary.
167
+ */
661
168
  await jail.set(
662
- "_log",
663
- new ivm.Callback((...args: string[]) => {
664
- logMessages.push(args.join(" "));
665
- }),
169
+ "__oneuptimeHostLogCallback",
170
+ new ivm.Callback(
171
+ (message: string) => {
172
+ if (logMessages.length >= MAX_LOG_MESSAGES) {
173
+ return;
174
+ }
175
+
176
+ const messageBytes: number = Buffer.byteLength(message, "utf8");
177
+
178
+ if (logBytes + messageBytes > MAX_LOG_BYTES) {
179
+ return;
180
+ }
181
+
182
+ logBytes += messageBytes;
183
+ logMessages.push(message);
184
+ },
185
+ { sync: true },
186
+ ),
666
187
  );
667
188
 
668
189
  await context.eval(`
669
- const console = { log: (...a) => _log(...a.map(v => {
670
- try { return typeof v === 'object' ? JSON.stringify(v) : String(v); }
671
- catch(_) { return String(v); }
672
- }))};
190
+ (() => {
191
+ const hostLog = globalThis.__oneuptimeHostLogCallback;
192
+ delete globalThis.__oneuptimeHostLogCallback;
193
+ let sandboxLogCount = 0;
194
+ let sandboxLogCharacters = 0;
195
+
196
+ const sandboxConsole = Object.freeze({
197
+ log: (...args) => {
198
+ if (sandboxLogCount >= 1000 || sandboxLogCharacters >= 500000) {
199
+ return;
200
+ }
201
+
202
+ const message = args.map(value => {
203
+ try {
204
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
205
+ } catch (_) {
206
+ return String(value);
207
+ }
208
+ }).join(' ').substring(0, 250000);
209
+
210
+ if (sandboxLogCharacters + message.length > 500000) {
211
+ return;
212
+ }
213
+
214
+ sandboxLogCount += 1;
215
+ sandboxLogCharacters += message.length;
216
+ hostLog(message);
217
+ }
218
+ });
219
+
220
+ Object.defineProperty(globalThis, 'console', {
221
+ value: sandboxConsole,
222
+ writable: false,
223
+ configurable: false,
224
+ });
225
+ })();
673
226
  `);
674
227
 
675
- // oneuptime.captureMetric - fire-and-forget callback
676
228
  await jail.set(
677
- "_captureMetric",
229
+ "__oneuptimeHostMetricCallback",
678
230
  new ivm.Callback(
679
231
  (name: string, value: string, attributesJson?: string) => {
680
232
  if (capturedMetrics.length >= MAX_METRICS) {
@@ -697,27 +249,47 @@ export default class VMRunner {
697
249
  }
698
250
  capturedMetrics.push(metric);
699
251
  },
252
+ { sync: true },
700
253
  ),
701
254
  );
702
255
 
703
256
  await context.eval(`
704
- const oneuptime = {
705
- captureMetric: (name, value, attributes) => {
706
- if (typeof name !== 'string' || name.length === 0) return;
707
- if (typeof value !== 'number' || isNaN(value)) return;
708
- const attrJson = attributes ? JSON.stringify(attributes) : undefined;
709
- _captureMetric(String(name), String(value), attrJson);
710
- }
711
- };
257
+ (() => {
258
+ const hostCaptureMetric = globalThis.__oneuptimeHostMetricCallback;
259
+ delete globalThis.__oneuptimeHostMetricCallback;
260
+
261
+ const sandboxOneUptime = Object.freeze({
262
+ captureMetric: (name, value, attributes) => {
263
+ if (typeof name !== 'string' || name.length === 0) return;
264
+ if (typeof value !== 'number' || isNaN(value)) return;
265
+ const attrJson = attributes ? JSON.stringify(attributes) : undefined;
266
+ hostCaptureMetric(String(name), String(value), attrJson);
267
+ }
268
+ });
269
+
270
+ Object.defineProperty(globalThis, 'oneuptime', {
271
+ value: sandboxOneUptime,
272
+ writable: false,
273
+ configurable: false,
274
+ });
275
+ })();
712
276
  `);
713
277
 
714
278
  // args - deep copy into isolate
715
- if (options.args) {
716
- await jail.set("_args", new ivm.ExternalCopy(options.args).copyInto());
717
- await context.eval("const args = _args;");
718
- } else {
719
- await context.eval("const args = {};");
720
- }
279
+ await jail.set("__oneuptimeCopiedArgs", options.args || {}, {
280
+ copy: true,
281
+ });
282
+ await context.eval(`
283
+ (() => {
284
+ const copiedArgs = globalThis.__oneuptimeCopiedArgs;
285
+ delete globalThis.__oneuptimeCopiedArgs;
286
+ Object.defineProperty(globalThis, 'args', {
287
+ value: copiedArgs,
288
+ writable: false,
289
+ configurable: false,
290
+ });
291
+ })();
292
+ `);
721
293
 
722
294
  /*
723
295
  * http / https - provide Agent constructors that serialize across the boundary.
@@ -745,212 +317,317 @@ export default class VMRunner {
745
317
 
746
318
  /*
747
319
  * axios (get, head, options, post, put, patch, delete, request)
748
- * bridged via applySyncPromise.
320
+ * bridged through a copied async callback.
749
321
  *
750
322
  * For GET/HEAD/OPTIONS/DELETE: args = [method, url, configJson?]
751
323
  * For POST/PUT/PATCH: args = [method, url, bodyJson?, configJson?]
752
324
  * For REQUEST: args = ['request', '', configJson]
753
325
  */
754
- const axiosRef: ivm.Reference<
755
- (
756
- method: string,
757
- url: string,
758
- arg1?: string,
759
- arg2?: string,
760
- ) => Promise<string>
761
- > = new ivm.Reference(
762
- async (
763
- method: string,
764
- url: string,
765
- arg1?: string,
766
- arg2?: string,
767
- ): Promise<string> => {
768
- const methodsWithBody: string[] = ["post", "put", "patch"];
769
- const hasBody: boolean = methodsWithBody.includes(method);
326
+ const executeAxiosRequest: (
327
+ signal: AbortSignal,
328
+ method: string,
329
+ url: string,
330
+ arg1?: string,
331
+ arg2?: string,
332
+ ) => Promise<string> = async (
333
+ signal: AbortSignal,
334
+ method: string,
335
+ url: string,
336
+ arg1?: string,
337
+ arg2?: string,
338
+ ): Promise<string> => {
339
+ const methodsWithBody: string[] = ["post", "put", "patch"];
340
+ const hasBody: boolean = methodsWithBody.includes(method);
770
341
 
771
- /*
772
- * For POST/PUT/PATCH: arg1=body, arg2=config
773
- * For GET/HEAD/OPTIONS/DELETE/REQUEST: arg1=config
774
- */
775
- const body: JSONObject | undefined =
776
- hasBody && arg1 ? (JSON.parse(arg1) as JSONObject) : undefined;
777
-
778
- const configStr: string | undefined = hasBody ? arg2 : arg1;
779
- let config: JSONObject | undefined = configStr
780
- ? (JSON.parse(configStr) as JSONObject)
781
- : undefined;
782
-
783
- // Reconstruct real http/https Agents from serialized markers
784
- if (config) {
785
- const httpsAgentConfig: JSONObject | undefined = config[
786
- "httpsAgent"
787
- ] as JSONObject | undefined;
788
-
789
- if (
790
- httpsAgentConfig &&
791
- httpsAgentConfig["__agentType"] === "__https_agent__"
792
- ) {
793
- config["httpsAgent"] = new https.Agent(
794
- httpsAgentConfig["options"] as https.AgentOptions,
795
- ) as unknown as JSONObject;
796
- }
342
+ /*
343
+ * For POST/PUT/PATCH: arg1=body, arg2=config
344
+ * For GET/HEAD/OPTIONS/DELETE/REQUEST: arg1=config
345
+ */
346
+ const body: JSONObject | undefined =
347
+ hasBody && arg1 ? (JSON.parse(arg1) as JSONObject) : undefined;
797
348
 
798
- const httpAgentConfig: JSONObject | undefined = config[
799
- "httpAgent"
800
- ] as JSONObject | undefined;
801
-
802
- if (
803
- httpAgentConfig &&
804
- httpAgentConfig["__agentType"] === "__http_agent__"
805
- ) {
806
- config["httpAgent"] = new http.Agent(
807
- httpAgentConfig["options"] as http.AgentOptions,
808
- ) as unknown as JSONObject;
809
- }
349
+ const configStr: string | undefined = hasBody ? arg2 : arg1;
350
+ let config: JSONObject | undefined = configStr
351
+ ? (JSON.parse(configStr) as JSONObject)
352
+ : undefined;
353
+
354
+ // Reconstruct real http/https Agents from serialized markers
355
+ if (config) {
356
+ const httpsAgentConfig: JSONObject | undefined = config[
357
+ "httpsAgent"
358
+ ] as JSONObject | undefined;
359
+
360
+ if (
361
+ httpsAgentConfig &&
362
+ httpsAgentConfig["__agentType"] === "__https_agent__"
363
+ ) {
364
+ config["httpsAgent"] = new https.Agent(
365
+ httpsAgentConfig["options"] as https.AgentOptions,
366
+ ) as unknown as JSONObject;
810
367
  }
811
368
 
812
- /**
813
- * Helper: convert AxiosHeaders (or any header-like object) to a
814
- * plain record so it can be safely JSON-serialised.
815
- */
816
- const toPlainHeaders: (
817
- headers: unknown,
818
- ) => Record<string, unknown> = (
819
- headers: unknown,
820
- ): Record<string, unknown> => {
821
- const plain: Record<string, unknown> = {};
822
- if (headers) {
823
- for (const hKey of Object.keys(
824
- headers as Record<string, unknown>,
825
- )) {
826
- plain[hKey] = (headers as Record<string, unknown>)[hKey];
827
- }
369
+ const httpAgentConfig: JSONObject | undefined = config[
370
+ "httpAgent"
371
+ ] as JSONObject | undefined;
372
+
373
+ if (
374
+ httpAgentConfig &&
375
+ httpAgentConfig["__agentType"] === "__http_agent__"
376
+ ) {
377
+ config["httpAgent"] = new http.Agent(
378
+ httpAgentConfig["options"] as http.AgentOptions,
379
+ ) as unknown as JSONObject;
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Helper: convert AxiosHeaders (or any header-like object) to a
385
+ * plain record so it can be safely JSON-serialised.
386
+ */
387
+ const toPlainHeaders: (headers: unknown) => Record<string, unknown> = (
388
+ headers: unknown,
389
+ ): Record<string, unknown> => {
390
+ const plain: Record<string, unknown> = {};
391
+ if (headers) {
392
+ for (const hKey of Object.keys(
393
+ headers as Record<string, unknown>,
394
+ )) {
395
+ plain[hKey] = (headers as Record<string, unknown>)[hKey];
828
396
  }
829
- return plain;
830
- };
397
+ }
398
+ return plain;
399
+ };
400
+
401
+ /*
402
+ * SSRF guard (GHSA-v5xh-rw9h-77fv).
403
+ *
404
+ * This bridge hands the host process's real axios to code the user
405
+ * wrote - the Custom JavaScript workflow component documents "you can
406
+ * use axios module" - so without a check here it is a strictly more
407
+ * capable version of the hole that was reported against the API
408
+ * components: arbitrary method, headers and body against the internal
409
+ * network, with the full response marshalled back into the sandbox
410
+ * and on into the workflow log.
411
+ *
412
+ * It has to live on THIS side of the isolate boundary. The
413
+ * sandbox-side axios shim is attacker-editable - user code can simply
414
+ * redefine it - so a check there guards nothing.
415
+ */
416
+ const effectiveUrl: string = VMRunner.resolveEffectiveRequestUrl({
417
+ method,
418
+ url,
419
+ config,
420
+ });
421
+
422
+ await SSRFProtection.validateWebhookTargetIsSafe(effectiveUrl);
423
+
424
+ /*
425
+ * The URL that was just validated has to be the URL that gets
426
+ * dialled. Each of these would otherwise steer the connection
427
+ * somewhere else entirely, past the check above:
428
+ * proxy - sends the request to an arbitrary host:port
429
+ * socketPath - connects to a unix socket (/var/run/docker.sock)
430
+ * transport / adapter - replaces the transport wholesale
431
+ * and a redirect would let a validated public host nominate an
432
+ * internal one on the second hop.
433
+ */
434
+ const safeConfig: JSONObject = config || {};
435
+ delete safeConfig["proxy"];
436
+ delete safeConfig["socketPath"];
437
+ delete safeConfig["transport"];
438
+ delete safeConfig["adapter"];
439
+ safeConfig["maxRedirects"] = 0;
440
+ Object.defineProperty(safeConfig, "signal", {
441
+ value: signal,
442
+ enumerable: true,
443
+ configurable: true,
444
+ });
445
+ config = safeConfig;
446
+
447
+ try {
448
+ let response: AxiosResponse;
449
+
450
+ switch (method) {
451
+ case "get":
452
+ response = await axios.get(url, config);
453
+ break;
454
+ case "head":
455
+ response = await axios.head(url, config);
456
+ break;
457
+ case "options":
458
+ response = await axios.options(url, config);
459
+ break;
460
+ case "post":
461
+ response = await axios.post(url, body, config);
462
+ break;
463
+ case "put":
464
+ response = await axios.put(url, body, config);
465
+ break;
466
+ case "patch":
467
+ response = await axios.patch(url, body, config);
468
+ break;
469
+ case "delete":
470
+ response = await axios.delete(url, config);
471
+ break;
472
+ case "request":
473
+ response = await axios.request(
474
+ config as Parameters<typeof axios.request>[0],
475
+ );
476
+ break;
477
+ default:
478
+ throw new Error(`Unsupported HTTP method: ${method}`);
479
+ }
831
480
 
832
481
  /*
833
- * SSRF guard (GHSA-v5xh-rw9h-77fv).
834
- *
835
- * This bridge hands the host process's real axios to code the user
836
- * wrote - the Custom JavaScript workflow component documents "you can
837
- * use axios module" - so without a check here it is a strictly more
838
- * capable version of the hole that was reported against the API
839
- * components: arbitrary method, headers and body against the internal
840
- * network, with the full response marshalled back into the sandbox
841
- * and on into the workflow log.
842
- *
843
- * It has to live on THIS side of the isolate boundary. The
844
- * sandbox-side axios shim is attacker-editable - user code can simply
845
- * redefine it - so a check there guards nothing.
482
+ * Convert AxiosHeaders to a plain object before serializing.
483
+ * JSON.stringify calls AxiosHeaders.toJSON(key) with a truthy key,
484
+ * which makes it join array headers (like set-cookie) with commas.
485
+ * This produces invalid Cookie headers when user code forwards them.
846
486
  */
847
- const effectiveUrl: string = VMRunner.resolveEffectiveRequestUrl({
848
- method,
849
- url,
850
- config,
487
+ return JSON.stringify({
488
+ status: response.status,
489
+ headers: toPlainHeaders(response.headers),
490
+ data: response.data,
851
491
  });
852
-
853
- await SSRFProtection.validateWebhookTargetIsSafe(effectiveUrl);
854
-
492
+ } catch (err: unknown) {
855
493
  /*
856
- * The URL that was just validated has to be the URL that gets
857
- * dialled. Each of these would otherwise steer the connection
858
- * somewhere else entirely, past the check above:
859
- * proxy - sends the request to an arbitrary host:port
860
- * socketPath - connects to a unix socket (/var/run/docker.sock)
861
- * transport / adapter - replaces the transport wholesale
862
- * and a redirect would let a validated public host nominate an
863
- * internal one on the second hop.
494
+ * If this is an axios error with a response (4xx, 5xx, etc.),
495
+ * return the error details as JSON so the sandbox-side axios
496
+ * wrapper can reconstruct error.response for user code.
864
497
  */
865
- const safeConfig: JSONObject = config || {};
866
- delete safeConfig["proxy"];
867
- delete safeConfig["socketPath"];
868
- delete safeConfig["transport"];
869
- delete safeConfig["adapter"];
870
- safeConfig["maxRedirects"] = 0;
871
- config = safeConfig;
872
-
873
- try {
874
- let response: AxiosResponse;
875
-
876
- switch (method) {
877
- case "get":
878
- response = await axios.get(url, config);
879
- break;
880
- case "head":
881
- response = await axios.head(url, config);
882
- break;
883
- case "options":
884
- response = await axios.options(url, config);
885
- break;
886
- case "post":
887
- response = await axios.post(url, body, config);
888
- break;
889
- case "put":
890
- response = await axios.put(url, body, config);
891
- break;
892
- case "patch":
893
- response = await axios.patch(url, body, config);
894
- break;
895
- case "delete":
896
- response = await axios.delete(url, config);
897
- break;
898
- case "request":
899
- response = await axios.request(
900
- config as Parameters<typeof axios.request>[0],
901
- );
902
- break;
903
- default:
904
- throw new Error(`Unsupported HTTP method: ${method}`);
905
- }
498
+ const axiosErr: {
499
+ isAxiosError?: boolean;
500
+ response?: AxiosResponse<any, any, Record<string, unknown>>;
501
+ message?: string;
502
+ } = err as {
503
+ isAxiosError?: boolean;
504
+ response?: AxiosResponse;
505
+ message?: string;
506
+ };
906
507
 
907
- /*
908
- * Convert AxiosHeaders to a plain object before serializing.
909
- * JSON.stringify calls AxiosHeaders.toJSON(key) with a truthy key,
910
- * which makes it join array headers (like set-cookie) with commas.
911
- * This produces invalid Cookie headers when user code forwards them.
912
- */
508
+ if (axiosErr.isAxiosError && axiosErr.response) {
913
509
  return JSON.stringify({
914
- status: response.status,
915
- headers: toPlainHeaders(response.headers),
916
- data: response.data,
510
+ __isAxiosError: true,
511
+ message: axiosErr.message || "Request failed",
512
+ status: axiosErr.response.status,
513
+ statusText: axiosErr.response.statusText,
514
+ headers: toPlainHeaders(axiosErr.response.headers),
515
+ data: axiosErr.response.data,
917
516
  });
918
- } catch (err: unknown) {
919
- /*
920
- * If this is an axios error with a response (4xx, 5xx, etc.),
921
- * return the error details as JSON so the sandbox-side axios
922
- * wrapper can reconstruct error.response for user code.
923
- */
924
- const axiosErr: {
925
- isAxiosError?: boolean;
926
- response?: AxiosResponse<any, any, Record<string, unknown>>;
927
- message?: string;
928
- } = err as {
929
- isAxiosError?: boolean;
930
- response?: AxiosResponse;
931
- message?: string;
932
- };
517
+ }
933
518
 
934
- if (axiosErr.isAxiosError && axiosErr.response) {
935
- return JSON.stringify({
936
- __isAxiosError: true,
937
- message: axiosErr.message || "Request failed",
938
- status: axiosErr.response.status,
939
- statusText: axiosErr.response.statusText,
940
- headers: toPlainHeaders(axiosErr.response.headers),
941
- data: axiosErr.response.data,
942
- });
943
- }
519
+ throw err;
520
+ }
521
+ };
944
522
 
945
- throw err;
523
+ const axiosStartCallback: ivm.Callback<
524
+ (method: string, url: string, arg1?: string, arg2?: string) => string
525
+ > = new ivm.Callback(
526
+ (method: string, url: string, arg1?: string, arg2?: string): string => {
527
+ const operationId: string = String(++nextAxiosOperationId);
528
+
529
+ if (!acceptingHostOperations) {
530
+ return operationId;
531
+ }
532
+
533
+ if (pendingAxiosOperations.size >= 100) {
534
+ pendingAxiosOperations.set(operationId, {
535
+ status: "rejected",
536
+ errorMessage: "Too many pending HTTP requests",
537
+ });
538
+ return operationId;
946
539
  }
540
+
541
+ pendingAxiosOperations.set(operationId, { status: "pending" });
542
+ const abortController: AbortController = new AbortController();
543
+ pendingAxiosControllers.add(abortController);
544
+ void executeAxiosRequest(
545
+ abortController.signal,
546
+ method,
547
+ url,
548
+ arg1,
549
+ arg2,
550
+ )
551
+ .then(
552
+ (value: string) => {
553
+ if (pendingAxiosOperations.has(operationId)) {
554
+ pendingAxiosOperations.set(operationId, {
555
+ status: "fulfilled",
556
+ value,
557
+ });
558
+ }
559
+ },
560
+ (error: unknown) => {
561
+ if (pendingAxiosOperations.has(operationId)) {
562
+ pendingAxiosOperations.set(operationId, {
563
+ status: "rejected",
564
+ errorMessage: sanitizeScriptError(error).message,
565
+ });
566
+ }
567
+ },
568
+ )
569
+ .finally(() => {
570
+ pendingAxiosControllers.delete(abortController);
571
+ });
572
+
573
+ return operationId;
947
574
  },
575
+ { async: true },
948
576
  );
949
577
 
950
- await jail.set("_axiosRef", axiosRef);
578
+ const axiosPollCallback: ivm.Callback<(operationId: string) => string> =
579
+ new ivm.Callback(
580
+ (operationId: string): string => {
581
+ const operation: PendingAxiosOperation | undefined =
582
+ pendingAxiosOperations.get(operationId);
583
+
584
+ if (!operation) {
585
+ return JSON.stringify({
586
+ status: "rejected",
587
+ errorMessage: "HTTP request result is unavailable",
588
+ });
589
+ }
590
+
591
+ if (operation.status !== "pending") {
592
+ pendingAxiosOperations.delete(operationId);
593
+ }
594
+
595
+ return JSON.stringify(operation);
596
+ },
597
+ { async: true },
598
+ );
599
+
600
+ await jail.set("__oneuptimeHostAxiosStartCallback", axiosStartCallback);
601
+ await jail.set("__oneuptimeHostAxiosPollCallback", axiosPollCallback);
951
602
 
952
603
  await context.eval(`
953
- function _assertNoFunctions(obj, path) {
604
+ (() => {
605
+ const hostAxiosStart = globalThis.__oneuptimeHostAxiosStartCallback;
606
+ const hostAxiosPoll = globalThis.__oneuptimeHostAxiosPollCallback;
607
+ delete globalThis.__oneuptimeHostAxiosStartCallback;
608
+ delete globalThis.__oneuptimeHostAxiosPollCallback;
609
+ const axiosPollWaitArray = new Int32Array(new SharedArrayBuffer(4));
610
+
611
+ async function hostAxios(method, url, arg1, arg2) {
612
+ const operationId = await hostAxiosStart(method, url, arg1, arg2);
613
+
614
+ while (true) {
615
+ const operation = JSON.parse(await hostAxiosPoll(operationId));
616
+
617
+ if (operation.status === 'pending') {
618
+ Atomics.wait(axiosPollWaitArray, 0, 0, 1);
619
+ continue;
620
+ }
621
+
622
+ if (operation.status === 'rejected') {
623
+ throw new Error(operation.errorMessage);
624
+ }
625
+
626
+ return operation.value;
627
+ }
628
+ }
629
+
630
+ function assertNoFunctions(obj, path) {
954
631
  if (!obj || typeof obj !== 'object') return;
955
632
  if (Array.isArray(obj)) {
956
633
  for (let i = 0; i < obj.length; i++) {
@@ -962,7 +639,7 @@ export default class VMRunner {
962
639
  );
963
640
  }
964
641
  if (obj[i] && typeof obj[i] === 'object') {
965
- _assertNoFunctions(obj[i], fullPath);
642
+ assertNoFunctions(obj[i], fullPath);
966
643
  }
967
644
  }
968
645
  return;
@@ -976,12 +653,12 @@ export default class VMRunner {
976
653
  );
977
654
  }
978
655
  if (obj[key] && typeof obj[key] === 'object') {
979
- _assertNoFunctions(obj[key], fullPath);
656
+ assertNoFunctions(obj[key], fullPath);
980
657
  }
981
658
  }
982
659
  }
983
660
 
984
- function _parseAxiosResult(r) {
661
+ function parseAxiosResult(r) {
985
662
  const parsed = JSON.parse(r);
986
663
  if (parsed && parsed.__isAxiosError) {
987
664
  const err = new Error(parsed.message);
@@ -998,7 +675,7 @@ export default class VMRunner {
998
675
  return parsed;
999
676
  }
1000
677
 
1001
- function _makeAxiosInstance(defaults) {
678
+ function makeAxiosInstance(defaults) {
1002
679
  function mergeConfig(overrides) {
1003
680
  if (!defaults && !overrides) return undefined;
1004
681
  if (!defaults) return overrides;
@@ -1012,9 +689,9 @@ export default class VMRunner {
1012
689
 
1013
690
  async function _request(config) {
1014
691
  const merged = mergeConfig(config);
1015
- if (merged) _assertNoFunctions(merged, 'config');
1016
- const r = await _axiosRef.applySyncPromise(undefined, ['request', '', merged ? JSON.stringify(merged) : undefined]);
1017
- return _parseAxiosResult(r);
692
+ if (merged) assertNoFunctions(merged, 'config');
693
+ const r = await hostAxios('request', '', merged ? JSON.stringify(merged) : undefined);
694
+ return parseAxiosResult(r);
1018
695
  }
1019
696
 
1020
697
  // Make instance callable: axios(config) or axios(url, config)
@@ -1028,141 +705,258 @@ export default class VMRunner {
1028
705
  instance.request = _request;
1029
706
  instance.get = async (url, config) => {
1030
707
  const merged = mergeConfig(config);
1031
- if (merged) _assertNoFunctions(merged, 'config');
1032
- const r = await _axiosRef.applySyncPromise(undefined, ['get', url, merged ? JSON.stringify(merged) : undefined]);
1033
- return _parseAxiosResult(r);
708
+ if (merged) assertNoFunctions(merged, 'config');
709
+ const r = await hostAxios('get', url, merged ? JSON.stringify(merged) : undefined);
710
+ return parseAxiosResult(r);
1034
711
  };
1035
712
  instance.head = async (url, config) => {
1036
713
  const merged = mergeConfig(config);
1037
- if (merged) _assertNoFunctions(merged, 'config');
1038
- const r = await _axiosRef.applySyncPromise(undefined, ['head', url, merged ? JSON.stringify(merged) : undefined]);
1039
- return _parseAxiosResult(r);
714
+ if (merged) assertNoFunctions(merged, 'config');
715
+ const r = await hostAxios('head', url, merged ? JSON.stringify(merged) : undefined);
716
+ return parseAxiosResult(r);
1040
717
  };
1041
718
  instance.options = async (url, config) => {
1042
719
  const merged = mergeConfig(config);
1043
- if (merged) _assertNoFunctions(merged, 'config');
1044
- const r = await _axiosRef.applySyncPromise(undefined, ['options', url, merged ? JSON.stringify(merged) : undefined]);
1045
- return _parseAxiosResult(r);
720
+ if (merged) assertNoFunctions(merged, 'config');
721
+ const r = await hostAxios('options', url, merged ? JSON.stringify(merged) : undefined);
722
+ return parseAxiosResult(r);
1046
723
  };
1047
724
  instance.post = async (url, data, config) => {
1048
725
  const merged = mergeConfig(config);
1049
- if (data) _assertNoFunctions(data, 'data');
1050
- if (merged) _assertNoFunctions(merged, 'config');
1051
- const r = await _axiosRef.applySyncPromise(undefined, ['post', url, data ? JSON.stringify(data) : undefined, merged ? JSON.stringify(merged) : undefined]);
1052
- return _parseAxiosResult(r);
726
+ if (data) assertNoFunctions(data, 'data');
727
+ if (merged) assertNoFunctions(merged, 'config');
728
+ const r = await hostAxios('post', url, data ? JSON.stringify(data) : undefined, merged ? JSON.stringify(merged) : undefined);
729
+ return parseAxiosResult(r);
1053
730
  };
1054
731
  instance.put = async (url, data, config) => {
1055
732
  const merged = mergeConfig(config);
1056
- if (data) _assertNoFunctions(data, 'data');
1057
- if (merged) _assertNoFunctions(merged, 'config');
1058
- const r = await _axiosRef.applySyncPromise(undefined, ['put', url, data ? JSON.stringify(data) : undefined, merged ? JSON.stringify(merged) : undefined]);
1059
- return _parseAxiosResult(r);
733
+ if (data) assertNoFunctions(data, 'data');
734
+ if (merged) assertNoFunctions(merged, 'config');
735
+ const r = await hostAxios('put', url, data ? JSON.stringify(data) : undefined, merged ? JSON.stringify(merged) : undefined);
736
+ return parseAxiosResult(r);
1060
737
  };
1061
738
  instance.patch = async (url, data, config) => {
1062
739
  const merged = mergeConfig(config);
1063
- if (data) _assertNoFunctions(data, 'data');
1064
- if (merged) _assertNoFunctions(merged, 'config');
1065
- const r = await _axiosRef.applySyncPromise(undefined, ['patch', url, data ? JSON.stringify(data) : undefined, merged ? JSON.stringify(merged) : undefined]);
1066
- return _parseAxiosResult(r);
740
+ if (data) assertNoFunctions(data, 'data');
741
+ if (merged) assertNoFunctions(merged, 'config');
742
+ const r = await hostAxios('patch', url, data ? JSON.stringify(data) : undefined, merged ? JSON.stringify(merged) : undefined);
743
+ return parseAxiosResult(r);
1067
744
  };
1068
745
  instance.delete = async (url, config) => {
1069
746
  const merged = mergeConfig(config);
1070
- if (merged) _assertNoFunctions(merged, 'config');
1071
- const r = await _axiosRef.applySyncPromise(undefined, ['delete', url, merged ? JSON.stringify(merged) : undefined]);
1072
- return _parseAxiosResult(r);
747
+ if (merged) assertNoFunctions(merged, 'config');
748
+ const r = await hostAxios('delete', url, merged ? JSON.stringify(merged) : undefined);
749
+ return parseAxiosResult(r);
1073
750
  };
1074
751
  instance.create = (instanceDefaults) => {
1075
- if (instanceDefaults) _assertNoFunctions(instanceDefaults, 'defaults');
752
+ if (instanceDefaults) assertNoFunctions(instanceDefaults, 'defaults');
1076
753
  const combinedDefaults = mergeConfig(instanceDefaults);
1077
- return _makeAxiosInstance(combinedDefaults);
754
+ return makeAxiosInstance(combinedDefaults);
1078
755
  };
1079
756
 
1080
757
  return instance;
1081
758
  }
1082
759
 
1083
- const axios = _makeAxiosInstance(null);
760
+ Object.defineProperty(globalThis, 'axios', {
761
+ value: makeAxiosInstance(null),
762
+ writable: false,
763
+ configurable: false,
764
+ });
765
+ })();
1084
766
  `);
1085
767
 
1086
- // crypto (createHash, createHmac, randomBytes, randomUUID, randomInt) - bridged via applySync
1087
- const cryptoRef: ivm.Reference<
768
+ // crypto (createHash, createHmac, randomBytes, randomUUID, randomInt)
769
+ const cryptoCallback: ivm.Callback<
1088
770
  (op: string, ...args: string[]) => string
1089
- > = new ivm.Reference((op: string, ...args: string[]): string => {
1090
- switch (op) {
1091
- case "createHash": {
1092
- const [algorithm, inputData, encoding] = args;
1093
- return crypto
1094
- .createHash(algorithm!)
1095
- .update(inputData!)
1096
- .digest((encoding as crypto.BinaryToTextEncoding) || "hex");
1097
- }
1098
- case "createHmac": {
1099
- const [algorithm, key, inputData, encoding] = args;
1100
- return crypto
1101
- .createHmac(algorithm!, key!)
1102
- .update(inputData!)
1103
- .digest((encoding as crypto.BinaryToTextEncoding) || "hex");
1104
- }
1105
- case "randomBytes": {
1106
- const [size] = args;
1107
- return crypto.randomBytes(parseInt(size!)).toString("hex");
1108
- }
1109
- case "randomUUID": {
1110
- return crypto.randomUUID();
1111
- }
1112
- case "randomInt": {
1113
- const [min, max] = args;
1114
- return String(crypto.randomInt(parseInt(min!), parseInt(max!)));
771
+ > = new ivm.Callback(
772
+ (op: string, ...args: string[]): string => {
773
+ switch (op) {
774
+ case "createHash": {
775
+ const [algorithm, inputData, encoding] = args;
776
+ return crypto
777
+ .createHash(algorithm!)
778
+ .update(inputData!)
779
+ .digest((encoding as crypto.BinaryToTextEncoding) || "hex");
780
+ }
781
+ case "createHmac": {
782
+ const [algorithm, key, inputData, encoding] = args;
783
+ return crypto
784
+ .createHmac(algorithm!, key!)
785
+ .update(inputData!)
786
+ .digest((encoding as crypto.BinaryToTextEncoding) || "hex");
787
+ }
788
+ case "randomBytes": {
789
+ const [size] = args;
790
+ return crypto.randomBytes(parseInt(size!)).toString("hex");
791
+ }
792
+ case "randomUUID": {
793
+ return crypto.randomUUID();
794
+ }
795
+ case "randomInt": {
796
+ const [min, max] = args;
797
+ return String(crypto.randomInt(parseInt(min!), parseInt(max!)));
798
+ }
799
+ default:
800
+ throw new Error(`Unsupported crypto operation: ${op}`);
1115
801
  }
1116
- default:
1117
- throw new Error(`Unsupported crypto operation: ${op}`);
1118
- }
1119
- });
802
+ },
803
+ { sync: true },
804
+ );
1120
805
 
1121
- await jail.set("_cryptoRef", cryptoRef);
806
+ await jail.set("__oneuptimeHostCryptoCallback", cryptoCallback);
1122
807
 
1123
808
  await context.eval(`
1124
- const crypto = {
809
+ (() => {
810
+ const hostCrypto = globalThis.__oneuptimeHostCryptoCallback;
811
+ delete globalThis.__oneuptimeHostCryptoCallback;
812
+
813
+ const sandboxCrypto = {
1125
814
  createHash: (algorithm) => ({
1126
815
  _alg: algorithm, _data: '',
1127
816
  update(d) { this._data = d; return this; },
1128
- digest(enc) { return _cryptoRef.applySync(undefined, ['createHash', this._alg, this._data, enc || 'hex']); }
817
+ digest(enc) { return hostCrypto('createHash', this._alg, this._data, enc || 'hex'); }
1129
818
  }),
1130
819
  createHmac: (algorithm, key) => ({
1131
820
  _alg: algorithm, _key: key, _data: '',
1132
821
  update(d) { this._data = d; return this; },
1133
- digest(enc) { return _cryptoRef.applySync(undefined, ['createHmac', this._alg, this._key, this._data, enc || 'hex']); }
822
+ digest(enc) { return hostCrypto('createHmac', this._alg, this._key, this._data, enc || 'hex'); }
1134
823
  }),
1135
824
  randomBytes: (size) => ({
1136
- toString(enc) { return _cryptoRef.applySync(undefined, ['randomBytes', String(size)]); }
825
+ toString(enc) { return hostCrypto('randomBytes', String(size)); }
1137
826
  }),
1138
827
  randomUUID: () => {
1139
- return _cryptoRef.applySync(undefined, ['randomUUID']);
828
+ return hostCrypto('randomUUID');
1140
829
  },
1141
830
  randomInt: (minOrMax, max) => {
1142
831
  if (max === undefined) { max = minOrMax; minOrMax = 0; }
1143
- return Number(_cryptoRef.applySync(undefined, ['randomInt', String(minOrMax), String(max)]));
832
+ return Number(hostCrypto('randomInt', String(minOrMax), String(max)));
1144
833
  },
1145
834
  };
1146
- `);
1147
835
 
1148
- // setTimeout / sleep - bridged via applySyncPromise
1149
- const sleepRef: ivm.Reference<(ms: number) => Promise<void>> =
1150
- new ivm.Reference((ms: number): Promise<void> => {
1151
- return new Promise((resolve: () => void) => {
1152
- global.setTimeout(resolve, Math.min(ms, timeout));
1153
- });
836
+ Object.defineProperty(globalThis, 'crypto', {
837
+ value: sandboxCrypto,
838
+ writable: false,
839
+ configurable: false,
1154
840
  });
841
+ })();
842
+ `);
843
+
844
+ // setTimeout / sleep - bridged through copied start/poll callbacks
845
+ const sleepStartCallback: ivm.Callback<(ms: number) => string> =
846
+ new ivm.Callback(
847
+ (ms: number): string => {
848
+ const operationId: string = String(++nextSleepOperationId);
849
+
850
+ if (!acceptingHostOperations) {
851
+ return operationId;
852
+ }
853
+
854
+ const numericDelay: number = Number(ms);
855
+ const boundedDelay: number = Number.isFinite(numericDelay)
856
+ ? Math.max(0, Math.min(numericDelay, timeout))
857
+ : 0;
858
+ const timeoutHandle: ReturnType<typeof global.setTimeout> =
859
+ global.setTimeout(() => {
860
+ const operation: PendingSleepOperation | undefined =
861
+ pendingSleepOperations.get(operationId);
862
+
863
+ if (operation) {
864
+ operation.settled = true;
865
+ }
866
+ pendingHostTimeouts.delete(timeoutHandle);
867
+ }, boundedDelay);
868
+
869
+ pendingHostTimeouts.add(timeoutHandle);
870
+ pendingSleepOperations.set(operationId, {
871
+ settled: false,
872
+ });
1155
873
 
1156
- await jail.set("_sleepRef", sleepRef);
874
+ return operationId;
875
+ },
876
+ { async: true },
877
+ );
878
+
879
+ const sleepPollCallback: ivm.Callback<(operationId: string) => boolean> =
880
+ new ivm.Callback(
881
+ (operationId: string): boolean => {
882
+ const operation: PendingSleepOperation | undefined =
883
+ pendingSleepOperations.get(operationId);
884
+
885
+ if (!operation || operation.settled) {
886
+ pendingSleepOperations.delete(operationId);
887
+ return true;
888
+ }
889
+
890
+ return false;
891
+ },
892
+ { async: true },
893
+ );
894
+
895
+ await jail.set("__oneuptimeHostSleepStartCallback", sleepStartCallback);
896
+ await jail.set("__oneuptimeHostSleepPollCallback", sleepPollCallback);
1157
897
 
1158
898
  await context.eval(`
1159
- function setTimeout(fn, ms) {
1160
- _sleepRef.applySyncPromise(undefined, [ms || 0]);
1161
- if (typeof fn === 'function') fn();
1162
- }
1163
- async function sleep(ms) {
1164
- await _sleepRef.applySyncPromise(undefined, [ms || 0]);
1165
- }
899
+ (() => {
900
+ const hostSleepStart = globalThis.__oneuptimeHostSleepStartCallback;
901
+ const hostSleepPoll = globalThis.__oneuptimeHostSleepPollCallback;
902
+ delete globalThis.__oneuptimeHostSleepStartCallback;
903
+ delete globalThis.__oneuptimeHostSleepPollCallback;
904
+ const activeTimers = new WeakSet();
905
+ const pollWaitArray = new Int32Array(new SharedArrayBuffer(4));
906
+
907
+ async function hostSleep(ms) {
908
+ const operationId = await hostSleepStart(ms || 0);
909
+
910
+ while (!(await hostSleepPoll(operationId))) {
911
+ // Pace polling on the isolate worker without blocking Node's
912
+ // event loop.
913
+ Atomics.wait(pollWaitArray, 0, 0, 1);
914
+ }
915
+ }
916
+
917
+ function sandboxSetTimeout(fn, ms, ...args) {
918
+ if (typeof fn !== 'function') {
919
+ throw new TypeError('setTimeout callback must be a function');
920
+ }
921
+
922
+ const handle = {};
923
+ activeTimers.add(handle);
924
+ hostSleep(ms || 0).then(() => {
925
+ if (activeTimers.delete(handle)) {
926
+ fn(...args);
927
+ }
928
+ });
929
+ return handle;
930
+ }
931
+
932
+ function sandboxClearTimeout(handle) {
933
+ if (handle && typeof handle === 'object') {
934
+ activeTimers.delete(handle);
935
+ }
936
+ }
937
+
938
+ async function sandboxSleep(ms) {
939
+ await hostSleep(ms || 0);
940
+ }
941
+
942
+ Object.defineProperties(globalThis, {
943
+ setTimeout: {
944
+ value: sandboxSetTimeout,
945
+ writable: false,
946
+ configurable: false,
947
+ },
948
+ clearTimeout: {
949
+ value: sandboxClearTimeout,
950
+ writable: false,
951
+ configurable: false,
952
+ },
953
+ sleep: {
954
+ value: sandboxSleep,
955
+ writable: false,
956
+ configurable: false,
957
+ },
958
+ });
959
+ })();
1166
960
  `);
1167
961
 
1168
962
  /*
@@ -1179,24 +973,35 @@ export default class VMRunner {
1179
973
  catch(_) { return undefined; }
1180
974
  })()`;
1181
975
 
1182
- // Run with overall timeout covering both CPU and I/O wait
1183
- const resultPromise: Promise<unknown> = context.eval(wrappedCode, {
1184
- promise: true,
1185
- timeout: timeout,
1186
- });
976
+ let result: unknown;
977
+ let scriptError: Error | undefined;
1187
978
 
1188
- const overallTimeout: Promise<never> = new Promise(
1189
- (_resolve: (value: never) => void, reject: (reason: Error) => void) => {
1190
- global.setTimeout(() => {
1191
- reject(new Error("Script execution timed out"));
1192
- }, timeout + 5000); // 5s grace period beyond isolate timeout
1193
- },
1194
- );
979
+ try {
980
+ // Run with overall timeout covering both CPU and I/O wait.
981
+ const resultPromise: Promise<unknown> = context.eval(wrappedCode, {
982
+ promise: true,
983
+ timeout: timeout,
984
+ });
985
+
986
+ const overallTimeout: Promise<never> = new Promise(
987
+ (
988
+ _resolve: (value: never) => void,
989
+ reject: (reason: Error) => void,
990
+ ) => {
991
+ const timeoutHandle: ReturnType<typeof global.setTimeout> =
992
+ global.setTimeout(() => {
993
+ pendingHostTimeouts.delete(timeoutHandle);
994
+ reject(new Error("Script execution timed out"));
995
+ }, timeout + 5000); // 5s grace period beyond isolate timeout
1195
996
 
1196
- const result: unknown = await Promise.race([
1197
- resultPromise,
1198
- overallTimeout,
1199
- ]);
997
+ pendingHostTimeouts.add(timeoutHandle);
998
+ },
999
+ );
1000
+
1001
+ result = await Promise.race([resultPromise, overallTimeout]);
1002
+ } catch (error: unknown) {
1003
+ scriptError = sanitizeScriptError(error);
1004
+ }
1200
1005
 
1201
1006
  // Parse the JSON string returned from inside the isolate
1202
1007
  let returnValue: unknown;
@@ -1215,8 +1020,22 @@ export default class VMRunner {
1215
1020
  returnValue,
1216
1021
  logMessages,
1217
1022
  capturedMetrics,
1023
+ scriptError,
1218
1024
  };
1219
1025
  } finally {
1026
+ acceptingHostOperations = false;
1027
+
1028
+ for (const timeoutHandle of pendingHostTimeouts) {
1029
+ global.clearTimeout(timeoutHandle);
1030
+ }
1031
+ for (const abortController of pendingAxiosControllers) {
1032
+ abortController.abort();
1033
+ }
1034
+ pendingHostTimeouts.clear();
1035
+ pendingAxiosControllers.clear();
1036
+ pendingSleepOperations.clear();
1037
+ pendingAxiosOperations.clear();
1038
+
1220
1039
  if (!isolate.isDisposed) {
1221
1040
  isolate.dispose();
1222
1041
  }