hecks 3.12.0 → 3.14.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/examples/pizzas/bluebook/pizzas.bluebook +4 -4
- data/lib/hecks/adapters/driven/script_runner.adapter +3 -0
- data/lib/hecks/adapters/driven/script_runner.rb +93 -0
- data/lib/hecks/adapters/driven.rb +1 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +16 -12
- data/lib/hecks/bluebook/meta_validator.rb +28 -21
- data/lib/hecks/bluebook/named_pattern.rb +33 -0
- data/lib/hecks/bluebook.rb +1 -0
- data/lib/hecks/cli/domain_stub.rb +1 -1
- data/lib/hecks/cli/interview_aggregate.rb +4 -4
- data/lib/hecks/deploy/bluebook/deploy.bluebook +35 -35
- data/lib/hecks/framework/bluebook/change.behaviors +30 -0
- data/lib/hecks/framework/bluebook/change.bluebook +162 -0
- data/lib/hecks/framework/bluebook/change.hecksagon +21 -0
- data/lib/hecks/framework/change.oidc.json +34 -0
- data/lib/hecks/framework/console_settings.oidc.json +103 -0
- data/lib/hecks/framework/governance.oidc.json +28 -0
- data/lib/hecks/framework/identity.oidc.json +18 -0
- data/lib/hecks/framework/privacy.oidc.json +30 -0
- data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
- data/lib/hecks/gate/stages.yml +3 -0
- data/lib/hecks/grammar/translation.oidc.json +48 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +12 -1
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -1
- data/lib/hecks/hecks/codebase.bluebook +37 -37
- data/lib/hecks/hecks/custodian.bluebook +14 -14
- data/lib/hecks/hecks/hecks.bluebook +2 -2
- data/lib/hecks/hecks/rehearsal.bluebook +3 -3
- data/lib/hecks/hecks/tooling.bluebook +22 -22
- data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
- data/lib/hecks/language/bluebook/command.bluebook +1 -0
- data/lib/hecks/language/bluebook/entity.bluebook +1 -0
- data/lib/hecks/language/bluebook/query.bluebook +1 -0
- data/lib/hecks/language/bluebook/shape.bluebook +8 -7
- data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
- data/lib/hecks/projections/deploy/fargate/cdn/lines.rb +12 -1
- data/lib/hecks/projections/site/admin_cms/templates/membership.rb +10 -7
- data/lib/hecks/projections/site/edge/behaviours.rb +10 -6
- data/lib/hecks/projections/site/edge/checks.rb +2 -0
- data/lib/hecks/projections/site/edge/function_checks.rb +104 -0
- data/lib/hecks/projections/site/edge/function_code.rb +86 -0
- data/lib/hecks/projections/site/edge/redirect_host.js.tmpl +65 -0
- data/lib/hecks/projections/site/edge.rb +35 -9
- data/lib/hecks/projections/site/site_cdn.rb +21 -7
- data/lib/hecks/projections/site/table/row_builder.rb +12 -2
- data/lib/hecks/projections/site/table.rb +4 -2
- data/lib/hecks/quality_control/quality_control.bluebook +51 -51
- data/lib/hecks/site/bluebook/site.bluebook +21 -6
- data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
- data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
- data/lib/hecks/tools/oidc_manifests.rb +44 -6
- data/lib/hecks/tools/site_routes/templates.rb +13 -9
- data/lib/hecks/version.rb +1 -1
- data/prebuilt/{d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal → 1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal} +0 -0
- data/prebuilt/{9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal → a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal} +0 -0
- data/prebuilt/verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json +1 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/auth.rs +58 -0
- data/rust/host/src/payments/roles.rs +159 -0
- data/rust/host/src/payments/tests.rs +25 -0
- data/rust/host/src/payments.rs +20 -11
- data/rust/host/src/platform_signin/session.rs +1 -1
- data/rust/parser/src/build/mod.rs +1 -0
- data/rust/parser/src/build/named_pattern.rs +23 -0
- data/rust/parser/src/keywords.rs +6 -0
- data/rust/parser/src/parse/mod.rs +22 -1
- metadata +24 -7
- data/prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json +0 -1
- /data/lib/hecks/framework/{oidc.json → compliance.oidc.json} +0 -0
- /data/lib/hecks/grammar/{oidc.json → expression.oidc.json} +0 -0
|
@@ -10,19 +10,19 @@ Hecks.bluebook "Hecks" do
|
|
|
10
10
|
identified_by :domain
|
|
11
11
|
|
|
12
12
|
value_object "DomainPath" do
|
|
13
|
-
attribute :value, String,
|
|
13
|
+
attribute :value, String, matches: :not_empty
|
|
14
14
|
|
|
15
15
|
invariant("a domain is named by its path") { !value.to_s.empty? }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
value_object "ChapterName" do
|
|
19
|
-
attribute :value, String,
|
|
19
|
+
attribute :value, String, matches: :present
|
|
20
20
|
|
|
21
21
|
invariant("a chapter is named") { !value.to_s.empty? }
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
value_object "AggregateName" do
|
|
25
|
-
attribute :value, String,
|
|
25
|
+
attribute :value, String, matches: :present
|
|
26
26
|
|
|
27
27
|
invariant("an aggregate is named") { !value.to_s.empty? }
|
|
28
28
|
end
|
|
@@ -128,13 +128,13 @@ Hecks.bluebook "Hecks" do
|
|
|
128
128
|
identified_by :run
|
|
129
129
|
|
|
130
130
|
value_object "RunKey" do
|
|
131
|
-
attribute :value, String,
|
|
131
|
+
attribute :value, String, matches: :present
|
|
132
132
|
|
|
133
133
|
invariant("a run is named") { !value.to_s.empty? }
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
value_object "DomainList" do
|
|
137
|
-
attribute :value, String,
|
|
137
|
+
attribute :value, String, matches: :not_empty
|
|
138
138
|
end
|
|
139
139
|
|
|
140
140
|
value_object "ProfileName" do
|
|
@@ -254,7 +254,7 @@ Hecks.bluebook "Hecks" do
|
|
|
254
254
|
|
|
255
255
|
# A directory: the source repository (`from`), or the project the files land in (`root`).
|
|
256
256
|
value_object "Location" do
|
|
257
|
-
attribute :value, String,
|
|
257
|
+
attribute :value, String, matches: :not_empty
|
|
258
258
|
end
|
|
259
259
|
|
|
260
260
|
value_object "Report" do
|
|
@@ -383,18 +383,18 @@ Hecks.bluebook "Hecks" do
|
|
|
383
383
|
identified_by :run
|
|
384
384
|
|
|
385
385
|
value_object "RegistryKey" do
|
|
386
|
-
attribute :value, String,
|
|
386
|
+
attribute :value, String, matches: :present
|
|
387
387
|
|
|
388
388
|
invariant("a release is named") { !value.to_s.empty? }
|
|
389
389
|
end
|
|
390
390
|
|
|
391
391
|
value_object "PackageName" do
|
|
392
|
-
attribute :value, String,
|
|
392
|
+
attribute :value, String, matches: :snake_name
|
|
393
393
|
end
|
|
394
394
|
|
|
395
395
|
# The registry repository: the current directory when absent.
|
|
396
396
|
value_object "Location" do
|
|
397
|
-
attribute :value, String,
|
|
397
|
+
attribute :value, String, matches: :not_empty
|
|
398
398
|
end
|
|
399
399
|
|
|
400
400
|
value_object "Report" do
|
|
@@ -520,14 +520,14 @@ Hecks.bluebook "Hecks" do
|
|
|
520
520
|
identified_by :run
|
|
521
521
|
|
|
522
522
|
value_object "LaunchKey" do
|
|
523
|
-
attribute :value, String,
|
|
523
|
+
attribute :value, String, matches: :present
|
|
524
524
|
|
|
525
525
|
invariant("a launch is named") { !value.to_s.empty? }
|
|
526
526
|
end
|
|
527
527
|
|
|
528
528
|
# Domain directories under the project, comma separated.
|
|
529
529
|
value_object "DomainList" do
|
|
530
|
-
attribute :value, String,
|
|
530
|
+
attribute :value, String, matches: :not_empty
|
|
531
531
|
end
|
|
532
532
|
|
|
533
533
|
value_object "Switch" do
|
|
@@ -546,12 +546,12 @@ Hecks.bluebook "Hecks" do
|
|
|
546
546
|
|
|
547
547
|
# Where the new domain goes; the domain's snake-cased name under the current directory when absent.
|
|
548
548
|
value_object "TargetDir" do
|
|
549
|
-
attribute :value, String,
|
|
549
|
+
attribute :value, String, matches: :not_empty
|
|
550
550
|
end
|
|
551
551
|
|
|
552
552
|
# Who is interviewed; asked for at the terminal when absent.
|
|
553
553
|
value_object "ExpertName" do
|
|
554
|
-
attribute :value, String,
|
|
554
|
+
attribute :value, String, matches: :not_empty
|
|
555
555
|
end
|
|
556
556
|
|
|
557
557
|
value_object "Note" do
|
|
@@ -724,7 +724,7 @@ Hecks.bluebook "Hecks" do
|
|
|
724
724
|
identified_by :run
|
|
725
725
|
|
|
726
726
|
value_object "BuildKey" do
|
|
727
|
-
attribute :value, String,
|
|
727
|
+
attribute :value, String, matches: :present
|
|
728
728
|
|
|
729
729
|
invariant("a build is named") { !value.to_s.empty? }
|
|
730
730
|
end
|
|
@@ -737,18 +737,18 @@ Hecks.bluebook "Hecks" do
|
|
|
737
737
|
|
|
738
738
|
# A generated module: the directory name a domain was built as.
|
|
739
739
|
value_object "ModuleName" do
|
|
740
|
-
attribute :value, String,
|
|
740
|
+
attribute :value, String, matches: :snake_name
|
|
741
741
|
end
|
|
742
742
|
|
|
743
743
|
# A JSON file of steps for a domain to replay.
|
|
744
744
|
value_object "ScriptPath" do
|
|
745
|
-
attribute :value, String,
|
|
745
|
+
attribute :value, String, matches: :not_empty
|
|
746
746
|
end
|
|
747
747
|
|
|
748
748
|
# What a build is replayed through: `native`, `build` (the domain's own feature), a `.wasm`
|
|
749
749
|
# module, or a binary.
|
|
750
750
|
value_object "Artifact" do
|
|
751
|
-
attribute :value, String,
|
|
751
|
+
attribute :value, String, matches: :present
|
|
752
752
|
end
|
|
753
753
|
|
|
754
754
|
# A Rust target triple: `<arch>-<vendor>-<os>` with an optional `-<env>`. The toolchain
|
|
@@ -759,7 +759,7 @@ Hecks.bluebook "Hecks" do
|
|
|
759
759
|
|
|
760
760
|
# The directory a host build stages its files in.
|
|
761
761
|
value_object "StagePath" do
|
|
762
|
-
attribute :value, String,
|
|
762
|
+
attribute :value, String, matches: :not_empty
|
|
763
763
|
end
|
|
764
764
|
|
|
765
765
|
value_object "Count" do
|
|
@@ -1014,14 +1014,14 @@ Hecks.bluebook "Hecks" do
|
|
|
1014
1014
|
identified_by :run
|
|
1015
1015
|
|
|
1016
1016
|
value_object "FuzzKey" do
|
|
1017
|
-
attribute :value, String,
|
|
1017
|
+
attribute :value, String, matches: :present
|
|
1018
1018
|
|
|
1019
1019
|
invariant("a sweep is named") { !value.to_s.empty? }
|
|
1020
1020
|
end
|
|
1021
1021
|
|
|
1022
1022
|
# A domain directory; a sweep with none covers every domain of a hecks checkout.
|
|
1023
1023
|
value_object "DomainPath" do
|
|
1024
|
-
attribute :value, String,
|
|
1024
|
+
attribute :value, String, matches: :not_empty
|
|
1025
1025
|
|
|
1026
1026
|
invariant("a domain is named by its path") { !value.to_s.empty? }
|
|
1027
1027
|
end
|
|
@@ -1054,7 +1054,7 @@ Hecks.bluebook "Hecks" do
|
|
|
1054
1054
|
end
|
|
1055
1055
|
|
|
1056
1056
|
value_object "BinaryPath" do
|
|
1057
|
-
attribute :value, String,
|
|
1057
|
+
attribute :value, String, matches: :not_empty
|
|
1058
1058
|
end
|
|
1059
1059
|
|
|
1060
1060
|
value_object "Format" do
|
|
@@ -1062,7 +1062,7 @@ Hecks.bluebook "Hecks" do
|
|
|
1062
1062
|
end
|
|
1063
1063
|
|
|
1064
1064
|
value_object "OutputPath" do
|
|
1065
|
-
attribute :value, String,
|
|
1065
|
+
attribute :value, String, matches: :not_empty
|
|
1066
1066
|
end
|
|
1067
1067
|
|
|
1068
1068
|
value_object "Report" do
|
|
@@ -440,6 +440,7 @@ Hecks.bluebook "Bluebook" do
|
|
|
440
440
|
member keyword: "attribute", context: "Aggregate", at: "", named: "default", kind: "literal", required: "false", fills: "default"
|
|
441
441
|
member keyword: "attribute", context: "Aggregate", at: "", named: "optional", kind: "flag", required: "false", fills: "optional"
|
|
442
442
|
member keyword: "attribute", context: "Aggregate", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern"
|
|
443
|
+
member keyword: "attribute", context: "Aggregate", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches"
|
|
443
444
|
member keyword: "attribute", context: "Aggregate", at: "", named: "admits", kind: "text", required: "false", fills: "admits"
|
|
444
445
|
member keyword: "attribute", context: "Aggregate", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of"
|
|
445
446
|
member keyword: "invariant", context: "Aggregate", at: "1", named: "", kind: "text", required: "true", fills: "description"
|
|
@@ -368,6 +368,7 @@ Hecks.bluebook "Bluebook" do
|
|
|
368
368
|
member keyword: "attribute", context: "Command", at: "", named: "default", kind: "literal", required: "false", fills: "default"
|
|
369
369
|
member keyword: "attribute", context: "Command", at: "", named: "optional", kind: "flag", required: "false", fills: "optional"
|
|
370
370
|
member keyword: "attribute", context: "Command", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern"
|
|
371
|
+
member keyword: "attribute", context: "Command", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches"
|
|
371
372
|
member keyword: "attribute", context: "Command", at: "", named: "admits", kind: "text", required: "false", fills: "admits"
|
|
372
373
|
member keyword: "attribute", context: "Command", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of"
|
|
373
374
|
# `reason` is required in practice; CommandBuilder#corrects_impl refuses its absence.
|
|
@@ -319,6 +319,7 @@ Hecks.bluebook "Bluebook" do
|
|
|
319
319
|
member keyword: "attribute", context: "Entity", at: "", named: "default", kind: "literal", required: "false", fills: "default"
|
|
320
320
|
member keyword: "attribute", context: "Entity", at: "", named: "optional", kind: "flag", required: "false", fills: "optional"
|
|
321
321
|
member keyword: "attribute", context: "Entity", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern"
|
|
322
|
+
member keyword: "attribute", context: "Entity", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches"
|
|
322
323
|
member keyword: "attribute", context: "Entity", at: "", named: "admits", kind: "text", required: "false", fills: "admits"
|
|
323
324
|
member keyword: "attribute", context: "Entity", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of"
|
|
324
325
|
member keyword: "reference_to", context: "Entity", at: "1", named: "", kind: "constant", required: "true", fills: "type"
|
|
@@ -229,6 +229,7 @@ Hecks.bluebook "Bluebook" do
|
|
|
229
229
|
member keyword: "attribute", context: "Query", at: "", named: "default", kind: "literal", required: "false", fills: "default"
|
|
230
230
|
member keyword: "attribute", context: "Query", at: "", named: "optional", kind: "flag", required: "false", fills: "optional"
|
|
231
231
|
member keyword: "attribute", context: "Query", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern"
|
|
232
|
+
member keyword: "attribute", context: "Query", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches"
|
|
232
233
|
member keyword: "attribute", context: "Query", at: "", named: "admits", kind: "text", required: "false", fills: "admits"
|
|
233
234
|
member keyword: "attribute", context: "Query", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of"
|
|
234
235
|
member keyword: "reference_to", context: "Query", at: "1", named: "", kind: "constant", required: "true", fills: "type"
|
|
@@ -19,14 +19,14 @@ Hecks.bluebook "Bluebook" do
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
value_object "ValueObjectName" do
|
|
22
|
-
attribute :value, String,
|
|
22
|
+
attribute :value, String, matches: :present
|
|
23
23
|
invariant("a value object is named") { !value.to_s.empty? }
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
value_object "ShapeField" do
|
|
27
|
-
attribute :name, String,
|
|
28
|
-
attribute :type, String,
|
|
29
|
-
attribute :list, String,
|
|
27
|
+
attribute :name, String, matches: :present
|
|
28
|
+
attribute :type, String, matches: :present
|
|
29
|
+
attribute :list, String, matches: :present
|
|
30
30
|
attribute :optional, String, optional: true
|
|
31
31
|
attribute :pattern, String, optional: true
|
|
32
32
|
# optional: a declaration with no default has none
|
|
@@ -40,8 +40,8 @@ Hecks.bluebook "Bluebook" do
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
value_object "Assertion" do
|
|
43
|
-
attribute :description, String,
|
|
44
|
-
attribute :canonical, String,
|
|
43
|
+
attribute :description, String, matches: :present
|
|
44
|
+
attribute :canonical, String, matches: :present
|
|
45
45
|
invariant("an assertion description is present") { !description.to_s.empty? }
|
|
46
46
|
invariant("an assertion canonical form is present") { !canonical.to_s.empty? }
|
|
47
47
|
end
|
|
@@ -59,7 +59,7 @@ Hecks.bluebook "Bluebook" do
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
value_object "Pair" do
|
|
62
|
-
attribute :key, String,
|
|
62
|
+
attribute :key, String, matches: :present
|
|
63
63
|
attribute :value, String
|
|
64
64
|
# Keys must be named; values may be empty or numeric.
|
|
65
65
|
invariant("a pair key is present") { !key.to_s.empty? }
|
|
@@ -230,6 +230,7 @@ Hecks.bluebook "Bluebook" do
|
|
|
230
230
|
member keyword: "attribute", context: "ValueObject", at: "", named: "default", kind: "literal", required: "false", fills: "default"
|
|
231
231
|
member keyword: "attribute", context: "ValueObject", at: "", named: "optional", kind: "flag", required: "false", fills: "optional"
|
|
232
232
|
member keyword: "attribute", context: "ValueObject", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern"
|
|
233
|
+
member keyword: "attribute", context: "ValueObject", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches"
|
|
233
234
|
member keyword: "attribute", context: "ValueObject", at: "", named: "admits", kind: "text", required: "false", fills: "admits"
|
|
234
235
|
member keyword: "attribute", context: "ValueObject", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of"
|
|
235
236
|
member keyword: "invariant", context: "ValueObject", at: "1", named: "", kind: "text", required: "true", fills: "description"
|
|
@@ -80,6 +80,7 @@ Hecks.bluebook "Hecksagon" do
|
|
|
80
80
|
member keyword: "attribute", context: "PortOperation", at: "", named: "default", kind: "literal", required: "false", fills: "default"
|
|
81
81
|
member keyword: "attribute", context: "PortOperation", at: "", named: "optional", kind: "flag", required: "false", fills: "optional"
|
|
82
82
|
member keyword: "attribute", context: "PortOperation", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern"
|
|
83
|
+
member keyword: "attribute", context: "PortOperation", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches"
|
|
83
84
|
member keyword: "attribute", context: "PortOperation", at: "", named: "admits", kind: "text", required: "false", fills: "admits"
|
|
84
85
|
member keyword: "attribute", context: "PortOperation", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of"
|
|
85
86
|
member keyword: "emits", context: "PortOperation", at: "1", named: "", kind: "text", required: "true", fills: "emits"
|
|
@@ -13,7 +13,8 @@ module Hecks
|
|
|
13
13
|
# (`:origin`,
|
|
14
14
|
# `:path` for an item, `:viewer_protocol`, `:methods`, `:compress`, `:cache_policy`,
|
|
15
15
|
# `:origin_request_policy`), and optionally `:response_headers_policy`, each policy as
|
|
16
|
-
# `[id_or_intrinsic, comment_or_nil]
|
|
16
|
+
# `[id_or_intrinsic, comment_or_nil]`, and `:edge_function`, the logical id of a
|
|
17
|
+
# viewer-request function
|
|
17
18
|
# @param base [String] the indentation of `key`, or of the list item's dash
|
|
18
19
|
# @return [Array<String>] the lines
|
|
19
20
|
def behavior_lines(key, entry, base)
|
|
@@ -91,6 +92,16 @@ module Hecks
|
|
|
91
92
|
lines << "#{pad}Compress: #{entry[:compress]}" if entry.key?(:compress)
|
|
92
93
|
lines << policy_line(pad, "CachePolicyId", entry[:cache_policy])
|
|
93
94
|
lines.concat(optional_policy_lines(entry, pad))
|
|
95
|
+
lines.concat(function_lines(entry, pad))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The viewer-request function a behaviour names (`:edge_function`, the logical id of an
|
|
99
|
+
# `AWS::CloudFront::Function` in the same template), if any.
|
|
100
|
+
def function_lines(entry, pad)
|
|
101
|
+
return [] unless entry[:edge_function]
|
|
102
|
+
|
|
103
|
+
["#{pad}FunctionAssociations:", "#{pad} - EventType: viewer-request",
|
|
104
|
+
"#{pad} FunctionARN: !GetAtt #{entry[:edge_function]}.FunctionARN"]
|
|
94
105
|
end
|
|
95
106
|
|
|
96
107
|
def optional_policy_lines(entry, pad)
|
|
@@ -15,10 +15,12 @@ module Hecks
|
|
|
15
15
|
// Signing in (endpoints/sso.ts) proved the person was admitted then; this asks again, so that
|
|
16
16
|
// disabling or removing someone locks them out on their next request, not when their session runs out.
|
|
17
17
|
//
|
|
18
|
-
// The question is the host's
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// The question is the host's members list, which the host answers (403) only to a person holding
|
|
19
|
+
// one of the admin roles, and the list's own row for the caller must hold such a role and not be
|
|
20
|
+
// disabled. It is asked with a short account token signed by the secret the host checks (the content
|
|
21
|
+
// system's AUTH_SECRET is the host's SESSION_SECRET). The answer is remembered for a minute per
|
|
22
|
+
// email so every admin request does not cost a round trip. Being admitted to the site is not enough:
|
|
23
|
+
// a member with no admin role is refused here, as the site's own admin gate refuses them.
|
|
22
24
|
import { accountToken, resolveAccountCookieName } from "@hecks/client";
|
|
23
25
|
|
|
24
26
|
const DEFAULT_URL = __HOST_DEFAULT__;
|
|
@@ -38,7 +40,7 @@ module Hecks
|
|
|
38
40
|
|
|
39
41
|
export interface MembershipOptions {
|
|
40
42
|
url?: string;
|
|
41
|
-
/** The host route asked: the account route (any admitted person) by default
|
|
43
|
+
/** The host route asked: the account route (any admitted person) by default; `adminCheck` asks the members list, for admin roles only. */
|
|
42
44
|
path?: string;
|
|
43
45
|
/** Decides from the host's answer; the default is any 2xx. */
|
|
44
46
|
accept?: (res: Response, email: string) => Promise<boolean>;
|
|
@@ -83,8 +85,6 @@ module Hecks
|
|
|
83
85
|
};
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
export const membership = membershipCheck();
|
|
87
|
-
|
|
88
88
|
/** The roles the host's own admin gate passes. */
|
|
89
89
|
export const ADMIN_ROLES = __ROLES__;
|
|
90
90
|
|
|
@@ -106,6 +106,9 @@ module Hecks
|
|
|
106
106
|
path: __MEMBERS_PATH__,
|
|
107
107
|
accept: async (res, email) => holdsAdminRole(await res.json().catch(() => null), email),
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
/** The check the sign-in endpoint and the session strategy use: admin roles only, never a plain member. */
|
|
111
|
+
export const membership = adminCheck();
|
|
109
112
|
TS
|
|
110
113
|
end
|
|
111
114
|
end
|
|
@@ -10,7 +10,8 @@ module Hecks
|
|
|
10
10
|
# compression give it. A route needs none of its own when the behaviour that would match its
|
|
11
11
|
# path anyway (the nearest broader route's, else the default one) is the same. The rest keep
|
|
12
12
|
# the order the table declares, which is the order CloudFront matches in, and a route that
|
|
13
|
-
# a broader, different one before it would shadow is refused.
|
|
13
|
+
# a broader, different one before it would shadow is refused. The edge function a route
|
|
14
|
+
# names is part of its behaviour, so a route is folded only into one that names the same.
|
|
14
15
|
class Behaviours
|
|
15
16
|
# The default behaviour's path: the row that holds the settings of everything else.
|
|
16
17
|
DEFAULT_PATH = "/*"
|
|
@@ -58,11 +59,14 @@ module Hecks
|
|
|
58
59
|
policy = @edge.policy(row.cache, row.origin)
|
|
59
60
|
{
|
|
60
61
|
path: pattern, origin: @edge.upstream(row.origin).id, methods: methods([row]),
|
|
61
|
-
viewer_protocol: protocol(row), compress: row.compress
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
viewer_protocol: protocol(row), compress: row.compress
|
|
63
|
+
}.merge(policies(policy)).merge(row.edge ? { edge_function: row.edge } : {})
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def policies(policy)
|
|
67
|
+
{ cache_policy: Checks.resolve(policy.cache),
|
|
68
|
+
origin_request_policy: reference(policy.origin_request),
|
|
69
|
+
response_headers_policy: reference(policy.response_headers) }
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def reference(text) = text.nil? || text == "none" ? nil : Checks.resolve(text)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "findings"
|
|
4
4
|
require_relative "route_checks"
|
|
5
|
+
require_relative "function_checks"
|
|
5
6
|
|
|
6
7
|
module Hecks
|
|
7
8
|
module Projections
|
|
@@ -47,6 +48,7 @@ module Hecks
|
|
|
47
48
|
check_policies
|
|
48
49
|
check_upstreams
|
|
49
50
|
RouteChecks.new(@edge, @rows, @vocabulary, @problems).call
|
|
51
|
+
FunctionChecks.new(@edge, @rows, @vocabulary, @problems).call
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
private
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "findings"
|
|
4
|
+
require_relative "function_code"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Projections
|
|
8
|
+
module Site
|
|
9
|
+
class Edge
|
|
10
|
+
# The part of the edge checks that reads the `EdgeFunction` rows and the routes that name
|
|
11
|
+
# them: each row is well formed and safe to write into a function, each route names a row
|
|
12
|
+
# that exists, and no row is left that nothing names.
|
|
13
|
+
class FunctionChecks
|
|
14
|
+
include Findings
|
|
15
|
+
|
|
16
|
+
# The longest logical id a function takes; the name it is given in the account adds the
|
|
17
|
+
# stack name, and CloudFront allows 64 characters in all.
|
|
18
|
+
NAME_LIMIT = 40
|
|
19
|
+
|
|
20
|
+
# @param edge [Edge] the edge being read, its rows set
|
|
21
|
+
# @param rows [Array<Table::Row>] the route table's rows
|
|
22
|
+
# @param vocabulary [Hash{Symbol => Array<String>}] the Site chapter's closed sets
|
|
23
|
+
# @param problems [Array<String>] collects each problem found, one line each
|
|
24
|
+
def initialize(edge, rows, vocabulary, problems)
|
|
25
|
+
@edge = edge
|
|
26
|
+
@rows = rows
|
|
27
|
+
@vocabulary = vocabulary
|
|
28
|
+
@problems = problems
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [void]
|
|
32
|
+
def call
|
|
33
|
+
@edge.functions.each { |function| check_function(function) }
|
|
34
|
+
repeated(@edge.functions.map(&:name), "EdgeFunction", &:itself)
|
|
35
|
+
check_derived_names
|
|
36
|
+
check_routes
|
|
37
|
+
check_unused
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def check_function(function)
|
|
43
|
+
label = "EdgeFunction #{function.name}"
|
|
44
|
+
check_name(label, function)
|
|
45
|
+
member_of(label, "kind", function.kind, :edge_function_kind)
|
|
46
|
+
check_host(label, "host", function.host)
|
|
47
|
+
function.alias_list.each { |host| check_alias(label, function, host) }
|
|
48
|
+
check_paths(label, function)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Two logical ids that differ only in case would be given one function name.
|
|
52
|
+
def check_derived_names
|
|
53
|
+
names = @edge.functions.map { |function| function.name.to_s }.uniq
|
|
54
|
+
names.group_by { |name| FunctionCode.kebab(name) }.each do |kebab, together|
|
|
55
|
+
problem("EdgeFunction", "#{together.join(" and ")} would both be named #{kebab}") if together.size > 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_name(label, function)
|
|
60
|
+
name = function.name.to_s
|
|
61
|
+
problem(label, "is not a logical id") unless Checks::LOGICAL_ID.match?(name)
|
|
62
|
+
problem(label, "is longer than #{NAME_LIMIT} characters") if name.size > NAME_LIMIT
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def check_host(label, field, host)
|
|
66
|
+
return if FunctionCode.host?(host)
|
|
67
|
+
|
|
68
|
+
problem(label, "has #{field} #{host.inspect}; #{field == "alias" ? "an" : "a"} #{field} is a lowercase hostname " \
|
|
69
|
+
"such as app.example.test")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def check_alias(label, function, host)
|
|
73
|
+
check_host(label, "alias", host)
|
|
74
|
+
problem(label, "has alias #{host}, which is its host; an alias is another host") if host == function.host
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def check_paths(label, function)
|
|
78
|
+
problem(label, "needs at least one path to watch") if function.path_list.empty?
|
|
79
|
+
function.path_list.each do |path|
|
|
80
|
+
next if FunctionCode.path?(path)
|
|
81
|
+
|
|
82
|
+
problem(label, "has path #{path.inspect}; a path is exact, starts with / and holds letters, digits and . _ ~ - /")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Every `edge:` names a row, and a route the CDN never sees has no edge to run it.
|
|
87
|
+
def check_routes
|
|
88
|
+
@rows.select(&:edge).each do |row|
|
|
89
|
+
problem(row.path, "names edge #{row.edge}, which no EdgeFunction declares") unless @edge.function(row.edge)
|
|
90
|
+
problem(row.path, "names edge #{row.edge} and has cdn false; the edge never sees it") unless row.cdn
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def check_unused
|
|
95
|
+
named = @rows.select(&:edge).map(&:edge)
|
|
96
|
+
@edge.functions.reject { |function| named.include?(function.name) }.each do |function|
|
|
97
|
+
problem("EdgeFunction #{function.name}", "is named by no route; remove the row or add edge: to a route")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Projections
|
|
7
|
+
module Site
|
|
8
|
+
class Edge
|
|
9
|
+
# The CloudFront function resource of a `redirect_host` row, and the JavaScript in it.
|
|
10
|
+
#
|
|
11
|
+
# The code is a fixed template (`redirect_host.js.tmpl`) with three values filled in: the
|
|
12
|
+
# canonical host, the alias hosts and the watched paths. A value is never escaped. One
|
|
13
|
+
# outside the strict shape of a lowercase hostname, or of a path made of unreserved
|
|
14
|
+
# characters, is refused, so no value can end a JavaScript string, a YAML block or a
|
|
15
|
+
# CloudFormation expression. The checks refuse such a row when it is read, and `code`
|
|
16
|
+
# refuses again, since it is the last step before the text is written.
|
|
17
|
+
module FunctionCode
|
|
18
|
+
# A lowercase hostname of at least two labels, as the host header is compared after
|
|
19
|
+
# lowercasing.
|
|
20
|
+
HOST = /\A([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}\z/
|
|
21
|
+
|
|
22
|
+
# A path of unreserved characters and slashes; no query, no percent escape, no quote.
|
|
23
|
+
PATH = %r{\A/[A-Za-z0-9._~/-]*\z}
|
|
24
|
+
|
|
25
|
+
# The longest hostname and path a function takes.
|
|
26
|
+
HOST_LIMIT = 253
|
|
27
|
+
PATH_LIMIT = 200
|
|
28
|
+
|
|
29
|
+
TEMPLATE = File.join(__dir__, "redirect_host.js.tmpl")
|
|
30
|
+
PLACEHOLDER = /__(CANONICAL|ALIASES|WATCHED)__/
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# @param text [String] a candidate hostname
|
|
35
|
+
# @return [Boolean] whether it may be written into a function
|
|
36
|
+
def host?(text) = text.is_a?(String) && text.size <= HOST_LIMIT && HOST.match?(text)
|
|
37
|
+
|
|
38
|
+
# @param text [String] a candidate path
|
|
39
|
+
# @return [Boolean] whether it may be written into a function
|
|
40
|
+
def path?(text) = text.is_a?(String) && text.size <= PATH_LIMIT && PATH.match?(text)
|
|
41
|
+
|
|
42
|
+
# @param function [Edge::Function] a `redirect_host` row
|
|
43
|
+
# @return [String] the function's JavaScript, ending in a newline
|
|
44
|
+
# @raise [ArgumentError] when a host or path is outside the strict shapes
|
|
45
|
+
def code(function)
|
|
46
|
+
ensure_safe(function)
|
|
47
|
+
values = { "CANONICAL" => function.host, "ALIASES" => function.alias_list, "WATCHED" => function.path_list }
|
|
48
|
+
File.read(TEMPLATE).gsub(PLACEHOLDER) { JSON.generate(values.fetch(Regexp.last_match(1))) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param name [String] a function's logical id
|
|
52
|
+
# @return [String] the hyphenated name the function carries, such as `host-redirect`
|
|
53
|
+
def kebab(name) = name.gsub(/([a-z0-9])([A-Z])/, '\1-\2').downcase
|
|
54
|
+
|
|
55
|
+
# @param function [Edge::Function] a `redirect_host` row
|
|
56
|
+
# @return [String] the resource, from the left margin, ending in a newline
|
|
57
|
+
def resource(function)
|
|
58
|
+
[
|
|
59
|
+
"#{function.name}:", " Type: AWS::CloudFront::Function", " Properties:",
|
|
60
|
+
" Name: !Sub \"${AWS::StackName}-#{kebab(function.name)}\"", " AutoPublish: true",
|
|
61
|
+
" FunctionConfig:", " Comment: Redirect the watched paths from an alias to the canonical host",
|
|
62
|
+
" Runtime: cloudfront-js-2.0", " FunctionCode: |",
|
|
63
|
+
*code(function).each_line.map { |line| line.strip.empty? ? "" : " #{line.chomp}" }
|
|
64
|
+
].join("\n") << "\n"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ensure_safe(function)
|
|
68
|
+
raise ArgumentError, "EdgeFunction #{function.name} has no path to watch" if function.path_list.empty?
|
|
69
|
+
|
|
70
|
+
bad = unsafe_values(function)
|
|
71
|
+
return if bad.empty?
|
|
72
|
+
|
|
73
|
+
raise ArgumentError, "EdgeFunction #{function.name} has values outside the hostname and path shapes: " \
|
|
74
|
+
"#{bad.map(&:inspect).join(", ")}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def unsafe_values(function)
|
|
78
|
+
[function.host, *function.alias_list].reject { |host| host?(host) } +
|
|
79
|
+
function.path_list.reject { |path| path?(path) }
|
|
80
|
+
end
|
|
81
|
+
private_class_method :ensure_safe, :unsafe_values
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function handler(event) {
|
|
2
|
+
var request = event.request;
|
|
3
|
+
var canonical = __CANONICAL__;
|
|
4
|
+
var aliases = __ALIASES__;
|
|
5
|
+
var watched = __WATCHED__;
|
|
6
|
+
var header = request.headers.host;
|
|
7
|
+
var host = header && header.value ? header.value.toLowerCase().replace(/:\d+$/, "") : "";
|
|
8
|
+
var reads = request.method === "GET" || request.method === "HEAD";
|
|
9
|
+
if (!reads || watched.indexOf(request.uri) < 0) { return request; }
|
|
10
|
+
if (host === canonical || aliases.indexOf(host) < 0) { return request; }
|
|
11
|
+
var parts = [];
|
|
12
|
+
Object.keys(request.querystring).forEach(function (key) {
|
|
13
|
+
var entry = request.querystring[key];
|
|
14
|
+
var values = entry.multiValue ? entry.multiValue.map(function (item) { return item.value; }) : [entry.value];
|
|
15
|
+
values.forEach(function (value) {
|
|
16
|
+
var name = encode(key);
|
|
17
|
+
parts.push(value === "" || value === undefined ? name : name + "=" + encode(value));
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
var query = parts.length ? "?" + parts.join("&") : "";
|
|
21
|
+
return {
|
|
22
|
+
statusCode: 302,
|
|
23
|
+
statusDescription: "Found",
|
|
24
|
+
headers: {
|
|
25
|
+
location: { value: "https://" + canonical + request.uri + query },
|
|
26
|
+
"cache-control": { value: "no-store" }
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Percent-encodes what cannot stand in a query string and keeps what is already encoded, so the
|
|
32
|
+
// location header carries no space, control character, "&" or "#" whether the runtime hands over
|
|
33
|
+
// the raw text or the decoded one. It walks the text one UTF-16 unit at a time and pairs the
|
|
34
|
+
// surrogates itself (a lone one becomes U+FFFD), because CloudFront's regex engine refuses a
|
|
35
|
+
// pattern that names a surrogate code point and fails the whole function at compile time.
|
|
36
|
+
var SAFE = /[A-Za-z0-9\-._~!$'()*,;:@\/?+]/;
|
|
37
|
+
var HEX = /^[0-9A-Fa-f]{2}$/;
|
|
38
|
+
var REPLACEMENT = "%EF%BF%BD";
|
|
39
|
+
|
|
40
|
+
function encode(text) {
|
|
41
|
+
var input = String(text);
|
|
42
|
+
var output = "";
|
|
43
|
+
var index = 0;
|
|
44
|
+
while (index < input.length) {
|
|
45
|
+
var unit = input.charAt(index);
|
|
46
|
+
var code = input.charCodeAt(index);
|
|
47
|
+
index += 1;
|
|
48
|
+
if (code >= 55296 && code <= 56319) {
|
|
49
|
+
var next = index < input.length ? input.charCodeAt(index) : 0;
|
|
50
|
+
if (next >= 56320 && next <= 57343) {
|
|
51
|
+
output += encodeURIComponent(unit + input.charAt(index));
|
|
52
|
+
index += 1;
|
|
53
|
+
} else {
|
|
54
|
+
output += REPLACEMENT;
|
|
55
|
+
}
|
|
56
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
57
|
+
output += REPLACEMENT;
|
|
58
|
+
} else if (unit === "%") {
|
|
59
|
+
output += HEX.test(input.substr(index, 2)) ? unit : "%25";
|
|
60
|
+
} else {
|
|
61
|
+
output += SAFE.test(unit) ? unit : encodeURIComponent(unit);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return output;
|
|
65
|
+
}
|