browserctl 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +3 -3
  4. data/bin/browserctl +39 -32
  5. data/lib/browserctl/callable_definition.rb +114 -0
  6. data/lib/browserctl/client.rb +0 -27
  7. data/lib/browserctl/commands/cli_output.rb +17 -3
  8. data/lib/browserctl/commands/daemon.rb +10 -6
  9. data/lib/browserctl/commands/flow.rb +7 -5
  10. data/lib/browserctl/commands/init.rb +20 -7
  11. data/lib/browserctl/commands/migrate.rb +56 -8
  12. data/lib/browserctl/commands/output_format.rb +144 -0
  13. data/lib/browserctl/commands/page.rb +9 -5
  14. data/lib/browserctl/commands/{record.rb → recording.rb} +14 -13
  15. data/lib/browserctl/commands/resume.rb +1 -1
  16. data/lib/browserctl/commands/screenshot.rb +2 -2
  17. data/lib/browserctl/commands/snapshot.rb +8 -3
  18. data/lib/browserctl/commands/state.rb +3 -2
  19. data/lib/browserctl/commands/trace.rb +40 -11
  20. data/lib/browserctl/commands/workflow.rb +9 -7
  21. data/lib/browserctl/contextual_persistence.rb +58 -0
  22. data/lib/browserctl/driver/cdp.rb +2 -3
  23. data/lib/browserctl/encryption_service.rb +84 -0
  24. data/lib/browserctl/flow.rb +35 -59
  25. data/lib/browserctl/flows/stdlib/cloudflare_solve.rb +4 -4
  26. data/lib/browserctl/recording/log_writer.rb +82 -0
  27. data/lib/browserctl/recording/redactor.rb +58 -0
  28. data/lib/browserctl/recording/state.rb +44 -0
  29. data/lib/browserctl/recording/workflow_renderer.rb +214 -0
  30. data/lib/browserctl/recording.rb +33 -294
  31. data/lib/browserctl/server/command_dispatcher.rb +25 -16
  32. data/lib/browserctl/server/handlers/state.rb +7 -5
  33. data/lib/browserctl/server.rb +2 -1
  34. data/lib/browserctl/state/bundle.rb +20 -47
  35. data/lib/browserctl/state.rb +46 -9
  36. data/lib/browserctl/version.rb +1 -1
  37. data/lib/browserctl/workflow/recovery_manager.rb +87 -0
  38. data/lib/browserctl/workflow.rb +61 -237
  39. metadata +11 -8
  40. data/examples/session_reuse.rb +0 -75
  41. data/lib/browserctl/commands/session.rb +0 -243
  42. data/lib/browserctl/driver/base.rb +0 -13
  43. data/lib/browserctl/driver.rb +0 -5
  44. data/lib/browserctl/server/handlers/session.rb +0 -94
  45. data/lib/browserctl/session.rb +0 -206
@@ -42,6 +42,41 @@ module Browserctl
42
42
  EXTENSION = ".bctl"
43
43
  MANIFEST_VERSION = 1
44
44
 
45
+ # Value object bundling everything needed to persist a state bundle. The
46
+ # browser-side data lives in `cookies`, `local_storage`, and
47
+ # `session_storage`; the manifest extras live in `origins`, `flow`, and
48
+ # `flow_version`; `passphrase` flips the bundle into an encrypted variant.
49
+ Payload = Data.define(
50
+ :cookies,
51
+ :local_storage,
52
+ :session_storage,
53
+ :origins,
54
+ :flow,
55
+ :flow_version,
56
+ :passphrase
57
+ ) do
58
+ def self.build(cookies: [], local_storage: {}, session_storage: {}, # rubocop:disable Metrics/ParameterLists
59
+ origins: nil, flow: nil, flow_version: nil, passphrase: nil)
60
+ new(
61
+ cookies: cookies,
62
+ local_storage: local_storage,
63
+ session_storage: session_storage,
64
+ origins: origins,
65
+ flow: flow,
66
+ flow_version: flow_version,
67
+ passphrase: passphrase
68
+ )
69
+ end
70
+
71
+ def to_bundle_payload
72
+ {
73
+ cookies: cookies,
74
+ local_storage: local_storage,
75
+ session_storage: session_storage
76
+ }
77
+ end
78
+ end
79
+
45
80
  def self.path(name) = File.join(BASE_DIR, "#{name}#{EXTENSION}")
46
81
  def self.exist?(name) = File.exist?(path(name))
