hecks 3.2.0 → 3.2.1
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/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
- data/lib/hecks/cli/check_era.rb +3 -39
- data/lib/hecks/doors/mcp_door.rb +16 -2
- data/lib/hecks/doors/mcp_door_scope.rb +43 -4
- data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
- data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
- data/lib/hecks/hecks/adapters/site_toolchain.rb +21 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +7 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
- data/lib/hecks/projections/deploy/box/settings.rb +23 -2
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
- data/lib/hecks/projections/deploy/box.rb +13 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
- data/lib/hecks/projections/site/edge/checks.rb +31 -5
- data/lib/hecks/projections/site/edge.rb +25 -11
- data/lib/hecks/projections/site/routes_ts.rb +50 -27
- data/lib/hecks/projections/site/site_cdn.rb +7 -4
- data/lib/hecks/projections/site/table/checks.rb +53 -7
- data/lib/hecks/projections/site/table/link_builder.rb +64 -0
- data/lib/hecks/projections/site/table/row_builder.rb +21 -6
- data/lib/hecks/projections/site/table.rb +16 -5
- data/lib/hecks/rust_build/kernel_input.rb +18 -6
- data/lib/hecks/rust_build/project_rust.rb +6 -132
- data/lib/hecks/site/bluebook/site.bluebook +27 -13
- data/lib/hecks/storehouse.rb +35 -1
- data/lib/hecks/tools/comment_style.rb +4 -2
- data/lib/hecks/tools/rust_comment_style.rb +1 -1
- data/lib/hecks/tools/site_routes.rb +69 -28
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -1
- data/lib/hecks.rb +1 -0
- data/rust/build/src/lineage_pass.rs +1 -1
- data/rust/build/src/pipeline.rs +1 -1
- data/rust/codegen/src/attr.rs +1 -1
- data/rust/codegen/src/bridging.rs +1 -1
- data/rust/codegen/src/commands.rs +1 -1
- data/rust/codegen/src/constraints.rs +1 -1
- data/rust/codegen/src/dependency_planning.rs +1 -1
- data/rust/codegen/src/domain_generator.rs +2 -2
- data/rust/codegen/src/exemplar.rs +1 -1
- data/rust/codegen/src/expr_emitter.rs +1 -1
- data/rust/codegen/src/fielded.rs +1 -1
- data/rust/codegen/src/json_codec.rs +1 -1
- data/rust/codegen/src/main.rs +3 -4
- data/rust/codegen/src/mutations.rs +1 -1
- data/rust/codegen/src/naming.rs +1 -1
- data/rust/codegen/src/ports.rs +1 -1
- data/rust/codegen/src/queries.rs +3 -3
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/read_models.rs +2 -2
- data/rust/codegen/src/reference_specs.rs +1 -1
- data/rust/codegen/src/registry.rs +4 -4
- data/rust/codegen/src/shared.rs +1 -1
- data/rust/codegen/src/skip_reason.rs +1 -1
- data/rust/codegen/src/types.rs +4 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/needs.rs +94 -27
- data/rust/src/exemplar/commands.rs +1 -1
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/fielded.rs +1 -1
- data/rust/src/exemplar/json.rs +1 -1
- data/rust/src/exemplar/mod.rs +1 -1
- data/rust/src/exemplar/mutations.rs +1 -1
- data/rust/src/exemplar/queries.rs +1 -1
- data/rust/src/exemplar/reactions.rs +1 -1
- data/rust/src/exemplar/read_models.rs +1 -1
- data/rust/src/exemplar/registry.rs +1 -1
- data/rust/src/exemplar/types.rs +1 -1
- data/rust/src/kernel/cli.rs +2 -2
- data/rust/src/kernel/read_model.rs +1 -1
- data/rust/src/lib.rs +1 -1
- metadata +3 -23
- data/rust/project/bridging.rb +0 -257
- data/rust/project/commands.rb +0 -877
- data/rust/project/constraints.rb +0 -86
- data/rust/project/dependency_planning.rb +0 -178
- data/rust/project/domain_generator.rb +0 -983
- data/rust/project/exemplar.rb +0 -148
- data/rust/project/expr_emitter.rb +0 -72
- data/rust/project/fielded.rb +0 -141
- data/rust/project/json_codec.rb +0 -641
- data/rust/project/mutations.rb +0 -695
- data/rust/project/naming.rb +0 -223
- data/rust/project/ports.rb +0 -65
- data/rust/project/queries.rb +0 -493
- data/rust/project/reactions.rb +0 -317
- data/rust/project/read_models.rb +0 -690
- data/rust/project/reference_specs.rb +0 -27
- data/rust/project/registry.rb +0 -396
- data/rust/project/skip_reason.rb +0 -29
- data/rust/project/types.rb +0 -225
- data/rust/project/write_if_changed.rb +0 -7
- data/rust/project.rb +0 -30
- data/rust/project_rust_pipeline.rb +0 -391
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b963870a34a1483bc0156042a2967a72f8fc1b8e907ba5d8cac5a5382d4999d
|
|
4
|
+
data.tar.gz: 26697af02778cda282a44c1ca099e36a9866bf2d204ab5abb23df8ac2a86283a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a81b0ffb4b0e04024518169e3c6dd51a3f55b5da39659a852c2a0c9528453fe0eb9facf07d7b18bd35d7924c5a08331b2f1ad82a7e5c4c43c61fae55490d46ed
|
|
7
|
+
data.tar.gz: f6780cb0c58069a5049081c7a319424787c9fea1fd94a5085153d7b99533c0e1974c23dfeb3515b66f3b2d8d3c7ca83a08dcd5f024df02da2b2fc9609c3ba6b7
|
|
@@ -57,9 +57,9 @@ module Hecks
|
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
# The release that drops the deprecated spellings of `attaches`;
|
|
61
|
-
# `Hecks::Doors::REMOVAL`, which
|
|
62
|
-
REMOVAL = "3.
|
|
60
|
+
# The release that drops the deprecated spellings of `attaches`; independent of
|
|
61
|
+
# `Hecks::Doors::REMOVAL`, which covers the older `Facade` aliases.
|
|
62
|
+
REMOVAL = "3.3.0".freeze
|
|
63
63
|
|
|
64
64
|
attr_reader :binds, :subscriptions, :attachments
|
|
65
65
|
|
data/lib/hecks/cli/check_era.rb
CHANGED
|
@@ -2,11 +2,9 @@ require_relative "../ports/persistence/plugins/era/expected_era"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module CLI
|
|
5
|
-
# The
|
|
6
|
-
#
|
|
5
|
+
# The check behind Custodian's `Host.CheckEra`: asks a running host which era it reports and
|
|
6
|
+
# compares it with an allow-list file. Read-only: one GET, no writes.
|
|
7
7
|
module CheckEra
|
|
8
|
-
USAGE = "usage: hecks check_era <url> <expected-era-file> [--timeout=<seconds>]".freeze
|
|
9
|
-
|
|
10
8
|
# What a check found.
|
|
11
9
|
#
|
|
12
10
|
# @!attribute [r] verdict
|
|
@@ -14,7 +12,7 @@ module Hecks
|
|
|
14
12
|
# @!attribute [r] version
|
|
15
13
|
# @return [String] the gem version the host reports, empty when it reports none
|
|
16
14
|
# @!attribute [r] line
|
|
17
|
-
# @return [String] the sentence
|
|
15
|
+
# @return [String] the sentence the check reports for this finding
|
|
18
16
|
Finding = Struct.new(:verdict, :version, :line)
|
|
19
17
|
|
|
20
18
|
module_function
|
|
@@ -36,35 +34,6 @@ module Hecks
|
|
|
36
34
|
Finding.new(verdict, document.fetch("version"), sentence(verdict, file))
|
|
37
35
|
end
|
|
38
36
|
|
|
39
|
-
# Runs `hecks check_era`: prints the finding and answers the exit status.
|
|
40
|
-
#
|
|
41
|
-
# @param argv [Array<String>] the url, the allow-list file and an optional `--timeout=N`
|
|
42
|
-
# @param out [IO] where a finding goes
|
|
43
|
-
# @param err [IO] where a refusal goes
|
|
44
|
-
# @return [Integer] 0 when the era is expected or unlisted, 1 on a mismatch, 2 on bad
|
|
45
|
-
# usage or an unreadable file, 3 when the host cannot be reached or answers no era
|
|
46
|
-
def run(argv, out: $stdout, err: $stderr)
|
|
47
|
-
timeout = 10
|
|
48
|
-
args = argv.reject do |arg|
|
|
49
|
-
next false unless arg.start_with?("--timeout=")
|
|
50
|
-
|
|
51
|
-
timeout = arg.delete_prefix("--timeout=").to_f
|
|
52
|
-
true
|
|
53
|
-
end
|
|
54
|
-
url, file = args
|
|
55
|
-
return usage(err) unless url && file && args.size == 2 && timeout.positive?
|
|
56
|
-
|
|
57
|
-
finding = assess(url, file, timeout: timeout)
|
|
58
|
-
(finding.verdict.ok? ? out : err).puts(finding.line)
|
|
59
|
-
finding.verdict.ok? ? 0 : 1
|
|
60
|
-
rescue Errno::ENOENT, Errno::EACCES => e
|
|
61
|
-
err.puts("cannot read #{file}: #{e.message}")
|
|
62
|
-
2
|
|
63
|
-
rescue Runtime::EraCheck::ExpectedEra::Unreachable, Runtime::EraCheck::ExpectedEra::BadResponse => e
|
|
64
|
-
err.puts("era check failed: #{e.message}")
|
|
65
|
-
3
|
|
66
|
-
end
|
|
67
|
-
|
|
68
37
|
# @param verdict [Runtime::EraCheck::ExpectedEra::Verdict] the comparison
|
|
69
38
|
# @param file [String] the allow-list file, named in the sentence
|
|
70
39
|
# @return [String] the one line that says what the comparison found
|
|
@@ -78,11 +47,6 @@ module Hecks
|
|
|
78
47
|
"host reports era #{verdict.era}, which is not in #{file} (expected: #{verdict.allowed.join(', ')})"
|
|
79
48
|
end
|
|
80
49
|
end
|
|
81
|
-
|
|
82
|
-
def usage(err)
|
|
83
|
-
err.puts(USAGE)
|
|
84
|
-
2
|
|
85
|
-
end
|
|
86
50
|
end
|
|
87
51
|
end
|
|
88
52
|
end
|
data/lib/hecks/doors/mcp_door.rb
CHANGED
|
@@ -343,7 +343,20 @@ module Hecks
|
|
|
343
343
|
def answer_tool(name, args)
|
|
344
344
|
return scope.refusal(name) unless scope.permits_tool?(name)
|
|
345
345
|
|
|
346
|
-
call_tool(name, args)
|
|
346
|
+
call_tool(name, scope.with_default_domain(args))
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# One line per allowed command: what it takes and the role it declares, read from the booted
|
|
350
|
+
# domain the door serves. Nil when the door serves no single domain or it will not boot, so
|
|
351
|
+
# `tools/list` still answers, with the commands as a bare list.
|
|
352
|
+
def command_guide
|
|
353
|
+
return unless scope.commands_mode? && scope.default_domain
|
|
354
|
+
|
|
355
|
+
McpGuideCache.remember(scope.default_domain, scope.allowed_commands) do
|
|
356
|
+
Storehouse.command_guide(boot(scope.default_domain), scope.allowed_commands)
|
|
357
|
+
end
|
|
358
|
+
rescue StandardError
|
|
359
|
+
nil
|
|
347
360
|
end
|
|
348
361
|
|
|
349
362
|
def tool_result(payload)
|
|
@@ -374,7 +387,8 @@ module Hecks
|
|
|
374
387
|
nil # a notification — no id, no response
|
|
375
388
|
when "tools/list"
|
|
376
389
|
served = TOOLS.select { |tool| scope.permits_tool?(tool[:name]) }
|
|
377
|
-
|
|
390
|
+
guide = command_guide
|
|
391
|
+
send_response(id, { tools: served.map { |tool| scope.present(tool, guide) } })
|
|
378
392
|
when "tools/call"
|
|
379
393
|
send_response(id, tool_result(answer_tool(params["name"], params["arguments"] || {})))
|
|
380
394
|
when "ping"
|
|
@@ -201,18 +201,40 @@ module Hecks
|
|
|
201
201
|
end
|
|
202
202
|
end
|
|
203
203
|
|
|
204
|
-
# The
|
|
205
|
-
#
|
|
204
|
+
# The one domain a restricted door serves, when it serves exactly one: a call may leave
|
|
205
|
+
# `domain:` out and get it. Nil for an unrestricted door and for one that names several.
|
|
206
|
+
#
|
|
207
|
+
# @return [String, nil] the domain path as `HECKS_DOOR_DOMAINS` named it, resolved
|
|
208
|
+
def default_domain
|
|
209
|
+
@allowed_domains.first if restricted? && @allowed_domains.size == 1
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The arguments with the default domain filled in when the caller left it out.
|
|
213
|
+
#
|
|
214
|
+
# @param args [Hash] a tool call's arguments
|
|
215
|
+
# @return [Hash] `args`, with `"domain"` set when the door has a default and none was given
|
|
216
|
+
def with_default_domain(args)
|
|
217
|
+
return args unless default_domain && args["domain"].to_s.strip.empty?
|
|
218
|
+
|
|
219
|
+
args.merge("domain" => default_domain)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The tool as `tools/list` shows it. A door that serves one domain makes `domain:` optional.
|
|
223
|
+
# On a commands door, `dispatch` also names the commands it serves as an enum, so a caller
|
|
224
|
+
# sees them as typed choices instead of finding out by being refused, and says what each
|
|
225
|
+
# takes and which role it declares.
|
|
206
226
|
#
|
|
207
227
|
# @param tool [Hash] a tool definition from `McpDoor::TOOLS`
|
|
228
|
+
# @param guide [Array<String>, nil] one line per allowed command (`Storehouse.command_guide`)
|
|
208
229
|
# @return [Hash] the definition, narrowed for this door
|
|
209
|
-
def present(tool)
|
|
230
|
+
def present(tool, guide = nil)
|
|
231
|
+
tool = domain_optional(tool) if default_domain
|
|
210
232
|
return tool unless commands_mode? && tool[:name] == "dispatch"
|
|
211
233
|
|
|
212
234
|
properties = tool[:inputSchema][:properties]
|
|
213
235
|
narrowed = properties.merge(command: with_enum(properties[:command]),
|
|
214
236
|
steps: properties[:steps].merge(items: step_items(properties[:steps][:items])))
|
|
215
|
-
tool.merge(description:
|
|
237
|
+
tool.merge(description: dispatch_description(tool[:description], guide),
|
|
216
238
|
inputSchema: tool[:inputSchema].merge(properties: narrowed))
|
|
217
239
|
end
|
|
218
240
|
|
|
@@ -287,6 +309,23 @@ module Hecks
|
|
|
287
309
|
File.join(File.realpath(current), *missing)
|
|
288
310
|
end
|
|
289
311
|
|
|
312
|
+
def domain_optional(tool)
|
|
313
|
+
schema = tool[:inputSchema]
|
|
314
|
+
return tool unless schema[:properties].key?(:domain)
|
|
315
|
+
|
|
316
|
+
note = "Optional: this door serves one domain, #{default_domain}, and uses it when you leave this out."
|
|
317
|
+
properties = schema[:properties].merge(domain: schema[:properties][:domain].merge(description: note))
|
|
318
|
+
tool.merge(inputSchema: schema.merge(properties: properties, required: schema[:required] - ["domain"]))
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def dispatch_description(base, guide)
|
|
322
|
+
intro = "#{base} This door dispatches only the commands below, each with the role it declares: pass " \
|
|
323
|
+
"that role as `role`. A command that takes `run` takes a key you choose, and the call answers " \
|
|
324
|
+
"the record as it stands once its reactions have run; an argument marked * is required."
|
|
325
|
+
entries = Array(guide).empty? ? allowed_commands : guide
|
|
326
|
+
([intro] + entries.map { |entry| "- #{entry}" }).join("\n")
|
|
327
|
+
end
|
|
328
|
+
|
|
290
329
|
def with_enum(property)
|
|
291
330
|
property.merge(enum: allowed_commands)
|
|
292
331
|
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../cache_dir"
|
|
5
|
+
require_relative "../storehouse"
|
|
6
|
+
require_relative "../bluebook/meta_validator/verdict_cache"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Doors
|
|
10
|
+
# Remembers the command guide a commands door builds for `tools/list`, so a door that starts
|
|
11
|
+
# again does not boot its domain to describe its commands. A client waits only a moment for a
|
|
12
|
+
# server's tools, and a boot at that point costs more than the wait.
|
|
13
|
+
#
|
|
14
|
+
# An entry is keyed by the domain directory's files, a digest of the hecks code, and the
|
|
15
|
+
# allowed commands, so a change to any of them builds a new guide. The file is JSON, read only
|
|
16
|
+
# from a directory and a file that only this user can write (the verdict cache's own check),
|
|
17
|
+
# and only when it is a list of strings: a guide is text an agent reads as instructions.
|
|
18
|
+
module McpGuideCache
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# The guide for a domain and its allowed commands, from the cache or else from the block,
|
|
22
|
+
# which is then remembered. A cache that cannot be written is not an error: the next start
|
|
23
|
+
# builds the guide again.
|
|
24
|
+
#
|
|
25
|
+
# @param domain [String] the domain directory the guide describes
|
|
26
|
+
# @param commands [Array<String>] the allowed command names
|
|
27
|
+
# @yieldreturn [Array<String>, nil] the guide, built when none is remembered
|
|
28
|
+
# @return [Array<String>, nil] the guide
|
|
29
|
+
def remember(domain, commands)
|
|
30
|
+
file = File.join(dir, "#{key(domain, commands)}.json")
|
|
31
|
+
cached = read(file)
|
|
32
|
+
return cached if cached
|
|
33
|
+
|
|
34
|
+
guide = yield
|
|
35
|
+
write(file, guide) if guide.is_a?(Array) && guide.any?
|
|
36
|
+
guide
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [String] the directory entries live in
|
|
40
|
+
def dir = Hecks::CacheDir.path("mcp_door_guides")
|
|
41
|
+
|
|
42
|
+
# @param domain [String] the domain directory
|
|
43
|
+
# @param commands [Array<String>] the allowed command names
|
|
44
|
+
# @return [String] a hex SHA-256 over the domain's files, the hecks code and the commands
|
|
45
|
+
def key(domain, commands)
|
|
46
|
+
parts = [Storehouse.fingerprint(domain), Bluebook::MetaValidator::VerdictCache.code_digest,
|
|
47
|
+
commands.sort.join(",")]
|
|
48
|
+
Digest::SHA256.hexdigest(parts.join("\0"))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param file [String] an entry's path
|
|
52
|
+
# @return [Array<String>, nil] the guide, or nil when absent, untrusted or malformed
|
|
53
|
+
def read(file)
|
|
54
|
+
return unless File.file?(file) && Bluebook::MetaValidator::VerdictCache.trusted?(file)
|
|
55
|
+
|
|
56
|
+
guide = JSON.parse(File.read(file))
|
|
57
|
+
guide if guide.is_a?(Array) && guide.all?(String)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param file [String] an entry's path
|
|
63
|
+
# @param guide [Array<String>] the guide to remember
|
|
64
|
+
# @return [void]
|
|
65
|
+
def write(file, guide)
|
|
66
|
+
FileUtils.mkdir_p(dir, mode: 0o700)
|
|
67
|
+
temp = "#{file}.#{Process.pid}.tmp"
|
|
68
|
+
File.write(temp, JSON.generate(guide), perm: 0o600)
|
|
69
|
+
File.rename(temp, file)
|
|
70
|
+
rescue StandardError
|
|
71
|
+
FileUtils.rm_f(temp) if temp
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -118,8 +118,9 @@ module Hecks
|
|
|
118
118
|
|
|
119
119
|
owner = owner_for_verb(bluebooks, entry[:verb]) || aggregate
|
|
120
120
|
|
|
121
|
+
args = with_declared_defaults(command, entry[:args])
|
|
121
122
|
command.mutations.select { |m| RECOMPUTABLE_MUTATION_OPS.include?(m.op) }.filter_map do |mutation|
|
|
122
|
-
expected = recompute_mutation(mutation, entry[:before][mutation.target],
|
|
123
|
+
expected = recompute_mutation(mutation, entry[:before][mutation.target], args, entry[:before],
|
|
123
124
|
aggregate, command, owner)
|
|
124
125
|
next if expected == :unrecomputable
|
|
125
126
|
|
|
@@ -134,6 +135,21 @@ module Hecks
|
|
|
134
135
|
offenders.empty? || offenders.join("; ")
|
|
135
136
|
end
|
|
136
137
|
|
|
138
|
+
# The arguments the command actually ran with: an argument the caller left out takes the
|
|
139
|
+
# default its attribute declares, as dispatch fills it, so the recomputation starts from
|
|
140
|
+
# the same facts.
|
|
141
|
+
#
|
|
142
|
+
# @param command [Object] the command the step dispatched
|
|
143
|
+
# @param args [Hash] the arguments the step offered
|
|
144
|
+
# @return [Hash] `args`, with each omitted defaulted argument filled
|
|
145
|
+
def with_declared_defaults(command, args)
|
|
146
|
+
command.attributes.each_with_object(args.dup) do |attribute, held|
|
|
147
|
+
next if attribute.default.nil? || held.key?(attribute.name.to_sym) || held.key?(attribute.name.to_s)
|
|
148
|
+
|
|
149
|
+
held[attribute.name.to_sym] = attribute.default
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
137
153
|
# Root aggregate for `verb`, derived from the verb alone because hand-built
|
|
138
154
|
# entries may lack `:domain`/`:aggregate`. Value-object types resolve against
|
|
139
155
|
# the root's namespace only.
|
|
@@ -38,7 +38,9 @@ module Hecks
|
|
|
38
38
|
status = Dir.chdir(@tree.root) { runner.call(["--example", example, path], out, out) }
|
|
39
39
|
raise ConsoleCapture::Failure, out.string.strip unless status.zero?
|
|
40
40
|
|
|
41
|
-
out.string.strip
|
|
41
|
+
out.string.strip.tap do |report|
|
|
42
|
+
raise ConsoleCapture::Failure, "the test runner printed nothing for #{file}" if report.empty?
|
|
43
|
+
end
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
private
|
|
@@ -47,7 +49,13 @@ module Hecks
|
|
|
47
49
|
return self.class.runner if self.class.runner
|
|
48
50
|
|
|
49
51
|
require "rspec/core"
|
|
50
|
-
|
|
52
|
+
# A process that runs one example after another, as a resident door does, has to reset
|
|
53
|
+
# RSpec between runs: its reporter keeps the first run's output stream, so a later run
|
|
54
|
+
# would write into that and answer an empty report.
|
|
55
|
+
lambda do |args, err, out|
|
|
56
|
+
RSpec.reset
|
|
57
|
+
RSpec::Core::Runner.run(args, err, out)
|
|
58
|
+
end
|
|
51
59
|
end
|
|
52
60
|
end
|
|
53
61
|
end
|
|
@@ -22,7 +22,7 @@ module Hecks
|
|
|
22
22
|
CHECKOUT_MARKER = "hecks.gemspec"
|
|
23
23
|
|
|
24
24
|
# What a copy holds: the workspace manifest, the kernel and every crate a domain build uses.
|
|
25
|
-
PACKAGED = %w[Cargo.toml Cargo.lock
|
|
25
|
+
PACKAGED = %w[Cargo.toml Cargo.lock src codegen parser
|
|
26
26
|
host build web lsp].freeze
|
|
27
27
|
|
|
28
28
|
# Left out of a copy: build output anywhere, the corpus tests, and the corpus domains.
|
|
@@ -9,13 +9,16 @@ module Hecks
|
|
|
9
9
|
# The `SiteToolchain` port's adapter: projects a site's route table into its `routes.ts`, or
|
|
10
10
|
# checks the file on disk is current.
|
|
11
11
|
#
|
|
12
|
-
# The projection runs the `project_site` tool in
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# The projection runs the `project_site` tool in this process, with its printing and exit status
|
|
13
|
+
# captured: a tool that ends non-zero is a refusal whose reason is what it printed. The tool
|
|
14
|
+
# ships in the gem and reads only the project it is given, so it needs no hecks checkout.
|
|
15
15
|
class SiteToolchain
|
|
16
16
|
# The `Hecks::Tools` tool the ask runs, by ask.
|
|
17
17
|
SCRIPTS = { project: "project_site" }.freeze
|
|
18
18
|
|
|
19
|
+
# The tool's flag for each `SiteProjection` field that takes a value.
|
|
20
|
+
FLAGS = { "out" => :out, "template" => :template, "extension" => :extension }.freeze
|
|
21
|
+
|
|
19
22
|
# Accepts the arguments every driven adapter is built with and keeps none of them.
|
|
20
23
|
#
|
|
21
24
|
# @param aggregate [Object, nil] unused
|
|
@@ -25,17 +28,17 @@ module Hecks
|
|
|
25
28
|
|
|
26
29
|
# Writes a project's `routes.ts` from its declared route table, or with `check` only compares.
|
|
27
30
|
#
|
|
28
|
-
# @param held [Hash] the `SiteProjection` record: `domain`, and `out`
|
|
31
|
+
# @param held [Hash] the `SiteProjection` record: `domain`, and `out`, `template`, `extension`
|
|
32
|
+
# and `check` when set
|
|
29
33
|
# @return [Hash{Symbol => Hash}] `output:` one line per file written or found current
|
|
30
|
-
# @raise [ConsoleCapture::Failure] when the
|
|
31
|
-
#
|
|
34
|
+
# @raise [ConsoleCapture::Failure] when the route table is refused (an unknown cache class,
|
|
35
|
+
# a duplicate path), a path or the extension is refused, or under `check` a file is out
|
|
36
|
+
# of date
|
|
32
37
|
def project(**held)
|
|
33
|
-
flags = []
|
|
34
|
-
flags << "--out=#{plain(held[:out])}" unless plain(held[:out]).nil?
|
|
38
|
+
flags = FLAGS.filter_map { |flag, key| "--#{flag}=#{located(key, held[key])}" unless plain(held[key]).nil? }
|
|
35
39
|
flags << "--check" if plain(held[:check]) == true
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
result = Codebase::RubyChild.new(tree).capture(SCRIPTS.fetch(:project), *flags, plain(held[:domain]))
|
|
40
|
+
result = Codebase::RubyChild.new(Codebase::Tree.new).capture(SCRIPTS.fetch(:project), *flags,
|
|
41
|
+
located(:domain, held[:domain]))
|
|
39
42
|
raise ConsoleCapture::Failure, message_of(result) unless result.ok?
|
|
40
43
|
|
|
41
44
|
{ output: { value: result.out } }
|
|
@@ -48,6 +51,13 @@ module Hecks
|
|
|
48
51
|
text.empty? ? "the tool ended with status #{result.status.exitstatus}" : text
|
|
49
52
|
end
|
|
50
53
|
|
|
54
|
+
# The tool runs from the tool's own root, so a path the caller gave relative to where they
|
|
55
|
+
# stand is made absolute first; the extension is not a path.
|
|
56
|
+
def located(key, argument)
|
|
57
|
+
value = plain(argument)
|
|
58
|
+
value.nil? || key == :extension ? value : File.expand_path(value)
|
|
59
|
+
end
|
|
60
|
+
|
|
51
61
|
def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
|
|
52
62
|
end
|
|
53
63
|
end
|
|
@@ -625,16 +625,16 @@ Hecks.bluebook "Bluebook" do
|
|
|
625
625
|
pattern: '^lib/hecks/runtime/', push: "skip",
|
|
626
626
|
label: "does this change touch lib/hecks/runtime/**?"
|
|
627
627
|
member name: "postgres_io_relevant_changed", workflow: "ci-postgres-io-parallel.yml", mode: "skips_unless",
|
|
628
|
-
pattern: '^(docs/|editors/|release/|deploy/|\.claude/|\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests
|
|
628
|
+
pattern: '^(docs/|editors/|release/|deploy/|\.claude/|\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests)/|\.rubocop\.yml$|\.rubocop_todo\.yml$|\.mcp\.json$|\.rspec-local\.example$|README\.md$|CHANGELOG\.md$|CONTRIBUTING\.md$|SECURITY\.md$|LICENSE$|\.gitignore$)',
|
|
629
629
|
push: "before_sha",
|
|
630
630
|
label: "does this change touch anything rspec_postgres_io_parallel covers?"
|
|
631
631
|
end
|
|
632
632
|
|
|
633
633
|
# Rust keywords (strict + reserved) a generated identifier must not
|
|
634
|
-
# collide with.
|
|
635
|
-
#
|
|
636
|
-
#
|
|
637
|
-
#
|
|
634
|
+
# collide with. ModelCheck reads this; hecks project_reserved_names
|
|
635
|
+
# projects it into rust/codegen/src/reserved_names.rs, so Ruby and Rust
|
|
636
|
+
# escape or refuse the same words from one declaration instead of two
|
|
637
|
+
# hand-typed copies.
|
|
638
638
|
value_object "RustReservedWord" do
|
|
639
639
|
attribute :word, String,
|
|
640
640
|
one_of: ["as", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false",
|
|
@@ -648,8 +648,8 @@ Hecks.bluebook "Bluebook" do
|
|
|
648
648
|
# as a Cargo `[features]` key: every key rust/Cargo.toml already uses
|
|
649
649
|
# outside `[features]` (`[package]`, `[lib]`, `[[bin]]`) plus `default`,
|
|
650
650
|
# which Cargo reserves for the auto-enabled feature set. Read by
|
|
651
|
-
#
|
|
652
|
-
#
|
|
651
|
+
# Hecks::RustBuild::DomainName and projected by hecks
|
|
652
|
+
# project_reserved_names, same as RustReservedWord above.
|
|
653
653
|
value_object "CargoReservedName" do
|
|
654
654
|
attribute :name, String,
|
|
655
655
|
one_of: ["name", "version", "edition", "path", "authors", "license", "description", "default",
|
|
@@ -83,7 +83,7 @@ Hecks.bluebook "Hecksagon" do
|
|
|
83
83
|
member word: "hecksagon", context: "File", body: "keywords", inner: "Hecksagon", opens: "Hecksagon", fills: ""
|
|
84
84
|
member word: "subscribe", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "subscriptions"
|
|
85
85
|
member word: "attaches", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments"
|
|
86
|
-
# Deprecated spellings of `attaches`; they warn and are removed in 3.
|
|
86
|
+
# Deprecated spellings of `attaches`; they warn and are removed in 3.3.0.
|
|
87
87
|
member word: "uses_framework", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated"
|
|
88
88
|
member word: "uses_embryonaut_bluebook", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated"
|
|
89
89
|
member word: "port", context: "Hecksagon", body: "keywords", inner: "DomainPort", opens: "DomainPort", fills: "", calls: "port_impl"
|
|
@@ -58,7 +58,7 @@ module Hecks
|
|
|
58
58
|
# @return [String] the `era` the host's `/version` document carries
|
|
59
59
|
# @raise [Unreachable] if the request fails or the status is not `200`
|
|
60
60
|
# @raise [BadResponse] if the body is not JSON or its `era` is not a non-empty string
|
|
61
|
-
def fetch_era(url, timeout:
|
|
61
|
+
def fetch_era(url, timeout:)
|
|
62
62
|
fetch_version(url, timeout: timeout).fetch("era")
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -70,7 +70,7 @@ module Hecks
|
|
|
70
70
|
# host reports none)
|
|
71
71
|
# @raise [Unreachable] if the request fails or the status is not `200`
|
|
72
72
|
# @raise [BadResponse] if the body is not JSON or its `era` is not a non-empty string
|
|
73
|
-
def fetch_version(url, timeout:
|
|
73
|
+
def fetch_version(url, timeout:)
|
|
74
74
|
response = get(version_url(url), timeout)
|
|
75
75
|
raise Unreachable, "#{version_url(url)} answered #{response.code}" unless response.code == "200"
|
|
76
76
|
|
|
@@ -105,7 +105,7 @@ module Hecks
|
|
|
105
105
|
# @raise [Errno::ENOENT] if the allow-list file does not exist
|
|
106
106
|
# @raise [Unreachable] see `fetch_era`
|
|
107
107
|
# @raise [BadResponse] see `fetch_era`
|
|
108
|
-
def check(url, file, timeout:
|
|
108
|
+
def check(url, file, timeout:)
|
|
109
109
|
allowed = parse(File.read(file))
|
|
110
110
|
verdict(fetch_era(url, timeout: timeout), allowed)
|
|
111
111
|
end
|
|
@@ -34,6 +34,8 @@ module Hecks
|
|
|
34
34
|
"@sha256:226d1f059b75399fe19182893c7184591c07b97afc8dfcf44eeb80c9a77a530f".freeze
|
|
35
35
|
TUNNEL_SHAPE = "tunnel: a hash needs `to` (the container it forwards to) and `token_secret` " \
|
|
36
36
|
"(the secret holding the tunnel token)".freeze
|
|
37
|
+
ORIGIN_ENV_SHAPE = "origin_env: the container environment variable names that hold the origin secret " \
|
|
38
|
+
"need an origin_secret and a task_definition to compare against".freeze
|
|
37
39
|
ORIGIN_PAIR = "origin_header and origin_secret go together: the header a CDN sends, " \
|
|
38
40
|
"and the secret that holds its value".freeze
|
|
39
41
|
|
|
@@ -79,7 +81,7 @@ module Hecks
|
|
|
79
81
|
:infra_name, :stack_prefix, :instance_type, :volume_gb, :swap_gb, :database_class,
|
|
80
82
|
:storage_gb, :backup_days, :snapshots_keep, :database_name, :engine_version,
|
|
81
83
|
:containers, :routes, :default_container, :origin_header, :origin_secret,
|
|
82
|
-
:secret_prefixes, :writable_secrets, :tunnel, :tunnel_service, :proxy_image, :task_definition,
|
|
84
|
+
:secret_prefixes, :writable_secrets, :origin_env, :tunnel, :tunnel_service, :proxy_image, :task_definition,
|
|
83
85
|
:migration, :s3_buckets, keyword_init: true
|
|
84
86
|
) do
|
|
85
87
|
# @return [String] the CloudFormation stack that holds the database
|
|
@@ -110,6 +112,7 @@ module Hecks
|
|
|
110
112
|
task_definition = read_task_definition(s[:task_definition], listed)
|
|
111
113
|
containers = read_containers(listed, infra_name)
|
|
112
114
|
header, secret = read_origin(s)
|
|
115
|
+
origin_env = read_origin_env(s, secret, task_definition)
|
|
113
116
|
tunnel, tunnel_service = read_tunnel(s.fetch(:tunnel, false), containers)
|
|
114
117
|
|
|
115
118
|
Plan.new(
|
|
@@ -118,7 +121,7 @@ module Hecks
|
|
|
118
121
|
engine_version: check(:engine_version, s.fetch(:engine_version, "16").to_s, ENGINE),
|
|
119
122
|
containers: containers, routes: read_routes(s.fetch(:routes, []), containers),
|
|
120
123
|
default_container: read_default(s[:default_container], containers), origin_header: header,
|
|
121
|
-
origin_secret: secret, secret_prefixes: read_prefixes(s, infra_name),
|
|
124
|
+
origin_secret: secret, origin_env: origin_env, secret_prefixes: read_prefixes(s, infra_name),
|
|
122
125
|
writable_secrets: read_writable_secrets(s),
|
|
123
126
|
tunnel: tunnel, tunnel_service: tunnel_service,
|
|
124
127
|
proxy_image: check(:proxy_image, s.fetch(:proxy_image, PROXY_IMAGE), IMAGE),
|
|
@@ -263,6 +266,24 @@ module Hecks
|
|
|
263
266
|
[check(:origin_header, header, HEADER), check(:origin_secret, secret, SECRET_NAME)]
|
|
264
267
|
end
|
|
265
268
|
|
|
269
|
+
# The container environment variables that hold the origin secret in the task
|
|
270
|
+
# definition. Only Caddy reads the named secret; the containers keep the task
|
|
271
|
+
# definition's copy, so a copy that differs makes every request through the CDN
|
|
272
|
+
# fail. Naming them lets deploy refuse a mismatch.
|
|
273
|
+
#
|
|
274
|
+
# @param settings [Hash{Symbol => Object}] the world's `AwsBox` settings
|
|
275
|
+
# @param secret [String, nil] the declared origin secret
|
|
276
|
+
# @param task_definition [String, nil] the declared task definition family
|
|
277
|
+
# @return [Array<String>] the variable names, empty when none are declared
|
|
278
|
+
# @raise [ArgumentError] when named without an origin secret and a task definition
|
|
279
|
+
def read_origin_env(settings, secret, task_definition)
|
|
280
|
+
names = Array(settings.fetch(:origin_env, []))
|
|
281
|
+
return [] if names.empty?
|
|
282
|
+
raise ArgumentError, ORIGIN_ENV_SHAPE if secret.nil? || task_definition.nil?
|
|
283
|
+
|
|
284
|
+
names.map { |name| check(:origin_env, name, ENV_KEY) }.uniq
|
|
285
|
+
end
|
|
286
|
+
|
|
266
287
|
# `tunnel true` only opens the outbound port; a hash also runs cloudflared as a service.
|
|
267
288
|
#
|
|
268
289
|
# @param value [Boolean, Hash{Symbol => Object}] the world's `tunnel` setting
|
|
@@ -55,6 +55,7 @@ ROLL=$(jq -n --arg compose "$(B64 "$WORK/compose.json")" --arg secrets "$(B64 "$
|
|
|
55
55
|
--arg registry "$ACCOUNT.dkr.ecr.$REGION.amazonaws.com" --arg dir "$DIR" '
|
|
56
56
|
{commands: ["cloud-init status --wait >/dev/null 2>&1 || true",
|
|
57
57
|
"set -e", "mkdir -p \($dir)/caddy-extra && cd \($dir)", "umask 077",
|
|
58
|
+
"CADDY_BEFORE=$(cat Caddyfile 2>/dev/null | sha256sum)",
|
|
58
59
|
"echo \($compose) | base64 -d > compose.json; echo \($secrets) | base64 -d > secrets.json",
|
|
59
60
|
"echo \($caddy) | base64 -d > Caddyfile; echo \($fetch) | base64 -d > fetch-secrets.sh",
|
|
60
61
|
"bash fetch-secrets.sh",
|
|
@@ -62,6 +63,7 @@ ROLL=$(jq -n --arg compose "$(B64 "$WORK/compose.json")" --arg secrets "$(B64 "$
|
|
|
62
63
|
"aws ecr get-login-password --region '"$REGION"' | docker login --username AWS --password-stdin \($registry) >/dev/null",
|
|
63
64
|
"docker compose -f compose.json pull --quiet",
|
|
64
65
|
"docker compose -f compose.json up -d --remove-orphans",
|
|
66
|
+
"[ \"$(sha256sum < Caddyfile)\" = \"$CADDY_BEFORE\" ] || docker compose -f compose.json restart caddy",
|
|
65
67
|
"for i in $(seq 1 40); do docker compose -f compose.json ps --format \"{{.Status}}\" | grep -qE \"^Up (Less than a second|[0-4] seconds?)|Restarting|Created|Exited\" || break; sleep 1; done; docker compose -f compose.json ps --format \"table {{.Service}}\\t{{.Status}}\""],
|
|
66
68
|
executionTimeout: ["900"]}')
|
|
67
69
|
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
# are replaced with the RDS stack's values where the task defines them, and every container runs on
|
|
10
10
|
# the box's own network, so the proxy reaches each one on 127.0.0.1:<port>. Secrets (name + valueFrom)
|
|
11
11
|
# are written to secrets.json for fetch-secrets.sh to resolve on the box.
|
|
12
|
+
#
|
|
13
|
+
# When the world names origin_env, the named origin secret is compared with those variables in the
|
|
14
|
+
# task definition and the render refuses on any difference (Caddy checks the CDN's header against
|
|
15
|
+
# the secret; the containers check it against their own copy). Values are never printed.
|
|
12
16
|
set -euo pipefail
|
|
13
17
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
14
18
|
DB_HOST=${1:?rds endpoint}
|
|
@@ -18,6 +22,19 @@ TD=${3:-@@FAMILY@@}
|
|
|
18
22
|
DEF=$(aws ecs describe-task-definition --task-definition "$TD" --query 'taskDefinition.containerDefinitions' --output json)
|
|
19
23
|
echo "rendering from task definition $TD" >&2
|
|
20
24
|
|
|
25
|
+
ORIGIN_ENV=$(jq -r '.origin.env // [] | .[]' "$HERE/services.json")
|
|
26
|
+
if [ -n "$ORIGIN_ENV" ]; then
|
|
27
|
+
ORIGIN_SECRET_ID=$(jq -r '.origin.secret' "$HERE/services.json")
|
|
28
|
+
ORIGIN_VALUE=$(aws secretsmanager get-secret-value --secret-id "$ORIGIN_SECRET_ID" --query SecretString --output text)
|
|
29
|
+
for NAME in $ORIGIN_ENV; do
|
|
30
|
+
HOLDERS=$(echo "$DEF" | jq -r --arg n "$NAME" '[.[] | select(any((.environment // [])[]; .name == $n)) | .name] | join(" ")')
|
|
31
|
+
[ -n "$HOLDERS" ] || { echo "refusing to render: origin_env names $NAME, which no container in $TD sets" >&2; exit 1; }
|
|
32
|
+
DIFFERENT=$(echo "$DEF" | jq -r --arg n "$NAME" --arg v "$ORIGIN_VALUE" \
|
|
33
|
+
'[.[] | select(any((.environment // [])[]; .name == $n and .value != $v)) | .name] | join(" ")')
|
|
34
|
+
[ -z "$DIFFERENT" ] || { echo "refusing to render: $NAME in $TD (container: $DIFFERENT) differs from the origin secret $ORIGIN_SECRET_ID; the proxy would refuse every request from the CDN. Make them equal." >&2; exit 1; }
|
|
35
|
+
done
|
|
36
|
+
fi
|
|
37
|
+
|
|
21
38
|
jq --argjson def "$DEF" --arg host "$DB_HOST" --arg secret "$DB_SECRET" '
|
|
22
39
|
def log: {driver: "json-file", options: {"max-size": "10m", "max-file": "3"}};
|
|
23
40
|
def env(c): ((c.environment // []) | map({(.name): .value}) | add // {})
|
|
@@ -379,9 +379,19 @@ module Hecks
|
|
|
379
379
|
def route_blocks(plan)
|
|
380
380
|
routes = plan.routes.each_with_index.map do |route, i|
|
|
381
381
|
port = plan.containers.find { |c| c.name == route.container }.port
|
|
382
|
-
"@r#{i + 1} path #{route.paths.join(' ')}\nhandle @r#{i + 1} {\n
|
|
382
|
+
"@r#{i + 1} path #{route.paths.join(' ')}\nhandle @r#{i + 1} {\n#{upstream(port)}}\n\n"
|
|
383
383
|
end
|
|
384
|
-
"#{routes.join}handle {\n
|
|
384
|
+
"#{routes.join}handle {\n#{upstream(plan.default.port)}}\n"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# One upstream. A request that arrives while its container is being replaced
|
|
388
|
+
# waits, and is tried again every quarter second for up to 15 seconds, so a
|
|
389
|
+
# deploy shows a visitor a slow page instead of a 502.
|
|
390
|
+
#
|
|
391
|
+
# @param port [Integer] the container's port
|
|
392
|
+
# @return [String] the `reverse_proxy` block, one tab in, ending in a newline
|
|
393
|
+
def upstream(port)
|
|
394
|
+
"\treverse_proxy 127.0.0.1:#{port} {\n\t\tlb_try_duration 15s\n\t\tlb_try_interval 250ms\n\t}\n"
|
|
385
395
|
end
|
|
386
396
|
|
|
387
397
|
# @param plan [Settings::Plan] the resolved settings
|
|
@@ -425,6 +435,7 @@ module Hecks
|
|
|
425
435
|
def services_json(plan)
|
|
426
436
|
services = plan.containers.to_h { |c| [c.name, service_entry(plan, c)] }
|
|
427
437
|
origin = plan.origin_secret ? { "header" => plan.origin_header, "secret" => plan.origin_secret } : nil
|
|
438
|
+
origin["env"] = plan.origin_env if origin && plan.origin_env.any?
|
|
428
439
|
document = { "services" => services, "origin" => origin }
|
|
429
440
|
document["task_definition"] = plan.task_definition if plan.task_definition
|
|
430
441
|
document["tunnel"] = tunnel_entry(plan.tunnel_service) if plan.tunnel_service
|
|
@@ -63,9 +63,11 @@ module Hecks
|
|
|
63
63
|
|
|
64
64
|
def reference(text) = text.nil? || text == "none" ? nil : Checks.resolve(text)
|
|
65
65
|
|
|
66
|
-
# CloudFront takes three sets of methods: read, read with `OPTIONS`, and all.
|
|
66
|
+
# CloudFront takes three sets of methods: read, read with `OPTIONS`, and all. A route's
|
|
67
|
+
# `edge_methods` are the verbs the edge lets through; they are its own `methods` unless
|
|
68
|
+
# the row says more.
|
|
67
69
|
def methods(rows)
|
|
68
|
-
verbs = rows.flat_map(&:
|
|
70
|
+
verbs = rows.flat_map(&:edge_verbs).uniq
|
|
69
71
|
return Deploy::Fargate::Cdn::METHODS.fetch("all") unless (verbs - ["GET"]).empty?
|
|
70
72
|
|
|
71
73
|
assets = rows.all? { |row| row.origin == "assets" }
|
|
@@ -76,7 +78,7 @@ module Hecks
|
|
|
76
78
|
# http and redirected; anything else (a form, an API, a signed or admin page, a route of
|
|
77
79
|
# the cms or the domain) is served over https only.
|
|
78
80
|
def protocol(row)
|
|
79
|
-
plain = row.auth == "public" && row.
|
|
81
|
+
plain = row.auth == "public" && row.edge_verbs == ["GET"] && %w[website assets].include?(row.origin)
|
|
80
82
|
plain ? "redirect-to-https" : "https-only"
|
|
81
83
|
end
|
|
82
84
|
|