ruby_everywhere 0.3.0 → 0.5.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.
- checksums.yaml +4 -4
- data/README.md +339 -0
- data/bridge/README.md +70 -1
- data/bridge/everywhere/bridge.js +211 -3
- data/bridge/everywhere/native.css +61 -0
- data/lib/everywhere/android_resources.rb +217 -0
- data/lib/everywhere/android_sdk.rb +330 -0
- data/lib/everywhere/asset_catalog.rb +185 -0
- data/lib/everywhere/auth_handoff.rb +404 -0
- data/lib/everywhere/auth_token.rb +118 -0
- data/lib/everywhere/builders/android.rb +844 -0
- data/lib/everywhere/builders/desktop.rb +326 -0
- data/lib/everywhere/builders/ios.rb +40 -3
- data/lib/everywhere/child_processes.rb +74 -0
- data/lib/everywhere/commands/build.rb +95 -14
- data/lib/everywhere/commands/clean.rb +10 -5
- data/lib/everywhere/commands/dev.rb +462 -57
- data/lib/everywhere/commands/doctor.rb +89 -3
- data/lib/everywhere/commands/install.rb +21 -1
- data/lib/everywhere/commands/logs.rb +117 -8
- data/lib/everywhere/commands/release.rb +2 -2
- data/lib/everywhere/config.rb +703 -13
- data/lib/everywhere/console.rb +117 -0
- data/lib/everywhere/desktop_assets.rb +150 -0
- data/lib/everywhere/dock/footer.rb +150 -0
- data/lib/everywhere/dock/screen.rb +114 -0
- data/lib/everywhere/dock/state.rb +59 -0
- data/lib/everywhere/dock.rb +238 -0
- data/lib/everywhere/emulator.rb +405 -0
- data/lib/everywhere/engine.rb +12 -0
- data/lib/everywhere/fatal.rb +20 -0
- data/lib/everywhere/icon.rb +196 -0
- data/lib/everywhere/line_pump.rb +89 -0
- data/lib/everywhere/log_filter.rb +111 -0
- data/lib/everywhere/native_helper.rb +39 -9
- data/lib/everywhere/omniauth.rb +88 -0
- data/lib/everywhere/paths.rb +109 -11
- data/lib/everywhere/relay.rb +77 -0
- data/lib/everywhere/shellout.rb +90 -15
- data/lib/everywhere/task_pool.rb +123 -0
- data/lib/everywhere/ui.rb +54 -11
- data/lib/everywhere/version.rb +1 -1
- data/support/desktop/README.md +121 -0
- data/support/{shell → desktop}/src-tauri/Cargo.lock +23 -0
- data/support/{shell → desktop}/src-tauri/Cargo.toml +19 -1
- data/support/{shell → desktop}/src-tauri/capabilities/default.json +7 -0
- data/support/desktop/src-tauri/gen/schemas/capabilities.json +1 -0
- data/support/desktop/src-tauri/src/extension_host.rs +53 -0
- data/support/desktop/src-tauri/src/extensions/mod.rs +39 -0
- data/support/{shell → desktop}/src-tauri/src/main.rs +355 -10
- data/support/{shell → desktop}/src-tauri/tauri.conf.json +2 -2
- data/support/mobile/android/README.md +217 -0
- data/support/mobile/android/app/build.gradle.kts +156 -0
- data/support/mobile/android/app/everywhere.properties +8 -0
- data/support/mobile/android/app/native-packages.gradle.kts +16 -0
- data/support/mobile/android/app/proguard-rules.pro +21 -0
- data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
- data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
- data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
- data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
- data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
- data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
- data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
- data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
- data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
- data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
- data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
- data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
- data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
- data/support/mobile/android/build.gradle.kts +8 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
- data/support/mobile/android/gradle.properties +14 -0
- data/support/mobile/android/gradlew +251 -0
- data/support/mobile/android/gradlew.bat +94 -0
- data/support/mobile/android/settings.gradle.kts +28 -0
- data/support/mobile/ios/App/AppDelegate.swift +5 -0
- data/support/mobile/ios/App/AuthFlow.swift +101 -0
- data/support/mobile/ios/App/EverywhereConfig.swift +84 -0
- data/support/mobile/ios/App/SceneDelegate.swift +103 -0
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +4 -0
- data/support/mobile/ios/README.md +4 -1
- data/support/{macos → release/macos}/notarize.sh +2 -2
- metadata +97 -17
- data/support/github/build.yml +0 -85
- data/support/shell/src-tauri/gen/schemas/capabilities.json +0 -1
- /data/support/{shell → desktop}/splash/index.html +0 -0
- /data/support/{shell → desktop}/src-tauri/build.rs +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/acl-manifests.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/desktop-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/macOS-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/icons/icon.png +0 -0
- /data/support/{shell → desktop}/src-tauri/src/updater.rs +0 -0
- /data/support/{macos → release/macos}/entitlements.plist +0 -0
data/lib/everywhere/icon.rb
CHANGED
|
@@ -37,6 +37,83 @@ module Everywhere
|
|
|
37
37
|
ICO_SIZES = [16, 32, 48, 64, 128, 256].freeze
|
|
38
38
|
LINUX_SIZES = [16, 32, 48, 64, 128, 256, 512].freeze
|
|
39
39
|
|
|
40
|
+
# The adaptive icon's foreground layer, one entry per density bucket (mdpi
|
|
41
|
+
# is 1x, the rest step 1.5/2/3/4x) of the 108dp layer. No legacy
|
|
42
|
+
# mipmap-<dpi>/ic_launcher.png companions: the shell's minSdk is 28, so
|
|
43
|
+
# every device that can install the app resolves @mipmap/ic_launcher to the
|
|
44
|
+
# anydpi-v26 adaptive icon and could never load them.
|
|
45
|
+
ANDROID_ADAPTIVE_SIZES = {
|
|
46
|
+
"mdpi" => 108, "hdpi" => 162, "xhdpi" => 216, "xxhdpi" => 324, "xxxhdpi" => 432
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Adaptive-icon safe-zone geometry, as a fraction of the 108dp layer.
|
|
50
|
+
#
|
|
51
|
+
# The launcher masks both layers to a shape *it* chooses and can parallax
|
|
52
|
+
# them, so only the centre 72dp is ever guaranteed visible — the outer 18dp
|
|
53
|
+
# on each side is headroom. The most aggressive stock mask is the circle
|
|
54
|
+
# inscribed in that 72dp viewport, and Google's keyline pulls the safe zone
|
|
55
|
+
# one step further in, to a 66dp circle. So a logo goes *inside* that
|
|
56
|
+
# circle, bounding box and all: side = 66/sqrt(2) = 46.7dp, i.e. 0.432 of
|
|
57
|
+
# the layer. Scaling a logo to the 72dp viewport instead is the usual
|
|
58
|
+
# mistake — it looks correct in a square preview and loses every corner on
|
|
59
|
+
# the round-mask launchers most devices ship with.
|
|
60
|
+
ADAPTIVE_ART = (66.0 / Math.sqrt(2)) / 108.0
|
|
61
|
+
|
|
62
|
+
# Alpha at or below this is background, not artwork — it survives PNG
|
|
63
|
+
# export slop and antialiased edges without counting them as content.
|
|
64
|
+
CONTENT_ALPHA = 8
|
|
65
|
+
|
|
66
|
+
# Opaque fraction of the source above which it is treated as full-bleed art
|
|
67
|
+
# rather than a logo. `config.icon` is one shared icon.png feeding macOS,
|
|
68
|
+
# iOS *and* Android, and the iOS path wants it opaque corner to corner, so
|
|
69
|
+
# in practice it usually is. Insetting that into the safe zone would render
|
|
70
|
+
# the source's own baked background as a shrunken tile floating inside the
|
|
71
|
+
# launcher's background colour — two nested backgrounds and a tiny logo.
|
|
72
|
+
#
|
|
73
|
+
# 0.95 sits in an empty gap. Shaped-but-full-bleed sources stay above it: a
|
|
74
|
+
# Big Sur squircle keeps ~99% of its bounding square, a 22%-rounded rect
|
|
75
|
+
# ~97%. Anything with a deliberate margin falls far below — even a mere 5%
|
|
76
|
+
# border leaves 81%. Nothing real lands near the line.
|
|
77
|
+
FULL_BLEED_COVERAGE = 0.95
|
|
78
|
+
|
|
79
|
+
# Both launcher aliases resolve to the same layers; ic_launcher_round exists
|
|
80
|
+
# only because the manifest's android:roundIcon asks for it by name. The
|
|
81
|
+
# layers are @mipmap, not the template placeholder's @drawable, because
|
|
82
|
+
# what gets stamped is a raster per density rather than one vector.
|
|
83
|
+
ANDROID_ADAPTIVE_ICON_XML = <<~XML
|
|
84
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
85
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
86
|
+
<background android:drawable="@color/ic_launcher_background" />
|
|
87
|
+
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
|
88
|
+
</adaptive-icon>
|
|
89
|
+
XML
|
|
90
|
+
|
|
91
|
+
# Android 13+ themed ("Material You") icons tint the monochrome layer's
|
|
92
|
+
# *alpha* and throw its colour away, so the layer is only worth emitting
|
|
93
|
+
# when the alpha is already a silhouette — i.e. the inset branch, where the
|
|
94
|
+
# source arrived as a logo on transparency. Full-bleed art would tint as a
|
|
95
|
+
# solid filled square, which is worse than the launcher's own fallback, so
|
|
96
|
+
# that branch ships the icon without a monochrome layer and lets the
|
|
97
|
+
# fallback happen.
|
|
98
|
+
ANDROID_THEMED_ADAPTIVE_ICON_XML = <<~XML
|
|
99
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
100
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
101
|
+
<background android:drawable="@color/ic_launcher_background" />
|
|
102
|
+
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
|
103
|
+
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
|
|
104
|
+
</adaptive-icon>
|
|
105
|
+
XML
|
|
106
|
+
|
|
107
|
+
# The background layer is a colour, not a bitmap: it has to survive being
|
|
108
|
+
# scaled and shifted by the launcher's parallax, which a PNG can't do
|
|
109
|
+
# without banding or seams at the edges.
|
|
110
|
+
ANDROID_BACKGROUND_XML = <<~XML
|
|
111
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
112
|
+
<resources>
|
|
113
|
+
<color name="ic_launcher_background">%s</color>
|
|
114
|
+
</resources>
|
|
115
|
+
XML
|
|
116
|
+
|
|
40
117
|
# Modern (Xcode 14+) single-size asset catalog entry: one 1024px universal
|
|
41
118
|
# image, iOS derives every size itself.
|
|
42
119
|
IOS_APPICON_CONTENTS = <<~JSON
|
|
@@ -112,6 +189,48 @@ module Everywhere
|
|
|
112
189
|
true
|
|
113
190
|
end
|
|
114
191
|
|
|
192
|
+
# Write the Android launcher icon under `res_dir`: an adaptive icon's
|
|
193
|
+
# foreground layer per density, the solid-colour background resource, and
|
|
194
|
+
# the anydpi-v26 <adaptive-icon> pair that binds them (ic_launcher and the
|
|
195
|
+
# ic_launcher_round alias the manifest names). How large the artwork lands,
|
|
196
|
+
# and whether it can carry a themed-icon layer, both follow from what the
|
|
197
|
+
# source turns out to be — see android_art. `background` is an [r, g, b]
|
|
198
|
+
# triple as Icon.hex_rgb returns, default white. Returns true, or false if
|
|
199
|
+
# the source PNG can't be read — same contract as write_ios_appiconset, so
|
|
200
|
+
# the builder can warn and ship the placeholder.
|
|
201
|
+
def write_android_mipmaps(source, res_dir, background: nil)
|
|
202
|
+
raster = Raster.decode_png(source) or return false
|
|
203
|
+
|
|
204
|
+
rgb = Array(background || [255, 255, 255])[0, 3]
|
|
205
|
+
art, full_bleed = android_art(raster)
|
|
206
|
+
|
|
207
|
+
ANDROID_ADAPTIVE_SIZES.each do |density, layer|
|
|
208
|
+
dir = File.join(res_dir, "mipmap-#{density}")
|
|
209
|
+
FileUtils.mkdir_p(dir)
|
|
210
|
+
|
|
211
|
+
# The foreground keeps its alpha — the launcher composites it over the
|
|
212
|
+
# background layer, and its mask needs the surround empty to cut into.
|
|
213
|
+
foreground = if full_bleed
|
|
214
|
+
filling(art, layer)
|
|
215
|
+
else
|
|
216
|
+
centred(art, layer, (layer * ADAPTIVE_ART).round)
|
|
217
|
+
end
|
|
218
|
+
foreground.write_png(File.join(dir, "ic_launcher_foreground.png"))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
xml = full_bleed ? ANDROID_ADAPTIVE_ICON_XML : ANDROID_THEMED_ADAPTIVE_ICON_XML
|
|
222
|
+
anydpi = File.join(res_dir, "mipmap-anydpi-v26")
|
|
223
|
+
FileUtils.mkdir_p(anydpi)
|
|
224
|
+
File.write(File.join(anydpi, "ic_launcher.xml"), xml)
|
|
225
|
+
File.write(File.join(anydpi, "ic_launcher_round.xml"), xml)
|
|
226
|
+
|
|
227
|
+
values = File.join(res_dir, "values")
|
|
228
|
+
FileUtils.mkdir_p(values)
|
|
229
|
+
File.write(File.join(values, "ic_launcher_background.xml"),
|
|
230
|
+
format(ANDROID_BACKGROUND_XML, format("#%02X%02X%02X", *rgb)))
|
|
231
|
+
true
|
|
232
|
+
end
|
|
233
|
+
|
|
115
234
|
# Build a macOS .icns from an already-shaped master PNG (ideally 1024px).
|
|
116
235
|
# Returns true on success.
|
|
117
236
|
def write_icns(master_png, dest)
|
|
@@ -167,6 +286,83 @@ module Everywhere
|
|
|
167
286
|
m.captures.map { |c| c.to_i(16) }
|
|
168
287
|
end
|
|
169
288
|
|
|
289
|
+
# How this source wants to be scaled onto a launcher layer. Returns
|
|
290
|
+
# [art, full_bleed] — either the raster untouched, to be scaled edge to
|
|
291
|
+
# edge because it is art designed to be cropped by the mask, or the source
|
|
292
|
+
# cropped to its opaque bounding box, to be inset into the safe zone.
|
|
293
|
+
#
|
|
294
|
+
# Cropping matters for the inset branch: scaling the whole canvas would let
|
|
295
|
+
# the source's own transparent padding eat into the 66dp circle and land
|
|
296
|
+
# the logo smaller than the safe zone actually allows. A source with no
|
|
297
|
+
# content at all is nothing to measure, so it takes the full-bleed path.
|
|
298
|
+
def android_art(raster)
|
|
299
|
+
bounds, coverage = content_bounds(raster)
|
|
300
|
+
return [raster, true] if bounds.nil? || coverage >= FULL_BLEED_COVERAGE
|
|
301
|
+
|
|
302
|
+
[crop(raster, *bounds), false]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# [[x0, y0, x1, y1], coverage] for the pixels above CONTENT_ALPHA — the
|
|
306
|
+
# half-open bounding box of the artwork and the fraction of the canvas it
|
|
307
|
+
# actually paints. [nil, 0.0] when the raster is entirely transparent.
|
|
308
|
+
def content_bounds(raster)
|
|
309
|
+
x0 = raster.width
|
|
310
|
+
y0 = raster.height
|
|
311
|
+
x1 = 0
|
|
312
|
+
y1 = 0
|
|
313
|
+
opaque = 0
|
|
314
|
+
|
|
315
|
+
raster.height.times do |y|
|
|
316
|
+
base = y * raster.width
|
|
317
|
+
raster.width.times do |x|
|
|
318
|
+
next if raster.pixels.getbyte(((base + x) * 4) + 3) <= CONTENT_ALPHA
|
|
319
|
+
|
|
320
|
+
opaque += 1
|
|
321
|
+
x0 = x if x < x0
|
|
322
|
+
x1 = x + 1 if x >= x1
|
|
323
|
+
y0 = y if y < y0
|
|
324
|
+
y1 = y + 1 if y >= y1
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
return [nil, 0.0] if opaque.zero?
|
|
329
|
+
|
|
330
|
+
[[x0, y0, x1, y1], opaque.to_f / (raster.width * raster.height)]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# The [x0, y0, x1, y1) region of `raster` as a new raster.
|
|
334
|
+
def crop(raster, x0, y0, x1, y1)
|
|
335
|
+
w = x1 - x0
|
|
336
|
+
h = y1 - y0
|
|
337
|
+
out = String.new(capacity: w * h * 4)
|
|
338
|
+
h.times { |y| out << raster.pixels.byteslice(((((y0 + y) * raster.width) + x0) * 4), w * 4) }
|
|
339
|
+
Raster.new(w, h, out)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# `raster` scaled to cover a `canvas`x`canvas` square and centre-cropped to
|
|
343
|
+
# it — no transparent border anywhere. Full-bleed art is meant to be
|
|
344
|
+
# cropped, so a non-square source loses its overhang rather than exposing
|
|
345
|
+
# bands of background the launcher's mask would read as a mistake.
|
|
346
|
+
def filling(raster, canvas)
|
|
347
|
+
fill = [canvas.to_f / raster.width, canvas.to_f / raster.height].max
|
|
348
|
+
sw = [(raster.width * fill).ceil, canvas].max
|
|
349
|
+
sh = [(raster.height * fill).ceil, canvas].max
|
|
350
|
+
crop(raster.resample(sw, sh), (sw - canvas) / 2, (sh - canvas) / 2,
|
|
351
|
+
((sw - canvas) / 2) + canvas, ((sh - canvas) / 2) + canvas)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# `raster` scaled to fit a `box`-sized square (aspect preserved, never
|
|
355
|
+
# stretched) and centred on a transparent `canvas`x`canvas` raster.
|
|
356
|
+
def centred(raster, canvas, box)
|
|
357
|
+
fit = [box.to_f / raster.width, box.to_f / raster.height].min
|
|
358
|
+
sw = (raster.width * fit).round.clamp(1, box)
|
|
359
|
+
sh = (raster.height * fit).round.clamp(1, box)
|
|
360
|
+
|
|
361
|
+
out = Raster.transparent(canvas, canvas)
|
|
362
|
+
out.paste!(raster.resample(sw, sh), (canvas - sw) / 2, (canvas - sh) / 2)
|
|
363
|
+
out
|
|
364
|
+
end
|
|
365
|
+
|
|
170
366
|
# PNG bytes of `master` at `size`x`size` (no-op resample when already sized).
|
|
171
367
|
def sized_png(master, size)
|
|
172
368
|
(size == master.width && size == master.height ? master : master.resample(size, size)).encode_png
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
# Turns the raw byte stream off a pty master into whole lines.
|
|
5
|
+
#
|
|
6
|
+
# `every dev` relays the dev server and the desktop shell itself so it can tag
|
|
7
|
+
# each line with its source. That relay has to be line-oriented — a half-line
|
|
8
|
+
# written to the terminal would leave the cursor mid-row and the dock repaints
|
|
9
|
+
# from column 1 — but a pty hands over arbitrary chunks, so this buffers.
|
|
10
|
+
#
|
|
11
|
+
# Two pty-specific quirks it exists to absorb:
|
|
12
|
+
#
|
|
13
|
+
# * ONLCR. The slave's line discipline turns every LF into CRLF, so lines
|
|
14
|
+
# arrive as "text\r\n". The trailing CR is an artifact, not content.
|
|
15
|
+
# * In-place rewrites. cargo (and anything with a progress bar) redraws a
|
|
16
|
+
# row by writing CR and the new text without a newline. What the terminal
|
|
17
|
+
# would have shown is whatever followed the LAST CR, so that is what gets
|
|
18
|
+
# emitted — one settled line instead of thirty flickering ones.
|
|
19
|
+
#
|
|
20
|
+
# Pure: feed it strings, get lines back. No IO, no threads, no clock beyond
|
|
21
|
+
# the idle deadline the caller passes in.
|
|
22
|
+
class LinePump
|
|
23
|
+
# How long a partial line may sit unterminated before it is emitted anyway.
|
|
24
|
+
# Without this a progress bar that never writes a newline — or a prompt like
|
|
25
|
+
# "Overwrite? [Yn]" — would be invisible for as long as it mattered.
|
|
26
|
+
IDLE_FLUSH = 0.2
|
|
27
|
+
|
|
28
|
+
def initialize(idle_flush: IDLE_FLUSH, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
29
|
+
# Bytes in, text out. A pty hands over BINARY, and a chunk boundary can
|
|
30
|
+
# land in the middle of a multi-byte character, so buffering and splitting
|
|
31
|
+
# happen on bytes — UTF-8 is self-synchronizing, so a newline is never
|
|
32
|
+
# inside a character — and each settled line is transcoded on the way out.
|
|
33
|
+
@buffer = String.new(encoding: Encoding::BINARY)
|
|
34
|
+
@idle_flush = idle_flush
|
|
35
|
+
@clock = clock
|
|
36
|
+
@touched = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Feed a chunk; returns the complete lines it produced (without newlines).
|
|
40
|
+
def <<(chunk)
|
|
41
|
+
@buffer << chunk.b
|
|
42
|
+
@touched = @clock.call
|
|
43
|
+
lines = []
|
|
44
|
+
while (index = @buffer.index("\n"))
|
|
45
|
+
raw = @buffer.slice!(0, index + 1)
|
|
46
|
+
line = settle(raw.chomp("\n"))
|
|
47
|
+
lines << line if line
|
|
48
|
+
end
|
|
49
|
+
lines
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Emit a still-unterminated partial line once it has gone quiet. Call this
|
|
53
|
+
# from the relay loop whenever a read times out.
|
|
54
|
+
def flush_idle
|
|
55
|
+
return [] if @buffer.empty?
|
|
56
|
+
return [] if @touched && (@clock.call - @touched) < @idle_flush
|
|
57
|
+
|
|
58
|
+
lines = drain
|
|
59
|
+
@touched = @clock.call
|
|
60
|
+
lines
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Emit whatever is left, terminated or not. For end-of-stream.
|
|
64
|
+
def drain
|
|
65
|
+
return [] if @buffer.empty?
|
|
66
|
+
|
|
67
|
+
line = settle(@buffer)
|
|
68
|
+
@buffer = +""
|
|
69
|
+
line ? [line] : []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pending? = !@buffer.empty?
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# chomp the ONLCR artifact first, THEN collapse an in-place rewrite —
|
|
77
|
+
# order matters. Doing it the other way round reads "bar\r" as a rewrite to
|
|
78
|
+
# the empty string and swallows the line.
|
|
79
|
+
#
|
|
80
|
+
# scrub, not just force_encoding: a build tool can emit a stray byte that is
|
|
81
|
+
# not valid UTF-8, and a String that claims an encoding it doesn't satisfy
|
|
82
|
+
# raises the moment anything tries to concatenate a tag onto it.
|
|
83
|
+
def settle(raw)
|
|
84
|
+
line = raw.chomp("\r")
|
|
85
|
+
line = line[(line.rindex("\r") + 1)..] if line.include?("\r")
|
|
86
|
+
line.dup.force_encoding(Encoding::UTF_8).scrub
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -30,6 +30,8 @@ module Everywhere
|
|
|
30
30
|
when :notarize then notarize(line)
|
|
31
31
|
when :tebako then tebako(line)
|
|
32
32
|
when :xcodebuild then xcodebuild(line)
|
|
33
|
+
when :gradle then gradle(line)
|
|
34
|
+
when :cargo then cargo(line)
|
|
33
35
|
end
|
|
34
36
|
end
|
|
35
37
|
|
|
@@ -153,6 +155,115 @@ module Everywhere
|
|
|
153
155
|
end
|
|
154
156
|
end
|
|
155
157
|
|
|
158
|
+
# --- gradle (Android shell) -----------------------------------------------
|
|
159
|
+
#
|
|
160
|
+
# Gradle is the quiet one: a cold build is ~120 lines, ~83 of them
|
|
161
|
+
# "> Task :app:…". So this filter isn't fighting volume like the two above —
|
|
162
|
+
# it exists because of two specific noises that make a healthy build look
|
|
163
|
+
# broken. Gradle 9 ends every build with a four-line "incompatible with
|
|
164
|
+
# Gradle 10" deprecation banner earned by the Android Gradle Plugin, not by
|
|
165
|
+
# anything the user wrote; and hotwire-native-android 1.3.0 compiled with
|
|
166
|
+
# Kotlin 2.3.0 emits exactly 7 `w:` warnings from navigation-fragments
|
|
167
|
+
# (deprecated overrides, getParcelableArrayList, KT-73255) that no app can
|
|
168
|
+
# act on. Both are suppressed here and both stay in the on-disk log.
|
|
169
|
+
#
|
|
170
|
+
# The line that decides a warning's fate is its path: app-authored Kotlin is
|
|
171
|
+
# stamped into extensions/, and that's the only code the user can fix.
|
|
172
|
+
MILESTONES = {
|
|
173
|
+
/:process\w*Manifest\b/ => "merging manifests",
|
|
174
|
+
/:merge\w*Resources\b/ => "merging resources",
|
|
175
|
+
/:merge\w*Assets\b/ => "bundling assets",
|
|
176
|
+
/:compile\w*Kotlin\b/ => "compiling Kotlin",
|
|
177
|
+
/:compile\w*JavaWithJavac\b/ => "compiling Java",
|
|
178
|
+
/:package\w+\b/ => "packaging APK"
|
|
179
|
+
}.freeze
|
|
180
|
+
|
|
181
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
182
|
+
def gradle(line)
|
|
183
|
+
short = UI.short_path(line)
|
|
184
|
+
case line
|
|
185
|
+
# Kotlin/javac/AAPT failures, and Gradle's own failure block.
|
|
186
|
+
when /\Ae: /, /\berror:/i, /\AFAILURE: /, /\A\* What went wrong:/, /\ABUILD FAILED/
|
|
187
|
+
" #{UI.red(short)}"
|
|
188
|
+
when /\Aw: /
|
|
189
|
+
return " #{UI.yellow(short)}" if line.include?("/extensions/")
|
|
190
|
+
|
|
191
|
+
once(:shell_warning) { UI.note_line("Kotlin warnings from the shell template — see the full log") }
|
|
192
|
+
when /\ABUILD SUCCESSFUL(?: in (.+))?/
|
|
193
|
+
UI.note_line("build succeeded#{Regexp.last_match(1) && " (#{Regexp.last_match(1)})"}",
|
|
194
|
+
marker: "✓", color: :green)
|
|
195
|
+
when /\A\d+ actionable tasks?:/
|
|
196
|
+
UI.note_line(line)
|
|
197
|
+
|
|
198
|
+
# First-run downloads are the whole reason a cold build takes minutes.
|
|
199
|
+
when /\ADownloading https:\/\/services\.gradle\.org/
|
|
200
|
+
once(:gradle_dist) { UI.note_line("downloading Gradle (first run only)") }
|
|
201
|
+
when /\AStarting a Gradle Daemon/
|
|
202
|
+
once(:daemon) { UI.note_line("starting the Gradle daemon") }
|
|
203
|
+
|
|
204
|
+
# ~83 task lines. Milestones once each; the rest, including everything
|
|
205
|
+
# already up to date, is background.
|
|
206
|
+
when /\A> Task (\S+)/
|
|
207
|
+
task = Regexp.last_match(1)
|
|
208
|
+
return :drop if line.match?(/(UP-TO-DATE|FROM-CACHE|NO-SOURCE|SKIPPED)\s*\z/)
|
|
209
|
+
|
|
210
|
+
label = MILESTONES.find { |pattern, _| task.match?(pattern) }&.last or return :drop
|
|
211
|
+
|
|
212
|
+
once("task-#{label}") { UI.note_line(label) }
|
|
213
|
+
|
|
214
|
+
# Gradle 9's deprecation banner and its follow-on lines, the incubating
|
|
215
|
+
# problems report, the daemon's welcome, and any progress redraw that
|
|
216
|
+
# survives --console=plain.
|
|
217
|
+
when /Deprecated Gradle features were used in this build/,
|
|
218
|
+
/--warning-mode all/,
|
|
219
|
+
/\AFor more information[, ].*docs\.gradle\.org/,
|
|
220
|
+
/\A\[Incubating\] Problems report/,
|
|
221
|
+
/\AWelcome to Gradle /,
|
|
222
|
+
/\A<[=\-]+>/,
|
|
223
|
+
/\A> (Configure|Starting|Loading|Calculating|IDLE)/
|
|
224
|
+
:drop
|
|
225
|
+
else
|
|
226
|
+
nil # unrecognized -> caller shows it dimmed
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
230
|
+
|
|
231
|
+
# --- `every dev`'s desktop shell (cargo) ----------------------------------
|
|
232
|
+
|
|
233
|
+
# A cold `cargo run` compiles ~300 crates, one "Compiling foo v0.1.0" line
|
|
234
|
+
# each, plus a progress bar it redraws several times a second. None of that
|
|
235
|
+
# is actionable while you're waiting on your own app — but warnings, errors
|
|
236
|
+
# and the moment the shell actually starts very much are.
|
|
237
|
+
#
|
|
238
|
+
# Unlike xcodebuild and Gradle (which we run with --console=plain), cargo is
|
|
239
|
+
# talking to a pty here and colors everything, so its status lines start
|
|
240
|
+
# with SGR codes and carry OSC-8 hyperlinks. Match on the stripped text;
|
|
241
|
+
# print the original, since that color is cargo's and worth keeping.
|
|
242
|
+
#
|
|
243
|
+
# The fall-through returns the line verbatim rather than nil, so it is NOT
|
|
244
|
+
# dimmed as background detail: once the shell is up, everything arriving on
|
|
245
|
+
# this stream is the app's own output, and the body of a Rust compile error
|
|
246
|
+
# is exactly what you need to read in full.
|
|
247
|
+
def cargo(line)
|
|
248
|
+
plain = UI.strip_ansi(line)
|
|
249
|
+
case plain
|
|
250
|
+
when /\A\s*error(\[E\d+\])?[:\[]/
|
|
251
|
+
" #{UI.red(UI.short_path(plain))}"
|
|
252
|
+
when /\A\s*warning: /
|
|
253
|
+
" #{UI.yellow(UI.short_path(plain))}"
|
|
254
|
+
when /\A\s*Compiling /
|
|
255
|
+
once(:compiling) { UI.note_line("compiling the desktop shell (first run is slow)") }
|
|
256
|
+
when /\A\s*(Downloaded|Downloading|Updating|Building \[|Blocking|Locking)/
|
|
257
|
+
:drop
|
|
258
|
+
when /\A\s*Finished\b/
|
|
259
|
+
UI.note_line("desktop shell compiled", marker: "✓", color: :green)
|
|
260
|
+
when /\A\s*Running\b/
|
|
261
|
+
UI.note_line("desktop shell running", marker: "✓", color: :green)
|
|
262
|
+
else
|
|
263
|
+
line
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
156
267
|
# Show a line only the first time its key is seen; :drop the repeats. Keeps a
|
|
157
268
|
# long notarization from scrolling a wall of identical "waiting" lines (and
|
|
158
269
|
# notarytool's thrice-echoed submission id from showing three times).
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
4
|
require "json"
|
|
5
|
+
require_relative "config"
|
|
5
6
|
|
|
6
7
|
module Everywhere
|
|
7
8
|
# Server-side view helpers mirroring the JS bridge's platform detection, so
|
|
@@ -9,27 +10,50 @@ module Everywhere
|
|
|
9
10
|
# does on the client:
|
|
10
11
|
#
|
|
11
12
|
# <%= link_to "Install the app", … unless native_app? %>
|
|
12
|
-
# <div class="<%= "native-inset" if
|
|
13
|
+
# <div class="<%= "native-inset" if mobile_app? %>">
|
|
13
14
|
# <% if native_version && native_version >= Gem::Version.new("1.2") %>
|
|
14
15
|
#
|
|
15
16
|
# Detection is by User-Agent: the RubyEverywhere shells prepend
|
|
16
17
|
# "RubyEverywhere/<version> (<os>)" to Hotwire Native's own
|
|
17
18
|
# "Hotwire Native iOS/Android" marker. Included into all Rails views by
|
|
18
19
|
# Everywhere::Engine.
|
|
20
|
+
#
|
|
21
|
+
# Pick the narrowest one that's true of what you're branching on. `native_app?`
|
|
22
|
+
# means "not a browser tab" and covers the desktop shell; `mobile_app?` means
|
|
23
|
+
# a phone, with a tab bar and safe-area insets; `desktop_app?` means a window,
|
|
24
|
+
# with a title bar and a menu bar. Safe-area padding is mobile_app?; hiding a
|
|
25
|
+
# web nav in favour of native chrome is usually mobile_app? too — the desktop
|
|
26
|
+
# shell has no native navigation to replace it with.
|
|
19
27
|
module NativeHelper
|
|
20
|
-
# True inside any RubyEverywhere native shell
|
|
28
|
+
# True inside any RubyEverywhere native shell — iOS, Android or desktop.
|
|
29
|
+
# Mirrors the bridge's `Everywhere.native`.
|
|
21
30
|
def native_app?
|
|
22
31
|
!native_platform.nil?
|
|
23
32
|
end
|
|
24
33
|
|
|
25
|
-
# :ios, :android, or nil in a plain browser. Reads
|
|
34
|
+
# :ios, :android, :desktop, or nil in a plain browser. Reads the shell's UA
|
|
26
35
|
# marker, so it's correct even before the JS bridge has booted.
|
|
27
36
|
def native_platform
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return :android if ua.include?("Hotwire Native Android")
|
|
37
|
+
Everywhere.native_platform_of(_everywhere_user_agent)
|
|
38
|
+
end
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
# True in the Tauri desktop shell. Use it for window chrome — reserving room
|
|
41
|
+
# for an overlay title bar, say — and for anything that assumes a pointer
|
|
42
|
+
# and a resizable window.
|
|
43
|
+
def desktop_app?
|
|
44
|
+
native_platform == :desktop
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# True in the iOS or Android shell, and the one to reach for when the
|
|
48
|
+
# affordance is a phone's: safe-area insets, the native tab bar, biometrics,
|
|
49
|
+
# the OAuth handoff. Mirrors the bridge's `Everywhere.platform === "mobile"`.
|
|
50
|
+
def mobile_app?
|
|
51
|
+
!mobile_platform.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# :ios, :android, or nil (including in the desktop shell).
|
|
55
|
+
def mobile_platform
|
|
56
|
+
Everywhere.mobile_platform_of(_everywhere_user_agent)
|
|
33
57
|
end
|
|
34
58
|
|
|
35
59
|
# The shell's version as a Gem::Version (from the "RubyEverywhere/<ver>"
|
|
@@ -52,8 +76,11 @@ module Everywhere
|
|
|
52
76
|
# redirect_to everywhere_auth_redirect(after_authentication_url)
|
|
53
77
|
#
|
|
54
78
|
# `to` may be a full URL or a path; only the same-origin path is forwarded.
|
|
79
|
+
# mobile_app?, not native_app?: the reset page exists to rebuild a phone
|
|
80
|
+
# shell's web views and re-fetch its tab bar. The desktop shell has neither,
|
|
81
|
+
# so a plain redirect is both correct and what it already did.
|
|
55
82
|
def everywhere_auth_redirect(to)
|
|
56
|
-
return to unless
|
|
83
|
+
return to unless mobile_app? && respond_to?(:everywhere_reset_path)
|
|
57
84
|
|
|
58
85
|
path = to.to_s.sub(%r{\Ahttps?://[^/]+}, "")
|
|
59
86
|
path = "/" if path.empty? || !path.start_with?("/")
|
|
@@ -107,7 +134,10 @@ module Everywhere
|
|
|
107
134
|
wrapper_class: nil, locked_class: nil,
|
|
108
135
|
message_class: nil, unlock_class: nil, &block)
|
|
109
136
|
content = respond_to?(:capture) ? capture(&block) : block.call
|
|
110
|
-
|
|
137
|
+
# mobile_app?: there's no Face ID on the desktop shell, so emitting the
|
|
138
|
+
# locked wrapper there would hide the content behind an unlock nothing can
|
|
139
|
+
# satisfy. Browsers and the desktop shell render it plainly.
|
|
140
|
+
return content unless mobile_app?
|
|
111
141
|
|
|
112
142
|
attrs = [%(data-everywhere-biometric-lock="#{_everywhere_attr(name)}")]
|
|
113
143
|
attrs << %(data-everywhere-biometric-reason="#{_everywhere_attr(reason)}") if reason
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "auth_handoff"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
# OmniAuth 2 and the native sign-in flow, introduced to each other.
|
|
7
|
+
#
|
|
8
|
+
# Since CVE-2015-9284, OmniAuth only starts a provider flow from a POST
|
|
9
|
+
# carrying a CSRF token (omniauth-rails_csrf_protection). Right for browsers —
|
|
10
|
+
# and impossible for the native flow, which enters the provider path through
|
|
11
|
+
# /everywhere/auth/start's redirect: a GET, in a browser session that has no
|
|
12
|
+
# CSRF token to give. Without help, tapping "Sign in with GitHub" in the app
|
|
13
|
+
# ends on OmniAuth's failure page.
|
|
14
|
+
#
|
|
15
|
+
# # config/initializers/omniauth.rb, after your provider setup
|
|
16
|
+
# require "everywhere/omniauth"
|
|
17
|
+
# Everywhere::OmniAuth.protect!
|
|
18
|
+
#
|
|
19
|
+
# Browsers keep exactly the validation they had — whatever
|
|
20
|
+
# request_validation_phase was set to keeps running. The one extra shape
|
|
21
|
+
# allowed through is the native flow's entry: a top-level, not-cross-site GET
|
|
22
|
+
# carrying a marker cookie that /everywhere/auth/start sealed seconds ago.
|
|
23
|
+
# Nobody else can mint that marker (AES-GCM under the app's secret), start
|
|
24
|
+
# refuses cross-site initiators outright, and the freshness window means a
|
|
25
|
+
# marker lingering in Safari's jar after a real sign-in stops vouching for
|
|
26
|
+
# anything almost immediately.
|
|
27
|
+
module OmniAuth
|
|
28
|
+
# How long after start seals the marker its GET still reads as the native
|
|
29
|
+
# flow arriving. The real gap is one redirect — milliseconds — but the
|
|
30
|
+
# sheet can sit on our page briefly on a slow network; anything past this
|
|
31
|
+
# is a browser wandering in.
|
|
32
|
+
ENTRY_WINDOW = 90
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Wire OmniAuth's global config: allow GET request phases (they're
|
|
36
|
+
# refused per-request by the validator unless they prove themselves),
|
|
37
|
+
# and wrap the configured validation phase so everything that isn't the
|
|
38
|
+
# native entry is judged exactly as before. Safe to call more than once —
|
|
39
|
+
# re-running replaces the wrapper, never stacks it.
|
|
40
|
+
def protect!(entry_window: ENTRY_WINDOW)
|
|
41
|
+
unless defined?(::OmniAuth) && ::OmniAuth.respond_to?(:config)
|
|
42
|
+
raise "Everywhere::OmniAuth.protect! needs OmniAuth loaded first — " \
|
|
43
|
+
"call it below your OmniAuth/Devise setup (config/initializers/omniauth.rb)"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
config = ::OmniAuth.config
|
|
47
|
+
config.allowed_request_methods = Array(config.allowed_request_methods) | [:get]
|
|
48
|
+
config.silence_get_warning = true if config.respond_to?(:silence_get_warning=)
|
|
49
|
+
|
|
50
|
+
fallback = config.request_validation_phase
|
|
51
|
+
fallback = fallback.fallback if fallback.is_a?(RequestValidator)
|
|
52
|
+
config.request_validation_phase = RequestValidator.new(fallback: fallback, entry_window: entry_window)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# OmniAuth request_validation_phase: passes the native flow's entry,
|
|
57
|
+
# delegates everything else. A nil fallback means the app had no request
|
|
58
|
+
# validation configured, and it still has none — this never tightens.
|
|
59
|
+
class RequestValidator
|
|
60
|
+
attr_reader :fallback
|
|
61
|
+
|
|
62
|
+
def initialize(fallback: nil, entry_window: ENTRY_WINDOW)
|
|
63
|
+
@fallback = fallback
|
|
64
|
+
@entry_window = entry_window
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call(env)
|
|
68
|
+
return if native_entry?(env)
|
|
69
|
+
|
|
70
|
+
@fallback&.call(env)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# The one shape the native flow produces: a GET, not from a cross-site
|
|
76
|
+
# initiator, carrying a marker minted moments ago. POSTs never qualify —
|
|
77
|
+
# they come from rendered pages that have a CSRF token to present.
|
|
78
|
+
def native_entry?(env)
|
|
79
|
+
return false unless env["REQUEST_METHOD"] == "GET"
|
|
80
|
+
return false if env["HTTP_SEC_FETCH_SITE"] == "cross-site"
|
|
81
|
+
|
|
82
|
+
marker = AuthHandoff.marker(env)
|
|
83
|
+
issued = marker && marker["iat"]
|
|
84
|
+
!!issued && Time.now.to_i - issued.to_i <= @entry_window
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|