47
82
 
@@ -51,22 +86,24 @@ module Browserctl
51
86
  raise ArgumentError, "invalid state name #{name.inspect} — use letters, digits, _ or - (max 64 chars)"
52
87
  end
53
88
 
54
- # Persist a bundle. `payload` is { cookies:, local_storage:, session_storage: }.
55
- # `manifest_extras` may carry origins (override), flow, flow_version.
56
- def self.save(name, payload:, origins: nil, flow: nil, flow_version: nil, passphrase: nil) # rubocop:disable Metrics/ParameterLists
89
+ # Persist a bundle. `payload` is a `State::Payload` value object carrying
90
+ # cookies, local/session storage, and the manifest extras (origins, flow,
91
+ # flow_version, passphrase).
92
+ def self.save(name, payload)
57
93
  validate_name!(name)
58
94
  FileUtils.mkdir_p(BASE_DIR)
59
95
 
96
+ bundle_payload = payload.to_bundle_payload
60
97
  manifest = build_manifest(
61
98
  name: name,
62
- origins: origins || derive_origins(payload),
63
- flow: flow,
64
- flow_version: flow_version,
65
- cookies: payload[:cookies] || payload["cookies"] || [],
66
- encrypted: !passphrase.nil?
99
+ origins: payload.origins || derive_origins(bundle_payload),
100
+ flow: payload.flow,
101
+ flow_version: payload.flow_version,
102
+ cookies: payload.cookies || [],
103
+ encrypted: !payload.passphrase.nil?
67
104
  )
68
105
 
69
- blob = Bundle.encode(manifest: manifest, payload: payload, passphrase: passphrase)
106
+ blob = Bundle.encode(manifest: manifest, payload: bundle_payload, passphrase: payload.passphrase)
70
107
  File.open(path(name), "wb", 0o600) { |f| f.write(blob) }
71
108
  manifest
72
109
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Browserctl
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browserctl
4
+ module Workflow
5
+ # Owns the AUTH_REQUIRED recovery state machine for `load_state`.
6
+ #
7
+ # When the daemon reports AUTH_REQUIRED on a `state_load` (e.g. expired
8
+ # cookies in the bundle), the manager either runs the bound flow or
9
+ # the caller-provided override, re-saves the bundle, and reloads it
10
+ # with `skip_auth_check: true`.
11
+ #
12
+ # Decoupled from {ContextualPersistence} so the multi-step recovery
13
+ # logic has a dedicated home and a dedicated spec. The host context
14
+ # only needs to expose `client` (for daemon RPCs) and `invoke` (for
15
+ # running the bound flow); see {ContextualPersistence#load_state}.
16
+ class RecoveryManager
17
+ AUTH_REQUIRED_CODE = "AUTH_REQUIRED"
18
+
19
+ # @param context [#client, #invoke] a workflow context exposing
20
+ # the daemon client and an `invoke(flow_name, page:)` entry point.
21
+ def initialize(context)
22
+ @context = context
23
+ end
24
+
25
+ # True when `res` is the daemon's AUTH_REQUIRED preflight signal.
26
+ def self.auth_required?(res)
27
+ (res[:code] || res["code"]) == AUTH_REQUIRED_CODE
28
+ end
29
+
30
+ # Run recovery for `state_name` given the daemon's initial AUTH_REQUIRED
31
+ # response. Returns the merged retry result (with `rotated: true`) or
32
+ # raises {WorkflowError} when no flow is bound and no override is
33
+ # supplied, or when the post-rotation reload still fails.
34
+ #
35
+ # @param state_name [String] the bundle name being loaded
36
+ # @param initial_res [Hash] the original AUTH_REQUIRED response
37
+ # @param on_auth_required [Proc, nil] optional override; when given,
38
+ # it runs in lieu of invoking the suggested flow.
39
+ def recover(state_name, initial_res, on_auth_required: nil)
40
+ if on_auth_required
41
+ on_auth_required.call
42
+ else
43
+ invoke_bound_flow(state_name, initial_res)
44
+ end
45
+
46
+ rotate_and_reload(state_name)
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :context
52
+
53
+ def invoke_bound_flow(state_name, initial_res)
54
+ flow_name = initial_res[:suggested_flow] || initial_res["suggested_flow"]
55
+ if flow_name.nil? || flow_name.to_s.empty?
56
+ raise WorkflowError,
57
+ "state '#{state_name}' needs auth but bundle has no bound flow — " \
58
+ "save with `save_state('#{state_name}', flow: :NAME)` or pass on_auth_required:"
59
+ end
60
+
61
+ # Match the daemon's `state load` preflight: it auth-checks the first
62
+ # open page (insertion order). Passing that same name to the flow
63
+ # gives stdlib flows a `page` proxy to drive (oauth_github reads
64
+ # `page.url`, totp_2fa calls `page.fill`, etc.). Falls back to no
65
+ # page only when nothing is open — `state_save` would have errored
66
+ # earlier in that case, so this is a defence-in-depth nil.
67
+ context.invoke(flow_name, page: first_open_page)
68
+ end
69
+
70
+ def rotate_and_reload(state_name)
71
+ after_save = context.client.state_save(state_name)
72
+ raise WorkflowError, after_save[:error] if after_save[:error]
73
+
74
+ retry_res = context.client.state_load(state_name, skip_auth_check: true)
75
+ raise WorkflowError, retry_res[:error] if retry_res[:error]
76
+
77
+ retry_res.merge(rotated: true)
78
+ end
79
+
80
+ def first_open_page
81
+ res = context.client.page_list
82
+ pages = res[:pages] || res["pages"] || []
83
+ pages.first
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "timeout"
3
+ require_relative "callable_definition"
4
4
  require_relative "client"
