ghostcrawl 2.3.4

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 (333) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +218 -0
  4. data/_generated/ghostcrawl.rb +0 -0
  5. data/_generated/ghostcrawl_client.rb +27 -0
  6. data/_generated/models/agent_request.rb +203 -0
  7. data/_generated/models/agent_request_mode.rb +8 -0
  8. data/_generated/models/agent_request_task.rb +164 -0
  9. data/_generated/models/agent_request_task_steps.rb +122 -0
  10. data/_generated/models/agent_request_task_steps_schema.rb +64 -0
  11. data/_generated/models/agent_request_task_steps_type.rb +9 -0
  12. data/_generated/models/auth_context.rb +204 -0
  13. data/_generated/models/batch_scrape_request.rb +644 -0
  14. data/_generated/models/batch_scrape_request_behavior_actions_member1.rb +62 -0
  15. data/_generated/models/batch_scrape_request_behavior_actions_member2.rb +62 -0
  16. data/_generated/models/batch_scrape_request_engine.rb +10 -0
  17. data/_generated/models/batch_scrape_request_extraction_strategy_member1.rb +62 -0
  18. data/_generated/models/batch_scrape_request_identity_country_member1.rb +62 -0
  19. data/_generated/models/batch_scrape_request_language_member1.rb +62 -0
  20. data/_generated/models/batch_scrape_request_output_format.rb +9 -0
  21. data/_generated/models/batch_scrape_request_profile_member1.rb +62 -0
  22. data/_generated/models/cdp_frame_request.rb +103 -0
  23. data/_generated/models/cdp_url_request.rb +82 -0
  24. data/_generated/models/content_request.rb +581 -0
  25. data/_generated/models/content_request_engine.rb +12 -0
  26. data/_generated/models/content_request_format.rb +10 -0
  27. data/_generated/models/content_request_identity_country_member1.rb +62 -0
  28. data/_generated/models/content_request_identity_member1.rb +62 -0
  29. data/_generated/models/content_request_language_member1.rb +62 -0
  30. data/_generated/models/content_request_profile_member1.rb +62 -0
  31. data/_generated/models/content_request_proxy_member1.rb +62 -0
  32. data/_generated/models/cookie_dict.rb +739 -0
  33. data/_generated/models/cookie_dict_domain_member1.rb +62 -0
  34. data/_generated/models/cookie_dict_expires_member1.rb +62 -0
  35. data/_generated/models/cookie_dict_http_only_member1.rb +62 -0
  36. data/_generated/models/cookie_dict_path_member1.rb +62 -0
  37. data/_generated/models/cookie_dict_same_site_member1.rb +62 -0
  38. data/_generated/models/cookie_dict_secure_member1.rb +62 -0
  39. data/_generated/models/cookie_dict_url_member1.rb +62 -0
  40. data/_generated/models/cookies_delete_body.rb +355 -0
  41. data/_generated/models/cookies_delete_body_domain_member1.rb +62 -0
  42. data/_generated/models/cookies_delete_body_name_member1.rb +62 -0
  43. data/_generated/models/cookies_delete_body_path_member1.rb +62 -0
  44. data/_generated/models/cookies_set_body.rb +102 -0
  45. data/_generated/models/deep_crawl_body.rb +542 -0
  46. data/_generated/models/deep_crawl_body_routing_mode.rb +11 -0
  47. data/_generated/models/deep_crawl_body_timeout_s_member1.rb +62 -0
  48. data/_generated/models/deep_crawl_body_wait_until_member1.rb +62 -0
  49. data/_generated/models/deep_crawl_body_webhook_url_member1.rb +62 -0
  50. data/_generated/models/dom_snapshot_body.rb +82 -0
  51. data/_generated/models/download_body.rb +102 -0
  52. data/_generated/models/error_code.rb +31 -0
  53. data/_generated/models/error_envelope.rb +264 -0
  54. data/_generated/models/error_envelope_limits.rb +162 -0
  55. data/_generated/models/eval_body.rb +102 -0
  56. data/_generated/models/extend_body.rb +155 -0
  57. data/_generated/models/extend_body_ttl_seconds_member1.rb +62 -0
  58. data/_generated/models/extract_request.rb +631 -0
  59. data/_generated/models/extract_request_behavior_actions_member1.rb +62 -0
  60. data/_generated/models/extract_request_behavior_actions_member2.rb +62 -0
  61. data/_generated/models/extract_request_engine.rb +12 -0
  62. data/_generated/models/extract_request_identity_country_member1.rb +62 -0
  63. data/_generated/models/extract_request_language_member1.rb +62 -0
  64. data/_generated/models/extract_request_prompt_member1.rb +62 -0
  65. data/_generated/models/extract_request_url_member1.rb +62 -0
  66. data/_generated/models/extract_request_urls_member1.rb +62 -0
  67. data/_generated/models/filter_spec.rb +62 -0
  68. data/_generated/models/har_body.rb +82 -0
  69. data/_generated/models/map_body.rb +238 -0
  70. data/_generated/models/map_body_search_member1.rb +62 -0
  71. data/_generated/models/map_response.rb +195 -0
  72. data/_generated/models/map_response_truncated_to_server_max_member1.rb +62 -0
  73. data/_generated/models/models.rb +152 -0
  74. data/_generated/models/pdf_request.rb +560 -0
  75. data/_generated/models/pdf_request_engine.rb +12 -0
  76. data/_generated/models/pdf_request_identity_country_member1.rb +62 -0
  77. data/_generated/models/pdf_request_identity_member1.rb +62 -0
  78. data/_generated/models/pdf_request_language_member1.rb +62 -0
  79. data/_generated/models/pdf_request_profile_member1.rb +62 -0
  80. data/_generated/models/pdf_request_proxy_member1.rb +62 -0
  81. data/_generated/models/problem_details.rb +226 -0
  82. data/_generated/models/profile_create_request.rb +175 -0
  83. data/_generated/models/profile_create_request_storage_state_id_member1.rb +62 -0
  84. data/_generated/models/profile_update_request.rb +246 -0
  85. data/_generated/models/profile_update_request_name_member1.rb +62 -0
  86. data/_generated/models/profile_update_request_storage_state_id_member1.rb +62 -0
  87. data/_generated/models/proxy_config.rb +116 -0
  88. data/_generated/models/retry_delivery_response.rb +145 -0
  89. data/_generated/models/rotate_secret_response.rb +145 -0
  90. data/_generated/models/schedule_create_request.rb +256 -0
  91. data/_generated/models/schedule_create_request_job_params.rb +64 -0
  92. data/_generated/models/schedule_create_request_notify_webhook_member1.rb +62 -0
  93. data/_generated/models/scorer_spec.rb +102 -0
  94. data/_generated/models/scrape_page_result.rb +62 -0
  95. data/_generated/models/scrape_request.rb +1254 -0
  96. data/_generated/models/scrape_request_batch_identity_mode.rb +10 -0
  97. data/_generated/models/scrape_request_behavior_actions_member1.rb +62 -0
  98. data/_generated/models/scrape_request_behavior_actions_member2.rb +62 -0
  99. data/_generated/models/scrape_request_engine.rb +12 -0
  100. data/_generated/models/scrape_request_format.rb +11 -0
  101. data/_generated/models/scrape_request_identity_country_member1.rb +62 -0
  102. data/_generated/models/scrape_request_identity_member1.rb +62 -0
  103. data/_generated/models/scrape_request_language_member1.rb +62 -0
  104. data/_generated/models/scrape_request_profile_member1.rb +62 -0
  105. data/_generated/models/scrape_request_proxy_member1.rb +62 -0
  106. data/_generated/models/scrape_request_routing_mode.rb +11 -0
  107. data/_generated/models/scrape_request_screenshot_selector_member1.rb +62 -0
  108. data/_generated/models/scrape_request_session_member1.rb +62 -0
  109. data/_generated/models/scrape_request_timeout_ms_member1.rb +62 -0
  110. data/_generated/models/scrape_request_url_member1.rb +62 -0
  111. data/_generated/models/scrape_request_urls_member1.rb +62 -0
  112. data/_generated/models/scrape_result.rb +630 -0
  113. data/_generated/models/scrape_result_format_member1.rb +62 -0
  114. data/_generated/models/scrape_result_identity_id_member1.rb +62 -0
  115. data/_generated/models/scrape_result_markdown_member1.rb +62 -0
  116. data/_generated/models/scrape_result_status_member1.rb +62 -0
  117. data/_generated/models/scrape_result_token_estimate_member1.rb +62 -0
  118. data/_generated/models/scrape_result_warnings_member1.rb +62 -0
  119. data/_generated/models/screenshot_request.rb +672 -0
  120. data/_generated/models/screenshot_request_engine.rb +12 -0
  121. data/_generated/models/screenshot_request_identity_country_member1.rb +62 -0
  122. data/_generated/models/screenshot_request_identity_member1.rb +62 -0
  123. data/_generated/models/screenshot_request_language_member1.rb +62 -0
  124. data/_generated/models/screenshot_request_profile_member1.rb +62 -0
  125. data/_generated/models/screenshot_request_proxy_member1.rb +62 -0
  126. data/_generated/models/screenshot_request_screenshot_selector_member1.rb +62 -0
  127. data/_generated/models/scroll_body.rb +284 -0
  128. data/_generated/models/scroll_body_direction.rb +11 -0
  129. data/_generated/models/scroll_body_distance_px_member1.rb +62 -0
  130. data/_generated/models/scroll_body_selector_member1.rb +62 -0
  131. data/_generated/models/search_request.rb +469 -0
  132. data/_generated/models/search_request_country_member1.rb +62 -0
  133. data/_generated/models/search_request_engine_member1.rb +62 -0
  134. data/_generated/models/search_request_freshness_member1.rb +62 -0
  135. data/_generated/models/search_request_vertical_member1.rb +62 -0
  136. data/_generated/models/session_create_request.rb +174 -0
  137. data/_generated/models/session_create_request_engine.rb +10 -0
  138. data/_generated/models/session_create_request_profile_member1.rb +62 -0
  139. data/_generated/models/storage_state_attach_request.rb +58 -0
  140. data/_generated/models/storage_state_detach_request.rb +58 -0
  141. data/_generated/models/takeover_action_body.rb +82 -0
  142. data/_generated/models/upload_body.rb +162 -0
  143. data/_generated/models/wait_body.rb +194 -0
  144. data/_generated/models/wait_body_selector_member1.rb +62 -0
  145. data/_generated/models/webhook_create_request.rb +125 -0
  146. data/_generated/models/webhook_create_response.rb +104 -0
  147. data/_generated/models/webhook_delivery_list_response.rb +195 -0
  148. data/_generated/models/webhook_delivery_list_response_next_cursor_member1.rb +62 -0
  149. data/_generated/models/webhook_delivery_public.rb +680 -0
  150. data/_generated/models/webhook_delivery_public_delivered_at_member1.rb +62 -0
  151. data/_generated/models/webhook_delivery_public_error_class_member1.rb +62 -0
  152. data/_generated/models/webhook_delivery_public_replay_of_member1.rb +62 -0
  153. data/_generated/models/webhook_delivery_public_response_body_preview_member1.rb +62 -0
  154. data/_generated/models/webhook_delivery_public_response_status_member1.rb +62 -0
  155. data/_generated/models/webhook_list_response.rb +195 -0
  156. data/_generated/models/webhook_list_response_next_cursor_member1.rb +62 -0
  157. data/_generated/models/webhook_public.rb +316 -0
  158. data/_generated/models/webhook_public_secret_rotated_at_member1.rb +62 -0
  159. data/_generated/v1/agent/agent.rb +1 -0
  160. data/_generated/v1/agent/agent404_error.rb +87 -0
  161. data/_generated/v1/agent/agent_request_builder.rb +73 -0
  162. data/_generated/v1/binary/binary.rb +9 -0
  163. data/_generated/v1/cdp/cdp.rb +0 -0
  164. data/_generated/v1/cdp/cdp_request_builder.rb +37 -0
  165. data/_generated/v1/cdp/frame/frame.rb +0 -0
  166. data/_generated/v1/cdp/frame/frame_request_builder.rb +74 -0
  167. data/_generated/v1/cdp/url/url.rb +0 -0
  168. data/_generated/v1/cdp/url/url_request_builder.rb +74 -0
  169. data/_generated/v1/content/content.rb +0 -0
  170. data/_generated/v1/content/content_request_builder.rb +71 -0
  171. data/_generated/v1/crawl/crawl.rb +0 -0
  172. data/_generated/v1/crawl/crawl_request_builder.rb +31 -0
  173. data/_generated/v1/crawl/deep/deep.rb +0 -0
  174. data/_generated/v1/crawl/deep/deep_request_builder.rb +86 -0
  175. data/_generated/v1/crawl/deep/item/item.rb +0 -0
  176. data/_generated/v1/crawl/deep/item/with_run_item_request_builder.rb +71 -0
  177. data/_generated/v1/crawl_runs/crawl_runs.rb +1 -0
  178. data/_generated/v1/crawl_runs/crawl_runs_post_request_body.rb +65 -0
  179. data/_generated/v1/crawl_runs/crawl_runs_request_builder.rb +131 -0
  180. data/_generated/v1/crawl_runs/item/cancel/cancel.rb +0 -0
  181. data/_generated/v1/crawl_runs/item/cancel/cancel_request_builder.rb +71 -0
  182. data/_generated/v1/crawl_runs/item/item.rb +0 -0
  183. data/_generated/v1/crawl_runs/item/resume/resume.rb +0 -0
  184. data/_generated/v1/crawl_runs/item/resume/resume_request_builder.rb +71 -0
  185. data/_generated/v1/crawl_runs/item/with_run_item_request_builder.rb +96 -0
  186. data/_generated/v1/datasets/datasets.rb +1 -0
  187. data/_generated/v1/datasets/datasets_post_request_body.rb +65 -0
  188. data/_generated/v1/datasets/datasets_request_builder.rb +124 -0
  189. data/_generated/v1/datasets/item/item.rb +0 -0
  190. data/_generated/v1/datasets/item/rows/append/append.rb +1 -0
  191. data/_generated/v1/datasets/item/rows/append/append_post_request_body.rb +74 -0
  192. data/_generated/v1/datasets/item/rows/append/append_request_builder.rb +79 -0
  193. data/_generated/v1/datasets/item/rows/rows.rb +0 -0
  194. data/_generated/v1/datasets/item/rows/rows_request_builder.rb +86 -0
  195. data/_generated/v1/datasets/item/with_name_item_request_builder.rb +104 -0
  196. data/_generated/v1/downloads/downloads.rb +0 -0
  197. data/_generated/v1/downloads/downloads_request_builder.rb +76 -0
  198. data/_generated/v1/downloads/item/item.rb +0 -0
  199. data/_generated/v1/downloads/item/with_download_item_request_builder.rb +97 -0
  200. data/_generated/v1/extract/extract.rb +1 -0
  201. data/_generated/v1/extract/extract_post_response.rb +65 -0
  202. data/_generated/v1/extract/extract_request_builder.rb +78 -0
  203. data/_generated/v1/kv/item/item.rb +1 -0
  204. data/_generated/v1/kv/item/with_key_item_request_builder.rb +133 -0
  205. data/_generated/v1/kv/item/with_key_put_request_body.rb +68 -0
  206. data/_generated/v1/kv/kv.rb +0 -0
  207. data/_generated/v1/kv/kv_request_builder.rb +37 -0
  208. data/_generated/v1/map/map.rb +0 -0
  209. data/_generated/v1/map/map_request_builder.rb +72 -0
  210. data/_generated/v1/page/cookies/cookies.rb +0 -0
  211. data/_generated/v1/page/cookies/cookies_request_builder.rb +150 -0
  212. data/_generated/v1/page/dom_snapshot/dom_snapshot.rb +0 -0
  213. data/_generated/v1/page/dom_snapshot/dom_snapshot_request_builder.rb +74 -0
  214. data/_generated/v1/page/download/download.rb +0 -0
  215. data/_generated/v1/page/download/download_request_builder.rb +74 -0
  216. data/_generated/v1/page/eval/eval.rb +0 -0
  217. data/_generated/v1/page/eval/eval_request_builder.rb +74 -0
  218. data/_generated/v1/page/har/har.rb +0 -0
  219. data/_generated/v1/page/har/har_request_builder.rb +74 -0
  220. data/_generated/v1/page/page.rb +0 -0
  221. data/_generated/v1/page/page_request_builder.rb +73 -0
  222. data/_generated/v1/page/scroll/scroll.rb +0 -0
  223. data/_generated/v1/page/scroll/scroll_request_builder.rb +74 -0
  224. data/_generated/v1/page/upload/upload.rb +0 -0
  225. data/_generated/v1/page/upload/upload_request_builder.rb +74 -0
  226. data/_generated/v1/page/wait/wait.rb +0 -0
  227. data/_generated/v1/page/wait/wait_request_builder.rb +74 -0
  228. data/_generated/v1/pdf/pdf.rb +0 -0
  229. data/_generated/v1/pdf/pdf_request_builder.rb +71 -0
  230. data/_generated/v1/profiles/item/item.rb +0 -0
  231. data/_generated/v1/profiles/item/with_name_item_request_builder.rb +134 -0
  232. data/_generated/v1/profiles/profiles.rb +0 -0
  233. data/_generated/v1/profiles/profiles_request_builder.rb +125 -0
  234. data/_generated/v1/recordings/item/item.rb +0 -0
  235. data/_generated/v1/recordings/item/recording_item_request_builder.rb +102 -0
  236. data/_generated/v1/recordings/item/visual/frames/frames.rb +0 -0
  237. data/_generated/v1/recordings/item/visual/frames/frames_request_builder.rb +93 -0
  238. data/_generated/v1/recordings/item/visual/start/start.rb +0 -0
  239. data/_generated/v1/recordings/item/visual/start/start_request_builder.rb +80 -0
  240. data/_generated/v1/recordings/item/visual/stop/stop.rb +0 -0
  241. data/_generated/v1/recordings/item/visual/stop/stop_request_builder.rb +80 -0
  242. data/_generated/v1/recordings/item/visual/visual.rb +0 -0
  243. data/_generated/v1/recordings/item/visual/visual_request_builder.rb +95 -0
  244. data/_generated/v1/recordings/recordings.rb +0 -0
  245. data/_generated/v1/recordings/recordings_request_builder.rb +89 -0
  246. data/_generated/v1/schedules/item/item.rb +0 -0
  247. data/_generated/v1/schedules/item/runs/runs.rb +0 -0
  248. data/_generated/v1/schedules/item/runs/runs_request_builder.rb +71 -0
  249. data/_generated/v1/schedules/item/with_schedule_item_request_builder.rb +103 -0
  250. data/_generated/v1/schedules/schedules.rb +0 -0
  251. data/_generated/v1/schedules/schedules_request_builder.rb +113 -0
  252. data/_generated/v1/scrape/batch/batch.rb +0 -0
  253. data/_generated/v1/scrape/batch/batch_request_builder.rb +81 -0
  254. data/_generated/v1/scrape/scrape.rb +0 -0
  255. data/_generated/v1/scrape/scrape_request_builder.rb +86 -0
  256. data/_generated/v1/screenshot/screenshot.rb +0 -0
  257. data/_generated/v1/screenshot/screenshot_request_builder.rb +71 -0
  258. data/_generated/v1/screenshot_blobs/item/item.rb +0 -0
  259. data/_generated/v1/screenshot_blobs/item/with_ref_item_request_builder.rb +68 -0
  260. data/_generated/v1/screenshot_blobs/screenshot_blobs.rb +0 -0
  261. data/_generated/v1/screenshot_blobs/screenshot_blobs_request_builder.rb +37 -0
  262. data/_generated/v1/search/jobs/item/item.rb +0 -0
  263. data/_generated/v1/search/jobs/item/with_job_item_request_builder.rb +71 -0
  264. data/_generated/v1/search/jobs/jobs.rb +0 -0
  265. data/_generated/v1/search/jobs/jobs_request_builder.rb +40 -0
  266. data/_generated/v1/search/search.rb +0 -0
  267. data/_generated/v1/search/search_request_builder.rb +84 -0
  268. data/_generated/v1/selfhost/byo/behavior/behavior.rb +1 -0
  269. data/_generated/v1/selfhost/byo/behavior/behavior_put_request_body.rb +71 -0
  270. data/_generated/v1/selfhost/byo/behavior/behavior_request_builder.rb +76 -0
  271. data/_generated/v1/selfhost/byo/byo.rb +0 -0
  272. data/_generated/v1/selfhost/byo/byo_request_builder.rb +46 -0
  273. data/_generated/v1/selfhost/byo/identity/identity.rb +1 -0
  274. data/_generated/v1/selfhost/byo/identity/identity_put_request_body.rb +71 -0
  275. data/_generated/v1/selfhost/byo/identity/identity_request_builder.rb +76 -0
  276. data/_generated/v1/selfhost/byo/proxy/proxy.rb +1 -0
  277. data/_generated/v1/selfhost/byo/proxy/proxy_put_request_body.rb +71 -0
  278. data/_generated/v1/selfhost/byo/proxy/proxy_request_builder.rb +76 -0
  279. data/_generated/v1/selfhost/selfhost.rb +0 -0
  280. data/_generated/v1/selfhost/selfhost_request_builder.rb +31 -0
  281. data/_generated/v1/sessions/create/create.rb +0 -0
  282. data/_generated/v1/sessions/create/create_request_builder.rb +74 -0
  283. data/_generated/v1/sessions/item/extend/extend.rb +0 -0
  284. data/_generated/v1/sessions/item/extend/extend_request_builder.rb +77 -0
  285. data/_generated/v1/sessions/item/item.rb +0 -0
  286. data/_generated/v1/sessions/item/pin/pin.rb +0 -0
  287. data/_generated/v1/sessions/item/pin/pin_request_builder.rb +71 -0
  288. data/_generated/v1/sessions/item/profile_item_request_builder.rb +70 -0
  289. data/_generated/v1/sessions/item/recording/recording.rb +0 -0
  290. data/_generated/v1/sessions/item/recording/recording_request_builder.rb +43 -0
  291. data/_generated/v1/sessions/item/recording/start/start.rb +0 -0
  292. data/_generated/v1/sessions/item/recording/start/start_request_builder.rb +74 -0
  293. data/_generated/v1/sessions/item/recording/stop/stop.rb +0 -0
  294. data/_generated/v1/sessions/item/recording/stop/stop_request_builder.rb +74 -0
  295. data/_generated/v1/sessions/item/release/release.rb +0 -0
  296. data/_generated/v1/sessions/item/release/release_request_builder.rb +71 -0
  297. data/_generated/v1/sessions/item/takeover/takeover.rb +0 -0
  298. data/_generated/v1/sessions/item/takeover/takeover_request_builder.rb +77 -0
  299. data/_generated/v1/sessions/item/takeover_release/takeover_release.rb +0 -0
  300. data/_generated/v1/sessions/item/takeover_release/takeover_release_request_builder.rb +71 -0
  301. data/_generated/v1/sessions/item/takeover_token/takeover_token.rb +0 -0
  302. data/_generated/v1/sessions/item/takeover_token/takeover_token_request_builder.rb +71 -0
  303. data/_generated/v1/sessions/sessions.rb +0 -0
  304. data/_generated/v1/sessions/sessions_request_builder.rb +95 -0
  305. data/_generated/v1/storage_states/detach/detach.rb +0 -0
  306. data/_generated/v1/storage_states/detach/detach_request_builder.rb +74 -0
  307. data/_generated/v1/storage_states/item/attach/attach.rb +0 -0
  308. data/_generated/v1/storage_states/item/attach/attach_request_builder.rb +77 -0
  309. data/_generated/v1/storage_states/item/item.rb +0 -0
  310. data/_generated/v1/storage_states/item/with_id_or_name_item_request_builder.rb +104 -0
  311. data/_generated/v1/storage_states/storage_states.rb +1 -0
  312. data/_generated/v1/storage_states/storage_states_post_request_body.rb +65 -0
  313. data/_generated/v1/storage_states/storage_states_request_builder.rb +127 -0
  314. data/_generated/v1/v1.rb +0 -0
  315. data/_generated/v1/v1_request_builder.rb +161 -0
  316. data/_generated/v1/webhooks/item/deliveries/deliveries.rb +0 -0
  317. data/_generated/v1/webhooks/item/deliveries/deliveries_request_builder.rb +96 -0
  318. data/_generated/v1/webhooks/item/deliveries/item/item.rb +0 -0
  319. data/_generated/v1/webhooks/item/deliveries/item/retry_escaped/retry_escaped.rb +0 -0
  320. data/_generated/v1/webhooks/item/deliveries/item/retry_escaped/retry_request_builder.rb +78 -0
  321. data/_generated/v1/webhooks/item/deliveries/item/with_delivery_item_request_builder.rb +40 -0
  322. data/_generated/v1/webhooks/item/item.rb +0 -0
  323. data/_generated/v1/webhooks/item/rotate_secret/rotate_secret.rb +0 -0
  324. data/_generated/v1/webhooks/item/rotate_secret/rotate_secret_request_builder.rb +72 -0
  325. data/_generated/v1/webhooks/item/with_webhook_item_request_builder.rb +111 -0
  326. data/_generated/v1/webhooks/webhooks.rb +0 -0
  327. data/_generated/v1/webhooks/webhooks_request_builder.rb +127 -0
  328. data/lib/ghostcrawl/client.rb +1351 -0
  329. data/lib/ghostcrawl/error_codes.rb +111 -0
  330. data/lib/ghostcrawl/errors.rb +137 -0
  331. data/lib/ghostcrawl/version.rb +5 -0
  332. data/lib/ghostcrawl.rb +17 -0
  333. metadata +422 -0
