ruby_everywhere 0.1.7 → 0.1.9

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.
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Everywhere
7
+ # The update-feed contract: bucket key layout + latest.json / index.json
8
+ # bodies. This is THE interface between `every publish`, the shell's Rust
9
+ # updater, and the Platform's release publishing — all three speak exactly
10
+ # these shapes, so keep changes here schema-versioned and additive.
11
+ #
12
+ # Layout (channel is a *release channel* — stable/beta — never the receipt's
13
+ # distribution_channel):
14
+ #
15
+ # {prefix?}/{channel}/{os}/{arch}/
16
+ # latest.json mutable pointer, written last, Cache-Control: no-cache
17
+ # index.json append-only publish history (rollback + stats source)
18
+ # My-App-1.2.0.zip immutable + .minisig sibling
19
+ # My-App-latest.zip convenience alias (CopyObject) for download links
20
+ #
21
+ # The shell only ever does:
22
+ # GET {updates.url}/{channel}/{os}/{arch}/latest.json?current={version}
23
+ # — a raw bucket/CDN or a dynamic Platform endpoint serve the identical JSON.
24
+ module UpdateManifest
25
+ SCHEMA = 1
26
+
27
+ IMMUTABLE_CACHE = "public, max-age=31536000, immutable"
28
+ MUTABLE_CACHE = "no-cache"
29
+
30
+ module_function
31
+
32
+ def dir(channel:, os:, arch:, prefix: nil)
33
+ [prefix, channel, os, arch].compact.reject(&:empty?).join("/")
34
+ end
35
+
36
+ def artifact_key(filename, **loc) = "#{dir(**loc)}/#{filename}"
37
+ def latest_key(**loc) = "#{dir(**loc)}/latest.json"
38
+ def index_key(**loc) = "#{dir(**loc)}/index.json"
39
+
40
+ # Where images embedded in release notes live: channel-scoped (shared by
41
+ # every os/arch target of the release), versioned so they're immutable and
42
+ # rollback-safe. notes_html rewrites embedded images to these URLs at
43
+ # publish time — signed/expiring storage URLs must never leak into a feed.
44
+ def notes_asset_key(filename, channel:, version:, prefix: nil)
45
+ "#{[ prefix, channel ].compact.reject(&:empty?).join("/")}/notes/#{version}/#{filename}"
46
+ end
47
+
48
+ # "My-App-1.2.0.zip" -> "My-App-latest.zip" (keeps the extension).
49
+ def latest_alias(filename, version)
50
+ aliased = filename.sub("-#{version}", "-latest")
51
+ aliased == filename ? nil : aliased
52
+ end
53
+
54
+ # One latest.json body (also an index.json entry), built from a build
55
+ # receipt (dist/release.json) + publish-time facts.
56
+ #
57
+ # Release notes ship in two forms: `notes` is the human-authored source
58
+ # (markdown; plain text is valid markdown), `notes_html` is pre-rendered
59
+ # sanitized-by-construction HTML the app can inject straight into a
60
+ # changelog modal via the bridge. Pass notes_html explicitly to override
61
+ # the default markdown rendering (the Platform does — its notes are
62
+ # authored rich).
63
+ def entry(receipt:, url:, signature:, channel:, notes: nil, notes_html: nil, published_at: Time.now.utc)
64
+ artifact = receipt.fetch("artifact")
65
+ sha256 = artifact.fetch("checksum").to_s.delete_prefix("sha256:")
66
+ notes_html ||= render_notes_html(notes)
67
+
68
+ {
69
+ "schema" => SCHEMA,
70
+ "app" => {
71
+ "name" => receipt.dig("app", "name"),
72
+ "bundle_id" => receipt.dig("app", "bundle_id")
73
+ },
74
+ "channel" => channel,
75
+ "os" => receipt.dig("target", "os"),
76
+ "arch" => receipt.dig("target", "arch"),
77
+ "version" => receipt.dig("app", "version"),
78
+ "published_at" => published_at.iso8601,
79
+ "notes" => notes,
80
+ "notes_html" => notes_html,
81
+ "url" => url,
82
+ "sha256" => sha256,
83
+ "size_bytes" => artifact.fetch("size_bytes"),
84
+ "signature" => signature,
85
+ "manifest_checksum" => receipt["manifest_checksum"]
86
+ }.compact
87
+ end
88
+
89
+ # index.json: append-only history, newest first. Re-publishing the same
90
+ # version replaces its entry (idempotent re-runs don't duplicate).
91
+ def index_append(existing_json, entry)
92
+ entries = existing_json ? JSON.parse(existing_json).fetch("entries", []) : []
93
+ entries = entries.reject { |e| e["version"] == entry["version"] }
94
+ JSON.pretty_generate({ "schema" => SCHEMA, "entries" => [entry] + entries }) << "\n"
95
+ end
96
+
97
+ def index_find(index_json, version)
98
+ return nil unless index_json
99
+
100
+ JSON.parse(index_json).fetch("entries", []).find { |e| e["version"] == version }
101
+ end
102
+
103
+ def to_json(entry) = JSON.pretty_generate(entry) << "\n"
104
+
105
+ # Markdown -> HTML for notes_html. Kramdown with GFM-ish niceties off the
106
+ # shelf; raw HTML in the source passes through (the feed is the app
107
+ # developer's own signed content — same trust as the artifact itself).
108
+ def render_notes_html(notes)
109
+ return nil if notes.nil? || notes.strip.empty?
110
+
111
+ require "kramdown"
112
+ Kramdown::Document.new(notes, auto_ids: false, entity_output: :as_char).to_html.strip
113
+ end
114
+ end
115
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Everywhere
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.9"
5
5
 