5
+ require_relative "contextual_persistence"
5
6
  require_relative "errors"
6
7
  require_relative "flow_registry"
7
8
  require_relative "replay/context"
8
9
  require_relative "replay/fingerprint_matcher"
9
10
  require_relative "replay/snapshot_diff"
10
- require_relative "secret_resolvers"
11
- require_relative "session"
12
11
 
13
12
  module Browserctl
14
13
  # Workflow-file format version. Workflows are Ruby files; the schema gate
@@ -66,11 +65,15 @@ module Browserctl
66
65
  version
67
66
  end
68
67
 
69
- ParamDef = Struct.new(:name, :required, :secret, :default, :secret_ref, keyword_init: true)
68
+ # Back-compat aliases exposed in the public surface (flow_wrapper specs,
69
+ # workflow specs reference these directly).
70
+ ParamDef = CallableDefinition::ParamDef
71
+ StepDef = CallableDefinition::StepDef
70
72
  StepResult = Struct.new(:name, :ok, :error, keyword_init: true)
71
- StepDef = Struct.new(:label, :block, :retry_count, :timeout, keyword_init: true)
72
73
 
73
74
  class WorkflowContext
75
+ include ContextualPersistence
76
+
74
77
  attr_reader :client, :replay_context, :params
75
78
 
76
79
  def initialize(params, client, replay_context: nil)
@@ -79,20 +82,6 @@ module Browserctl
79
82
  @replay_context = replay_context
80
83
  end
81
84
 
82
- def store(key, value)
83
- res = @client.store(key.to_s, value)
84
- raise WorkflowError, res[:error] if res[:error]
85
-
86
- value
87
- end
88
-
89
- def fetch(key)
90
- res = @client.fetch(key.to_s)
91
- raise WorkflowError, res[:error] if res[:error]
92
-
93
- res[:value]
94
- end
95
-
96
85
  def method_missing(name, *args)
97
86
  return @params[name] if @params.key?(name)
98
87
 
@@ -121,66 +110,6 @@ module Browserctl
121
110
  res
122
111
  end
123
112
 
