axe-cuprite 0.1.1 → 0.2.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/CHANGELOG.md +59 -0
- data/README.md +26 -2
- data/lib/axe/cuprite/configuration.rb +8 -0
- data/lib/axe/cuprite/injector.rb +51 -14
- data/lib/axe/cuprite/results.rb +26 -4
- data/lib/axe/cuprite/rspec/matchers.rb +1 -1
- data/lib/axe/cuprite/tasks/axe.rake +110 -14
- data/lib/axe/cuprite/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 91d61bb330869ef24f40d7b9c09859daf23212f99dd8b6c14506238d48cfe9f2
|
|
4
|
+
data.tar.gz: 0d023ead7e8245bf03d428ef4912c093a32888c7528d0e95307ca3036a77db2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b41901a481d94378f0945a89cc4e7e4d6c3f198d45e06737d910841657aaaba6b4f8ccb32538cfb7af45607ea25049787e8a47b85e6e63a45845af7744a1033c
|
|
7
|
+
data.tar.gz: 906bb0a2bc25c10140185a9385192c1f629ef3803447cf31862f96e0c9f9d41589da8cb317b8220c6d3ffa37390eb8fe33f1f72729e243c8034104df12e4ae6a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-06-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `config.include_html` (default `true`) — set to `false` to suppress the
|
|
14
|
+
truncated outer-HTML snippets that failure messages and `report_only` logs
|
|
15
|
+
print for each offending element. Useful for suites that render sensitive data
|
|
16
|
+
(staging-backed tests, seeded PII), keeping page content out of CI logs while
|
|
17
|
+
still reporting rule id, selector, and check message. Documented the snippet
|
|
18
|
+
behavior and `logger` guidance in the README
|
|
19
|
+
([#14](https://github.com/Guided-Rails/axe-cuprite/issues/14)).
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- The result wrappers (`Results`/`Violation`/`Node`/`ContrastData`) now honor
|
|
23
|
+
their documented read-only contract: `@raw` is deep-frozen at construction, so
|
|
24
|
+
`raw` and `to_h` can safely expose the live underlying hash without a caller
|
|
25
|
+
being able to mutate the wrapper's internal state (which, via memoization,
|
|
26
|
+
could previously desync `violations`/`incomplete` from `raw`)
|
|
27
|
+
([#17](https://github.com/Guided-Rails/axe-cuprite/issues/17)).
|
|
28
|
+
- `Injector#inject_source!` no longer hides the real cause of an injection
|
|
29
|
+
failure behind a blanket Content-Security-Policy message. When both the
|
|
30
|
+
primary `execute_script` path and the `add_script_tag` fallback fail, the
|
|
31
|
+
exceptions they raised are now captured and appended to the `InjectionError`
|
|
32
|
+
message (`Underlying errors: execute_script: ...; add_script_tag: ...`), so a
|
|
33
|
+
dead browser, dead CDP session, or misconfigured driver is no longer
|
|
34
|
+
misattributed to CSP ([#15](https://github.com/Guided-Rails/axe-cuprite/issues/15)).
|
|
35
|
+
- `Injector#timeout_error?` no longer reclassifies arbitrary failures as
|
|
36
|
+
`AxeCuprite::TimeoutError` just because the error message mentions a timeout.
|
|
37
|
+
A genuine page-side JavaScript error (e.g. a `Ferrum::JavaScriptError` from axe
|
|
38
|
+
or the app whose text happens to contain "timeout") now propagates untouched
|
|
39
|
+
instead of being rewritten with misleading "increase the timeout / scope the
|
|
40
|
+
run" guidance. Classification is driven by error class (Ferrum's
|
|
41
|
+
timeout/script-timeout classes), with a narrow class-gated message check only
|
|
42
|
+
for Ferrum's async-evaluation "timed out promise" case; this also removes a
|
|
43
|
+
dead code branch that could never affect the result
|
|
44
|
+
([#16](https://github.com/Guided-Rails/axe-cuprite/issues/16)).
|
|
45
|
+
|
|
46
|
+
### Security
|
|
47
|
+
- `rake axe:update` now vendors axe-core from the official npm registry tarball
|
|
48
|
+
(`registry.npmjs.org`) instead of the unpkg CDN, verifies the tarball against
|
|
49
|
+
the registry-published sha512 `dist.integrity` (and legacy `dist.shasum`)
|
|
50
|
+
**before writing anything**, and treats a banner/version mismatch as fatal
|
|
51
|
+
instead of a warning ([#11](https://github.com/Guided-Rails/axe-cuprite/issues/11)).
|
|
52
|
+
- The sha512 of the vendored `axe.min.js` is now recorded in
|
|
53
|
+
`lib/axe/cuprite/vendor/axe.min.js.sha512`; a new `rake axe:verify` task
|
|
54
|
+
re-checks the vendored engine against it, and CI runs it on every build.
|
|
55
|
+
- Hardened the CI workflow: added a least-privilege `permissions: contents: read`
|
|
56
|
+
block (the `GITHUB_TOKEN` previously inherited the repo default, potentially
|
|
57
|
+
write-all) and pinned `actions/checkout` and `ruby/setup-ruby` to full commit
|
|
58
|
+
SHAs instead of mutable major-version tags. Added a Dependabot config
|
|
59
|
+
(`.github/dependabot.yml`) for the `github-actions` and `bundler` ecosystems so
|
|
60
|
+
the pinned SHAs and gem dependencies get automated update PRs
|
|
61
|
+
([#12](https://github.com/Guided-Rails/axe-cuprite/issues/12)).
|
|
62
|
+
- `rake axe:update[VERSION]` now validates the `VERSION` argument against a
|
|
63
|
+
semver-ish pattern **before** any network call or file write. The value was
|
|
64
|
+
previously spliced unvalidated into the registry/download URL and into
|
|
65
|
+
`version.rb`, so a crafted string (e.g. `4.12.0/../other-pkg`, or one
|
|
66
|
+
containing a quote) could vendor a different package or break out of the
|
|
67
|
+
version string literal ([#13](https://github.com/Guided-Rails/axe-cuprite/issues/13)).
|
|
68
|
+
|
|
10
69
|
## [0.1.1] - 2026-06-09
|
|
11
70
|
|
|
12
71
|
### Added
|
data/README.md
CHANGED
|
@@ -156,6 +156,7 @@ AxeCuprite.configure do |c|
|
|
|
156
156
|
c.skip_rules = [:region] # globally disabled rules
|
|
157
157
|
c.auto_inject = true # (re)inject axe on demand inside #run
|
|
158
158
|
c.report_only = false # log violations instead of failing (see below)
|
|
159
|
+
c.include_html = true # include element HTML snippets in output (see below)
|
|
159
160
|
c.logger = Logger.new($stdout)
|
|
160
161
|
end
|
|
161
162
|
```
|
|
@@ -167,6 +168,22 @@ This eases incremental adoption on an existing app — you can see what axe find
|
|
|
167
168
|
without turning the suite red. Negated assertions (`expect(page).not_to
|
|
168
169
|
be_axe_clean`) ignore this flag.
|
|
169
170
|
|
|
171
|
+
### Sensitive page content in output (`include_html`)
|
|
172
|
+
|
|
173
|
+
Both failure messages and `report_only` logs include a **truncated outer-HTML
|
|
174
|
+
snippet** (capped at 200 chars) of each offending element, so you can see *what*
|
|
175
|
+
failed at a glance. On suites that render real data — staging-backed tests,
|
|
176
|
+
seeded PII — those snippets can carry page content into CI logs or log
|
|
177
|
+
aggregation. This is normal for a testing tool, but if it matters to you:
|
|
178
|
+
|
|
179
|
+
- Point `c.logger` somewhere appropriate (a redacted sink, a dropped stream)
|
|
180
|
+
rather than `$stdout`, so `report_only` output doesn't fan out to aggregation.
|
|
181
|
+
- Set `c.include_html = false` to drop the HTML snippets entirely. Output then
|
|
182
|
+
carries only the rule id, impact, help URL, element **selector**, and the axe
|
|
183
|
+
check message (for color-contrast, the fg/bg colors and ratio) — enough to
|
|
184
|
+
locate and fix a violation without echoing page content. Note the selector
|
|
185
|
+
itself can still reflect ids/classes from your markup.
|
|
186
|
+
|
|
170
187
|
## Caveats & engineering notes
|
|
171
188
|
|
|
172
189
|
### Timeout (decoupled from `default_max_wait_time`)
|
|
@@ -223,10 +240,17 @@ To refresh it:
|
|
|
223
240
|
rake 'axe:update[4.12.0]' # pin a version
|
|
224
241
|
rake axe:update # or grab the latest from npm
|
|
225
242
|
rake axe:version # print the currently vendored version
|
|
243
|
+
rake axe:verify # check axe.min.js against its recorded sha512
|
|
226
244
|
```
|
|
227
245
|
|
|
228
|
-
`axe:update` downloads `axe
|
|
229
|
-
|
|
246
|
+
`axe:update` downloads the official `axe-core` tarball from
|
|
247
|
+
**registry.npmjs.org**, verifies it against the sha512 integrity the registry
|
|
248
|
+
publishes (aborting on any mismatch before writing a byte), extracts
|
|
249
|
+
`axe.min.js` and its `LICENSE`, and bumps the `AXE_CORE_VERSION` constant. The
|
|
250
|
+
sha512 of the vendored engine is recorded in
|
|
251
|
+
`lib/axe/cuprite/vendor/axe.min.js.sha512` so it can be re-checked at any time
|
|
252
|
+
with `rake axe:verify` (CI does this on every run). Note the bump in
|
|
253
|
+
`CHANGELOG.md`.
|
|
230
254
|
|
|
231
255
|
## Licensing
|
|
232
256
|
|
|
@@ -34,6 +34,13 @@ module AxeCuprite
|
|
|
34
34
|
# Logger used for report_only output and warnings.
|
|
35
35
|
attr_accessor :logger
|
|
36
36
|
|
|
37
|
+
# When true (default), failure messages and report_only logs include a
|
|
38
|
+
# truncated outer-HTML snippet of each offending element. Set to false to
|
|
39
|
+
# suppress those snippets (rule id + selector + check message only) on
|
|
40
|
+
# suites that render sensitive data, so page content can't leak into CI
|
|
41
|
+
# logs. See BeAxeClean#format_node.
|
|
42
|
+
attr_accessor :include_html
|
|
43
|
+
|
|
37
44
|
def initialize
|
|
38
45
|
@timeout = 30
|
|
39
46
|
@default_options = {}
|
|
@@ -41,6 +48,7 @@ module AxeCuprite
|
|
|
41
48
|
@skip_rules = []
|
|
42
49
|
@auto_inject = true
|
|
43
50
|
@report_only = false
|
|
51
|
+
@include_html = true
|
|
44
52
|
@logger = default_logger
|
|
45
53
|
end
|
|
46
54
|
|
data/lib/axe/cuprite/injector.rb
CHANGED
|
@@ -46,6 +46,17 @@ module AxeCuprite
|
|
|
46
46
|
# JS expression that reports whether axe is loaded and runnable.
|
|
47
47
|
PRESENCE_JS = "typeof window.axe !== 'undefined' && typeof window.axe.run === 'function'"
|
|
48
48
|
|
|
49
|
+
# Dedicated timeout classes that mean "the evaluation timed out" regardless
|
|
50
|
+
# of message. Matched by name (not constant) so we keep no hard dependency on
|
|
51
|
+
# ferrum — it's a dev-only dep. Covers Ferrum's own timeouts (the Cuprite
|
|
52
|
+
# fast-path, via page.evaluate_async). A non-Ferrum driver's own script-timeout
|
|
53
|
+
# error propagates raw rather than being wrapped — we don't depend on or test
|
|
54
|
+
# any such driver here, so we don't guess at its class names. See #timeout_error?.
|
|
55
|
+
TIMEOUT_ERROR_CLASS_NAMES = [
|
|
56
|
+
"Ferrum::TimeoutError",
|
|
57
|
+
"Ferrum::ScriptTimeoutError"
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
49
60
|
def initialize(page, configuration = AxeCuprite.configuration)
|
|
50
61
|
@page = page
|
|
51
62
|
@config = configuration
|
|
@@ -73,20 +84,23 @@ module AxeCuprite
|
|
|
73
84
|
# to Ferrum's add_script_tag.
|
|
74
85
|
def inject_source!
|
|
75
86
|
source = AxeCuprite.axe_source
|
|
87
|
+
errors = {}
|
|
76
88
|
|
|
77
89
|
begin
|
|
78
90
|
@page.execute_script(source)
|
|
79
|
-
rescue StandardError
|
|
80
|
-
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
errors["execute_script"] = e
|
|
81
93
|
end
|
|
82
94
|
return true if injected?
|
|
83
95
|
|
|
84
|
-
|
|
96
|
+
begin
|
|
97
|
+
try_add_script_tag(source)
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
errors["add_script_tag"] = e
|
|
100
|
+
end
|
|
85
101
|
return true if injected?
|
|
86
102
|
|
|
87
|
-
raise InjectionError,
|
|
88
|
-
"axe-core did not load after injection. The page may be blocking " \
|
|
89
|
-
"script injection (e.g. a strict Content-Security-Policy)."
|
|
103
|
+
raise InjectionError, injection_failure_message(errors)
|
|
90
104
|
end
|
|
91
105
|
|
|
92
106
|
# Run axe and return a Results object. Injects on demand if needed.
|
|
@@ -154,24 +168,47 @@ module AxeCuprite
|
|
|
154
168
|
nil
|
|
155
169
|
end
|
|
156
170
|
|
|
157
|
-
# Best-effort CSP fallback via Ferrum's add_script_tag(content:).
|
|
171
|
+
# Best-effort CSP fallback via Ferrum's add_script_tag(content:). Returns
|
|
172
|
+
# false when no Ferrum add_script_tag is available (non-Ferrum drivers); lets
|
|
173
|
+
# a genuine injection failure propagate so inject_source! can report it.
|
|
158
174
|
def try_add_script_tag(source)
|
|
159
175
|
fpage = ferrum_page
|
|
160
176
|
return false unless fpage.respond_to?(:add_script_tag)
|
|
161
177
|
|
|
162
178
|
fpage.add_script_tag(content: source)
|
|
163
179
|
true
|
|
164
|
-
rescue StandardError
|
|
165
|
-
false
|
|
166
180
|
end
|
|
167
181
|
|
|
182
|
+
# Build the InjectionError message, appending whatever the injection paths
|
|
183
|
+
# actually raised so a non-CSP failure (dead browser, dead CDP session,
|
|
184
|
+
# misconfigured driver) isn't silently blamed on Content-Security-Policy.
|
|
185
|
+
def injection_failure_message(errors)
|
|
186
|
+
message = "axe-core did not load after injection. The page may be blocking " \
|
|
187
|
+
"script injection (e.g. a strict Content-Security-Policy)."
|
|
188
|
+
return message if errors.empty?
|
|
189
|
+
|
|
190
|
+
detail = errors.map { |path, e| "#{path}: #{e.class}: #{e.message}" }.join("; ")
|
|
191
|
+
"#{message}\nUnderlying errors: #{detail}"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Did `evaluate_axe` fail because axe.run genuinely timed out (so the
|
|
195
|
+
# "increase the timeout / scope the run" guidance is right), or did it hit a
|
|
196
|
+
# real page-side error that must propagate untouched?
|
|
197
|
+
#
|
|
198
|
+
# Classification is driven by error CLASS, not message. A message that merely
|
|
199
|
+
# mentions a timeout is deliberately NOT sufficient: a real Ferrum::JavaScriptError
|
|
200
|
+
# from axe or the app whose text happens to contain "timeout" must not be
|
|
201
|
+
# rewritten as an AxeCuprite::TimeoutError with misleading guidance.
|
|
202
|
+
#
|
|
203
|
+
# The one message check is narrow and class-gated: Ferrum reports its own
|
|
204
|
+
# async-evaluation timeout (page.evaluate_async) as a generic JavaScriptError
|
|
205
|
+
# carrying a "timed out promise" message, so for that class — and only that
|
|
206
|
+
# class — the message is what tells a timeout apart from a real JS error.
|
|
168
207
|
def timeout_error?(error)
|
|
169
|
-
|
|
208
|
+
name = error.class.name.to_s
|
|
209
|
+
return true if TIMEOUT_ERROR_CLASS_NAMES.include?(name)
|
|
170
210
|
|
|
171
|
-
|
|
172
|
-
# hard dependency on the Ferrum constants (ferrum is only a dev dep here).
|
|
173
|
-
error.class.name.to_s =~ /Ferrum::(Timeout|JavaScript)Error/ &&
|
|
174
|
-
error.message.to_s =~ /timed out promise/i
|
|
211
|
+
name == "Ferrum::JavaScriptError" && error.message.to_s.match?(/timed out promise/i)
|
|
175
212
|
end
|
|
176
213
|
end
|
|
177
214
|
end
|
data/lib/axe/cuprite/results.rb
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AxeCuprite
|
|
4
|
+
# Recursively freezes a parsed-JSON tree (hashes/arrays of scalars) so the
|
|
5
|
+
# read-only wrappers below can hand out `raw`/`to_h` without a caller being
|
|
6
|
+
# able to mutate internal state (which, via memoization, could otherwise
|
|
7
|
+
# desync `violations`/`incomplete` from `raw`). Idempotent and cheap on the
|
|
8
|
+
# slimmed payload we carry back.
|
|
9
|
+
module DeepFreeze
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(obj)
|
|
13
|
+
case obj
|
|
14
|
+
when Hash
|
|
15
|
+
obj.each { |k, v| [k, v].each { |o| call(o) } }
|
|
16
|
+
when Array
|
|
17
|
+
obj.each { |v| call(v) }
|
|
18
|
+
end
|
|
19
|
+
obj.freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
4
23
|
# Wraps the (slimmed) payload returned by axe.run. We deliberately only carry
|
|
5
24
|
# `violations` and `incomplete` across the CDP boundary plus a little metadata
|
|
6
25
|
# — the full results object (with `passes`/`inapplicable`) can be huge.
|
|
26
|
+
#
|
|
27
|
+
# The wrappers are read-only: `@raw` is deep-frozen at construction, so `raw`
|
|
28
|
+
# and `to_h` expose the live underlying hash safely (callers cannot mutate it).
|
|
7
29
|
class Results
|
|
8
30
|
attr_reader :raw
|
|
9
31
|
|
|
10
32
|
def initialize(raw)
|
|
11
|
-
@raw = raw || {}
|
|
33
|
+
@raw = DeepFreeze.call(raw || {})
|
|
12
34
|
end
|
|
13
35
|
|
|
14
36
|
def violations
|
|
@@ -47,7 +69,7 @@ module AxeCuprite
|
|
|
47
69
|
attr_reader :raw
|
|
48
70
|
|
|
49
71
|
def initialize(raw)
|
|
50
|
-
@raw = raw || {}
|
|
72
|
+
@raw = DeepFreeze.call(raw || {})
|
|
51
73
|
end
|
|
52
74
|
|
|
53
75
|
def id
|
|
@@ -88,7 +110,7 @@ module AxeCuprite
|
|
|
88
110
|
attr_reader :raw
|
|
89
111
|
|
|
90
112
|
def initialize(raw)
|
|
91
|
-
@raw = raw || {}
|
|
113
|
+
@raw = DeepFreeze.call(raw || {})
|
|
92
114
|
end
|
|
93
115
|
|
|
94
116
|
# axe gives `target` as an array of CSS selectors (one per frame depth).
|
|
@@ -137,7 +159,7 @@ module AxeCuprite
|
|
|
137
159
|
attr_reader :raw
|
|
138
160
|
|
|
139
161
|
def initialize(raw)
|
|
140
|
-
@raw = raw || {}
|
|
162
|
+
@raw = DeepFreeze.call(raw || {})
|
|
141
163
|
end
|
|
142
164
|
|
|
143
165
|
def fg_color
|
|
@@ -193,7 +193,7 @@ module AxeCuprite
|
|
|
193
193
|
|
|
194
194
|
def format_node(node)
|
|
195
195
|
lines = [" - #{node.selector}"]
|
|
196
|
-
lines << " #{truncate(node.html)}" if node.html
|
|
196
|
+
lines << " #{truncate(node.html)}" if node.html && config.include_html
|
|
197
197
|
|
|
198
198
|
if (cd = node.contrast_data)
|
|
199
199
|
lines << " contrast #{cd.contrast_ratio}:1 (needs #{cd.expected_contrast_ratio}:1) — " \
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "digest"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "json"
|
|
6
|
+
require "open-uri"
|
|
7
|
+
require "rubygems/package"
|
|
8
|
+
require "stringio"
|
|
9
|
+
require "zlib"
|
|
6
10
|
|
|
7
11
|
namespace :axe do
|
|
8
12
|
desc "Refresh the vendored axe-core engine. Usage: rake 'axe:update[4.12.0]' or VERSION=4.12.0 rake axe:update (default: latest)"
|
|
@@ -15,40 +19,60 @@ namespace :axe do
|
|
|
15
19
|
task :version do
|
|
16
20
|
puts AxeCupriteVendor.vendored_version
|
|
17
21
|
end
|
|
22
|
+
|
|
23
|
+
desc "Verify the vendored axe.min.js against its recorded sha512 checksum"
|
|
24
|
+
task :verify do
|
|
25
|
+
AxeCupriteVendor.verify_vendored!
|
|
26
|
+
end
|
|
18
27
|
end
|
|
19
28
|
|
|
20
29
|
# Helpers for vendoring axe-core. Kept in a module so the rake tasks stay thin
|
|
21
30
|
# and the logic is easy to read. Never used at runtime — vendoring is a
|
|
22
31
|
# development-time step; the engine ships in the gem.
|
|
32
|
+
#
|
|
33
|
+
# Supply-chain note: the vendored axe.min.js is the JavaScript this gem injects
|
|
34
|
+
# into every consumer's browser session, so its integrity is the gem's most
|
|
35
|
+
# important supply-chain property. We therefore download the *official npm
|
|
36
|
+
# tarball* from registry.npmjs.org (not a CDN re-serving it), verify it against
|
|
37
|
+
# the sha512 integrity the registry publishes in its metadata, and abort on any
|
|
38
|
+
# mismatch before a single byte is written. The verified content hash of the
|
|
39
|
+
# extracted axe.min.js is recorded next to it (axe.min.js.sha512) so reviewers
|
|
40
|
+
# and CI can re-check the vendored artifact without re-downloading.
|
|
23
41
|
module AxeCupriteVendor
|
|
24
42
|
VENDOR_DIR = File.expand_path("../vendor", __dir__)
|
|
25
43
|
AXE_JS = File.join(VENDOR_DIR, "axe.min.js")
|
|
44
|
+
AXE_SHA512 = File.join(VENDOR_DIR, "axe.min.js.sha512")
|
|
26
45
|
AXE_LICENSE = File.join(VENDOR_DIR, "axe-core-LICENSE.txt")
|
|
27
46
|
VERSION_FILE = File.expand_path("../version.rb", __dir__)
|
|
28
47
|
REGISTRY = "https://registry.npmjs.org/axe-core"
|
|
29
|
-
CDN = "https://unpkg.com/axe-core"
|
|
30
48
|
|
|
31
49
|
module_function
|
|
32
50
|
|
|
51
|
+
# A semver-ish version string: three dot-separated numbers with an optional
|
|
52
|
+
# prerelease suffix. Rejecting anything else keeps the value safe to splice
|
|
53
|
+
# into the registry URL and into version.rb, and catches honest typos before
|
|
54
|
+
# they corrupt the vendor directory.
|
|
55
|
+
VERSION_FORMAT = /\A\d+\.\d+\.\d+(-[\w.]+)?\z/
|
|
56
|
+
|
|
33
57
|
def update!(version)
|
|
58
|
+
abort "Invalid version: #{version.inspect}" unless version.match?(VERSION_FORMAT)
|
|
59
|
+
|
|
34
60
|
FileUtils.mkdir_p(VENDOR_DIR)
|
|
35
61
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
license =
|
|
62
|
+
dist = registry_dist(version)
|
|
63
|
+
puts "Downloading #{dist.fetch("tarball")} ..."
|
|
64
|
+
tarball = download(dist.fetch("tarball"))
|
|
65
|
+
verify_tarball!(tarball, dist)
|
|
66
|
+
|
|
67
|
+
js, license = extract(tarball, "package/axe.min.js", "package/LICENSE")
|
|
68
|
+
verify_banner!(js, version)
|
|
42
69
|
|
|
43
70
|
File.write(AXE_JS, js)
|
|
44
71
|
File.write(AXE_LICENSE, license)
|
|
72
|
+
File.write(AXE_SHA512, "#{Digest::SHA512.hexdigest(js)} axe.min.js\n")
|
|
45
73
|
bump_version_constant(version)
|
|
46
74
|
|
|
47
|
-
|
|
48
|
-
puts " #{AXE_JS} (#{js.bytesize} bytes)"
|
|
49
|
-
puts " #{AXE_LICENSE}"
|
|
50
|
-
puts " updated AXE_CORE_VERSION in #{VERSION_FILE}"
|
|
51
|
-
puts "Remember to note the version bump in CHANGELOG.md."
|
|
75
|
+
report(version, js)
|
|
52
76
|
end
|
|
53
77
|
|
|
54
78
|
def latest_version
|
|
@@ -59,8 +83,71 @@ module AxeCupriteVendor
|
|
|
59
83
|
File.read(AXE_JS)[/axe v([0-9][0-9.]*)/, 1] || "unknown"
|
|
60
84
|
end
|
|
61
85
|
|
|
86
|
+
def verify_vendored!
|
|
87
|
+
abort "No recorded checksum at #{AXE_SHA512} — run rake axe:update first." unless File.exist?(AXE_SHA512)
|
|
88
|
+
|
|
89
|
+
expected = File.read(AXE_SHA512).split.first
|
|
90
|
+
actual = Digest::SHA512.hexdigest(File.binread(AXE_JS))
|
|
91
|
+
unless actual == expected
|
|
92
|
+
abort <<~MSG
|
|
93
|
+
MISMATCH: #{AXE_JS} does not match its recorded sha512.
|
|
94
|
+
recorded: #{expected}
|
|
95
|
+
actual: #{actual}
|
|
96
|
+
MSG
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
puts "OK: axe.min.js matches its recorded sha512."
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def registry_dist(version)
|
|
103
|
+
puts "Fetching axe-core@#{version} metadata from #{REGISTRY} ..."
|
|
104
|
+
JSON.parse(download("#{REGISTRY}/#{version}")).fetch("dist")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Abort unless the downloaded tarball matches the integrity values the npm
|
|
108
|
+
# registry publishes for it (dist.integrity sha512, plus legacy dist.shasum).
|
|
109
|
+
def verify_tarball!(tarball, dist)
|
|
110
|
+
integrity = dist["integrity"]
|
|
111
|
+
abort "Registry metadata has no sha512 integrity for the tarball — refusing to vendor." unless integrity&.start_with?("sha512-")
|
|
112
|
+
|
|
113
|
+
actual = "sha512-#{Digest::SHA512.base64digest(tarball)}"
|
|
114
|
+
unless actual == integrity
|
|
115
|
+
abort <<~MSG
|
|
116
|
+
Tarball integrity check FAILED — refusing to vendor.
|
|
117
|
+
expected (registry dist.integrity): #{integrity}
|
|
118
|
+
actual (downloaded tarball): #{actual}
|
|
119
|
+
MSG
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
shasum = dist["shasum"]
|
|
123
|
+
if shasum && Digest::SHA1.hexdigest(tarball) != shasum
|
|
124
|
+
abort "Tarball sha1 shasum mismatch (registry says #{shasum}) — refusing to vendor."
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
puts " tarball integrity verified (#{integrity})"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Secondary sanity check on the extracted engine itself; fatal on mismatch.
|
|
131
|
+
def verify_banner!(engine_js, version)
|
|
132
|
+
return if engine_js.match?(/axe v#{Regexp.escape(version)}\b/)
|
|
133
|
+
|
|
134
|
+
abort "Extracted axe.min.js banner does not mention v#{version} — refusing to vendor."
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def extract(tarball, *paths)
|
|
138
|
+
found = {}
|
|
139
|
+
Zlib::GzipReader.wrap(StringIO.new(tarball)) do |gz|
|
|
140
|
+
Gem::Package::TarReader.new(gz) do |tar|
|
|
141
|
+
tar.each { |entry| found[entry.full_name] = entry.read if paths.include?(entry.full_name) }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
missing = paths - found.keys
|
|
145
|
+
abort "Tarball is missing expected file(s): #{missing.join(", ")}" unless missing.empty?
|
|
146
|
+
found.values_at(*paths)
|
|
147
|
+
end
|
|
148
|
+
|
|
62
149
|
def download(url)
|
|
63
|
-
URI.parse(url).open(&:read)
|
|
150
|
+
URI.parse(url).open("rb", &:read)
|
|
64
151
|
rescue OpenURI::HTTPError => e
|
|
65
152
|
abort "Failed to download #{url}: #{e.message}"
|
|
66
153
|
end
|
|
@@ -70,4 +157,13 @@ module AxeCupriteVendor
|
|
|
70
157
|
updated = contents.sub(/(AXE_CORE_VERSION\s*=\s*)"[^"]*"/, %(\\1"#{version}"))
|
|
71
158
|
File.write(VERSION_FILE, updated)
|
|
72
159
|
end
|
|
160
|
+
|
|
161
|
+
def report(version, engine_js)
|
|
162
|
+
puts "Vendored axe-core #{version} (tarball integrity verified):"
|
|
163
|
+
puts " #{AXE_JS} (#{engine_js.bytesize} bytes)"
|
|
164
|
+
puts " #{AXE_SHA512}"
|
|
165
|
+
puts " #{AXE_LICENSE}"
|
|
166
|
+
puts " updated AXE_CORE_VERSION in #{VERSION_FILE}"
|
|
167
|
+
puts "Remember to note the version bump in CHANGELOG.md."
|
|
168
|
+
end
|
|
73
169
|
end
|
data/lib/axe/cuprite/version.rb
CHANGED