@@ -0,0 +1,1351 @@
1
+ # frozen_string_literal: true
2
+
3
+ # GhostCrawl Ruby SDK — idiomatic facade over the Kiota-generated core.
4
+ #
5
+ # Architecture:
6
+ #
7
+ # _generated/ Kiota core — spec-faithful 98-op request-builder (models, transport, auth)
8
+ # This FACADE — thin idiomatic layer delegating to the generated builders
9
+ #
10
+ # All HTTP transport, URL routing, serialization, and auth come from the generated core.
11
+ # The facade maps idiomatic calls (client.scrape) to generated builders via
12
+ # BaseBearerTokenAuthenticationProvider + FaradayRequestAdapter.
13
+ #
14
+ # Usage:
15
+ # require "ghostcrawl"
16
+ # client = GhostCrawl::Client.new(token: "gck_live_YOUR_KEY")
17
+ # result = client.scrape(url: "https://example.com")
18
+
19
+ require "json"
20
+ require "microsoft_kiota_abstractions"
21
+ require "microsoft_kiota_faraday"
22
+ require "microsoft_kiota_serialization_json"
23
+ require_relative "../../_generated/ghostcrawl_client"
24
+ require_relative "../../_generated/models/scrape_request"
25
+ require_relative "../../_generated/models/search_request"
26
+ require_relative "../../_generated/models/extract_request"
27
+ require_relative "../../_generated/models/deep_crawl_body"
28
+ require_relative "../../_generated/models/map_body"
29
+ require_relative "../../_generated/models/session_create_request"
30
+ require_relative "../../_generated/models/extend_body"
31
+ require_relative "../../_generated/models/profile_create_request"
32
+ require_relative "../../_generated/models/profile_update_request"
33
+ require_relative "../../_generated/models/webhook_create_request"
34
+ require_relative "../../_generated/models/schedule_create_request"
35
+ require_relative "errors"
36
+
37
+ module GhostCrawl
38
+ # ---------------------------------------------------------------------------
39
+ # Fix: microsoft_kiota_serialization_json 0.9.2 has a bug where
40
+ # write_object_value(nil, body) creates a temp writer, serializes body into
41
+ # temp, then RETURNS temp without merging temp's content into self.@writer.
42
+ # set_content_from_parsable calls write_object_value(nil, body) and discards
43
+ # the return value, so @content = {}.to_json = "{}".
44
+ #
45
+ # Fix: monkey-patch write_object_value so that when key is nil, it serializes
46
+ # body into self directly (not a temp writer) — same as what the non-nil branch
47
+ # does, but without a key.
48
+ # ---------------------------------------------------------------------------
49
+
50
+ # @api private
51
+ module KiotaWriterFix
52
+ def write_object_value(key, value)
53
+ return unless value
54
+ if key.nil?
55
+ # Fix: serialize into self, not a temp. Merges all fields into @writer.
56
+ value.serialize(self)
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ # Fix 1b: the pinned Kiota writer's write_collection_of_object_values calls
63
+ # `self.write_object_value(nil, v).writer` on each element — it relies on
64
+ # write_object_value(nil, ...) returning a *writer object* that responds to
65
+ # `.writer`. Our write_object_value override above (Fix 1) changes that
66
+ # nil-key contract to serialize-into-self and return the serialize result
67
+ # (a Hash / nil), so the upstream `.writer` call raises
68
+ # `NoMethodError: undefined method 'writer' for {}:Hash`. That aborts
69
+ # serialize for ANY response model carrying a collection of typed objects
70
+ # (WebhookListResponse#items, etc.), which ResponseHelper.serialize_parsable
71
+ # then swallows -> the facade returns {} and silently drops the whole list.
72
+ #
73
+ # Fix: serialize each element into its OWN fresh writer and collect the raw
74
+ # per-element hashes directly, never depending on the write_object_value
75
+ # return contract. Mirrors the non-nil upstream branch (line 154) without the
76
+ # broken `.writer` deref.
77
+ def write_collection_of_object_values(key, values)
78
+ return unless values
79
+ hashes = values.map do |v|
80
+ temp = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new
81
+ v.serialize(temp)
82
+ temp.writer
83
+ end
84
+ if key.nil?
85
+ hashes
86
+ else
87
+ writer[key] = hashes
88
+ end
89
+ end
90
+
91
+ # Fix 1c: the pinned Kiota writer's write_any_value (used by
92
+ # write_additional_data, i.e. EVERY field AdditionalDataBody sends) has no
93
+ # case for a plain Hash and mis-handles an Array of Hashes:
94
+ # * a Hash hits the `value.is_a? Object` branch -> `return value.to_s`,
95
+ # which returns a string but NEVER assigns @writer[key] -> the whole
96
+ # nested object is SILENTLY DROPPED from the request body.
97
+ # * an Array is sent to write_collection_of_primitive_values, which mangles
98
+ # any non-primitive (Hash) element.
99
+ # This drops/mangles every nested field the facade sends: scrape(extract_schema),
100
+ # extract(schema), crawl/crawl_runs opts, datasets.append(rows: [ {..} ]),
101
+ # schedules.create(job_params: {..}) — the last of which the API REQUIRES,
102
+ # so the request 422s ("job_params required") even though the caller supplied it.
103
+ #
104
+ # Fix: intercept Hash and Array here and write a fully-recursed, JSON-native
105
+ # structure straight into @writer[key]. The writer emits @writer.to_json at the
106
+ # end, so plain nested Hash/Array values serialize correctly. Everything else
107
+ # falls through to the upstream primitive handling.
108
+ def write_any_value(key, value)
109
+ if value.is_a?(Hash) || value.is_a?(Array)
110
+ return super unless key # nil-key arrays keep upstream behavior
111
+ writer[key] = KiotaWriterFix.jsonable(value)
112
+ return
113
+ end
114
+ super
115
+ end
116
+
117
+ # Recursively converts a value into a JSON-native structure (Hash/Array of
118
+ # plain scalars), so it round-trips through @writer.to_json intact.
119
+ # @api private
120
+ def self.jsonable(value)
121
+ case value
122
+ when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = jsonable(v) }
123
+ when Array then value.map { |v| jsonable(v) }
124
+ else value
125
+ end
126
+ end
127
+ end
128
+
129
+ # Patch the writer class once, idempotently
130
+ MicrosoftKiotaSerializationJson::JsonSerializationWriter.prepend(KiotaWriterFix) \
131
+ unless MicrosoftKiotaSerializationJson::JsonSerializationWriter.ancestors.include?(KiotaWriterFix)
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Fix 2: Some generated request builders pass a Module (e.g. `Binary`) as
135
+ # the factory to `send_async`. The parse node's `get_object_value(factory)`
136
+ # calls `factory.call(self)`, which fails for a Module.
137
+ #
138
+ # Fix: when factory doesn't respond to `call`, fall back to parsing the
139
+ # response body as JSON and returning the raw hash. This gives us plain
140
+ # Ruby hashes (which ResponseHelper.to_hash handles correctly) without
141
+ # requiring generated response models.
142
+ # ---------------------------------------------------------------------------
143
+
144
+ # @api private
145
+ module KiotaParseNodeFix
146
+ def get_object_value(factory)
147
+ return super if factory.respond_to?(:call)
148
+ # Fallback: return the raw parsed node for non-callable factories (e.g. Binary).
149
+ # @current_node is already a Ruby Hash/Array/primitive parsed from JSON.
150
+ @current_node
151
+ rescue StandardError
152
+ super
153
+ end
154
+ end
155
+
156
+ MicrosoftKiotaSerializationJson::JsonParseNode.prepend(KiotaParseNodeFix) \
157
+ unless MicrosoftKiotaSerializationJson::JsonParseNode.ancestors.include?(KiotaParseNodeFix)
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # AdditionalDataBody — a minimal Parsable that wraps an arbitrary Hash.
161
+ #
162
+ # Used for all POST bodies. The generated models set typed defaults in their
163
+ # constructors that would be serialized as null/empty-enum values and cause
164
+ # 422 validation errors. AdditionalDataBody only serializes the fields we
165
+ # explicitly pass, producing clean JSON.
166
+ #
167
+ # Depends on KiotaWriterFix above to correctly handle the nil-key
168
+ # write_object_value(nil, body) call from set_content_from_parsable.
169
+ # ---------------------------------------------------------------------------
170
+
171
+ # @api private
172
+ class AdditionalDataBody
173
+ include MicrosoftKiotaAbstractions::Parsable
174
+
175
+ def initialize(data = {})
176
+ @data = data.transform_keys(&:to_s)
177
+ end
178
+
179
+ def get_field_deserializers
180
+ {}
181
+ end
182
+
183
+ def serialize(writer)
184
+ writer.write_additional_data(@data)
185
+ end
186
+
187
+ def additional_data
188
+ @data
189
+ end
190
+
191
+ def additional_data=(hash)
192
+ @data = hash.transform_keys(&:to_s)
193
+ end
194
+
195
+ def self.create_from_discriminator_value(_parse_node)
196
+ AdditionalDataBody.new
197
+ end
198
+ end
199
+ end
200
+
201
+ module GhostCrawl
202
+ DEFAULT_BASE_URL = "https://api.ghostcrawl.io"
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # Static bearer token provider — implements AccessTokenProvider
206
+ # ---------------------------------------------------------------------------
207
+
208
+ # @api private
209
+ class StaticTokenProvider
210
+ include MicrosoftKiotaAbstractions::AccessTokenProvider
211
+
212
+ def initialize(token)
213
+ @token = token
214
+ end
215
+
216
+ # The Kiota Faraday adapter calls .resume on the return value of this method,
217
+ # expecting a Fiber. Wrap the token in a Fiber to satisfy the contract.
218
+ def get_authorization_token(_uri, _additional_authentication_context = nil)
219
+ token = @token
220
+ Fiber.new { token }
221
+ end
222
+
223
+ def get_allowed_hosts_validator
224
+ MicrosoftKiotaAbstractions::AllowedHostsValidator.new([])
225
+ end
226
+ end
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Response helpers — convert Kiota Parsable/Hash responses to plain Hash
230
+ # ---------------------------------------------------------------------------
231
+
232
+ # @api private
233
+ module ResponseHelper
234
+ # Converts any Kiota response value to a plain Hash or Array.
235
+ # The Kiota Faraday adapter returns Fibers for async responses; call .resume
236
+ # to execute the request synchronously and get the actual response value.
237
+ def self.to_hash(value)
238
+ # Resolve Fibers (the Kiota Faraday adapter returns a Fiber for every
239
+ # request; .resume is what actually executes the HTTP call). Any non-2xx
240
+ # surfaces here as a raw transport exception — translate it into a typed
241
+ # GhostCrawl error so the documented rescue contract works.
242
+ if value.is_a?(Fiber)
243
+ begin
244
+ value = value.resume
245
+ rescue GhostCrawl::GhostCrawlError
246
+ raise
247
+ rescue StandardError => e
248
+ GhostCrawl.raise_translated(e)
249
+ end
250
+ end
251
+
252
+ case value
253
+ when Hash
254
+ value.transform_values { |v| to_hash(v) }
255
+ when Array
256
+ value.map { |v| to_hash(v) }
257
+ when NilClass
258
+ {}
259
+ else
260
+ # Typed Parsable (Kiota-generated model). Kiota models keep their spec
261
+ # fields in typed instance variables, NOT in +additional_data+ (which is
262
+ # only the overflow bucket for unmapped keys). Reading +additional_data+
263
+ # alone therefore returns +{}+ for a fully-typed response (e.g.
264
+ # MapResponse{@links,@success}, WebhookListResponse{@items,@total}),
265
+ # silently dropping the real payload.
266
+ #
267
+ # Recover the typed fields by round-tripping the model through its own
268
+ # +serialize+ (the Parsable contract) into a JSON writer and parsing the
269
+ # result back to a plain Hash. Then overlay any +additional_data+ (unmapped
270
+ # keys the server sent that the model didn't declare). Degrade gracefully:
271
+ # a model whose +serialize+ raises (e.g. an unresolved composed-type member)
272
+ # falls back to the prior additional_data/to_h behavior so nothing regresses.
273
+ parsable_to_hash(value)
274
+ end
275
+ end
276
+
277
+ # Converts a typed Kiota Parsable into a plain Hash by serializing it via its
278
+ # own +serialize+ and parsing the emitted JSON, then merging +additional_data+.
279
+ # Falls back to +additional_data+ / +to_h+ / the value itself on any failure.
280
+ # @api private
281
+ def self.parsable_to_hash(value)
282
+ serialized = serialize_parsable(value)
283
+ if serialized.is_a?(Hash)
284
+ extra = (value.additional_data if value.respond_to?(:additional_data)) || {}
285
+ merged = serialized.merge(extra.transform_keys(&:to_s))
286
+ return merged.transform_values { |v| to_hash(v) }
287
+ end
288
+
289
+ if value.respond_to?(:additional_data) && value.additional_data && !value.additional_data.empty?
290
+ value.additional_data.transform_values { |v| to_hash(v) }
291
+ elsif value.respond_to?(:to_h)
292
+ value.to_h.transform_values { |v| to_hash(v) }
293
+ elsif value.respond_to?(:additional_data) && value.additional_data
294
+ value.additional_data.transform_values { |v| to_hash(v) }
295
+ else
296
+ value
297
+ end
298
+ end
299
+
300
+ # Serializes a Parsable to a Ruby Hash using the pinned Kiota JSON writer.
301
+ # Returns the parsed Hash, or +nil+ when the model isn't a serializable
302
+ # Parsable or serialization/parse fails (caller then falls back).
303
+ # @api private
304
+ def self.serialize_parsable(value)
305
+ return nil unless value.respond_to?(:serialize)
306
+
307
+ writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new
308
+ value.serialize(writer)
309
+ content = writer.get_serialized_content
310
+ json = content.is_a?(String) ? content : content.read
311
+ return nil if json.nil? || json.empty?
312
+
313
+ parsed = JSON.parse(json)
314
+ parsed.is_a?(Hash) ? parsed : nil
315
+ rescue StandardError
316
+ nil
317
+ end
318
+
319
+ # Executes a request that returns NO response body (an HTTP 204, or any DELETE
320
+ # whose spec maps the success response to +void+) and returns a plain Hash.
321
+ #
322
+ # Why this exists — two upstream defects in the pinned Kiota Ruby runtime that
323
+ # make the generated void-DELETE builders unusable:
324
+ #
325
+ # 1. The generated +delete+ for a void response calls
326
+ # +send_async(request_info, nil, ...)+, and the Faraday adapter hard-raises
327
+ # +"factory cannot be null"+ BEFORE the request is ever sent — so the DELETE
328
+ # never reaches the server and the caller gets a bare +StandardError+
329
+ # (not a {GhostCrawlError}).
330
+ # 2. For void DELETEs the adapter DOES send maps to +Binary+, the response is an
331
+ # empty 204 body, and +get_root_parse_node+ then feeds +""+ to the JSON parser,
332
+ # raising +JSON::ParserError+ on a perfectly successful delete.
333
+ #
334
+ # This helper reuses the adapter's OWN public request pipeline
335
+ # (+convert_to_native_request_async+ applies base-url + auth, +run_request+
336
+ # sends it) but skips body deserialization entirely: a 2xx returns +{}+ (or the
337
+ # decoded JSON when the server did send a body, e.g. +{"deleted":true}+), and a
338
+ # >=400 is translated into the documented typed {GhostCrawlError} hierarchy.
339
+ #
340
+ # @param adapter [MicrosoftKiotaFaraday::FaradayRequestAdapter]
341
+ # @param request_info [MicrosoftKiotaAbstractions::RequestInformation]
342
+ # @return [Hash] +{}+ on an empty 2xx, or the decoded body when one is present
343
+ # @api private
344
+ def self.void_request!(adapter, request_info)
345
+ # Replicate the first half of the adapter's send_async by hand — apply the
346
+ # base URL, run the bearer-auth Fiber (which mutates request_info's headers),
347
+ # then build the native Faraday request. We deliberately do NOT call the
348
+ # adapter's convert_to_native_request_async helper: its body uses a bare
349
+ # +return+ inside a Fiber, which raises LocalJumpError("unexpected return")
350
+ # when resumed under Ruby 3.x.
351
+ request_info.path_parameters["baseurl"] = adapter.get_base_url
352
+ # authenticate_request returns a Fiber only when the Authorization header is
353
+ # not already present; it returns nil otherwise. Guard the resume.
354
+ auth_fiber = adapter.authentication_provider.authenticate_request(request_info)
355
+ auth_fiber.resume if auth_fiber.respond_to?(:resume)
356
+ request = adapter.get_request_from_request_info(request_info)
357
+ response = adapter.client.run_request(
358
+ request.http_method, request.path, request.body, request.headers
359
+ )
360
+
361
+ status = response.status
362
+ if status >= 400
363
+ # Reuse the typed-error translation: synthesize a message carrying the
364
+ # ":<status>" token raise_translated keys off, plus any body text.
365
+ body = response.body.to_s
366
+ err = MicrosoftKiotaAbstractions::ApiError.new(
367
+ "The server returned an unexpected status code:#{status}" \
368
+ "#{body.empty? ? '' : " #{body}"}"
369
+ )
370
+ GhostCrawl.raise_translated(err)
371
+ end
372
+
373
+ body = response.body.to_s
374
+ return {} if body.strip.empty?
375
+
376
+ parsed = (JSON.parse(body) rescue nil)
377
+ parsed.is_a?(Hash) ? parsed.transform_values { |v| to_hash(v) } : {}
378
+ rescue GhostCrawl::GhostCrawlError
379
+ raise
380
+ rescue StandardError => e
381
+ GhostCrawl.raise_translated(e)
382
+ end
383
+
384
+ # Executes a request whose success response is RAW BINARY (e.g. the
385
+ # +application/pdf+ body from +POST /v1/pdf+) and returns the bytes verbatim.
386
+ #
387
+ # Mirrors {void_request!}: it reuses the adapter's own request pipeline
388
+ # (base URL + bearer auth via +authenticate_request+, then +run_request+) but
389
+ # deliberately skips Kiota body deserialization — the response is not JSON, so
390
+ # there is no parse node to build. A 2xx returns the raw response body (an
391
+ # ASCII-8BIT String of bytes, ready to write to a file); a >=400 is translated
392
+ # into the documented typed {GhostCrawlError} hierarchy (the problem+json
393
+ # +detail+ rides along in the message).
394
+ #
395
+ # @param adapter [MicrosoftKiotaFaraday::FaradayRequestAdapter]
396
+ # @param request_info [MicrosoftKiotaAbstractions::RequestInformation]
397
+ # @return [String] the raw response bytes (ASCII-8BIT)
398
+ # @api private
399
+ def self.binary_request!(adapter, request_info)
400
+ request_info.path_parameters["baseurl"] = adapter.get_base_url
401
+ auth_fiber = adapter.authentication_provider.authenticate_request(request_info)
402
+ auth_fiber.resume if auth_fiber.respond_to?(:resume)
403
+ request = adapter.get_request_from_request_info(request_info)
404
+ response = adapter.client.run_request(
405
+ request.http_method, request.path, request.body, request.headers
406
+ )
407
+
408
+ status = response.status
409
+ if status >= 400
410
+ body = response.body.to_s
411
+ err = MicrosoftKiotaAbstractions::ApiError.new(
412
+ "The server returned an unexpected status code:#{status}" \
413
+ "#{body.empty? ? '' : " #{body}"}"
414
+ )
415
+ GhostCrawl.raise_translated(err)
416
+ end
417
+
418
+ response.body.to_s.b
419
+ rescue GhostCrawl::GhostCrawlError
420
+ raise
421
+ rescue StandardError => e
422
+ GhostCrawl.raise_translated(e)
423
+ end
424
+
425
+ # Inspects a decoded HTTP-200 response hash for a RESULT-channel failure (the
426
+ # target page could not be scraped) and raises {GhostCrawl::ScrapeError} when
427
+ # one is present. This is the reliable, highest-value error path: the body is
428
+ # always available here (unlike the dropped problem+json body on non-2xx).
429
+ #
430
+ # A failure is signalled by any of:
431
+ # * a +result_error+ Hash carrying a +code+
432
+ # * +ok+ explicitly +false+ (always a failure, even with no code)
433
+ # * a top-level +code+ that is a known RESULT-channel code
434
+ #
435
+ # A genuinely OK hash (ok: true, no +ok+ key, or no error code) is returned
436
+ # untouched and never raises.
437
+ #
438
+ # @param hash [Hash] the decoded response
439
+ # @return [Hash] the same hash, when it is not a failure
440
+ # @api private
441
+ def self.raise_on_result_error!(hash)
442
+ return hash unless hash.is_a?(Hash)
443
+
444
+ # Descend into a `results` envelope (scrape/extract wrap per-URL results) —
445
+ # the target failure lives on the INNER result, not the envelope top level.
446
+ inner = hash["results"]
447
+ if inner.is_a?(Array)
448
+ inner.each { |item| raise_on_result_error!(item) }
449
+ return hash
450
+ end
451
+
452
+ result_error = hash["result_error"]
453
+ result_error = nil unless result_error.is_a?(Hash)
454
+ top_code = hash["code"]
455
+ ok_false = hash["ok"] == false
456
+
457
+ # Pull the code: result_error wins, then a top-level RESULT-channel code.
458
+ code = nil
459
+ code = result_error["code"] if result_error
460
+ code ||= top_code if GhostCrawl::ErrorCodes.result_channel?(top_code)
461
+
462
+ # The flat markdown-build envelope reports a target failure ONLY via
463
+ # status="failed" (no ok/result_error) — don't count it as a success.
464
+ status_failed = hash["status"] == "failed"
465
+ code ||= top_code if status_failed && top_code.is_a?(String)
466
+
467
+ # Only raise when there is a concrete result-channel failure signal.
468
+ return hash unless code || ok_false || status_failed
469
+
470
+ # ok: false with no usable code -> treat as empty/unusable content.
471
+ code ||= GhostCrawl::ErrorCodes::EMPTY_CONTENT
472
+
473
+ retryable =
474
+ if result_error && result_error.key?("retryable")
475
+ result_error["retryable"]
476
+ else
477
+ GhostCrawl::ErrorCodes::RETRYABLE.fetch(code, false)
478
+ end
479
+
480
+ target_status = nil
481
+ target_status = result_error["target_status"] if result_error
482
+ target_status ||= hash["target_status"] # flat markdown-envelope path
483
+ reason = result_error && result_error["reason"]
484
+
485
+ msg = "scrape failed (#{code})"
486
+ msg += ": #{reason}" if reason && !reason.to_s.empty?
487
+ msg += " (target HTTP #{target_status})" if target_status
488
+
489
+ raise GhostCrawl::ScrapeError.new(
490
+ msg,
491
+ status_code: 200,
492
+ body: nil,
493
+ code: code,
494
+ retryable: retryable,
495
+ request_id: hash["request_id"],
496
+ target_status: target_status
497
+ )
498
+ end
499
+ end
500
+
501
+ # ---------------------------------------------------------------------------
502
+ # Sub-clients — each delegates to the generated v1 request builders
503
+ # ---------------------------------------------------------------------------
504
+
505
+ # Manage crawl runs — /v1/crawl-runs.
506
+ class CrawlRunsClient
507
+ # Terminal run states — a crawl run in any of these will never change again,
508
+ # so a wait can stop. Both spellings of "cancelled" are accepted.
509
+ TERMINAL_STATUSES = %w[completed failed cancelled canceled].freeze
510
+
511
+ # Per-request server-block window (seconds) for the long-poll wait. Each
512
+ # +GET ...?wait=true&timeout_s=WAIT_WINDOW_S+ makes the SERVER block for up
513
+ # to this long, so the client never sleeps between checks. Kept well under
514
+ # the client read timeout ({Client::DEFAULT_TIMEOUT}) so a single blocking
515
+ # request can never trip the transport timeout.
516
+ WAIT_WINDOW_S = 30
517
+
518
+ def initialize(v1, adapter = nil)
519
+ @v1 = v1
520
+ @adapter = adapter
521
+ end
522
+
523
+ # Start a new crawl run from a seed URL.
524
+ # Delegates to POST /v1/crawl-runs via the generated CrawlRunsRequestBuilder.
525
+ # The endpoint is a tagged union: a start request requires +action: "start"+
526
+ # and a +seed_urls+ array (not a bare +url+).
527
+ #
528
+ # Pass +wait: true+ to block until the run reaches a terminal state
529
+ # (completed | failed | cancelled) or +timeout+ seconds elapse. This sends
530
+ # +wait_until: "completed"+ so the SERVER blocks — no client-side poll loop,
531
+ # no +sleep+. When the run is still running at +timeout+ the current
532
+ # (non-terminal) record is returned; call {#wait_for_completion} again to
533
+ # keep waiting.
534
+ #
535
+ # @param url [String] seed URL
536
+ # @param max_depth [Integer] maximum crawl depth (default 2)
537
+ # @param max_pages [Integer] maximum pages (default 100)
538
+ # @param wait [Boolean] block until the run is terminal (default false)
539
+ # @param timeout [Integer] total seconds to wait when +wait: true+ (default 300)
540
+ # @param raise_on_result_error [Boolean] raise {GhostCrawl::ScrapeError} on a
541
+ # target-side (HTTP-200) failure instead of returning the raw hash (default true).
542
+ # Ignored on the +wait: true+ path, which always returns the terminal run so
543
+ # the caller can inspect a +failed+ status.
544
+ # @return [Hash] crawl run record with +run_id+ and +status+ (results present
545
+ # when it completed while waiting)
546
+ def start(url:, max_depth: 2, max_pages: 100, wait: false, timeout: 300,
547
+ raise_on_result_error: true, **opts)
548
+ data = { "action" => "start", "seed_urls" => [url],
549
+ "max_depth" => max_depth, "max_pages" => max_pages }
550
+ .merge(opts.transform_keys(&:to_s))
551
+
552
+ unless wait
553
+ hash = ResponseHelper.to_hash(@v1.crawl_runs.post(AdditionalDataBody.new(data)))
554
+ return raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
555
+ end
556
+
557
+ # Start-and-wait: ask the server to block until terminal. Bound the first
558
+ # server block to a safe window, then long-poll the rest via GET so a low
559
+ # per-request read timeout can never abort the whole wait.
560
+ window = [timeout, WAIT_WINDOW_S].min
561
+ data["wait_until"] = "completed"
562
+ data["timeout_s"] = window
563
+ hash = ResponseHelper.to_hash(@v1.crawl_runs.post(AdditionalDataBody.new(data)))
564
+ return hash if terminal?(hash)
565
+
566
+ run_id = hash["run_id"]
567
+ remaining = timeout - window
568
+ return hash if run_id.nil? || run_id.to_s.empty? || remaining <= 0
569
+ wait_for_completion(run_id, timeout: remaining)
570
+ end
571
+
572
+ # Block until an existing crawl run reaches a terminal state, or +timeout+
573
+ # seconds elapse. Event-driven: each iteration issues a SERVER-blocking
574
+ # +GET /v1/crawl-runs/{run_id}?wait=true&timeout_s=N+ that returns the moment
575
+ # the run goes terminal (or after its own window). There is no client +sleep+
576
+ # — the wait cost lives on the server, and successive blocking windows are
577
+ # chained until the caller's +timeout+ deadline.
578
+ #
579
+ # On timeout the current non-terminal run is returned (never raises for a
580
+ # slow run); a terminal run is returned as soon as it is observed.
581
+ #
582
+ # @param run_id [String] the run to wait on
583
+ # @param timeout [Integer] total seconds to wait (default 300)
584
+ # @return [Hash] the run record (terminal when it finished in time)
585
+ def wait_for_completion(run_id, timeout: 300)
586
+ if run_id.nil? || run_id.to_s.empty?
587
+ raise ArgumentError, "run_id is required"
588
+ end
589
+
590
+ deadline = monotonic + timeout
591
+ loop do
592
+ remaining = deadline - monotonic
593
+ # Deadline reached — one final non-blocking read of the current state.
594
+ return get(run_id) if remaining <= 0
595
+
596
+ window = [remaining, WAIT_WINDOW_S].min
597
+ run = fetch_waiting(run_id, window)
598
+ return run if terminal?(run)
599
+ # The server just blocked for ~window seconds; loop straight into the
600
+ # next blocking window. No client-side delay.
601
+ end
602
+ end
603
+
604
+ # List crawl runs.
605
+ # Delegates to GET /v1/crawl-runs via the generated CrawlRunsRequestBuilder.
606
+ def list
607
+ ResponseHelper.to_hash(@v1.crawl_runs.get)
608
+ end
609
+
610
+ # Get a single crawl run by ID.
611
+ # Delegates to GET /v1/crawl-runs/{run_id} via the generated builder.
612
+ def get(run_id)
613
+ ResponseHelper.to_hash(@v1.crawl_runs.by_run_id(run_id).get)
614
+ end
615
+
616
+ # Cancel a running crawl run.
617
+ # Delegates to POST /v1/crawl-runs/{run_id}/cancel via the generated builder.
618
+ def cancel(run_id)
619
+ ResponseHelper.to_hash(@v1.crawl_runs.by_run_id(run_id).cancel.post)
620
+ end
621
+
622
+ private
623
+
624
+ # True when +run+ is a Hash carrying a terminal +status+.
625
+ def terminal?(run)
626
+ run.is_a?(Hash) && TERMINAL_STATUSES.include?(run["status"].to_s)
627
+ end
628
+
629
+ # Issues one SERVER-blocking GET on the run with +?wait=true&timeout_s=N+.
630
+ #
631
+ # The generated item builder's +to_get_request_information+ does not wire
632
+ # query parameters (its URL template has no query placeholder), so we take
633
+ # the request it builds, extend the template with +{?wait,timeout_s}+, and
634
+ # set the params before sending through the raw adapter — reusing the
635
+ # Binary (raw-JSON) response factory the generated builders already use.
636
+ def fetch_waiting(run_id, timeout_s)
637
+ request_info = @v1.crawl_runs.by_run_id(run_id).to_get_request_information(nil)
638
+ request_info.url_template = "{+baseurl}/v1/crawl-runs/{run_id}{?wait,timeout_s}"
639
+ request_info.query_parameters["wait"] = "true"
640
+ request_info.query_parameters["timeout_s"] = timeout_s.to_i.to_s
641
+ ResponseHelper.to_hash(@adapter.send_async(request_info, GhostCrawl::V1::Binary, {}))
642
+ end
643
+
644
+ # Monotonic clock (seconds) — immune to wall-clock adjustments during a wait.
645
+ def monotonic
646
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
647
+ end
648
+ end
649
+
650
+ # Manage browser sessions — /v1/sessions.
651
+ class SessionsClient
652
+ def initialize(v1)
653
+ @v1 = v1
654
+ end
655
+
656
+ # List all active sessions.
657
+ # Delegates to GET /v1/sessions via the generated SessionsRequestBuilder.
658
+ def list
659
+ ResponseHelper.to_hash(@v1.sessions.get)
660
+ end
661
+
662
+ # Create a new browser session.
663
+ # Delegates to POST /v1/sessions/create via the generated builder.
664
+ # @param profile_name [String] identity profile to use
665
+ def create(profile_name:, **opts)
666
+ data = { "profile" => profile_name }.merge(opts.transform_keys(&:to_s))
667
+ ResponseHelper.to_hash(@v1.sessions.create.post(AdditionalDataBody.new(data)))
668
+ end
669
+
670
+ # Extend a session's TTL.
671
+ # Delegates to POST /v1/sessions/{id}/extend via the generated builder.
672
+ def extend(session_id, duration_seconds: 300)
673
+ ResponseHelper.to_hash(@v1.sessions.by_profile_id(session_id).extend.post(
674
+ AdditionalDataBody.new({ "ttl_seconds" => duration_seconds })
675
+ ))
676
+ end
677
+
678
+ # Release a session back to the pool.
679
+ # Delegates to POST /v1/sessions/{id}/release via the generated builder.
680
+ def release(session_id)
681
+ ResponseHelper.to_hash(@v1.sessions.by_profile_id(session_id).release.post)
682
+ end
683
+ end
684
+
685
+ # Manage identity profiles — /v1/profiles.
686
+ class ProfilesClient
687
+ def initialize(v1, adapter = nil)
688
+ @v1 = v1
689
+ @adapter = adapter
690
+ end
691
+
692
+ # List all profiles.
693
+ # Delegates to GET /v1/profiles via the generated ProfilesRequestBuilder.
694
+ def list
695
+ ResponseHelper.to_hash(@v1.profiles.get)
696
+ end
697
+
698
+ # Get a profile by name.
699
+ # Delegates to GET /v1/profiles/{name} via the generated builder.
700
+ def get(name)
701
+ ResponseHelper.to_hash(@v1.profiles.by_name(name).get)
702
+ end
703
+
704
+ # Create a new profile.
705
+ # Delegates to POST /v1/profiles via the generated ProfilesRequestBuilder.
706
+ def create(name:, **config)
707
+ body = AdditionalDataBody.new({ "name" => name }.merge(config.transform_keys(&:to_s)))
708
+ ResponseHelper.to_hash(@v1.profiles.post(body))
709
+ end
710
+
711
+ # Update a profile.
712
+ # Delegates to PUT /v1/profiles/{name} via the generated builder.
713
+ def update(name, **config)
714
+ ResponseHelper.to_hash(@v1.profiles.by_name(name).put(AdditionalDataBody.new(config.transform_keys(&:to_s))))
715
+ end
716
+
717
+ # Delete a profile.
718
+ # Delegates to DELETE /v1/profiles/{name} via the generated builder.
719
+ # The endpoint answers 204 No Content; routing through {ResponseHelper.void_request!}
720
+ # avoids the Kiota JSON parser choking on the empty body.
721
+ def delete(name)
722
+ ResponseHelper.void_request!(@adapter, @v1.profiles.by_name(name).to_delete_request_information(nil))
723
+ end
724
+ end
725
+
726
+ # Manage webhooks — /v1/webhooks.
727
+ class WebhooksClient
728
+ def initialize(v1, adapter = nil)
729
+ @v1 = v1
730
+ @adapter = adapter
731
+ end
732
+
733
+ # List all webhooks.
734
+ # Delegates to GET /v1/webhooks via the generated WebhooksRequestBuilder.
735
+ def list
736
+ ResponseHelper.to_hash(@v1.webhooks.get)
737
+ end
738
+
739
+ # Get a webhook by ID.
740
+ # Delegates to GET /v1/webhooks/{id} via the generated builder.
741
+ def get(webhook_id)
742
+ ResponseHelper.to_hash(@v1.webhooks.by_webhook_id(webhook_id).get)
743
+ end
744
+
745
+ # Register a new webhook endpoint.
746
+ # Delegates to POST /v1/webhooks via the generated WebhooksRequestBuilder.
747
+ def create(url:, event_types: nil, events: nil, **opts)
748
+ data = { "url" => url }.merge(opts.transform_keys(&:to_s))
749
+ # API field is "event_types"; "events" kept as a back-compat alias.
750
+ et = event_types.nil? ? events : event_types
751
+ data["event_types"] = et unless et.nil?
752
+ ResponseHelper.to_hash(@v1.webhooks.post(AdditionalDataBody.new(data)))
753
+ end
754
+
755
+ # Delete a webhook.
756
+ # Delegates to DELETE /v1/webhooks/{id}. The generated builder passes a +nil+
757
+ # response factory (204 void), which the Kiota adapter rejects with a bare
758
+ # StandardError BEFORE sending the request — so we build the request info and
759
+ # run it through {ResponseHelper.void_request!} instead, which actually fires
760
+ # the DELETE and returns +{}+ on success.
761
+ def delete(webhook_id)
762
+ ResponseHelper.void_request!(@adapter, @v1.webhooks.by_webhook_id(webhook_id).to_delete_request_information(nil))
763
+ end
764
+
765
+ # Rotate the signing secret for a webhook.
766
+ # Delegates to POST /v1/webhooks/{id}/rotate-secret via the generated builder.
767
+ def rotate_secret(webhook_id)
768
+ ResponseHelper.to_hash(@v1.webhooks.by_webhook_id(webhook_id).rotate_secret.post)
769
+ end
770
+ end
771
+
772
+ # Manage schedules — /v1/schedules.
773
+ class SchedulesClient
774
+ def initialize(v1, adapter = nil)
775
+ @v1 = v1
776
+ @adapter = adapter
777
+ end
778
+
779
+ # List all schedules.
780
+ # Delegates to GET /v1/schedules via the generated SchedulesRequestBuilder.
781
+ def list
782
+ ResponseHelper.to_hash(@v1.schedules.get)
783
+ end
784
+
785
+ # Get a schedule by ID.
786
+ # Delegates to GET /v1/schedules/{id} via the generated builder.
787
+ def get(schedule_id)
788
+ ResponseHelper.to_hash(@v1.schedules.by_schedule_id(schedule_id).get)
789
+ end
790
+
791
+ # Create a new schedule.
792
+ # Delegates to POST /v1/schedules via the generated SchedulesRequestBuilder.
793
+ #
794
+ # The API's ScheduleCreateRequest requires +name+, +job_type+
795
+ # ("scrape" | "crawl" | "change_monitor"), +cron_expr+, and +job_params+
796
+ # (the full scrape/crawl request body). It rejects any other top-level field
797
+ # with a 422 — so a legacy +task+ key must be TRANSLATED, never forwarded.
798
+ #
799
+ # @param cron [String] cron expression (sent as +cron_expr+)
800
+ # @param name [String] schedule name (required by the API)
801
+ # @param job_type [String, nil] "scrape" | "crawl" | "change_monitor"
802
+ # @param job_params [Hash, nil] full job request body (e.g. { url: ... })
803
+ # @param task [Hash, nil] DEPRECATED legacy shape { "action" => ..., ...rest };
804
+ # when given (and job_type/job_params are absent) it is split into
805
+ # +job_type+ (from +task["action"]+) and +job_params+ (the remaining keys).
806
+ def create(cron:, name: nil, job_type: nil, job_params: nil, task: nil, **opts)
807
+ # Back-compat: derive job_type/job_params from a legacy `task` hash.
808
+ if task.is_a?(Hash)
809
+ t = task.transform_keys(&:to_s)
810
+ job_type ||= t["action"] || t["job_type"] || t["type"]
811
+ job_params ||= t.reject { |k, _| %w[action job_type type].include?(k) }
812
+ end
813
+
814
+ data = { "cron_expr" => cron }
815
+ data["name"] = name unless name.nil?
816
+ data["job_type"] = job_type unless job_type.nil?
817
+ data["job_params"] = job_params unless job_params.nil?
818
+ # opts may override/supply name/job_type/job_params/notify_webhook/monitor_mode.
819
+ # Never forward a raw `task` field — the API 422s on it.
820
+ data.merge!(opts.transform_keys(&:to_s).reject { |k, _| k == "task" })
821
+
822
+ ResponseHelper.to_hash(@v1.schedules.post(AdditionalDataBody.new(data)))
823
+ end
824
+
825
+ # Delete a schedule.
826
+ # Delegates to DELETE /v1/schedules/{id}. The generated builder returns a Fiber
827
+ # that must be resumed to actually send the request (the old +; {}+ discarded it,
828
+ # so the DELETE never fired). {ResponseHelper.void_request!} runs it and tolerates
829
+ # the empty 204 body.
830
+ def delete(schedule_id)
831
+ ResponseHelper.void_request!(@adapter, @v1.schedules.by_schedule_id(schedule_id).to_delete_request_information(nil))
832
+ end
833
+ end
834
+
835
+ # Manage datasets — /v1/datasets.
836
+ class DatasetsClient
837
+ def initialize(v1)
838
+ @v1 = v1
839
+ end
840
+
841
+ # List all datasets.
842
+ # Delegates to GET /v1/datasets via the generated DatasetsRequestBuilder.
843
+ def list
844
+ ResponseHelper.to_hash(@v1.datasets.get)
845
+ end
846
+
847
+ # Get a dataset by name.
848
+ # Delegates to GET /v1/datasets/{name} via the generated builder.
849
+ def get(name)
850
+ ResponseHelper.to_hash(@v1.datasets.by_name(name).get)
851
+ end
852
+
853
+ # Create a new dataset.
854
+ # Delegates to POST /v1/datasets via the generated DatasetsRequestBuilder.
855
+ def create(name:, **opts)
856
+ body = AdditionalDataBody.new({ "name" => name }.merge(opts.transform_keys(&:to_s)))
857
+ ResponseHelper.to_hash(@v1.datasets.post(body))
858
+ end
859
+
860
+ # Delete a dataset.
861
+ # Delegates to DELETE /v1/datasets/{name} via the generated builder.
862
+ def delete(name)
863
+ ResponseHelper.to_hash(@v1.datasets.by_name(name).delete)
864
+ end
865
+
866
+ # Get rows from a dataset.
867
+ # Delegates to GET /v1/datasets/{name}/rows via the generated builder.
868
+ def rows(name)
869
+ ResponseHelper.to_hash(@v1.datasets.by_name(name).rows.get)
870
+ end
871
+
872
+ # Append rows to a dataset.
873
+ # Delegates to POST /v1/datasets/{name}/rows/append via the generated builder.
874
+ def append(name, rows)
875
+ body = AdditionalDataBody.new({ "rows" => rows })
876
+ ResponseHelper.to_hash(@v1.datasets.by_name(name).rows.append.post(body))
877
+ end
878
+ end
879
+
880
+ # Manage session recordings — /v1/recordings.
881
+ class RecordingsClient
882
+ def initialize(v1, adapter = nil)
883
+ @v1 = v1
884
+ @adapter = adapter
885
+ end
886
+
887
+ # List all recordings.
888
+ # Delegates to GET /v1/recordings via the generated RecordingsRequestBuilder.
889
+ def list
890
+ ResponseHelper.to_hash(@v1.recordings.get)
891
+ end
892
+
893
+ # Get a recording by ID.
894
+ # Delegates to GET /v1/recordings/{id} via the generated builder.
895
+ # (The generated accessor is +by_recording_id+, single underscore.)
896
+ def get(recording_id)
897
+ ResponseHelper.to_hash(@v1.recordings.by_recording_id(recording_id).get)
898
+ end
899
+
900
+ # Delete a recording.
901
+ # Delegates to DELETE /v1/recordings/{id}. Same void-DELETE defect as webhooks
902
+ # (generated builder passes a +nil+ factory → adapter raises before sending), so
903
+ # we route through {ResponseHelper.void_request!}.
904
+ def delete(recording_id)
905
+ ResponseHelper.void_request!(@adapter, @v1.recordings.by_recording_id(recording_id).to_delete_request_information(nil))
906
+ end
907
+ end
908
+
909
+ # Key-value store — /v1/kv.
910
+ class KVClient
911
+ def initialize(v1)
912
+ @v1 = v1
913
+ end
914
+
915
+ # Get a value by key.
916
+ # Delegates to GET /v1/kv/{key} via the generated KvRequestBuilder.
917
+ def get(key)
918
+ ResponseHelper.to_hash(@v1.kv.by_key(key).get)
919
+ end
920
+
921
+ # Set a key-value pair.
922
+ # Delegates to PUT /v1/kv/{key} via the generated builder.
923
+ def set(key, value)
924
+ body = AdditionalDataBody.new({ "value" => value })
925
+ ResponseHelper.to_hash(@v1.kv.by_key(key).put(body))
926
+ end
927
+
928
+ # Delete a key.
929
+ # Delegates to DELETE /v1/kv/{key} via the generated builder.
930
+ def delete(key)
931
+ ResponseHelper.to_hash(@v1.kv.by_key(key).delete)
932
+ end
933
+ end
934
+
935
+ # ---------------------------------------------------------------------------
936
+ # Main facade — Client
937
+ # ---------------------------------------------------------------------------
938
+
939
+ # GhostCrawl idiomatic API client.
940
+ #
941
+ # Delegates all HTTP transport, URL routing, serialization, and auth to the
942
+ # Kiota-generated canonical core (_generated/). This facade is the shipped API.
943
+ #
944
+ # @example
945
+ # require "ghostcrawl"
946
+ # client = GhostCrawl::Client.new(token: "gck_live_YOUR_KEY")
947
+ # result = client.scrape(url: "https://example.com")
948
+ class Client
949
+ # Default read timeout (seconds). Browser-rendered scrapes/crawls are slow,
950
+ # and the underlying net/http default (60s) is too short for them.
951
+ DEFAULT_TIMEOUT = 300
952
+
953
+ # @param token [String, nil] API key. Falls back to +GHOSTCRAWL_API_KEY+ env var.
954
+ # @param base_url [String, nil] Override API base URL. Falls back to +GHOSTCRAWL_BASE_URL+ env var.
955
+ # @param timeout [Integer, nil] Per-request read timeout in seconds. Falls back
956
+ # to +GHOSTCRAWL_TIMEOUT+ env var, then {DEFAULT_TIMEOUT}.
957
+ def initialize(token: nil, base_url: nil, timeout: nil)
958
+ resolved_token = token || ENV.fetch("GHOSTCRAWL_API_KEY", nil)
959
+ if resolved_token.nil? || resolved_token.empty?
960
+ raise ArgumentError,
961
+ "token is required — pass token: or set GHOSTCRAWL_API_KEY. " \
962
+ "Get your key at https://ghostcrawl.io"
963
+ end
964
+
965
+ resolved_base = (base_url ||
966
+ ENV.fetch("GHOSTCRAWL_BASE_URL", nil) ||
967
+ DEFAULT_BASE_URL).gsub(%r{/+$}, "")
968
+
969
+ resolved_timeout = (timeout ||
970
+ ENV.fetch("GHOSTCRAWL_TIMEOUT", nil) ||
971
+ DEFAULT_TIMEOUT).to_i
972
+
973
+ # Build the Kiota core via BaseBearerTokenAuthenticationProvider + FaradayRequestAdapter.
974
+ # All HTTP, auth, serialization, and URL routing delegate to the generated core.
975
+ auth_provider = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider.new(
976
+ StaticTokenProvider.new(resolved_token)
977
+ )
978
+ adapter = MicrosoftKiotaFaraday::FaradayRequestAdapter.new(auth_provider)
979
+ adapter.set_base_url(resolved_base)
980
+
981
+ # The default Faraday connection sets no timeout, so it inherits net/http's
982
+ # 60s read timeout — too short for browser-rendered work. Raise it.
983
+ if adapter.client.respond_to?(:options) && resolved_timeout.positive?
984
+ adapter.client.options.timeout = resolved_timeout
985
+ adapter.client.options.open_timeout = 30
986
+ end
987
+
988
+ @core = GhostCrawl::GhostCrawlClient.new(adapter)
989
+ @v1 = @core.v1
990
+ # Kept for the raw-adapter dispatch paths (void DELETEs, binary PDF/
991
+ # screenshot, the long-poll crawl-run wait, and the ad-hoc JSON routes
992
+ # the generated core has no builder for).
993
+ @adapter = adapter
994
+
995
+ # CrawlRunsClient needs the raw adapter for the event-driven long-poll wait
996
+ # (the generated item builder can't attach ?wait/&timeout_s query params).
997
+ @crawl_runs = CrawlRunsClient.new(@v1, @adapter)
998
+ @sessions = SessionsClient.new(@v1)
999
+ # Sub-clients with void-DELETE endpoints (204 No Content) also need the raw
1000
+ # adapter so ResponseHelper.void_request! can work around the broken generated
1001
+ # delete builders. See ResponseHelper.void_request! for the two Kiota defects.
1002
+ @profiles = ProfilesClient.new(@v1, @adapter)
1003
+ @webhooks = WebhooksClient.new(@v1, @adapter)
1004
+ @schedules = SchedulesClient.new(@v1, @adapter)
1005
+ @datasets = DatasetsClient.new(@v1)
1006
+ @recordings = RecordingsClient.new(@v1, @adapter)
1007
+ @kv = KVClient.new(@v1)
1008
+ end
1009
+
1010
+ # @return [CrawlRunsClient]
1011
+ attr_reader :crawl_runs
1012
+ # @return [SessionsClient]
1013
+ attr_reader :sessions
1014
+ # @return [ProfilesClient]
1015
+ attr_reader :profiles
1016
+ # @return [WebhooksClient]
1017
+ attr_reader :webhooks
1018
+ # @return [SchedulesClient]
1019
+ attr_reader :schedules
1020
+ # @return [DatasetsClient]
1021
+ attr_reader :datasets
1022
+ # @return [RecordingsClient]
1023
+ attr_reader :recordings
1024
+ # @return [KVClient]
1025
+ attr_reader :kv
1026
+
1027
+ # ---------------------------------------------------------------------------
1028
+ # Top-level facade methods — delegate to generated builders
1029
+ # ---------------------------------------------------------------------------
1030
+
1031
+ # Scrape a single URL and return the rendered content.
1032
+ # Delegates to POST /v1/scrape via the generated ScrapeRequestBuilder.
1033
+ # @param url [String] target URL
1034
+ # @param format [String] output format: "markdown" (default), "html", "text"
1035
+ # @param engine [String] browser engine: "auto" (default), "chrome", "firefox", "webkit"
1036
+ # @param javascript [Boolean] enable JavaScript rendering (default true)
1037
+ # @param extract_schema [Hash, nil] JSON Schema for structured extraction
1038
+ # @param raise_on_result_error [Boolean] raise {GhostCrawl::ScrapeError} on a
1039
+ # target-side (HTTP-200) failure instead of returning the raw hash (default true)
1040
+ # @return [Hash] response with +content+, +markdown+, +status+, and other fields
1041
+ def scrape(url:, format: "markdown", engine: "auto", javascript: true, extract_schema: nil,
1042
+ raise_on_result_error: true, **opts)
1043
+ # Use AdditionalDataBody to send only the fields we specify — the generated
1044
+ # ScrapeRequest model would serialize typed defaults (nulls + empty enums) that
1045
+ # cause 422 validation errors on the server.
1046
+ data = { "url" => url, "format" => format, "engine" => engine,
1047
+ "javascript_enabled" => javascript }.merge(opts.transform_keys(&:to_s))
1048
+ data["extract_schema"] = extract_schema unless extract_schema.nil?
1049
+ hash = ResponseHelper.to_hash(@v1.scrape.post(AdditionalDataBody.new(data)))
1050
+ normalize_scrape_content(hash)
1051
+ raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
1052
+ end
1053
+
1054
+ # Search the web and return results.
1055
+ # Delegates to POST /v1/search via the generated SearchRequestBuilder.
1056
+ #
1057
+ # /v1/search requires your own search-backend API key (BYO; GhostCrawl
1058
+ # charges no markup). Pass it as +provider_key+ — it is sent as the
1059
+ # +X-Provider-Authorization: Bearer <provider_key>+ header the backend
1060
+ # requires. Without it the API replies 401 search_backend_key_missing.
1061
+ # @param query [String] search query
1062
+ # @param engine [String] search engine: "google" (default), "bing", "duckduckgo"
1063
+ # @param limit [Integer] maximum results (default 10)
1064
+ # @param provider_key [String, nil] BYO search-backend key (sent as X-Provider-Authorization)
1065
+ # @return [Hash] response with +results+ list
1066
+ def search(query:, engine: "google", limit: 10, provider_key: nil, **opts)
1067
+ data = { "query" => query, "engine" => engine,
1068
+ "limit" => limit }.merge(opts.transform_keys(&:to_s))
1069
+ config = nil
1070
+ unless provider_key.nil?
1071
+ config = MicrosoftKiotaAbstractions::RequestConfiguration.new
1072
+ headers = MicrosoftKiotaAbstractions::RequestHeaders.new
1073
+ headers.add("X-Provider-Authorization", "Bearer #{provider_key}")
1074
+ config.headers = headers
1075
+ end
1076
+ ResponseHelper.to_hash(@v1.search.post(AdditionalDataBody.new(data), config))
1077
+ end
1078
+
1079
+ # Extract structured data from a URL using a JSON Schema.
1080
+ # Delegates to POST /v1/extract via the generated ExtractRequestBuilder.
1081
+ # @param url [String] target URL
1082
+ # @param schema [Hash] JSON Schema describing the shape to extract
1083
+ # @param raise_on_result_error [Boolean] raise {GhostCrawl::ScrapeError} on a
1084
+ # target-side (HTTP-200) failure instead of returning the raw hash (default true)
1085
+ # @return [Hash] extracted data
1086
+ def extract(url:, schema:, raise_on_result_error: true, **opts)
1087
+ data = { "url" => url, "schema" => schema }.merge(opts.transform_keys(&:to_s))
1088
+ hash = ResponseHelper.to_hash(@v1.extract.post(AdditionalDataBody.new(data)))
1089
+ raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
1090
+ end
1091
+
1092
+ # Start a deep crawl from a seed URL.
1093
+ # Delegates to POST /v1/crawl/deep via the generated CrawlDeepRequestBuilder.
1094
+ #
1095
+ # Pass +wait: true+ to block until the run is terminal. The deep-crawl start
1096
+ # returns a +run_id+, then the wait is handled by the same event-driven
1097
+ # server-blocking long-poll as {CrawlRunsClient#wait_for_completion} — no
1098
+ # client-side +sleep+ loop.
1099
+ #
1100
+ # @param url [String] seed URL
1101
+ # @param max_depth [Integer] maximum crawl depth (default 2)
1102
+ # @param max_pages [Integer] maximum pages (default 100)
1103
+ # @param wait [Boolean] block until the run is terminal (default false)
1104
+ # @param timeout [Integer] total seconds to wait when +wait: true+ (default 300)
1105
+ # @param raise_on_result_error [Boolean] raise {GhostCrawl::ScrapeError} on a
1106
+ # target-side (HTTP-200) failure instead of returning the raw hash (default true).
1107
+ # Ignored on the +wait: true+ path, which returns the terminal run.
1108
+ # @return [Hash] crawl run record (terminal when +wait: true+ and it finished in time)
1109
+ def crawl(url:, max_depth: 2, max_pages: 100, wait: false, timeout: 300,
1110
+ raise_on_result_error: true, **opts)
1111
+ data = { "seed_urls" => [url], "max_depth" => max_depth,
1112
+ "max_urls" => max_pages }.merge(opts.transform_keys(&:to_s))
1113
+ hash = ResponseHelper.to_hash(@v1.crawl.deep.post(AdditionalDataBody.new(data)))
1114
+
1115
+ unless wait
1116
+ return raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
1117
+ end
1118
+
1119
+ run_id = hash["run_id"]
1120
+ return hash if run_id.nil? || run_id.to_s.empty?
1121
+ @crawl_runs.wait_for_completion(run_id, timeout: timeout)
1122
+ end
1123
+
1124
+ # Map all URLs reachable from a seed URL.
1125
+ # Delegates to POST /v1/map via the generated MapRequestBuilder.
1126
+ # @param url [String] seed URL
1127
+ # @return [Hash] response with +urls+ list
1128
+ def map(url:, **opts)
1129
+ data = { "url" => url }.merge(opts.transform_keys(&:to_s))
1130
+ ResponseHelper.to_hash(@v1.map.post(AdditionalDataBody.new(data)))
1131
+ end
1132
+
1133
+ # Render a URL to a PDF document and return the raw +application/pdf+ bytes.
1134
+ # Delegates to POST /v1/pdf.
1135
+ #
1136
+ # +/v1/pdf+ responds with binary, not a JSON envelope, so this returns the
1137
+ # bytes verbatim (an ASCII-8BIT String) — write them straight to a file:
1138
+ #
1139
+ # data = client.pdf(url: "https://example.com")
1140
+ # File.binwrite("page.pdf", data)
1141
+ #
1142
+ # PDF output is Chrome-only; a request that resolves to a Firefox or WebKit
1143
+ # identity is rejected with 400 +pdf_engine_unsupported+ (a {GhostCrawlError}).
1144
+ #
1145
+ # +/v1/pdf+ has no generated request builder, so this hand-builds a
1146
+ # {MicrosoftKiotaAbstractions::RequestInformation} and routes it through the
1147
+ # SAME adapter (base URL + bearer auth + transport) as the modeled calls.
1148
+ #
1149
+ # @param url [String] target URL to render
1150
+ # @param paper_format [String] page size: "a4" (default), "letter", "legal", "tabloid"
1151
+ # @param landscape [Boolean] render in landscape orientation (default false)
1152
+ # @param engine [String] browser engine (PDF is Chrome-only; default "auto")
1153
+ # @return [String] the raw PDF bytes (ASCII-8BIT)
1154
+ def pdf(url:, paper_format: "a4", landscape: false, engine: "auto", **opts)
1155
+ data = { "url" => url, "paper_format" => paper_format,
1156
+ "landscape" => landscape, "engine" => engine }.merge(opts.transform_keys(&:to_s))
1157
+ request_info = MicrosoftKiotaAbstractions::RequestInformation.new
1158
+ request_info.http_method = :POST
1159
+ request_info.url_template = "{+baseurl}/v1/pdf"
1160
+ request_info.headers.try_add("Accept", "application/pdf")
1161
+ request_info.set_stream_content(JSON.generate(data), "application/json")
1162
+ ResponseHelper.binary_request!(@adapter, request_info)
1163
+ end
1164
+
1165
+ # Capture a screenshot of a URL and return the raw +image/png+ bytes.
1166
+ # Delegates to POST /v1/screenshot.
1167
+ #
1168
+ # +/v1/screenshot+ responds with a binary image, not a JSON envelope, so this
1169
+ # returns the bytes verbatim (an ASCII-8BIT String) — write them straight to a
1170
+ # file:
1171
+ #
1172
+ # data = client.screenshot(url: "https://example.com")
1173
+ # File.binwrite("page.png", data)
1174
+ #
1175
+ # +/v1/screenshot+ has no generated request builder, so this hand-builds a
1176
+ # {MicrosoftKiotaAbstractions::RequestInformation} and routes it through the
1177
+ # SAME adapter (base URL + bearer auth + transport) as the modeled calls,
1178
+ # mirroring {#pdf} exactly (the raw-bytes dispatch idiom).
1179
+ #
1180
+ # @param url [String] target URL to capture
1181
+ # @param format [String] image format: "png" (default), "jpeg", "webp"
1182
+ # @param full_page [Boolean] capture the full scrollable page (default false)
1183
+ # @param screenshot_selector [String, nil] CSS selector to clip to (optional)
1184
+ # @param engine [String] browser engine (default "auto")
1185
+ # @return [String] the raw image bytes (ASCII-8BIT)
1186
+ def screenshot(url:, format: "png", full_page: false, screenshot_selector: nil,
1187
+ engine: "auto", **opts)
1188
+ data = { "url" => url, "format" => format, "full_page" => full_page,
1189
+ "engine" => engine }
1190
+ data["screenshot_selector"] = screenshot_selector unless screenshot_selector.nil?
1191
+ data.merge!(opts.transform_keys(&:to_s))
1192
+ request_info = MicrosoftKiotaAbstractions::RequestInformation.new
1193
+ request_info.http_method = :POST
1194
+ request_info.url_template = "{+baseurl}/v1/screenshot"
1195
+ request_info.headers.try_add("Accept", "image/png")
1196
+ request_info.set_stream_content(JSON.generate(data), "application/json")
1197
+ ResponseHelper.binary_request!(@adapter, request_info)
1198
+ end
1199
+
1200
+ # Render a URL and return the rendered-content JSON envelope as a Hash.
1201
+ # Delegates to POST /v1/content.
1202
+ #
1203
+ # +/v1/content+ responds with +application/json+ ({+content+, +url+, +status+,
1204
+ # +format+, +status_code+, +bytes+}); this returns the decoded Hash. It uses
1205
+ # the same hand-built {MicrosoftKiotaAbstractions::RequestInformation} +
1206
+ # shared adapter dispatch as {#pdf}, parsing the JSON body at the end.
1207
+ #
1208
+ # @param url [String] target URL to render
1209
+ # @param engine [String] browser engine (default "auto")
1210
+ # @return [Hash] the rendered-content envelope (top-level +content+ = HTML)
1211
+ def content(url:, engine: "auto", **opts)
1212
+ data = { "url" => url, "engine" => engine }.merge(opts.transform_keys(&:to_s))
1213
+ request_info = MicrosoftKiotaAbstractions::RequestInformation.new
1214
+ request_info.http_method = :POST
1215
+ request_info.url_template = "{+baseurl}/v1/content"
1216
+ request_info.headers.try_add("Accept", "application/json")
1217
+ request_info.set_stream_content(JSON.generate(data), "application/json")
1218
+ bytes = ResponseHelper.binary_request!(@adapter, request_info)
1219
+ parsed = JSON.parse(bytes)
1220
+ parsed.is_a?(Hash) ? parsed : { "content" => parsed }
1221
+ end
1222
+
1223
+ # Execute an agent task via POST /v1/agent.
1224
+ #
1225
+ # The agent capability is gated per account; when it is not enabled the API
1226
+ # replies +404 not_found+ — this returns that problem+json body as a Hash
1227
+ # (carrying +"detail"+) rather than raising, so callers can branch on
1228
+ # +result.key?("detail")+. The agent brings its own LLM (BYO) — this method
1229
+ # handles the client serialize→POST→parse plumbing against the real route.
1230
+ #
1231
+ # +/v1/agent+ has no generated request builder, so this hand-builds a
1232
+ # {MicrosoftKiotaAbstractions::RequestInformation} and routes it through the
1233
+ # SAME adapter (base URL + bearer auth + transport) as the modeled calls.
1234
+ #
1235
+ # @param url [String, nil] optional starting URL (folded into +task+ as +start_url+)
1236
+ # @param instruction [String, nil] natural-language task instruction
1237
+ # @param task [Hash, nil] explicit structured task object (overrides url/instruction)
1238
+ # @return [Hash] the agent result, or the capability-gated 404 problem+json body (has +"detail"+)
1239
+ def agent(url: nil, instruction: nil, task: nil, **opts)
1240
+ payload = task || { "instruction" => instruction, "start_url" => url }
1241
+ data = { "task" => payload }.merge(opts.transform_keys(&:to_s))
1242
+ request_info = MicrosoftKiotaAbstractions::RequestInformation.new
1243
+ request_info.http_method = :POST
1244
+ request_info.url_template = "{+baseurl}/v1/agent"
1245
+ request_info.headers.try_add("Accept", "application/json")
1246
+ request_info.set_stream_content(JSON.generate(data), "application/json")
1247
+ bytes = ResponseHelper.binary_request!(@adapter, request_info)
1248
+ parsed = JSON.parse(bytes)
1249
+ parsed.is_a?(Hash) ? parsed : { "detail" => parsed }
1250
+ rescue GhostCrawl::GhostCrawlError => e
1251
+ # Agent is account-gated: a 404 not_found is the documented "capability
1252
+ # disabled" answer, not a transport failure. Surface the problem+json body
1253
+ # as a Hash (carrying "detail") rather than raising. Any other status is a
1254
+ # real error and is re-raised.
1255
+ raise unless e.status_code == 404
1256
+ body = e.body.to_s
1257
+ brace = body.index("{")
1258
+ decoded =
1259
+ if brace
1260
+ begin
1261
+ JSON.parse(body[brace..])
1262
+ rescue StandardError
1263
+ nil
1264
+ end
1265
+ end
1266
+ decoded.is_a?(Hash) ? decoded : { "detail" => e.message, "code" => (e.code || "not_found") }
1267
+ end
1268
+
1269
+ # List the account's persisted session storage-states.
1270
+ # Delegates to GET /v1/storage-states.
1271
+ # @return [Hash] the storage-states envelope
1272
+ def storage_states
1273
+ json_request(:GET, "/v1/storage-states", nil)
1274
+ end
1275
+
1276
+ private
1277
+
1278
+ # Route an ad-hoc JSON request through the SAME adapter (base URL + bearer
1279
+ # auth + transport) as the modeled calls, for routes the generated core has
1280
+ # no builder for. Returns the decoded Hash. Mirrors {#content}'s dispatch.
1281
+ # @api private
1282
+ def json_request(method, path, body)
1283
+ request_info = MicrosoftKiotaAbstractions::RequestInformation.new
1284
+ request_info.http_method = method
1285
+ request_info.url_template = "{+baseurl}#{path}"
1286
+ request_info.headers.try_add("Accept", "application/json")
1287
+ if !body.nil? && method != :GET
1288
+ request_info.set_stream_content(JSON.generate(body), "application/json")
1289
+ end
1290
+ bytes = ResponseHelper.binary_request!(@adapter, request_info)
1291
+ parsed = (JSON.parse(bytes) if bytes && !bytes.empty?)
1292
+ parsed.is_a?(Hash) ? parsed : {}
1293
+ end
1294
+
1295
+ # Normalizes a +"content"+ key onto a decoded scrape response.
1296
+ #
1297
+ # The API has two scrape response shapes:
1298
+ # * the FLAT +format="markdown"+ shape, which carries the rendered page at
1299
+ # the top level under the format-specific key (+"markdown"+/+"html"+/
1300
+ # +"text"+) but omits +identity_id+; and
1301
+ # * the standard ENVELOPE ({ +status+, +results+, +routing_mode+,
1302
+ # +request_class+, +identity_id+ }), where the rendered page lives under
1303
+ # +results[0]+ and the top-level +identity_id+ is present.
1304
+ # The documented quickstart reads +result["content"]+, so this mirrors the
1305
+ # rendered page onto +"content"+ in place from WHICHEVER shape is present,
1306
+ # KEEPING the original keys intact (backward compatible).
1307
+ #
1308
+ # No-op unless +result+ is a Hash that does not already carry +"content"+.
1309
+ # The value chosen is: the field named by +result["format"]+ when that field
1310
+ # is a String, else the first String among +"markdown"+, +"html"+, +"text"+ —
1311
+ # looked up first at the top level (flat shape), then on +results[0]+
1312
+ # (envelope shape).
1313
+ #
1314
+ # @param result [Object] the decoded response (only mutated when a Hash)
1315
+ # @return [Object] the same +result+, unchanged reference
1316
+ # @api private
1317
+ def normalize_scrape_content(result)
1318
+ return result unless result.is_a?(Hash) && !result.key?("content")
1319
+
1320
+ fmt = result["format"]
1321
+ value = scrape_content_from(result, fmt)
1322
+
1323
+ # Envelope shape: the page is under results[0], not at the top level.
1324
+ unless value.is_a?(String)
1325
+ first = result["results"]
1326
+ first = first.first if first.is_a?(Array)
1327
+ value = scrape_content_from(first, fmt) if first.is_a?(Hash)
1328
+ end
1329
+
1330
+ result["content"] = value if value.is_a?(String)
1331
+ result
1332
+ end
1333
+
1334
+ # Picks the rendered-page String out of a scrape result Hash: the field named
1335
+ # by +fmt+ when present, else the first String among +"markdown"+, +"html"+,
1336
+ # +"text"+. Returns +nil+ when +src+ carries no such String.
1337
+ # @api private
1338
+ def scrape_content_from(src, fmt)
1339
+ return nil unless src.is_a?(Hash)
1340
+
1341
+ value = src[fmt] if fmt.is_a?(String)
1342
+ return value if value.is_a?(String)
1343
+
1344
+ %w[markdown html text].each do |key|
1345
+ candidate = src[key]
1346
+ return candidate if candidate.is_a?(String)
1347
+ end
1348
+ nil
1349
+ end
1350
+ end
1351
+ end