124
- def save_session(session_name, encrypt: false)
125
- res = @client.session_save(session_name, encrypt: encrypt)
126
- raise WorkflowError, res[:error] if res[:error]
127
-
128
- res
129
- end
130
-
131
- # Persists the daemon's current cookies + storage as a .bctl bundle.
132
- # Optional flow binding lets `load_state` auto-rotate when the bundle
133
- # is detected as needing authentication.
134
- def save_state(name, flow: nil, origins: nil, encrypt: false)
135
- passphrase = encrypt ? ENV.fetch("BROWSERCTL_STATE_PASSPHRASE", nil) : nil
136
- res = @client.state_save(name.to_s,
137
- flow: flow&.to_s, origins: origins, passphrase: passphrase)
138
- raise WorkflowError, res[:error] if res[:error]
139
-
140
- res
141
- end
142
-
143
- # Restores a .bctl bundle. When the daemon detects AUTH_REQUIRED before
144
- # applying (e.g. expired cookies in the payload), this rotates the bound
145
- # flow and retries — no caller code change required.
146
- #
147
- # @param on_auth_required [Proc, nil] override the auto-rotate path. The
148
- # block runs in the workflow context, in lieu of invoking the manifest's
149
- # bound flow. Use this when the recovery procedure is bespoke.
150
- def load_state(name, on_auth_required: nil)
151
- res = @client.state_load(name.to_s)
152
- return res unless auth_required_response?(res)
153
-
154
- recover_auth_required_state(name.to_s, res, on_auth_required)
155
- end
156
- DEPRECATED_LOAD_SESSION_FALLBACK = <<~MSG
157
- [browserctl] DEPRECATION: `load_session(name, fallback:, expired_if:)` is superseded by
158
- `load_state(name)` with a flow-bound bundle (`save_state(name, flow: :name)`).
159
- `load_session` will be removed in v0.12. See docs/concepts/state.md.
160
- MSG
161
-
162
- def load_session(session_name, fallback: nil, expired_if: nil)
163
- warn DEPRECATED_LOAD_SESSION_FALLBACK if fallback || expired_if
164
- validate_expired_if!(expired_if)
165
- fallback_name = fallback&.to_s
166
- res = @client.session_load(session_name)
167
-
168
- if res[:error]
169
- raise WorkflowError, res[:error] unless fallback_name
170
-
171
- invoke(fallback_name)
172
- return load_after_fallback(session_name, fallback_name)
173
- end
174
-
175
- return res if expired_if.nil? || !call_expired_if(expired_if, session_name)
176
-
177
- recover_expired_session(session_name, fallback_name, expired_if)
178
- end
179
-
180
- def list_sessions
181
- @client.session_list[:sessions]
182
- end
183
-
184
113
  def ask(prompt)
185
114
  $stderr.print("[browserctl] #{prompt} ")
186
115
  $stdin.gets.chomp
@@ -229,95 +158,6 @@ module Browserctl
229
158
 
230
159
  private
231
160
 
232
- def auth_required_response?(res)
233
- (res[:code] || res["code"]) == "AUTH_REQUIRED"
234
- end
235
-
236
- def recover_auth_required_state(name, initial_res, on_auth_required)
237
- if on_auth_required
238
- on_auth_required.call
239
- else
240
- flow_name = initial_res[:suggested_flow] || initial_res["suggested_flow"]
241
- unless flow_name && !flow_name.to_s.empty?
242
- raise WorkflowError,
243
- "state '#{name}' needs auth but bundle has no bound flow — " \
244
- "save with `save_state('#{name}', flow: :NAME)` or pass on_auth_required:"
245
- end
246
-
247
- # Match the daemon's `state load` preflight: it auth-checks the first
248
- # open page (insertion order). Passing that same name to the flow
249
- # gives stdlib flows a `page` proxy to drive (oauth_github reads
250
- # `page.url`, totp_2fa calls `page.fill`, etc.). Falls back to no
251
- # page only when nothing is open — `state_save` would have errored
252
- # earlier in that case, so this is a defence-in-depth nil.
253
- invoke(flow_name, page: first_open_page)
254
- end
255
-
256
- after_save = @client.state_save(name)
257
- raise WorkflowError, after_save[:error] if after_save[:error]
258
-
259
- retry_res = @client.state_load(name, skip_auth_check: true)
260
- raise WorkflowError, retry_res[:error] if retry_res[:error]
261
-
262
- retry_res.merge(rotated: true)
263
- end
264
-
265
- def first_open_page
266
- res = @client.page_list
267
- pages = res[:pages] || res["pages"] || []
268
- pages.first
269
- end
270
-
271
- def validate_expired_if!(expired_if)
272
- return unless expired_if
273
-
274
- unless expired_if.lambda?
275
- raise ArgumentError,
276
- "expired_if: must be a lambda (-> { }), not a Proc — " \
277
- "bare return inside a Proc unwinds the caller"
278
- end
279
-
280
- return if expired_if.arity.zero?
281
-
282
- raise ArgumentError,
283
- "expired_if: lambda must take zero arguments (got #{expired_if.arity}) — " \
284
- "use -> { page(:name).url... } to access pages via the workflow context"
285
- end
286
-
287
- def call_expired_if(expired_if, session_name)
288
- expired_if.call
289
- rescue WorkflowError, StandardError => e
290
- raise WorkflowError, "expired_if check failed for session '#{session_name}': #{e.message}"
291
- end
292
-
293
- def recover_expired_session(session_name, fallback_name, expired_if)
294
- unless fallback_name
295
- raise WorkflowError,
296
- "session '#{session_name}' is expired; provide fallback: to auto-recover"
297
- end
298
-
299
- invoke(fallback_name)
300
- res = load_after_fallback(session_name, fallback_name)
301
-
302
- if call_expired_if(expired_if, session_name)
303
- raise WorkflowError,
304
- "session '#{session_name}' still expired after running fallback '#{fallback_name}'"
305
- end
306
-
307
- res
308
- end
309
-
310
- def load_after_fallback(session_name, fallback)
311
- res = @client.session_load(session_name)
312
- return res unless res[:error]
313
-
314
- msg = "session '#{session_name}' still unavailable after running fallback '#{fallback}'"
315
- unless Session.exist?(session_name)
316
- msg += "\n Hint: '#{fallback}' did not call save_session(\"#{session_name}\") — add it as the last step."
317
- end
318
- raise WorkflowError, msg
319
- end
320
-
321
161
  def invoke_stack
