Module: Browserctl::State
- Defined in:
- lib/browserctl/state.rb,
lib/browserctl/state/bundle.rb,
lib/browserctl/state/transport.rb,
lib/browserctl/state/transports/s3.rb,
lib/browserctl/state/transports/file.rb,
lib/browserctl/state/transports/one_password.rb
Overview
Top-level state store: a single .bctl bundle per name under ~/.browserctl/state/<name>.bctl. Wraps the Bundle codec with on-disk naming, validation, and a small inventory API used by ‘state list/info`.
Data shape inside a bundle:
manifest = {
name: String,
version: 1, # bundle schema version
producer: "browserctl/<gem-ver>",
created_at: ISO-8601,
origins: [String, ...],
flow: String | nil, # bound flow name, for `state rotate`
flow_version: String | nil,
expires_at: ISO-8601 | nil, # earliest cookie expiry
encrypted: Boolean
}
payload = {
cookies: [Hash, ...],
local_storage: { origin => { key => value } },
session_storage: { origin => { key => value } }
}
Defined Under Namespace
Modules: Transport, Transports Classes: Bundle
Constant Summary collapse
- BASE_DIR =
File.join(BROWSERCTL_DIR, "state")
- SAFE_NAME =
/\A[a-zA-Z0-9_-]{1,64}\z/- EXTENSION =
".bctl"- MANIFEST_VERSION =
1
Class Method Summary collapse
-
.all ⇒ Object
Read manifests for all stored bundles.
- .delete(name) ⇒ Object
- .exist?(name) ⇒ Boolean
-
.export(name, destination) ⇒ Object
Copies the on-disk .bctl bundle to a transport-addressable destination (file path, s3://bucket/key, op://Vault/Item, or any registered scheme).
-
.import(source, name: nil) ⇒ Object
Pulls a bundle from a transport-addressable source and stores it as a local state.
-
.info(name) ⇒ Object
Inspect a single bundle without decrypting the payload.
-
.load(name, passphrase: nil) ⇒ Object
Load and decode a bundle.
- .path(name) ⇒ Object
-
.save(name, payload:, origins: nil, flow: nil, flow_version: nil, passphrase: nil) ⇒ Object
Persist a bundle.
- .validate_name!(name) ⇒ Object
Class Method Details
.all ⇒ Object
Read manifests for all stored bundles. Errors on a single file are surfaced via { error: “…”, path: “…” } rather than aborting the list.
129 130 131 132 133 134 135 |
# File 'lib/browserctl/state.rb', line 129 def self.all return [] unless Dir.exist?(BASE_DIR) Dir[File.join(BASE_DIR, "*#{EXTENSION}")].map do |file| info_for(file) end end |
.delete(name) ⇒ Object
82 83 84 85 |
# File 'lib/browserctl/state.rb', line 82 def self.delete(name) validate_name!(name) FileUtils.rm_f(path(name)) end |
.exist?(name) ⇒ Boolean
46 |
# File 'lib/browserctl/state.rb', line 46 def self.exist?(name) = File.exist?(path(name)) |
.export(name, destination) ⇒ Object
Copies the on-disk .bctl bundle to a transport-addressable destination (file path, s3://bucket/key, op://Vault/Item, or any registered scheme). Bundle bytes are written verbatim — no re-encoding — so the receiving side can verify the manifest/payload exactly as produced.
91 92 93 94 95 96 97 98 99 |
# File 'lib/browserctl/state.rb', line 91 def self.export(name, destination) validate_name!(name) raise Browserctl::Error, "state '#{name}' not found" unless exist?(name) transport, parsed = Transport.for(destination) blob = ::File.binread(path(name)) transport.write(parsed, blob) { name: name, destination: destination, bytes: blob.bytesize } end |
.import(source, name: nil) ⇒ Object
Pulls a bundle from a transport-addressable source and stores it as a local state. Validates the magic header before persisting so we never leave a corrupt bundle in the state directory. ‘name` defaults to the source’s basename without ‘.bctl`.
105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/browserctl/state.rb', line 105 def self.import(source, name: nil) transport, parsed = Transport.for(source) blob = transport.read(parsed) raise Bundle::BundleError, "imported blob is not a .bctl bundle" unless blob.start_with?(Bundle::MAGIC) manifest = Bundle.peek_manifest(blob) target_name = name || derive_name(source) || manifest[:name] validate_name!(target_name) FileUtils.mkdir_p(BASE_DIR) ::File.open(path(target_name), "wb", 0o600) { |f| f.write(blob) } { name: target_name, source: source, bytes: blob.bytesize, encrypted: manifest[:encrypted] } end |
.info(name) ⇒ Object
Inspect a single bundle without decrypting the payload.
138 139 140 141 142 143 |
# File 'lib/browserctl/state.rb', line 138 def self.info(name) validate_name!(name) raise Browserctl::Error, "state '#{name}' not found" unless exist?(name) info_for(path(name)) end |
.load(name, passphrase: nil) ⇒ Object
Load and decode a bundle. Returns { manifest:, payload:, encrypted: }.
75 76 77 78 79 80 |
# File 'lib/browserctl/state.rb', line 75 def self.load(name, passphrase: nil) validate_name!(name) raise Browserctl::Error, "state '#{name}' not found" unless exist?(name) Bundle.decode(File.binread(path(name)), passphrase: passphrase) end |
.path(name) ⇒ Object
45 |
# File 'lib/browserctl/state.rb', line 45 def self.path(name) = File.join(BASE_DIR, "#{name}#{EXTENSION}") |
.save(name, payload:, origins: nil, flow: nil, flow_version: nil, passphrase: nil) ⇒ Object
Persist a bundle. ‘payload` is { cookies:, local_storage:, session_storage: }. `manifest_extras` may carry origins (override), flow, flow_version.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/browserctl/state.rb', line 56 def self.save(name, payload:, origins: nil, flow: nil, flow_version: nil, passphrase: nil) # rubocop:disable Metrics/ParameterLists validate_name!(name) FileUtils.mkdir_p(BASE_DIR) manifest = build_manifest( name: name, origins: origins || derive_origins(payload), flow: flow, flow_version: flow_version, cookies: payload[:cookies] || payload["cookies"] || [], encrypted: !passphrase.nil? ) blob = Bundle.encode(manifest: manifest, payload: payload, passphrase: passphrase) File.open(path(name), "wb", 0o600) { |f| f.write(blob) } manifest end |
.validate_name!(name) ⇒ Object
48 49 50 51 52 |
# File 'lib/browserctl/state.rb', line 48 def self.validate_name!(name) return if SAFE_NAME.match?(name.to_s) raise ArgumentError, "invalid state name #{name.inspect} — use letters, digits, _ or - (max 64 chars)" end |