runlight 0.3.0 → 0.3.2
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/assets/build.json +2 -2
- data/assets/dashboard.css +1 -1
- data/assets/dashboard.js +15 -15
- data/lib/runlight/accounts/web.rb +2 -2
- data/lib/runlight/dates.rb +38 -0
- data/lib/runlight/oauth.rb +3 -1
- data/lib/runlight/reports.rb +1 -1
- data/lib/runlight/routes.rb +9 -3
- data/lib/runlight/version.rb +1 -1
- metadata +2 -2
|
@@ -298,7 +298,7 @@ module Runlight
|
|
|
298
298
|
"to" => invite["email"],
|
|
299
299
|
"subject" => "#{invite["invitedBy"]} invited you to Runlight",
|
|
300
300
|
"text" => "#{invite["invitedBy"]} invited you to the Runlight at #{host} as #{what}.\n\nChoose a password to join:\n#{link}\n\nThe link works for seven days.\n",
|
|
301
|
-
"html" => "<div style=\"font:15px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;color:#111827;max-width:480px\"><
|
|
301
|
+
"html" => "<div style=\"font:15px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;color:#111827;max-width:480px\"><div role=\"img\" aria-label=\"Runlight\" style=\"width:48px;height:48px;margin-bottom:24px;border-radius:10px;background:#111827;color:#fff;font:700 26px/48px -apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;text-align:center\">R</div><p>" \
|
|
302
302
|
"#{esc(invite["invitedBy"])} invited you to the Runlight at #{esc(host)} as #{esc(what)}.</p><p><a href=\"#{esc(link)}\" " \
|
|
303
303
|
"style=\"display:inline-block;padding:10px 18px;border-radius:8px;background:#111827;color:#ffffff;text-decoration:none;font-weight:600\">" \
|
|
304
304
|
"Choose a password and join</a></p><p style=\"color:#6b7280;font-size:13px\">The link works for seven days. If you were not expecting this, " \
|
|
@@ -336,7 +336,7 @@ module Runlight
|
|
|
336
336
|
"subject" => "Sign in to Runlight",
|
|
337
337
|
"text" => "Someone, most likely you, signed in to Runlight at #{host} with your password while your account was held up by too many failed tries.\n\n" \
|
|
338
338
|
"Sign in with this link within fifteen minutes:\n#{link}\n\nIf this was not you, change your password, since someone knows it.\n",
|
|
339
|
-
"html" => "<div style=\"font:15px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;color:#111827;max-width:480px\"><
|
|
339
|
+
"html" => "<div style=\"font:15px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;color:#111827;max-width:480px\"><div role=\"img\" aria-label=\"Runlight\" style=\"width:48px;height:48px;margin-bottom:24px;border-radius:10px;background:#111827;color:#fff;font:700 26px/48px -apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;text-align:center\">R</div>" \
|
|
340
340
|
"<p>Someone, most likely you, signed in to Runlight at #{esc(host)} with your password while your account was held up by too many failed tries.</p>" \
|
|
341
341
|
"<p><a href=\"#{esc(link)}\" style=\"display:inline-block;padding:10px 18px;border-radius:8px;background:#111827;color:#ffffff;" \
|
|
342
342
|
"text-decoration:none;font-weight:600\">Sign in</a></p><p style=\"color:#6b7280;font-size:13px\">The link works for fifteen minutes. " \
|
data/lib/runlight/dates.rb
CHANGED
|
@@ -46,6 +46,44 @@ module Runlight
|
|
|
46
46
|
"systemv/hst10" => "Etc/GMT+10",
|
|
47
47
|
# Names the database dropped and ICU kept.
|
|
48
48
|
"canada/east-saskatchewan" => "America/Regina", "us/pacific-new" => "America/Los_Angeles",
|
|
49
|
+
# Old names the time zone database keeps as links in its backward file. Debian, and Ubuntu from 26.04, ship
|
|
50
|
+
# them apart (tzdata-legacy), so a system may not have them; ICU reads them as the links. From tzdata 2026e.
|
|
51
|
+
"africa/asmera" => "Africa/Asmara", "america/argentina/comodrivadavia" => "America/Argentina/Catamarca",
|
|
52
|
+
"america/buenos_aires" => "America/Argentina/Buenos_Aires", "america/catamarca" => "America/Argentina/Catamarca",
|
|
53
|
+
"america/cordoba" => "America/Argentina/Cordoba", "america/fort_wayne" => "America/Indiana/Indianapolis",
|
|
54
|
+
"america/godthab" => "America/Nuuk", "america/indianapolis" => "America/Indiana/Indianapolis",
|
|
55
|
+
"america/jujuy" => "America/Argentina/Jujuy", "america/knox_in" => "America/Indiana/Knox",
|
|
56
|
+
"america/louisville" => "America/Kentucky/Louisville", "america/mendoza" => "America/Argentina/Mendoza",
|
|
57
|
+
"america/rosario" => "America/Argentina/Cordoba", "antarctica/south_pole" => "Antarctica/McMurdo",
|
|
58
|
+
"asia/ashkhabad" => "Asia/Ashgabat", "asia/calcutta" => "Asia/Kolkata", "asia/choibalsan" => "Asia/Ulaanbaatar",
|
|
59
|
+
"asia/chungking" => "Asia/Shanghai", "asia/dacca" => "Asia/Dhaka", "asia/katmandu" => "Asia/Kathmandu",
|
|
60
|
+
"asia/macao" => "Asia/Macau", "asia/rangoon" => "Asia/Yangon", "asia/saigon" => "Asia/Ho_Chi_Minh",
|
|
61
|
+
"asia/thimbu" => "Asia/Thimphu", "asia/ujung_pandang" => "Asia/Makassar", "asia/ulan_bator" => "Asia/Ulaanbaatar",
|
|
62
|
+
"atlantic/faeroe" => "Atlantic/Faroe", "australia/act" => "Australia/Sydney", "australia/lhi" => "Australia/Lord_Howe",
|
|
63
|
+
"australia/north" => "Australia/Darwin", "australia/nsw" => "Australia/Sydney",
|
|
64
|
+
"australia/queensland" => "Australia/Brisbane", "australia/south" => "Australia/Adelaide",
|
|
65
|
+
"australia/tasmania" => "Australia/Hobart", "australia/victoria" => "Australia/Melbourne",
|
|
66
|
+
"australia/west" => "Australia/Perth", "brazil/acre" => "America/Rio_Branco", "brazil/denoronha" => "America/Noronha",
|
|
67
|
+
"brazil/east" => "America/Sao_Paulo", "brazil/west" => "America/Manaus", "canada/atlantic" => "America/Halifax",
|
|
68
|
+
"canada/central" => "America/Winnipeg", "canada/eastern" => "America/Toronto", "canada/mountain" => "America/Edmonton",
|
|
69
|
+
"canada/newfoundland" => "America/St_Johns", "canada/pacific" => "America/Vancouver",
|
|
70
|
+
"canada/saskatchewan" => "America/Regina", "canada/yukon" => "America/Whitehorse",
|
|
71
|
+
"chile/continental" => "America/Santiago", "chile/easterisland" => "Pacific/Easter", "cst6cdt" => "America/Chicago",
|
|
72
|
+
"cuba" => "America/Havana", "egypt" => "Africa/Cairo", "eire" => "Europe/Dublin", "est5edt" => "America/New_York",
|
|
73
|
+
"europe/kiev" => "Europe/Kyiv", "europe/uzhgorod" => "Europe/Kyiv", "europe/zaporozhye" => "Europe/Kyiv",
|
|
74
|
+
"gb" => "Europe/London", "gb-eire" => "Europe/London", "gmt0" => "Etc/GMT", "greenwich" => "Etc/GMT",
|
|
75
|
+
"hongkong" => "Asia/Hong_Kong", "iceland" => "Atlantic/Reykjavik", "iran" => "Asia/Tehran", "israel" => "Asia/Jerusalem",
|
|
76
|
+
"jamaica" => "America/Jamaica", "japan" => "Asia/Tokyo", "kwajalein" => "Pacific/Kwajalein", "libya" => "Africa/Tripoli",
|
|
77
|
+
"mexico/bajanorte" => "America/Tijuana", "mexico/bajasur" => "America/Mazatlan", "mexico/general" => "America/Mexico_City",
|
|
78
|
+
"mst7mdt" => "America/Denver", "navajo" => "America/Denver", "nz" => "Pacific/Auckland", "nz-chat" => "Pacific/Chatham",
|
|
79
|
+
"pacific/enderbury" => "Pacific/Kanton", "pacific/ponape" => "Pacific/Pohnpei", "pacific/truk" => "Pacific/Chuuk",
|
|
80
|
+
"poland" => "Europe/Warsaw", "portugal" => "Europe/Lisbon", "prc" => "Asia/Shanghai", "pst8pdt" => "America/Los_Angeles",
|
|
81
|
+
"roc" => "Asia/Taipei", "rok" => "Asia/Seoul", "singapore" => "Asia/Singapore", "turkey" => "Europe/Istanbul",
|
|
82
|
+
"universal" => "Etc/UTC", "us/alaska" => "America/Anchorage", "us/aleutian" => "America/Adak",
|
|
83
|
+
"us/arizona" => "America/Phoenix", "us/central" => "America/Chicago", "us/east-indiana" => "America/Indiana/Indianapolis",
|
|
84
|
+
"us/eastern" => "America/New_York", "us/hawaii" => "Pacific/Honolulu", "us/indiana-starke" => "America/Indiana/Knox",
|
|
85
|
+
"us/michigan" => "America/Detroit", "us/mountain" => "America/Denver", "us/pacific" => "America/Los_Angeles",
|
|
86
|
+
"us/samoa" => "Pacific/Pago_Pago", "w-su" => "Europe/Moscow", "zulu" => "Etc/UTC",
|
|
49
87
|
}.freeze
|
|
50
88
|
|
|
51
89
|
OFFSET = /\A([+\-]|−)([01][0-9]|2[0-3])(?::?([0-5][0-9]))?\z/
|
data/lib/runlight/oauth.rb
CHANGED
|
@@ -23,7 +23,8 @@ module Runlight
|
|
|
23
23
|
#
|
|
24
24
|
# The context the routes pass is a Hash: "runlight", "base", "isOwner" (callable(request) giving a bool), and
|
|
25
25
|
# optionally "signIn" (String), "isReader" (callable(request) giving a bool), "accountOf" (callable(request)
|
|
26
|
-
# giving an account id or nil),
|
|
26
|
+
# giving an account id or nil), "tokenMade" (callable(token, by) giving a bool), and "csrf" (callable giving the
|
|
27
|
+
# token the consent form sends back in its csrf field).
|
|
27
28
|
module OAuth
|
|
28
29
|
CODE_MS = 5 * 60_000
|
|
29
30
|
# An app stored before client ids were signed, which never finished connecting within a day, is removed.
|
|
@@ -206,6 +207,7 @@ module Runlight
|
|
|
206
207
|
%w[response_type client_id redirect_uri code_challenge code_challenge_method state scope resource].each do |k|
|
|
207
208
|
hidden << "<input type=\"hidden\" name=\"#{k}\" value=\"#{esc(form.get(k))}\">" unless form.get(k).nil?
|
|
208
209
|
end
|
|
210
|
+
hidden << "<input type=\"hidden\" name=\"csrf\" value=\"#{esc(ctx["csrf"].call.to_s)}\">" if ctx["csrf"]
|
|
209
211
|
# The app names itself, so the page also shows where the answer goes, which it cannot fake.
|
|
210
212
|
sends_to = "<p class=\"note\">#{h.call("oauth.sendsTo", { "host" => "<strong>#{esc(Http::Url.new(redirect).host)}</strong>" })}</p>"
|
|
211
213
|
buttons = "<div class=\"buttons\"><button type=\"submit\" name=\"decision\" value=\"deny\" class=\"ghost\">#{h.call("oauth.deny")}</button>" \
|
data/lib/runlight/reports.rb
CHANGED
|
@@ -183,7 +183,7 @@ module Runlight
|
|
|
183
183
|
u = Http::Url.parse(links["dashboard"])
|
|
184
184
|
where = u.nil? ? links["dashboard"] : u.host + u.pathname.sub(%r{/\z}, "")
|
|
185
185
|
at = t.call("email.at", { "where" => where })
|
|
186
|
-
mark = "<
|
|
186
|
+
mark = "<div role=\"img\" aria-label=\"Runlight\" style=\"width:48px;height:48px;margin-bottom:24px;border-radius:10px;background:#111827;color:#fff;font:700 26px/48px -apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;text-align:center\">R</div>"
|
|
187
187
|
|
|
188
188
|
footer = t.call("email.footer", { "frequency" => t.call(monthly ? "email.monthly" : "email.weekly"), "site" => site["name"] })
|
|
189
189
|
at_line = esc(t.call("email.at", { "where" => "\0" }))
|
data/lib/runlight/routes.rb
CHANGED
|
@@ -26,6 +26,10 @@ module Runlight
|
|
|
26
26
|
# - cms: the CMS that adds the script to every page, such as "WordPress", when Runlight runs inside one. The
|
|
27
27
|
# dashboard then says so in place of the steps to add the script, and pages of the same origin, the CMS's own
|
|
28
28
|
# admin, may show the whole dashboard in a frame. The WordPress, Drupal, and Craft plugins set it.
|
|
29
|
+
# - csrf: a callable returning a token the dashboard sends back with every change it makes, in an x-runlight-csrf
|
|
30
|
+
# header, and the OAuth consent form in a csrf field, for an authorize that checks where a change comes from, as
|
|
31
|
+
# a CMS checks its own forms. Asked for each time the dashboard or the form is shown. The WordPress plugin gives
|
|
32
|
+
# a nonce.
|
|
29
33
|
# - origin: the address people open the app at, such as https://example.com.
|
|
30
34
|
# - ownHosts: a callable returning more names the dashboard is reached at.
|
|
31
35
|
# - accountOf: a callable taking the Request and returning the account it comes from, or nil (internal, for the
|
|
@@ -182,6 +186,7 @@ module Runlight
|
|
|
182
186
|
oauth["signIn"] = @sign_in if !@sign_in.nil? && @sign_in != ""
|
|
183
187
|
oauth["accountOf"] = @account_of unless @account_of.nil?
|
|
184
188
|
oauth["tokenMade"] = @token_made unless @token_made.nil?
|
|
189
|
+
oauth["csrf"] = options["csrf"] unless options["csrf"].nil?
|
|
185
190
|
@oauth = oauth
|
|
186
191
|
end
|
|
187
192
|
|
|
@@ -387,7 +392,7 @@ module Runlight
|
|
|
387
392
|
# The dashboard's page, which holds no data: the API it calls checks access. `embed` is the dashboard inside a
|
|
388
393
|
# CMS's admin pages: { "session", "origin" }, its session (empty once its ticket was used or ran out) and the
|
|
389
394
|
# admin origin that frames it.
|
|
390
|
-
def dashboard(base, share = "", sign_out = "", geo_credit = false, accounts = false, sign_in = "", embed = nil, cms = "")
|
|
395
|
+
def dashboard(base, share = "", sign_out = "", geo_credit = false, accounts = false, sign_in = "", embed = nil, cms = "", csrf = "")
|
|
391
396
|
b = escape_attr(base)
|
|
392
397
|
hash = build_hash("dashboardHash")
|
|
393
398
|
attributes = (share != "" ? " data-share=\"#{escape_attr(share)}\"" : "") +
|
|
@@ -396,6 +401,7 @@ module Runlight
|
|
|
396
401
|
(geo_credit ? ' data-geo-credit=""' : "") +
|
|
397
402
|
(accounts ? ' data-accounts=""' : "") +
|
|
398
403
|
(cms != "" ? " data-cms=\"#{escape_attr(cms)}\"" : "") +
|
|
404
|
+
(csrf != "" ? " data-csrf=\"#{escape_attr(csrf)}\"" : "") +
|
|
399
405
|
(embed.nil? ? "" : " data-embed=\"#{escape_attr(embed["session"])}\" data-embed-origin=\"#{escape_attr(embed["origin"])}\"")
|
|
400
406
|
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex\">\n<title>Runlight</title>\n" \
|
|
401
407
|
"<link rel=\"icon\" href=\"#{Brand.runlight_icon}\">\n" \
|
|
@@ -1258,7 +1264,7 @@ module Runlight
|
|
|
1258
1264
|
"to" => to,
|
|
1259
1265
|
"subject" => t.call("email.test.subject"),
|
|
1260
1266
|
"text" => t.call("email.test.body", { "service" => name }),
|
|
1261
|
-
"html" => "<
|
|
1267
|
+
"html" => "<div role=\"img\" aria-label=\"Runlight\" style=\"width:48px;height:48px;margin-bottom:24px;border-radius:10px;background:#111827;color:#fff;font:700 26px/48px -apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;text-align:center\">R</div><p style=\"font-family:sans-serif;font-size:15px\">#{escape_html(t.call("email.test.body", { "service" => name }))}</p>",
|
|
1262
1268
|
})
|
|
1263
1269
|
# Counted once it went out, so a test that failed can be tried again as soon as the setting is fixed.
|
|
1264
1270
|
@lock.synchronize { @sample_sent[tested] = @rl.now }
|
|
@@ -2730,7 +2736,7 @@ module Runlight
|
|
|
2730
2736
|
# the page explains how to sign in when it is refused.
|
|
2731
2737
|
# Inside a CMS, its admin, on this same origin, shows the whole dashboard in a frame; nothing else may.
|
|
2732
2738
|
cms = @options["cms"].to_s
|
|
2733
|
-
return Http::Response.new(dashboard(base, "", @sign_out || "", Js.truthy?(@options["geoCredit"]), !@web.nil?, @sign_in || "", nil, cms), status: 200, headers: {
|
|
2739
|
+
return Http::Response.new(dashboard(base, "", @sign_out || "", Js.truthy?(@options["geoCredit"]), !@web.nil?, @sign_in || "", nil, cms, @options["csrf"].nil? ? "" : @options["csrf"].call.to_s), status: 200, headers: {
|
|
2734
2740
|
"content-type" => "text/html; charset=utf-8",
|
|
2735
2741
|
"cache-control" => "no-store",
|
|
2736
2742
|
"content-security-policy" => cms != "" ? DASHBOARD_CSP.sub("frame-ancestors 'none'", "frame-ancestors 'self'") : DASHBOARD_CSP,
|
data/lib/runlight/version.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Runlight
|
|
6
6
|
# The gem's own version, which the release keeps equal to the npm package's.
|
|
7
|
-
GEM_VERSION = "0.3.
|
|
7
|
+
GEM_VERSION = "0.3.2"
|
|
8
8
|
|
|
9
9
|
# The SDK's version and the HTTP API's, read from assets/build.json, which
|
|
10
10
|
# scripts/ruby-assets.mts writes from the TypeScript SDK, so the two always
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: runlight
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jon C. Phillips
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|