322
162
  @invoke_stack ||= []
323
163
  end
@@ -356,6 +196,21 @@ module Browserctl
356
196
  class PageProxy
357
197
  attr_accessor :replay_context
358
198
 
199
+ # Declarative wrapper for `unwrap @client.METHOD(@name, ...)` one-liners.
200
+ # Forwards positional + keyword args verbatim. Pass `extract:` to return
201
+ # a single key from the client response instead of unwrapping.
202
+ def self.delegate_unwrap(method_name, extract: nil)
203
+ if extract
204
+ define_method(method_name) do |*args, **kwargs|
205
+ @client.public_send(method_name, @name, *args, **kwargs)[extract]
206
+ end
207
+ else
208
+ define_method(method_name) do |*args, **kwargs|
209
+ unwrap @client.public_send(method_name, @name, *args, **kwargs)
210
+ end
211
+ end
212
+ end
213
+
359
214
  def initialize(name, client, replay_context: nil, matcher: nil)
360
215
  @name = name
361
216
  @client = client
@@ -363,7 +218,20 @@ module Browserctl
363
218
  @matcher = matcher || Replay::FingerprintMatcher.new
364
219
  end
365
220
 
366
- def navigate(url) = unwrap @client.navigate(@name, url)
221
+ delegate_unwrap :navigate
222
+ delegate_unwrap :snapshot
223
+ delegate_unwrap :screenshot
224
+ delegate_unwrap :wait
225
+ delegate_unwrap :delete_cookies
226
+ delegate_unwrap :press
227
+ delegate_unwrap :storage_set
228
+ delegate_unwrap :dialog_accept
229
+ delegate_unwrap :dialog_dismiss
230
+
231
+ delegate_unwrap :devtools, extract: :devtools_url
232
+ delegate_unwrap :url, extract: :url
233
+ delegate_unwrap :evaluate, extract: :result
234
+ delegate_unwrap :storage_get, extract: :value
367
235
 
368
236
  def fill(selector = nil, value = nil, ref: nil)
369
237
  with_selector_fallback(:fill, selector, ref) do |sel, r|
@@ -377,24 +245,6 @@ module Browserctl
377
245
  end
378
246
  end
379
247
 
380
- def snapshot(**) = unwrap @client.snapshot(@name, **)
381
- def screenshot(**) = unwrap @client.screenshot(@name, **)
382
- def wait(sel, timeout: 30) = unwrap @client.wait(@name, sel, timeout: timeout)
383
- def delete_cookies = unwrap @client.delete_cookies(@name)
384
- def devtools = @client.devtools(@name)[:devtools_url]
385
- def url = @client.url(@name)[:url]
386
- def evaluate(expr) = @client.evaluate(@name, expr)[:result]
387
-
388
- def storage_get(key, store: "local")
389
- @client.storage_get(@name, key, store: store)[:value]
390
- end
391
-
392
- def storage_set(key, value, store: "local")
393
- unwrap @client.storage_set(@name, key, value, store: store)
394
- end
395
-
396
- def press(key) = unwrap @client.press(@name, key)
397
-
398
248
  def hover(selector = nil, ref: nil)
399
249
  with_selector_fallback(:hover, selector, ref) do |sel, r|
400
250
  @client.hover(@name, sel, ref: r)
@@ -413,9 +263,6 @@ module Browserctl
413
263
  end