6
6
  # Version of the vendored @rubyeverywhere/bridge JS this gem ships. Tracks
7
7
  # bridge/package.json — bump it whenever lib/everywhere/javascript/bridge.js is
8
8
  # refreshed. `every install` stamps it into the vendored file so the build
9
9
  # receipt can record which bridge shipped; it versions independently of the CLI.
10
- BRIDGE_VERSION = "0.1.0"
10
+ BRIDGE_VERSION = "0.3.0"
11
11
  end
@@ -991,17 +991,21 @@ dependencies = [
991
991
 
992
992
  [[package]]
993
993
  name = "example-shell"
994
- version = "0.1.0"
994
+ version = "0.2.0"
995
995
  dependencies = [
996
+ "minisign-verify",
996
997
  "objc2",
997
998
  "objc2-foundation",
998
999
  "objc2-web-kit",
1000
+ "semver",
999
1001
  "serde_json",
1002
+ "sha2",
1000
1003
  "tauri",
1001
1004
  "tauri-build",
1002
1005
  "tauri-plugin-clipboard-manager",
1003
1006
  "tauri-plugin-dialog",
1004
1007
  "tauri-plugin-notification",
1008
+ "ureq",
1005
1009
  ]
1006
1010
 
1007
1011
  [[package]]
@@ -2087,6 +2091,12 @@ version = "0.3.17"
2087
2091
  source = "registry+https://github.com/rust-lang/crates.io-index"
2088
2092
  checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
2089
2093
 
2094
+ [[package]]
2095
+ name = "minisign-verify"
2096
+ version = "0.2.5"
2097
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2098
+ checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
2099
+
2090
2100
  [[package]]
2091
2101
  name = "miniz_oxide"
2092
2102
  version = "0.8.9"
@@ -2986,6 +2996,20 @@ dependencies = [
2986
2996
  "windows-sys 0.60.2",
2987
2997
  ]
2988
2998
 
2999
+ [[package]]
3000
+ name = "ring"
3001
+ version = "0.17.14"
3002
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3003
+ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
3004
+ dependencies = [
3005
+ "cc",
3006
+ "cfg-if",
3007
+ "getrandom 0.2.17",
3008
+ "libc",
3009
+ "untrusted",
3010
+ "windows-sys 0.52.0",
3011
+ ]
3012
+
2989
3013
  [[package]]
2990
3014
  name = "rustc-hash"
2991
3015
  version = "2.1.3"
@@ -3014,6 +3038,41 @@ dependencies = [
3014
3038
  "windows-sys 0.61.2",
3015
3039
  ]
3016
3040
 
3041
+ [[package]]
3042
+ name = "rustls"
3043
+ version = "0.23.42"
3044
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3045
+ checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
3046
+ dependencies = [
3047
+ "log",
3048
+ "once_cell",
3049
+ "ring",
3050
+ "rustls-pki-types",
3051
+ "rustls-webpki",
3052
+ "subtle",
3053
+ "zeroize",
3054
+ ]
3055
+
3056
+ [[package]]
3057
+ name = "rustls-pki-types"
3058
+ version = "1.15.0"
3059
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3060
+ checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
3061
+ dependencies = [
3062
+ "zeroize",
3063
+ ]
3064
+
3065
+ [[package]]
3066
+ name = "rustls-webpki"
3067
+ version = "0.103.13"
3068
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3069
+ checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
3070
+ dependencies = [
3071
+ "ring",
3072
+ "rustls-pki-types",
3073
+ "untrusted",
3074
+ ]
3075
+
3017
3076
  [[package]]
3018
3077
  name = "rustversion"
3019
3078
  version = "1.0.23"
@@ -3418,6 +3477,12 @@ version = "0.11.1"
3418
3477
  source = "registry+https://github.com/rust-lang/crates.io-index"
3419
3478
  checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
3420
3479
 
3480
+ [[package]]
3481
+ name = "subtle"
3482
+ version = "2.6.1"
3483
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3484
+ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
3485
+
3421
3486
  [[package]]
3422
3487
  name = "swift-rs"
3423
3488
  version = "1.0.7"
@@ -4326,6 +4391,30 @@ version = "1.13.3"
4326
4391
  source = "registry+https://github.com/rust-lang/crates.io-index"
4327
4392
  checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
4328
4393
 
4394
+ [[package]]
4395
+ name = "untrusted"
4396
+ version = "0.9.0"
4397
+ source = "registry+https://github.com/rust-lang/crates.io-index"
4398
+ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
4399
+
4400
+ [[package]]
4401
+ name = "ureq"
4402
+ version = "2.12.1"
4403
+ source = "registry+https://github.com/rust-lang/crates.io-index"
4404
+ checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
4405
+ dependencies = [
4406
+ "base64 0.22.1",
4407
+ "flate2",
4408
+ "log",
4409
+ "once_cell",
4410
+ "rustls",
4411
+ "rustls-pki-types",
4412
+ "serde",
4413
+ "serde_json",
4414
+ "url",
4415
+ "webpki-roots 0.26.11",
4416
+ ]
4417
+
4329
4418
  [[package]]
4330
4419
  name = "url"
4331
4420
  version = "2.5.8"
@@ -4639,6 +4728,24 @@ dependencies = [
4639
4728
  "system-deps",
4640
4729
  ]
4641
4730
 
4731
+ [[package]]
4732
+ name = "webpki-roots"
4733
+ version = "0.26.11"
4734
+ source = "registry+https://github.com/rust-lang/crates.io-index"
4735
+ checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
4736
+ dependencies = [
4737
+ "webpki-roots 1.0.9",
4738
+ ]
4739
+
4740
+ [[package]]
4741
+ name = "webpki-roots"
4742
+ version = "1.0.9"
4743
+ source = "registry+https://github.com/rust-lang/crates.io-index"
4744
+ checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
4745
+ dependencies = [
4746
+ "rustls-pki-types",
4747
+ ]
4748
+
4642
4749
  [[package]]
4643
4750
  name = "webview2-com"
4644
4751
  version = "0.38.2"
@@ -4875,6 +4982,15 @@ dependencies = [
4875
4982
  "windows-targets 0.42.2",
4876
4983
  ]
4877
4984
 
4985
+ [[package]]
4986
+ name = "windows-sys"
4987
+ version = "0.52.0"
4988
+ source = "registry+https://github.com/rust-lang/crates.io-index"
4989
+ checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
4990
+ dependencies = [
4991
+ "windows-targets 0.52.6",
4992
+ ]
4993
+
4878
4994
  [[package]]
4879
4995
  name = "windows-sys"
4880
4996
  version = "0.59.0"
@@ -5377,6 +5493,12 @@ dependencies = [
5377
5493
  "synstructure",
5378
5494
  ]
5379
5495
 
5496
+ [[package]]
5497
+ name = "zeroize"
5498
+ version = "1.9.0"
5499
+ source = "registry+https://github.com/rust-lang/crates.io-index"
5500
+ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
5501
+
5380
5502
  [[package]]
5381
5503
  name = "zerotrie"
5382
5504
  version = "0.2.4"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "example-shell"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Generic desktop shell: boots a packaged Rails app as a sidecar and shows it in a webview"
5
5
  edition = "2021"
6
6
 
@@ -13,6 +13,12 @@ serde_json = "1"
13
13
  tauri-plugin-notification = "2"
14
14
  tauri-plugin-dialog = "2"
15
15
  tauri-plugin-clipboard-manager = "2"
16
+ # Self-updater (src/updater.rs): blocking HTTP fits the shell's plain-thread
17
+ # style; minisign-verify is the zero-dep verifier for our publish signatures.
18
+ ureq = { version = "2", features = ["json"] }
19
+ semver = "1"
20
+ sha2 = "0.10"
21
+ minisign-verify = "0.2"
16
22
 
17
23
  # macOS: set the WebView's `applicationNameForUserAgent`, which WKWebView
18
24
  # natively APPENDS to its real default UA — so we get the true system UA plus
@@ -19,6 +19,8 @@ use std::time::{Duration, Instant};
19
19
 
20
20
  use tauri::{Manager, RunEvent, WebviewUrl, WebviewWindowBuilder};
21
21
 
22
+ mod updater;
23
+
22
24
  struct Sidecar(Mutex<Option<Child>>);
23
25
 
24
26
  struct MenuEntry {
@@ -141,6 +143,16 @@ fn main() {
141
143
  .plugin(tauri_plugin_clipboard_manager::init())
142
144
  .invoke_handler(tauri::generate_handler![notify_command])
143
145
  .setup(move |app| {
146
+ // A persisted user channel choice (Everywhere.updates.setChannel)
147
+ // overrides everywhere.yml's updates.channel. Fold it in before
148
+ // anything reads raw_json — the updater, the menu, and the
149
+ // __EVERYWHERE_CONFIG__ page injection all see the same channel.
150
+ let mut config = config;
151
+ if let Ok(mut value) = serde_json::from_str::<serde_json::Value>(&config.raw_json) {
152
+ updater::apply_channel_override(app.handle(), &mut value);
153
+ config.raw_json = value.to_string();
154
+ }
155
+
144
156
  // Bridge channel: pages emit events (the sanctioned webview->shell
145
157
  // path for remote-origin content in Tauri v2; invoke is reserved
146
158
  // for local content). Fire-and-forget, Strada-style.
@@ -158,6 +170,7 @@ fn main() {
158
170
  });
159
171
  }
160
172
  build_menu(app, &config)?;
173
+ updater::init(app, &config.raw_json);
161
174
 
162
175
  // Dev mode (`every dev`): no sidecar, just point the webview at the
163
176
  // developer's normal dev server.
@@ -295,6 +308,9 @@ fn main() {
295
308
  .expect("error while building tauri application")
296
309
  .run(|app, event| {
297
310
  if let RunEvent::Exit = event {
311
+ // A fully staged silent update (updates.auto: install) swaps
312
+ // in now — two renames, no perceptible quit latency.
313
+ updater::install_pending_on_exit(app);
298
314
  // No sidecar state in dev mode (NATIVE_DEV_URL).
299
315
  if let Some(sidecar) = app.try_state::<Sidecar>() {
300
316
  if let Some(mut child) = sidecar.0.lock().unwrap().take() {
@@ -332,6 +348,11 @@ fn build_menu(app: &tauri::App, config: &AppConfig) -> tauri::Result<()> {
332
348
  .name(Some(config.name.clone()))
333
349
  .build();
334
350
  let mut app_menu = SubmenuBuilder::new(app, &config.name).about(Some(about));
351
+ if updater::enabled(&config.raw_json) {
352
+ app_menu = app_menu.separator().item(
353
+ &MenuItemBuilder::with_id("update-check", "Check for Updates…").build(app)?,
354
+ );
355
+ }
335
356
  if !config.menu.is_empty() {
336
357
  app_menu = app_menu.separator();
337
358
  for entry in &config.menu {
@@ -406,6 +427,11 @@ fn handle_menu_id(app: &tauri::AppHandle, id: &str) {
406
427
  match id {
407
428
  "quit" => app.exit(0),
408
429
  "show" => focus_main(app),
430
+ // Blocking dialogs + network — never on the main thread.
431
+ "update-check" => {
432
+ let handle = app.clone();
433
+ std::thread::spawn(move || updater::interactive_check(&handle));
434
+ }
409
435
  "reload" => {
410
436
  if let Some(w) = app.get_webview_window("main") {
411
437
  let _ = w.eval("location.reload()");