@reticlehq/server 3.9.0 → 3.10.1

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 (351) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +43 -2
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/init.mdx +2 -2
  322. package/docs/cli/license.mdx +9 -7
  323. package/docs/cli/restart.mdx +7 -5
  324. package/docs/cli/rollback.mdx +5 -5
  325. package/docs/cli/stop.mdx +3 -3
  326. package/docs/cli/update.mdx +2 -2
  327. package/docs/cli/verify.mdx +23 -1
  328. package/docs/cli.mdx +33 -23
  329. package/docs/deploy-checks.md +1 -1
  330. package/docs/desktop.mdx +1 -1
  331. package/docs/enterprise.md +2 -2
  332. package/docs/environment.mdx +1 -1
  333. package/docs/flows.md +11 -1
  334. package/docs/frameworks.mdx +9 -9
  335. package/docs/getting-started.md +1 -1
  336. package/docs/install-manual.mdx +1 -1
  337. package/docs/license-activation.mdx +3 -3
  338. package/docs/packages/tauri.mdx +1 -1
  339. package/docs/packages.mdx +1 -1
  340. package/docs/predicates.mdx +1 -1
  341. package/docs/quickstart.mdx +68 -49
  342. package/docs/skill-setup.md +1 -1
  343. package/docs/skill.md +14 -13
  344. package/docs/telemetry.md +2 -2
  345. package/docs/tools/navigate.mdx +1 -1
  346. package/docs/tools/verify.mdx +4 -1
  347. package/docs/troubleshooting.mdx +30 -1
  348. package/docs/usage.md +17 -14
  349. package/package.json +11 -14
  350. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  351. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -35,10 +35,24 @@ export declare function grantLocalNetworkAccess(context: PooledContext, url: str
35
35
  * instead of holding the caller.
36
36
  */
37
37
  export declare function readNotificationPermission(page: PooledPage): Promise<string | undefined>;
38
+ /** How long the loopback CSP probe may take. Bounded because acquire waits on it. */
39
+ export declare const CSP_PROBE_TIMEOUT_MS = 2000;
40
+ /** The part of `fetch` the probe uses. */
41
+ export type CspProbeFetch = (url: string, init: {
42
+ signal: AbortSignal;
43
+ }) => Promise<Pick<Response, 'headers' | 'text'>>;
38
44
  /**
39
- * On a lease off loopback, add `daemonOrigin` to the page CSP `connect-src` (#2016). The returned
40
- * record gets `widenedFor` once a response was really widened, so the lease can say so.
45
+ * Whether the document at `url` sends a CSP (header or meta tag) that would block a dial to
46
+ * `daemonOrigin` (#2163). False when it cannot be asked: the lease then opens as before.
41
47
  */
42
- export declare function widenLeaseCsp(context: PooledContext, url: string, daemonOrigin: string | undefined): Promise<{
48
+ export declare function pageCspBlocksDaemon(url: string, daemonOrigin: string, probe?: CspProbeFetch): Promise<boolean>;
49
+ /**
50
+ * On a lease whose page's CSP would block the daemon, add `daemonOrigin` to its `connect-src`
51
+ * (#2016). Off loopback this always installs the route. On loopback it installs it only when a
52
+ * probe of the page finds a CSP that blocks the dial (#2163), because the route sees every request
53
+ * in the context, and a dev server that sends no CSP should not pay for it. The returned record
54
+ * gets `widenedFor` once a response was really widened, so the lease can say so.
55
+ */
56
+ export declare function widenLeaseCsp(context: PooledContext, url: string, daemonOrigin: string | undefined, probe?: CspProbeFetch): Promise<{
43
57
  widenedFor?: string;
44
58
  }>;
@@ -16,6 +16,7 @@
16
16
  * "granted" there would be reporting something the page cannot see.
17
17
  */
18
18
  import { isLoopbackHostname } from '@reticlehq/core';
19
+ import { allowDaemonInCsp, allowDaemonInHtml } from './csp-widen.js';
19
20
  /** The permission whose grant the headless shell does not pass on to `Notification.permission`. */
20
21
  export const NOTIFICATIONS_PERMISSION = 'notifications';
21
22
  /** Evaluated in the leased page. A string, because the pool's page surface evaluates scripts. */
@@ -106,14 +107,44 @@ export async function readNotificationPermission(page) {
106
107
  clearTimeout(timer);
107
108
  }
108
109
  }
110
+ /** How long the loopback CSP probe may take. Bounded because acquire waits on it. */
111
+ export const CSP_PROBE_TIMEOUT_MS = 2_000;
109
112
  /**
110
- * On a lease off loopback, add `daemonOrigin` to the page CSP `connect-src` (#2016). The returned
111
- * record gets `widenedFor` once a response was really widened, so the lease can say so.
113
+ * Whether the document at `url` sends a CSP (header or meta tag) that would block a dial to
114
+ * `daemonOrigin` (#2163). False when it cannot be asked: the lease then opens as before.
112
115
  */
113
- export async function widenLeaseCsp(context, url, daemonOrigin) {
116
+ export async function pageCspBlocksDaemon(url, daemonOrigin, probe = fetch) {
117
+ try {
118
+ const response = await probe(url, { signal: AbortSignal.timeout(CSP_PROBE_TIMEOUT_MS) });
119
+ const header = response.headers.get('content-security-policy');
120
+ if (header !== null && allowDaemonInCsp(header, daemonOrigin).changed) {
121
+ return true;
122
+ }
123
+ if (!/html/i.test(response.headers.get('content-type') ?? '')) {
124
+ return false;
125
+ }
126
+ return allowDaemonInHtml(await response.text(), daemonOrigin).changed;
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
132
+ /**
133
+ * On a lease whose page's CSP would block the daemon, add `daemonOrigin` to its `connect-src`
134
+ * (#2016). Off loopback this always installs the route. On loopback it installs it only when a
135
+ * probe of the page finds a CSP that blocks the dial (#2163), because the route sees every request
136
+ * in the context, and a dev server that sends no CSP should not pay for it. The returned record
137
+ * gets `widenedFor` once a response was really widened, so the lease can say so.
138
+ */
139
+ export async function widenLeaseCsp(context, url, daemonOrigin, probe) {
114
140
  const widening = {};
115
- if (daemonOrigin !== undefined && offLoopbackOrigin(url) !== undefined) {
116
- await context.widenCsp?.(daemonOrigin, () => {
141
+ if (daemonOrigin === undefined || context.widenCsp === undefined) {
142
+ return widening;
143
+ }
144
+ const needed = offLoopbackOrigin(url) !== undefined ||
145
+ (URL.canParse(url) && (await pageCspBlocksDaemon(url, daemonOrigin, probe)));
146
+ if (needed) {
147
+ await context.widenCsp(daemonOrigin, () => {
117
148
  widening.widenedFor = daemonOrigin;
118
149
  });
119
150
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A deterministic lease: the same page, driven the same way, gives the same pixels and text.
3
+ *
4
+ * Two runs of a canvas game differ because time and randomness differ. This script replaces both
5
+ * before any page script runs. Math.random and crypto take values from a seeded generator. Date,
6
+ * performance.now, the timers and requestAnimationFrame read a virtual clock that starts at a fixed
7
+ * epoch. The clock moves only when the caller steps frames, through `reticle_clock { stepFrames }`.
8
+ *
9
+ * The SDK in the page must keep real time, or it cannot wait, reconnect or send its heartbeat. The
10
+ * script keeps the real timers under `DETERMINISM_GLOBAL_KEY`, and the SDK reads them from there.
11
+ */
12
+ import { z } from 'zod';
13
+ /** What the caller asks for. */
14
+ export interface DeterminismOptions {
15
+ seed?: number;
16
+ /** The value of `navigator.platform`, for example "Linux x86_64". Absent keeps the real one. */
17
+ platform?: string;
18
+ }
19
+ /** What the page script is given. Plain data, because Playwright sends it as JSON. */
20
+ export interface DeterminismConfig {
21
+ key: string;
22
+ seed: number;
23
+ epochMs: number;
24
+ frameMs: number;
25
+ platform?: string;
26
+ }
27
+ /** What one step reports: the frames stepped since the page loaded, and the virtual time. */
28
+ export interface DeterminismStep {
29
+ frame: number;
30
+ nowMs: number;
31
+ }
32
+ export declare const DeterminismSchema: z.ZodObject<{
33
+ seed: z.ZodOptional<z.ZodNumber>;
34
+ platform: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strict>;
36
+ export declare const LEASE_DETERMINISTIC_ARG: z.ZodOptional<z.ZodObject<{
37
+ seed: z.ZodOptional<z.ZodNumber>;
38
+ platform: z.ZodOptional<z.ZodString>;
39
+ }, z.core.$strict>>;
40
+ /** A lease's `deterministic` argument, read; a field it does not know is refused by name. */
41
+ export declare function leaseDeterminismOf(raw: unknown): DeterminismOptions | undefined;
42
+ export declare function determinismConfigOf(opts: DeterminismOptions): DeterminismConfig;
43
+ export declare const StepFramesSchema: z.ZodNumber;
44
+ /** Step the page's virtual clock by `frames` frames. Undefined when the page runs no such clock. */
45
+ export declare function stepPage(page: {
46
+ evaluate?(script: string): Promise<unknown>;
47
+ }, frames: number): Promise<DeterminismStep | undefined>;
48
+ /**
49
+ * The page script. It must stay self-contained: Playwright sends its source text into the page, so
50
+ * it can read nothing from this module. The unit test runs it from its source text for that reason.
51
+ */
52
+ export declare function installDeterminism(config: DeterminismConfig): void;
@@ -0,0 +1,259 @@
1
+ /**
2
+ * A deterministic lease: the same page, driven the same way, gives the same pixels and text.
3
+ *
4
+ * Two runs of a canvas game differ because time and randomness differ. This script replaces both
5
+ * before any page script runs. Math.random and crypto take values from a seeded generator. Date,
6
+ * performance.now, the timers and requestAnimationFrame read a virtual clock that starts at a fixed
7
+ * epoch. The clock moves only when the caller steps frames, through `reticle_clock { stepFrames }`.
8
+ *
9
+ * The SDK in the page must keep real time, or it cannot wait, reconnect or send its heartbeat. The
10
+ * script keeps the real timers under `DETERMINISM_GLOBAL_KEY`, and the SDK reads them from there.
11
+ */
12
+ import { z } from 'zod';
13
+ import { DETERMINISM_DEFAULT_SEED, DETERMINISM_EPOCH_MS, DETERMINISM_FRAME_MS, DETERMINISM_GLOBAL_KEY, DETERMINISM_MAX_STEP_FRAMES, } from '@reticlehq/core';
14
+ export const DeterminismSchema = z
15
+ .object({
16
+ seed: z.number().int().min(0).max(0xffffffff).optional(),
17
+ platform: z.string().min(1).max(64).optional(),
18
+ })
19
+ .strict();
20
+ export const LEASE_DETERMINISTIC_ARG = DeterminismSchema.optional().describe('Make the leased tab deterministic, for canvas games and animations. Before any page script ' +
21
+ 'runs, Math.random and crypto.getRandomValues use a seeded generator (seed, default 1). Date, ' +
22
+ 'performance.now, setTimeout, setInterval and requestAnimationFrame use a virtual clock that ' +
23
+ 'starts at 2026-01-01T00:00:00Z and moves only when you call reticle_clock { stepFrames: N }. ' +
24
+ 'devicePixelRatio is 1. platform sets navigator.platform, for example "Linux x86_64". CSS ' +
25
+ 'animations, workers and media still run on real time. Example: deterministic: { seed: 7 }.');
26
+ /** A lease's `deterministic` argument, read; a field it does not know is refused by name. */
27
+ export function leaseDeterminismOf(raw) {
28
+ if (raw === undefined) {
29
+ return undefined;
30
+ }
31
+ const parsed = DeterminismSchema.safeParse(raw);
32
+ if (!parsed.success) {
33
+ const issue = parsed.error.issues[0];
34
+ const field = issue?.path.join('.') ?? '';
35
+ throw new Error(`deterministic${'' === field ? '' : `.${field}`}: ${issue?.message ?? 'not valid'}`);
36
+ }
37
+ const { seed, platform } = parsed.data;
38
+ return {
39
+ ...(seed === undefined ? {} : { seed }),
40
+ ...(platform === undefined ? {} : { platform }),
41
+ };
42
+ }
43
+ export function determinismConfigOf(opts) {
44
+ return {
45
+ key: DETERMINISM_GLOBAL_KEY,
46
+ seed: opts.seed ?? DETERMINISM_DEFAULT_SEED,
47
+ epochMs: DETERMINISM_EPOCH_MS,
48
+ frameMs: DETERMINISM_FRAME_MS,
49
+ ...(opts.platform === undefined ? {} : { platform: opts.platform }),
50
+ };
51
+ }
52
+ export const StepFramesSchema = z.number().int().min(1).max(DETERMINISM_MAX_STEP_FRAMES);
53
+ const StepResultSchema = z.object({ frame: z.number(), nowMs: z.number() });
54
+ /** Step the page's virtual clock by `frames` frames. Undefined when the page runs no such clock. */
55
+ export async function stepPage(page, frames) {
56
+ if (page.evaluate === undefined) {
57
+ return undefined;
58
+ }
59
+ const key = JSON.stringify(DETERMINISM_GLOBAL_KEY);
60
+ const script = `globalThis[Symbol.for(${key})]?.step(${String(StepFramesSchema.parse(frames))})`;
61
+ const parsed = StepResultSchema.safeParse(await page.evaluate(script));
62
+ return parsed.success ? parsed.data : undefined;
63
+ }
64
+ /**
65
+ * The page script. It must stay self-contained: Playwright sends its source text into the page, so
66
+ * it can read nothing from this module. The unit test runs it from its source text for that reason.
67
+ */
68
+ export function installDeterminism(config) {
69
+ const g = globalThis;
70
+ const define = (target, name, value) => {
71
+ Object.defineProperty(target, name, { value, configurable: true, writable: true });
72
+ };
73
+ const getter = (target, name, value) => {
74
+ Object.defineProperty(target, name, { get: () => value, configurable: true });
75
+ };
76
+ // The real timers, kept before anything is replaced, for the SDK and for this script.
77
+ const realSetTimeout = g.setTimeout?.bind(g);
78
+ const realClearTimeout = g.clearTimeout?.bind(g);
79
+ const realFrame = 'function' === typeof g.requestAnimationFrame ? g.requestAnimationFrame.bind(g) : undefined;
80
+ const realPerformanceNow = g.performance?.now.bind(g.performance);
81
+ // Random: mulberry32, a small 32-bit generator. The same seed gives the same sequence.
82
+ let state = config.seed >>> 0;
83
+ const next = () => {
84
+ state = (state + 0x6d2b79f5) >>> 0;
85
+ let t = state;
86
+ t = Math.imul(t ^ (t >>> 15), t | 1);
87
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
88
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
89
+ };
90
+ g.Math.random = next;
91
+ const fill = (view) => {
92
+ const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
93
+ for (let i = 0; i < bytes.length; i += 1) {
94
+ bytes[i] = Math.floor(next() * 256);
95
+ }
96
+ return view;
97
+ };
98
+ if (g.crypto !== undefined) {
99
+ define(g.crypto, 'getRandomValues', fill);
100
+ define(g.crypto, 'randomUUID', () => {
101
+ const b = fill(new Uint8Array(16));
102
+ b[6] = ((b[6] ?? 0) & 0x0f) | 0x40;
103
+ b[8] = ((b[8] ?? 0) & 0x3f) | 0x80;
104
+ const hex = Array.from(b, (n) => n.toString(16).padStart(2, '0')).join('');
105
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
106
+ });
107
+ }
108
+ // Clock: `elapsed` is the virtual performance.now. It moves only in `step`.
109
+ let elapsed = 0;
110
+ let frame = 0;
111
+ const RealDate = g.Date;
112
+ const wallNow = () => config.epochMs + elapsed;
113
+ const VirtualDate = function (...args) {
114
+ if (new.target === undefined) {
115
+ return new RealDate(wallNow()).toString();
116
+ }
117
+ return Reflect.construct(RealDate, 0 === args.length ? [wallNow()] : args, new.target);
118
+ };
119
+ VirtualDate.prototype = RealDate.prototype;
120
+ define(VirtualDate, 'now', wallNow);
121
+ define(VirtualDate, 'parse', RealDate.parse.bind(RealDate));
122
+ define(VirtualDate, 'UTC', RealDate.UTC.bind(RealDate));
123
+ g.Date = VirtualDate;
124
+ if (g.performance !== undefined) {
125
+ define(g.performance, 'now', () => elapsed);
126
+ }
127
+ let nextId = 1;
128
+ const timers = new Map();
129
+ const schedule = (cb, ms, args, repeat) => {
130
+ const id = nextId;
131
+ nextId += 1;
132
+ if ('function' !== typeof cb) {
133
+ return id;
134
+ }
135
+ const delay = Math.max(0, Number(ms) || 0);
136
+ const run = () => {
137
+ cb(...args);
138
+ };
139
+ timers.set(id, {
140
+ id,
141
+ at: elapsed + delay,
142
+ run,
143
+ ...(repeat ? { every: Math.max(1, delay) } : {}),
144
+ });
145
+ return id;
146
+ };
147
+ const cancel = (id) => {
148
+ timers.delete(Number(id));
149
+ };
150
+ g.setTimeout = (cb, ms, ...args) => schedule(cb, ms, args, false);
151
+ g.setInterval = (cb, ms, ...args) => schedule(cb, ms, args, true);
152
+ g.clearTimeout = cancel;
153
+ g.clearInterval = cancel;
154
+ let frames = [];
155
+ g.requestAnimationFrame = (cb) => {
156
+ const id = nextId;
157
+ nextId += 1;
158
+ if ('function' === typeof cb) {
159
+ frames.push({ id, cb: cb });
160
+ }
161
+ return id;
162
+ };
163
+ g.cancelAnimationFrame = (id) => {
164
+ frames = frames.filter((f) => f.id !== Number(id));
165
+ };
166
+ // A callback that throws is reported as the page reports it, and the rest still run. After each
167
+ // callback the microtasks run, as they do in a browser between two tasks.
168
+ const settle = () => new Promise((resolve) => {
169
+ if (g.MessageChannel === undefined) {
170
+ void Promise.resolve().then(resolve);
171
+ return;
172
+ }
173
+ const channel = new g.MessageChannel();
174
+ channel.port1.onmessage = () => {
175
+ resolve();
176
+ };
177
+ channel.port2.postMessage(0);
178
+ });
179
+ const invoke = async (run) => {
180
+ try {
181
+ run();
182
+ }
183
+ catch (error) {
184
+ realSetTimeout?.(() => {
185
+ throw error;
186
+ }, 0);
187
+ }
188
+ await settle();
189
+ };
190
+ const earliestDue = (until) => {
191
+ let found;
192
+ for (const t of timers.values()) {
193
+ if (t.at <= until && (found === undefined || t.at < found.at)) {
194
+ found = t;
195
+ }
196
+ }
197
+ return found;
198
+ };
199
+ const step = async (count) => {
200
+ for (let i = 0; i < count; i += 1) {
201
+ frame += 1;
202
+ const target = frame * config.frameMs;
203
+ for (let guard = 0; guard < 100000; guard += 1) {
204
+ const due = earliestDue(target);
205
+ if (due === undefined) {
206
+ break;
207
+ }
208
+ elapsed = Math.max(elapsed, due.at);
209
+ timers.delete(due.id);
210
+ if (due.every !== undefined) {
211
+ timers.set(due.id, { ...due, at: due.at + due.every });
212
+ }
213
+ await invoke(due.run);
214
+ }
215
+ elapsed = target;
216
+ const flush = frames;
217
+ frames = [];
218
+ for (const f of flush) {
219
+ await invoke(() => {
220
+ f.cb(target);
221
+ });
222
+ }
223
+ }
224
+ return { frame, nowMs: elapsed };
225
+ };
226
+ // Keyboard labels and shortcuts differ between Mac and Linux, so the platform can be set.
227
+ const nav = g.navigator;
228
+ if (config.platform !== undefined && nav !== undefined) {
229
+ const platform = config.platform;
230
+ const families = [
231
+ ['Mac', 'macOS', '(Macintosh; Intel Mac OS X 10_15_7)'],
232
+ ['Win', 'Windows', '(Windows NT 10.0; Win64; x64)'],
233
+ ['Linux', 'Linux', `(X11; ${platform})`],
234
+ ];
235
+ const family = families.find(([prefix]) => platform.startsWith(prefix));
236
+ getter(nav, 'platform', platform);
237
+ if (family !== undefined) {
238
+ if (nav.userAgentData !== undefined) {
239
+ getter(nav.userAgentData, 'platform', family[1]);
240
+ }
241
+ if (nav.userAgent !== undefined) {
242
+ getter(nav, 'userAgent', nav.userAgent.replace(/\([^)]*\)/, family[2]));
243
+ }
244
+ }
245
+ }
246
+ Object.defineProperty(g, Symbol.for(config.key), {
247
+ value: {
248
+ step,
249
+ natives: {
250
+ setTimeout: realSetTimeout,
251
+ clearTimeout: realClearTimeout,
252
+ requestAnimationFrame: realFrame,
253
+ performanceNow: realPerformanceNow,
254
+ },
255
+ },
256
+ configurable: false,
257
+ enumerable: false,
258
+ });
259
+ }
@@ -5,6 +5,7 @@
5
5
  * so the CSS half of reduced motion and the dark theme could not be checked live. A lease is a
6
6
  * Playwright context, which takes all three before the first paint and the media again per call.
7
7
  */
8
+ import type { DeterminismOptions } from './determinism.js';
8
9
  export declare const ColorScheme: {
9
10
  readonly LIGHT: "light";
10
11
  readonly DARK: "dark";
@@ -16,18 +17,50 @@ export declare const ReducedMotion: {
16
17
  readonly NO_PREFERENCE: "no-preference";
17
18
  };
18
19
  export type ReducedMotion = (typeof ReducedMotion)[keyof typeof ReducedMotion];
20
+ export declare const Pointer: {
21
+ readonly FINE: "fine";
22
+ readonly COARSE: "coarse";
23
+ };
24
+ export type Pointer = (typeof Pointer)[keyof typeof Pointer];
19
25
  /** The media features a page reads with matchMedia. */
20
26
  export interface LeaseMedia {
21
27
  colorScheme?: ColorScheme;
22
28
  reducedMotion?: ReducedMotion;
23
29
  }
24
- /** What a lease opens with: the media, and a viewport. */
30
+ /** What a lease opens with: the media, a viewport, and the pointer the page reads. */
25
31
  export interface LeaseEmulation extends LeaseMedia {
26
32
  viewport?: {
27
33
  width: number;
28
34
  height: number;
29
35
  };
36
+ /**
37
+ * The primary pointer `@media (pointer: ...)` reports. Unlike the media features, it is fixed
38
+ * when the context opens: Playwright's touch support is a context option, so a live lease cannot
39
+ * be given one. Without it a touch-only rule could not be reached at all, and a lease answered
40
+ * every `(pointer: coarse)` query with a mouse (#1903).
41
+ */
42
+ pointer?: Pointer;
43
+ /** A seeded random source and a stepped clock, installed before any page script (determinism.ts). */
44
+ deterministic?: DeterminismOptions;
30
45
  }
46
+ /** The same emulation in the names Playwright's own context options use. */
47
+ export interface ContextEmulation extends LeaseMedia {
48
+ viewport?: {
49
+ width: number;
50
+ height: number;
51
+ };
52
+ hasTouch?: boolean;
53
+ deviceScaleFactor?: number;
54
+ }
55
+ /**
56
+ * Translate a lease's emulation into context options. `pointer` is not one of Playwright's: a
57
+ * coarse primary pointer is what touch support produces, so it travels as `hasTouch`.
58
+ *
59
+ * Measured in real headless Chromium: `hasTouch: true` makes `(pointer: coarse)` match and
60
+ * `(pointer: fine)` stop matching, with `maxTouchPoints` at 1. `isMobile` does the same and also
61
+ * changes viewport-meta handling, so it is deliberately left out.
62
+ */
63
+ export declare function contextEmulationOf(emulate: LeaseEmulation): ContextEmulation;
31
64
  /** The size a page reports for itself, or undefined when it cannot be read. */
32
65
  export declare function readViewport(page: {
33
66
  evaluate?(script: string): Promise<unknown>;
@@ -11,6 +11,24 @@ export const ColorScheme = {
11
11
  NO_PREFERENCE: 'no-preference',
12
12
  };
13
13
  export const ReducedMotion = { REDUCE: 'reduce', NO_PREFERENCE: 'no-preference' };
14
+ export const Pointer = { FINE: 'fine', COARSE: 'coarse' };
15
+ /**
16
+ * Translate a lease's emulation into context options. `pointer` is not one of Playwright's: a
17
+ * coarse primary pointer is what touch support produces, so it travels as `hasTouch`.
18
+ *
19
+ * Measured in real headless Chromium: `hasTouch: true` makes `(pointer: coarse)` match and
20
+ * `(pointer: fine)` stop matching, with `maxTouchPoints` at 1. `isMobile` does the same and also
21
+ * changes viewport-meta handling, so it is deliberately left out.
22
+ */
23
+ export function contextEmulationOf(emulate) {
24
+ const { pointer, deterministic, ...rest } = emulate;
25
+ return {
26
+ ...rest,
27
+ ...(pointer === undefined ? {} : { hasTouch: Pointer.COARSE === pointer }),
28
+ // A deterministic page draws at one density on every machine.
29
+ ...(deterministic === undefined ? {} : { deviceScaleFactor: 1 }),
30
+ };
31
+ }
14
32
  /** The page's own layout viewport, read back after a resize. */
15
33
  const VIEWPORT_READ_SCRIPT = '[window.innerWidth, window.innerHeight]';
16
34
  /** The size a page reports for itself, or undefined when it cannot be read. */
@@ -5,6 +5,7 @@ interface LeaseMarkerArg {
5
5
  targetOrigin: string;
6
6
  sessionParam: string;
7
7
  projectParam: string;
8
+ sessionStorageKey: string;
8
9
  }
9
10
  /**
10
11
  * Init script restoring the marker a server redirect dropped, so the SDK registers under the lease id.
@@ -13,6 +14,11 @@ interface LeaseMarkerArg {
13
14
  * target origin (never an identity provider's or any third party's URL), and the pool installs it
14
15
  * only when the navigated URL's marker matches this lease's id. Must stay self-contained: Playwright
15
16
  * serialises the function source into the page.
17
+ *
18
+ * It also writes the lease id to the tab session store before page scripts run. An SPA that calls
19
+ * `history.replaceState` on load drops the URL marker before `connect()`, and the SDK then reads
20
+ * the id from that store (#2026). A value the tab holds already stays: a reload holds the lease id,
21
+ * and a different value is one the SDK stored for an explicit `connect({ session })`.
16
22
  */
17
23
  export declare function stampLeaseMarker(arg: LeaseMarkerArg): void;
18
24
  export declare function installLeaseMarker(page: PooledPage, leaseId: string, targetOrigin: string | undefined, project: string | undefined): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { RETICLE_URL_PARAM } from '@reticlehq/core';
1
+ import { RETICLE_SESSION_STORAGE_KEY, RETICLE_URL_PARAM } from '@reticlehq/core';
2
2
  /**
3
3
  * Init script restoring the marker a server redirect dropped, so the SDK registers under the lease id.
4
4
  * It runs only in pages of the lease's own browser context, so a person's tab at the same URL never
@@ -6,11 +6,24 @@ import { RETICLE_URL_PARAM } from '@reticlehq/core';
6
6
  * target origin (never an identity provider's or any third party's URL), and the pool installs it
7
7
  * only when the navigated URL's marker matches this lease's id. Must stay self-contained: Playwright
8
8
  * serialises the function source into the page.
9
+ *
10
+ * It also writes the lease id to the tab session store before page scripts run. An SPA that calls
11
+ * `history.replaceState` on load drops the URL marker before `connect()`, and the SDK then reads
12
+ * the id from that store (#2026). A value the tab holds already stays: a reload holds the lease id,
13
+ * and a different value is one the SDK stored for an explicit `connect({ session })`.
9
14
  */
10
15
  export function stampLeaseMarker(arg) {
11
16
  const win = globalThis.window;
12
17
  if (win === undefined || win.top !== win || win.location.origin !== arg.targetOrigin)
13
18
  return;
19
+ try {
20
+ if (null === win.sessionStorage.getItem(arg.sessionStorageKey)) {
21
+ win.sessionStorage.setItem(arg.sessionStorageKey, arg.session);
22
+ }
23
+ }
24
+ catch {
25
+ // A page with storage blocked still gets the URL marker below.
26
+ }
14
27
  try {
15
28
  const url = new URL(win.location.href);
16
29
  if (url.searchParams.has(arg.sessionParam))
@@ -34,6 +47,7 @@ export async function installLeaseMarker(page, leaseId, targetOrigin, project) {
34
47
  targetOrigin,
35
48
  sessionParam: RETICLE_URL_PARAM.SESSION,
36
49
  projectParam: RETICLE_URL_PARAM.PROJECT,
50
+ sessionStorageKey: RETICLE_SESSION_STORAGE_KEY,
37
51
  });
38
52
  }
39
53
  export function leaseMarkerOf(url) {
@@ -1,29 +1,56 @@
1
- /**
2
- * The page-error side of a lease: listen for the throws the SDK will never see, and tell the
3
- * wiring when the lease is gone.
4
- *
5
- * An app whose module graph throws during the first navigation fires its `error` event while the
6
- * page still has no Reticle SDK in it — the zero-install reader is only evaluated after `goto`
7
- * resolves — so nothing on the page records it, and a blank page reported as a bare console-error
8
- * count (#1966). The server's Playwright handle is attached before the navigation, so it is the
9
- * one witness that cannot miss the throw. This module owns that wiring for `BrowserPool`; the
10
- * journal that buffers and journals what arrives lives in `page-error-journal.ts`.
11
- */
12
- import type { PooledPage, PooledPageError } from './pool-contract.js';
1
+ import type { PooledDialog, PooledPage, PooledPageError } from './pool-contract.js';
13
2
  /** The page-error hooks `BrowserPoolOptions` carries, forwarded here verbatim. */
14
3
  export interface LeasePageErrorHooks {
15
4
  onPageError?: ((leaseId: string, error: PooledPageError) => void) | undefined;
16
5
  onLeaseClosed?: ((leaseId: string) => void) | undefined;
6
+ /** Each native dialog the pool dismissed. See `BrowserPoolOptions.onDialog`. */
7
+ onDialog?: ((leaseId: string, dialog: Pick<PooledDialog, 'kind' | 'message'>) => void) | undefined;
17
8
  }
18
9
  /**
19
- * Install the lease's uncaught-error listener. Call BEFORE the first navigation — the listener
20
- * timing is the whole fix: an import-time throw lands during `goto`, and one attached after it
21
- * reads a broken page as healthy. Forwarded with the lease id, because the session for this lease
22
- * usually does not exist yet, so the sink buffers.
10
+ * Install the lease's uncaught-error listener and its early console queue. Call BEFORE the first
11
+ * navigation — the timing is the whole fix: an import-time throw lands during `goto`, and one
12
+ * attached after it reads a broken page as healthy. Forwarded with the lease id, because the
13
+ * session for this lease usually does not exist yet, so the sink buffers.
23
14
  */
24
- export declare function listenForPageErrors(page: PooledPage, leaseId: string, hooks: LeasePageErrorHooks | undefined): void;
15
+ export declare function listenForPageErrors(page: PooledPage, leaseId: string, hooks: LeasePageErrorHooks | undefined): Promise<void>;
25
16
  /**
26
17
  * Report a closed lease — manual release, reaper and crash reclaim all funnel through the pool's
27
18
  * one release path. The wiring owns whether anything was kept FOR that lease and drops it.
28
19
  */
29
20
  export declare function reportLeaseClosed(hooks: LeasePageErrorHooks | undefined, leaseId: string): void;
21
+ /** How many console calls and error events a page keeps for an SDK that has not installed yet. */
22
+ export declare const EARLY_CAPTURE_LIMIT = 200;
23
+ /**
24
+ * The argument for `queueEarlyConsole`. Every driven page gets the same one: a pool lease, and
25
+ * `verify`'s own page. It looks for the SDK every `pollMs`, for at most `maxPolls` looks (30 s).
26
+ */
27
+ export declare const EARLY_CAPTURE_ARG: {
28
+ key: string;
29
+ limit: number;
30
+ pollMs: number;
31
+ maxPolls: number;
32
+ };
33
+ interface EarlyCaptureArg {
34
+ key: string;
35
+ limit: number;
36
+ pollMs: number;
37
+ maxPolls: number;
38
+ }
39
+ /**
40
+ * Init script: keep the console errors and warnings a page logs before the SDK installs.
41
+ *
42
+ * An app connects the SDK from a module script or a bundle, and both run after the page's own
43
+ * classic scripts. An error such a script logs while the page loads was lost, so "no console
44
+ * errors" passed on a page that logged one on every load. Playwright's own console event is not a
45
+ * substitute: it also reports messages the browser writes itself (a failed WebSocket, which is how
46
+ * the SDK's own first dial can fail), and the SDK's console channel has never counted those.
47
+ *
48
+ * So this keeps the RAW calls and events. Once the SDK has wrapped the console, this hands them to
49
+ * it: each call goes through the console again and each event is dispatched again, so the SDK's own
50
+ * handlers report them and the meaning of "console error" stays the SDK's. The SDK needs no code for
51
+ * this, so a page that Reticle does not drive downloads nothing for it. Entries the bound did not
52
+ * keep are declared with one console warning. Must stay self-contained: Playwright serialises the
53
+ * function source into the page.
54
+ */
55
+ export declare function queueEarlyConsole(arg: EarlyCaptureArg): void;
56
+ export {};