414
264
  end
415
265
 
416
- def dialog_accept(text: nil) = unwrap @client.dialog_accept(@name, text: text)
417
- def dialog_dismiss = unwrap @client.dialog_dismiss(@name)
418
-
419
266
  private
420
267
 
421
268
  # Issues the wrapped command. If the daemon returns selector_not_found
@@ -461,32 +308,24 @@ module Browserctl
461
308
  end
462
309
  end
463
310
 
464
- class WorkflowDefinition
465
- attr_reader :name, :description, :param_defs, :steps
466
-
467
- def initialize(name)
468
- @name = name
469
- @description = nil
470
- @param_defs = {}
471
- @steps = []
472
- end
473
-
474
- def desc(text)
475
- @description = text
476
- end
477
-
478
- def param(name, required: false, secret: false, default: nil, secret_ref: nil)
479
- secret = true if secret_ref
480
- @param_defs[name] =
481
- ParamDef.new(name: name, required: required, secret: secret, default: default, secret_ref: secret_ref)
482
- end
483
-
484
- def step(label, retry_count: 0, timeout: nil, &block)
485
- @steps << StepDef.new(label: label, block: block, retry_count: retry_count, timeout: timeout)
311
+ class WorkflowDefinition < CallableDefinition
312
+ def callable_kind
313
+ :workflow
486
314
  end
487
315
 
316
+ # Definition-time guard: composing a flow into a workflow would copy
317
+ # flow steps that may close over `page` (a flow-only DSL) into a
318
+ # context that doesn't expose it. Cross-kind composition is rejected
319
+ # here rather than failing later inside an `instance_exec`.
488
320
  def compose(workflow_name)
489
- source = Browserctl.lookup_workflow(workflow_name.to_s)
321
+ name = workflow_name.to_s
322
+ if Browserctl.lookup_flow(name)
323
+ raise ArgumentError,
324
+ "workflow '#{@name}' cannot compose flow '#{name}': flows return state, " \
325
+ "workflows share state — composition across kinds is not supported"
326
+ end
327
+
328
+ source = Browserctl.lookup_workflow(name)
490
329
  raise WorkflowError, "workflow '#{workflow_name}' not found for composition" unless source
491
330
 
492
331
  @steps.concat(source.steps)
@@ -499,6 +338,14 @@ module Browserctl
499
338
 
500
339
  private
501
340
 
341
+ def missing_param_error(name)
342
+ WorkflowError.new("required param '#{name}' missing")
343
+ end
344
+
345
+ def step_timeout_error(defn)
346
+ WorkflowError.new("step '#{defn.label}' timed out after #{defn.timeout}s")
347
+ end
348
+
502
349
  def execute_steps(ctx)
503
350
  @steps.map { |defn| run_step(ctx, defn) }.each do |r|
504
351
  raise WorkflowError, "step '#{r.name}' failed: #{r.error}" unless r.ok
@@ -508,36 +355,13 @@ module Browserctl
508
355
  def run_step(ctx, defn)
509
356
  last_error = nil
510
357
  (defn.retry_count + 1).times do
511
- execute_block(ctx, defn)
358
+ execute_step_block(ctx, defn)
512
359
  return StepResult.new(name: defn.label, ok: true)
513
360
  rescue StandardError => e
514
361
  last_error = e
515
362
  end
516
363
  StepResult.new(name: defn.label, ok: false, error: last_error.message)
517
364
  end
518
-
519
- def execute_block(ctx, defn)
520
- if defn.timeout
521
- ::Timeout.timeout(defn.timeout) { ctx.instance_exec(&defn.block) }
522
- else
523
- ctx.instance_exec(&defn.block)
524
- end
525
- rescue ::Timeout::Error
526
- raise WorkflowError, "step '#{defn.label}' timed out after #{defn.timeout}s"
527
- end
528
-
529
- def resolve_params(provided)
530
- @param_defs.each_with_object({}) do |(name, defn), out|
531
- val = if defn.secret_ref
532
- SecretResolverRegistry.resolve(defn.secret_ref)
533
- else
534
- provided[name] || defn.default
535
- end
536
- raise WorkflowError, "required param '#{name}' missing" if defn.required && val.nil?
537
-
538
- out[name] = val
539
- end
540
- end
541
365
  end
542
366
 
543
367
  @registry_mutex = Mutex.new
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: browserctl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -138,7 +138,6 @@ files:
138
138
  - bin/browserd
139
139
  - bin/setup
140
140
  - examples/cloudflare_hitl.rb
141
- - examples/session_reuse.rb
142
141
  - examples/test_automation_practices/advanced/ab_testing.rb
143
142
  - examples/test_automation_practices/advanced/broken_images.rb
144
143
  - examples/test_automation_practices/advanced/file_download.rb
@@ -165,6 +164,7 @@ files:
165
164
  - examples/the_internet/dynamic_loading.rb
166
165
  - examples/the_internet/login.rb
167
166
  - lib/browserctl.rb
167
+ - lib/browserctl/callable_definition.rb
168
168
  - lib/browserctl/client.rb
169
169
  - lib/browserctl/commands/ask.rb
170
170
  - lib/browserctl/commands/cli_output.rb
@@ -176,24 +176,24 @@ files:
176
176
  - lib/browserctl/commands/flow.rb
177
177
  - lib/browserctl/commands/init.rb
178
178
  - lib/browserctl/commands/migrate.rb
179
+ - lib/browserctl/commands/output_format.rb
179
180
  - lib/browserctl/commands/page.rb
180
- - lib/browserctl/commands/record.rb
181
+ - lib/browserctl/commands/recording.rb
181
182
  - lib/browserctl/commands/resume.rb
182
183
  - lib/browserctl/commands/screenshot.rb
183
- - lib/browserctl/commands/session.rb
184
184
  - lib/browserctl/commands/snapshot.rb
185
185
  - lib/browserctl/commands/state.rb
186
186
  - lib/browserctl/commands/storage.rb
187
187
  - lib/browserctl/commands/trace.rb
188
188
  - lib/browserctl/commands/workflow.rb
189
189
  - lib/browserctl/constants.rb
190
+ - lib/browserctl/contextual_persistence.rb
190
191
  - lib/browserctl/crash_report.rb
191
192
  - lib/browserctl/detectors.rb
192
193
  - lib/browserctl/detectors/auth_required.rb
193
- - lib/browserctl/driver.rb
194
- - lib/browserctl/driver/base.rb
195
194
  - lib/browserctl/driver/cdp.rb
196
195
  - lib/browserctl/driver/cdp_page.rb
196
+ - lib/browserctl/encryption_service.rb
197
197
  - lib/browserctl/error/codes.rb
198
198
  - lib/browserctl/error/exit_codes.rb
199
199
  - lib/browserctl/error/suggested_actions.rb
@@ -211,6 +211,10 @@ files:
211
211
  - lib/browserctl/migrations.rb
212
212
  - lib/browserctl/policy.rb
213
213
  - lib/browserctl/recording.rb
214
+ - lib/browserctl/recording/log_writer.rb
215
+ - lib/browserctl/recording/redactor.rb
216
+ - lib/browserctl/recording/state.rb
217
+ - lib/browserctl/recording/workflow_renderer.rb
214
218
  - lib/browserctl/redactor.rb
215
219
  - lib/browserctl/replay/context.rb
216
220
  - lib/browserctl/replay/fingerprint_matcher.rb
@@ -235,13 +239,11 @@ files:
235
239
  - lib/browserctl/server/handlers/navigation.rb
236
240
  - lib/browserctl/server/handlers/observation.rb
237
241
  - lib/browserctl/server/handlers/page_lifecycle.rb
238
- - lib/browserctl/server/handlers/session.rb
239
242
  - lib/browserctl/server/handlers/state.rb
240
243
  - lib/browserctl/server/handlers/storage.rb
241
244
  - lib/browserctl/server/idle_watcher.rb
242
245
  - lib/browserctl/server/page_session.rb
243
246
  - lib/browserctl/server/snapshot_builder.rb
244
- - lib/browserctl/session.rb
245
247
  - lib/browserctl/snapshot/annotator.rb
246
248
  - lib/browserctl/snapshot/extractor.rb
247
249
  - lib/browserctl/snapshot/fingerprint.rb
@@ -258,6 +260,7 @@ files:
258
260
  - lib/browserctl/workflow/flow_wrapper.rb
259
261
  - lib/browserctl/workflow/promoter.rb
260
262
  - lib/browserctl/workflow/promotion_ledger.rb
263
+ - lib/browserctl/workflow/recovery_manager.rb
261
264
  homepage: https://github.com/patrick204nqh/browserctl
262
265
  licenses:
263
266
  - MIT