capsium 0.2.0 → 0.3.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/.rubocop.yml +12 -0
- data/CHANGELOG.md +125 -0
- data/README.adoc +332 -5
- data/capsium.gemspec +1 -0
- data/lib/capsium/cli/formatting.rb +27 -0
- data/lib/capsium/cli/package.rb +68 -6
- data/lib/capsium/cli/reactor.rb +9 -1
- data/lib/capsium/cli.rb +1 -0
- data/lib/capsium/package/authentication.rb +38 -0
- data/lib/capsium/package/authentication_config.rb +78 -0
- data/lib/capsium/package/cipher.rb +197 -0
- data/lib/capsium/package/composition.rb +77 -0
- data/lib/capsium/package/dependency_resolver.rb +61 -0
- data/lib/capsium/package/encryption_config.rb +38 -0
- data/lib/capsium/package/merged_view.rb +168 -0
- data/lib/capsium/package/preparation.rb +81 -0
- data/lib/capsium/package/routes_config.rb +60 -6
- data/lib/capsium/package/security.rb +21 -4
- data/lib/capsium/package/signer.rb +201 -0
- data/lib/capsium/package/storage_config.rb +29 -1
- data/lib/capsium/package/store.rb +109 -0
- data/lib/capsium/package/testing/config_test.rb +70 -0
- data/lib/capsium/package/testing/context.rb +11 -0
- data/lib/capsium/package/testing/data_validation_test.rb +63 -0
- data/lib/capsium/package/testing/file_test.rb +45 -0
- data/lib/capsium/package/testing/report.rb +33 -0
- data/lib/capsium/package/testing/route_test.rb +64 -0
- data/lib/capsium/package/testing/test_case.rb +60 -0
- data/lib/capsium/package/testing/test_suite.rb +86 -0
- data/lib/capsium/package/testing.rb +20 -0
- data/lib/capsium/package/validator.rb +14 -3
- data/lib/capsium/package/verification.rb +32 -0
- data/lib/capsium/package/version.rb +204 -0
- data/lib/capsium/package.rb +59 -75
- data/lib/capsium/packager.rb +34 -1
- data/lib/capsium/reactor/authenticator.rb +180 -0
- data/lib/capsium/reactor/deploy.rb +71 -0
- data/lib/capsium/reactor/htpasswd.rb +154 -0
- data/lib/capsium/reactor/introspection.rb +4 -1
- data/lib/capsium/reactor/oauth2.rb +106 -0
- data/lib/capsium/reactor/serving.rb +75 -0
- data/lib/capsium/reactor/session.rb +82 -0
- data/lib/capsium/reactor.rb +43 -41
- data/lib/capsium/thor_ext.rb +1 -1
- data/lib/capsium/version.rb +1 -1
- data/sig/capsium/package/authentication.rbs +23 -0
- data/sig/capsium/package/authentication_config.rbs +71 -0
- data/sig/capsium/package/cipher.rbs +51 -0
- data/sig/capsium/package/composition.rbs +20 -0
- data/sig/capsium/package/dependency_resolver.rbs +49 -0
- data/sig/capsium/package/encryption_config.rbs +35 -0
- data/sig/capsium/package/merged_view.rbs +46 -0
- data/sig/capsium/package/preparation.rbs +23 -0
- data/sig/capsium/package/routes_config.rbs +36 -3
- data/sig/capsium/package/security.rbs +9 -2
- data/sig/capsium/package/signer.rbs +59 -0
- data/sig/capsium/package/storage_config.rbs +26 -2
- data/sig/capsium/package/store.rbs +37 -0
- data/sig/capsium/package/testing/config_test.rbs +21 -0
- data/sig/capsium/package/testing/context.rbs +14 -0
- data/sig/capsium/package/testing/data_validation_test.rbs +22 -0
- data/sig/capsium/package/testing/file_test.rbs +17 -0
- data/sig/capsium/package/testing/report.rbs +20 -0
- data/sig/capsium/package/testing/route_test.rbs +22 -0
- data/sig/capsium/package/testing/test_case.rbs +41 -0
- data/sig/capsium/package/testing/test_suite.rbs +20 -0
- data/sig/capsium/package/testing.rbs +7 -0
- data/sig/capsium/package/verification.rbs +24 -0
- data/sig/capsium/package/version.rbs +39 -0
- data/sig/capsium/package.rbs +25 -17
- data/sig/capsium/packager.rbs +9 -0
- data/sig/capsium/reactor/authenticator.rbs +40 -0
- data/sig/capsium/reactor/deploy.rbs +34 -0
- data/sig/capsium/reactor/htpasswd.rbs +36 -0
- data/sig/capsium/reactor/oauth2.rbs +31 -0
- data/sig/capsium/reactor/serving.rbs +22 -0
- data/sig/capsium/reactor/session.rbs +36 -0
- data/sig/capsium/reactor.rbs +6 -1
- metadata +70 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# The merged (overlay) view of a package's content, per ARCHITECTURE.md
|
|
4
|
+
# section 5a. Shared by Package (validation) and Reactor (serving).
|
|
5
|
+
class MergedView
|
|
6
|
+
# A single read layer: an absolute root directory mirroring the
|
|
7
|
+
# content/ tree, plus its parsed tombstone set.
|
|
8
|
+
class Layer
|
|
9
|
+
def self.new: (String root, String visibility, Set[String] tombstones) -> void
|
|
10
|
+
|
|
11
|
+
def file?: (String content_relative_path) -> bool
|
|
12
|
+
|
|
13
|
+
def absolute: (String content_relative_path) -> String
|
|
14
|
+
|
|
15
|
+
def root: () -> String
|
|
16
|
+
|
|
17
|
+
def visibility: () -> String
|
|
18
|
+
|
|
19
|
+
def tombstones: () -> Set[String]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
TOMBSTONE_FILE: String
|
|
23
|
+
CONTENT_PREFIX: String
|
|
24
|
+
|
|
25
|
+
attr_reader package_path: String
|
|
26
|
+
attr_reader layers: Array[Layer]
|
|
27
|
+
attr_reader dependency_views: Array[[String, MergedView]]
|
|
28
|
+
|
|
29
|
+
def initialize: (String package_path, storage: Storage storage,
|
|
30
|
+
manifest: Manifest manifest,
|
|
31
|
+
?dependency_views: Array[[String, MergedView]] dependency_views,
|
|
32
|
+
?exported_only: bool exported_only) -> void
|
|
33
|
+
|
|
34
|
+
# The absolute filesystem path serving a resource reference: a
|
|
35
|
+
# package-relative content path ("content/app.js"), or a dependency
|
|
36
|
+
# reference ("<dependency-guid>/content/app.js"). nil when no layer
|
|
37
|
+
# provides the path or it is tombstoned. Non-content paths never
|
|
38
|
+
# resolve through the view.
|
|
39
|
+
def resolve: (String? reference) -> String?
|
|
40
|
+
|
|
41
|
+
# The [GUID, view] pair whose GUID prefixes the reference (longest
|
|
42
|
+
# GUID first, so nested GUIDs address the innermost dependency).
|
|
43
|
+
def dependency_pair_for: (String reference) -> [String, MergedView]?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Package-source preparation (ARCHITECTURE.md section 1), mixed into
|
|
4
|
+
# Package: directory/.cap/encrypted-cap detection, extraction and
|
|
5
|
+
# decryption into a readable package directory.
|
|
6
|
+
module Preparation
|
|
7
|
+
def prepare_package: (Pathname path) -> (String | Pathname)
|
|
8
|
+
|
|
9
|
+
def decompress_cap_file: (String | Pathname file_path) -> String
|
|
10
|
+
|
|
11
|
+
# Decrypts an encrypted package (.cap file or uncompressed directory)
|
|
12
|
+
# into a temporary directory and returns its path. Raises
|
|
13
|
+
# Cipher::KeyRequiredError when no decryption_key was given.
|
|
14
|
+
def decrypt_cap_file: (String | Pathname source_path) -> String
|
|
15
|
+
|
|
16
|
+
def determine_load_type: (String | Pathname path) -> Symbol
|
|
17
|
+
|
|
18
|
+
# Whether the package was loaded from an encrypted source
|
|
19
|
+
# (ARCHITECTURE.md section 6b layout).
|
|
20
|
+
def encrypted?: () -> bool
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
module Capsium
|
|
2
2
|
class Package
|
|
3
|
+
# The "responseRewrite" object of an inherited route (05x-routing
|
|
4
|
+
# section "Route Inheritance"): replaces the served body and/or
|
|
5
|
+
# overrides response headers.
|
|
6
|
+
class ResponseRewrite
|
|
7
|
+
attr_accessor body: String?
|
|
8
|
+
attr_accessor headers: Hash[String, String]?
|
|
9
|
+
|
|
10
|
+
def initialize: (?body: String? body, ?headers: Hash[String, String]? headers) -> void
|
|
11
|
+
|
|
12
|
+
def to_json: (*untyped args) -> String
|
|
13
|
+
def to_hash: () -> Hash[String, untyped]
|
|
14
|
+
end
|
|
15
|
+
|
|
3
16
|
# A single route entry (ARCHITECTURE.md section 4). Kinds are
|
|
4
17
|
# discriminated by key, MECE: static file, dataset route, or dynamic
|
|
5
|
-
# handler (accepted-and-ignored; reactors respond 501).
|
|
18
|
+
# handler (accepted-and-ignored; reactors respond 501). Route
|
|
19
|
+
# inheritance attributes per 05x-routing.
|
|
6
20
|
class Route
|
|
7
21
|
DATASET_PATH_PREFIX: String
|
|
8
22
|
|
|
@@ -15,6 +29,10 @@ module Capsium
|
|
|
15
29
|
attr_accessor access_control: Hash[String, untyped]?
|
|
16
30
|
attr_accessor http_method: String?
|
|
17
31
|
attr_accessor handler: String?
|
|
32
|
+
attr_accessor remap: String?
|
|
33
|
+
attr_accessor response_rewrite: ResponseRewrite?
|
|
34
|
+
attr_accessor response_headers: Hash[String, String]?
|
|
35
|
+
attr_accessor request_headers: Hash[String, String]?
|
|
18
36
|
|
|
19
37
|
def initialize: (?path: String? path, ?resource: String? resource,
|
|
20
38
|
?headers: Hash[String, String]? headers,
|
|
@@ -22,7 +40,10 @@ module Capsium
|
|
|
22
40
|
?visibility: String? visibility, ?dataset: String? dataset,
|
|
23
41
|
?access_control: Hash[String, untyped]? access_control,
|
|
24
42
|
?http_method: String? http_method,
|
|
25
|
-
?handler: String? handler
|
|
43
|
+
?handler: String? handler, ?remap: String? remap,
|
|
44
|
+
?response_rewrite: ResponseRewrite? response_rewrite,
|
|
45
|
+
?response_headers: Hash[String, String]? response_headers,
|
|
46
|
+
?request_headers: Hash[String, String]? request_headers) -> void
|
|
26
47
|
|
|
27
48
|
def self.from_json: (String json) -> instance
|
|
28
49
|
|
|
@@ -32,11 +53,23 @@ module Capsium
|
|
|
32
53
|
|
|
33
54
|
def handler_route?: () -> bool
|
|
34
55
|
|
|
56
|
+
# The URL path this route answers at: the remapped path when the
|
|
57
|
+
# route remaps an inherited route, its own path otherwise.
|
|
58
|
+
def serving_path: () -> String?
|
|
59
|
+
|
|
60
|
+
# Whether the resource addresses content of a dependency package
|
|
61
|
+
# ("<dependency-guid>/<path>" — a URI rather than a
|
|
62
|
+
# package-relative path).
|
|
63
|
+
def dependency_reference?: () -> bool
|
|
64
|
+
|
|
65
|
+
# Whether the route carries route-inheritance attributes.
|
|
66
|
+
def inherited?: () -> bool
|
|
67
|
+
|
|
35
68
|
def fs_path: (String package_path) -> String?
|
|
36
69
|
|
|
37
70
|
def mime: (Manifest manifest) -> String?
|
|
38
71
|
|
|
39
|
-
def validate_target: (String package_path, Storage storage) -> void
|
|
72
|
+
def validate_target: (String package_path, Storage storage, ?merged_view: MergedView? merged_view) -> void
|
|
40
73
|
|
|
41
74
|
def to_json: (*untyped args) -> String
|
|
42
75
|
def to_hash: () -> Hash[String, untyped]
|
|
@@ -2,7 +2,7 @@ module Capsium
|
|
|
2
2
|
class Package
|
|
3
3
|
# Loads, generates and verifies security.json (ARCHITECTURE.md
|
|
4
4
|
# section 6). Checksums cover every file in the package except
|
|
5
|
-
# security.json itself.
|
|
5
|
+
# security.json itself and signature.sig.
|
|
6
6
|
class Security
|
|
7
7
|
class IntegrityError < Capsium::Error
|
|
8
8
|
end
|
|
@@ -42,14 +42,21 @@ module Capsium
|
|
|
42
42
|
|
|
43
43
|
def initialize: (String path, ?SecurityConfig? config) -> void
|
|
44
44
|
|
|
45
|
-
def self.generate: (String package_path) -> Security
|
|
45
|
+
def self.generate: (String package_path, ?digital_signatures: DigitalSignatures? digital_signatures) -> Security
|
|
46
46
|
|
|
47
47
|
def self.checksums_for: (String package_path) -> Hash[String, String]
|
|
48
48
|
|
|
49
49
|
def self.package_files: (String package_path) -> Array[String]
|
|
50
50
|
|
|
51
|
+
def self.excluded?: (String relative_path) -> bool
|
|
52
|
+
|
|
51
53
|
def present?: () -> bool
|
|
52
54
|
|
|
55
|
+
# The declared digitalSignatures block, or nil when absent.
|
|
56
|
+
def digital_signatures: () -> DigitalSignatures?
|
|
57
|
+
|
|
58
|
+
def signed?: () -> bool
|
|
59
|
+
|
|
53
60
|
def checksums: () -> Hash[String, String]
|
|
54
61
|
|
|
55
62
|
def verify: (String package_path) -> Array[ChecksumMismatch | MissingFile | UncheckedFile]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Signs and verifies Capsium packages with RSA-SHA256 digital
|
|
4
|
+
# signatures (05x-packaging "Digital Signature Using X.509",
|
|
5
|
+
# 05x-security "Digital Signatures"). The signed payload is the
|
|
6
|
+
# concatenation, in sorted package-relative path order, of the raw
|
|
7
|
+
# bytes of every file covered by the security.json integrity
|
|
8
|
+
# checksums (everything except security.json and signature.sig).
|
|
9
|
+
class Signer
|
|
10
|
+
ALGORITHM: String
|
|
11
|
+
MIN_KEY_BITS: Integer
|
|
12
|
+
|
|
13
|
+
# Package-relative name of the embedded public key PEM recorded in
|
|
14
|
+
# security.json digitalSignatures.publicKey.
|
|
15
|
+
PUBLIC_KEY_FILE: String
|
|
16
|
+
|
|
17
|
+
# Structural problems: no signature declared, missing signature or
|
|
18
|
+
# key files, unloadable keys, checksum-covered files missing.
|
|
19
|
+
class SignatureError < Capsium::Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A signature is declared but does not match the package contents.
|
|
23
|
+
class SignatureMismatchError < SignatureError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Signature operations requested on an unsigned package.
|
|
27
|
+
class UnsignedPackageError < SignatureError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader package_path: String
|
|
31
|
+
|
|
32
|
+
def initialize: (String package_path) -> void
|
|
33
|
+
|
|
34
|
+
# Signs a package directory in place, or a .cap file (unpacked,
|
|
35
|
+
# signed, recompressed). Returns the signed path.
|
|
36
|
+
def self.sign_package: (String path, String private_key_path, ?String? certificate_path) -> String
|
|
37
|
+
|
|
38
|
+
# Verifies the declared signature of a package directory or .cap
|
|
39
|
+
# file (false on mismatch; raises typed SignatureError subclasses
|
|
40
|
+
# on structural problems, e.g. an unsigned package).
|
|
41
|
+
def self.verify_package: (String path, ?String? public_key_path) -> bool
|
|
42
|
+
|
|
43
|
+
# Signs the package directory in place: embeds the public key PEM,
|
|
44
|
+
# regenerates security.json (checksums plus digitalSignatures) and
|
|
45
|
+
# writes the raw RSA-SHA256 signature to signature.sig. Returns the
|
|
46
|
+
# path of the written signature file.
|
|
47
|
+
def sign: (String private_key_path, ?String? certificate_path) -> String
|
|
48
|
+
|
|
49
|
+
# True when the declared signature verifies against the payload;
|
|
50
|
+
# false on mismatch. Raises typed SignatureError subclasses on
|
|
51
|
+
# structural problems.
|
|
52
|
+
def verify: (?String? public_key_path) -> bool
|
|
53
|
+
|
|
54
|
+
def verify!: (?String? public_key_path) -> bool
|
|
55
|
+
|
|
56
|
+
def signed?: () -> bool
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
module Capsium
|
|
2
2
|
class Package
|
|
3
|
+
# A single storage layer entry (ARCHITECTURE.md section 5a): a
|
|
4
|
+
# package-relative directory mirroring the content/ tree. Layers are
|
|
5
|
+
# stacked bottom -> top in declaration order; "visibility: private"
|
|
6
|
+
# layers are hidden from dependent packages.
|
|
7
|
+
class LayerConfig
|
|
8
|
+
attr_accessor path: String?
|
|
9
|
+
attr_accessor writable: bool
|
|
10
|
+
attr_accessor visibility: String
|
|
11
|
+
|
|
12
|
+
def initialize: (?path: String? path, ?writable: bool writable,
|
|
13
|
+
?visibility: String? visibility) -> void
|
|
14
|
+
|
|
15
|
+
def private?: () -> bool
|
|
16
|
+
|
|
17
|
+
def to_json: (*untyped args) -> String
|
|
18
|
+
def to_hash: () -> Hash[String, untyped]
|
|
19
|
+
end
|
|
20
|
+
|
|
3
21
|
# A single dataset entry (ARCHITECTURE.md section 5): schema-backed
|
|
4
22
|
# file (via "source") or SQLite ("databaseFile" + "table").
|
|
5
23
|
class DatasetConfig
|
|
@@ -29,11 +47,13 @@ module Capsium
|
|
|
29
47
|
def to_hash: () -> Hash[String, untyped]
|
|
30
48
|
end
|
|
31
49
|
|
|
32
|
-
# The "storage" object holding the dataSets map.
|
|
50
|
+
# The "storage" object holding the dataSets map and the layers stack.
|
|
33
51
|
class StorageData
|
|
34
52
|
attr_accessor data_sets: Hash[String, DatasetConfig]
|
|
53
|
+
attr_accessor layers: Array[LayerConfig]
|
|
35
54
|
|
|
36
|
-
def initialize: (?data_sets: Hash[String, DatasetConfig] data_sets
|
|
55
|
+
def initialize: (?data_sets: Hash[String, DatasetConfig] data_sets,
|
|
56
|
+
?layers: Array[LayerConfig] layers) -> void
|
|
37
57
|
|
|
38
58
|
def data_sets_from_json: (StorageData model, untyped value) -> void
|
|
39
59
|
|
|
@@ -54,6 +74,10 @@ module Capsium
|
|
|
54
74
|
|
|
55
75
|
def data_sets: () -> Hash[String, DatasetConfig]
|
|
56
76
|
|
|
77
|
+
# The configured storage layers, bottom -> top (empty when the
|
|
78
|
+
# package uses the single implicit content/ layer).
|
|
79
|
+
def layers: () -> Array[LayerConfig]
|
|
80
|
+
|
|
57
81
|
def to_json: (*untyped args) -> String
|
|
58
82
|
def to_hash: () -> Hash[String, untyped]
|
|
59
83
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# A package store directory (ARCHITECTURE.md section 4a):
|
|
4
|
+
# "<name>-<version>.cap" files plus an optional index.json mapping
|
|
5
|
+
# dependency GUID -> file name. Dependencies resolve to the newest
|
|
6
|
+
# version satisfying their semver range.
|
|
7
|
+
class Store
|
|
8
|
+
INDEX_FILE: String
|
|
9
|
+
CAP_GLOB: String
|
|
10
|
+
|
|
11
|
+
# One store .cap with its identity per its metadata.json.
|
|
12
|
+
class CatalogEntry
|
|
13
|
+
def self.new: (String path, String? guid, Version version) -> void
|
|
14
|
+
|
|
15
|
+
def path: () -> String
|
|
16
|
+
def guid: () -> String?
|
|
17
|
+
def version: () -> Version
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader dir: String
|
|
21
|
+
|
|
22
|
+
# The store configured via the CAPSIUM_STORE environment variable,
|
|
23
|
+
# or nil when unset.
|
|
24
|
+
def self.default: () -> Store?
|
|
25
|
+
|
|
26
|
+
def initialize: (String dir) -> void
|
|
27
|
+
|
|
28
|
+
# The newest .cap providing the dependency GUID whose version
|
|
29
|
+
# satisfies the range string. Raises DependencyNotFoundError or
|
|
30
|
+
# UnsatisfiableDependencyError when it cannot.
|
|
31
|
+
def find: (String guid, String range_string) -> String
|
|
32
|
+
|
|
33
|
+
# Every .cap in the store with its metadata identity (memoized).
|
|
34
|
+
def catalog: () -> Array[CatalogEntry]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
module Testing
|
|
4
|
+
# A "config" test (05x-testing): the configuration file must exist
|
|
5
|
+
# in the package and parse as the declared format; known Capsium
|
|
6
|
+
# package configs are additionally validated against their
|
|
7
|
+
# canonical models.
|
|
8
|
+
class ConfigTest < TestCase
|
|
9
|
+
FORMATS: Array[String]
|
|
10
|
+
|
|
11
|
+
attr_reader format: String
|
|
12
|
+
attr_reader config_file: String
|
|
13
|
+
|
|
14
|
+
def initialize: (name: String name, format: String format,
|
|
15
|
+
config_file: String config_file) -> void
|
|
16
|
+
|
|
17
|
+
def run: (Context context) -> TestCase::Result
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
module Testing
|
|
4
|
+
# What a test run needs: the extracted package directory and, when
|
|
5
|
+
# route tests run, the base URL of the reactor serving the package.
|
|
6
|
+
class Context
|
|
7
|
+
attr_reader package_path: String
|
|
8
|
+
attr_reader base_url: String?
|
|
9
|
+
|
|
10
|
+
def initialize: (package_path: String package_path, base_url: String? base_url) -> void
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
module Testing
|
|
4
|
+
# A "data_validation" test (05x-testing): the rows of the data
|
|
5
|
+
# file (format: json or yaml) must validate against the JSON
|
|
6
|
+
# schema file.
|
|
7
|
+
class DataValidationTest < TestCase
|
|
8
|
+
FORMATS: Array[String]
|
|
9
|
+
|
|
10
|
+
attr_reader format: String
|
|
11
|
+
attr_reader data_file: String
|
|
12
|
+
attr_reader schema_file: String
|
|
13
|
+
|
|
14
|
+
def initialize: (name: String name, format: String format,
|
|
15
|
+
data_file: String data_file,
|
|
16
|
+
schema_file: String schema_file) -> void
|
|
17
|
+
|
|
18
|
+
def run: (Context context) -> TestCase::Result
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
module Testing
|
|
4
|
+
# A "file" test (05x-testing): the file must exist in the package;
|
|
5
|
+
# optionally its content must contain a string.
|
|
6
|
+
class FileTest < TestCase
|
|
7
|
+
attr_reader path: String
|
|
8
|
+
attr_reader contains: String?
|
|
9
|
+
|
|
10
|
+
def initialize: (name: String name, path: String path,
|
|
11
|
+
?contains: String? contains) -> void
|
|
12
|
+
|
|
13
|
+
def run: (Context context) -> TestCase::Result
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
module Testing
|
|
4
|
+
# The outcome of a test suite run: one Result per test case.
|
|
5
|
+
class Report
|
|
6
|
+
attr_reader results: Array[TestCase::Result]
|
|
7
|
+
|
|
8
|
+
def initialize: (?Array[TestCase::Result] results) -> void
|
|
9
|
+
|
|
10
|
+
def <<: (TestCase::Result result) -> Report
|
|
11
|
+
|
|
12
|
+
def ok?: () -> bool
|
|
13
|
+
|
|
14
|
+
def failures: () -> Array[TestCase::Result]
|
|
15
|
+
|
|
16
|
+
def summary: () -> String
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
module Testing
|
|
4
|
+
# A "route" test (05x-testing): requests the URL path from the
|
|
5
|
+
# reactor serving the package and checks the expected status, and
|
|
6
|
+
# optionally the content type and a response body substring.
|
|
7
|
+
class RouteTest < TestCase
|
|
8
|
+
attr_reader url: String
|
|
9
|
+
attr_reader expected_status: Integer
|
|
10
|
+
attr_reader response_contains: String?
|
|
11
|
+
attr_reader expected_content_type: String?
|
|
12
|
+
|
|
13
|
+
def initialize: (name: String name, url: String url,
|
|
14
|
+
expected_status: Integer expected_status,
|
|
15
|
+
?response_contains: String? response_contains,
|
|
16
|
+
?expected_content_type: String? expected_content_type) -> void
|
|
17
|
+
|
|
18
|
+
def run: (Context context) -> TestCase::Result
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
module Testing
|
|
4
|
+
# Base class for one test of the Capsium testing YAML DSL
|
|
5
|
+
# (05x-testing). Subclasses implement a test kind and register
|
|
6
|
+
# themselves under their DSL type name (open/closed registry).
|
|
7
|
+
class TestCase
|
|
8
|
+
# The outcome of running one test.
|
|
9
|
+
class Result
|
|
10
|
+
attr_reader name: String
|
|
11
|
+
attr_reader ok: bool
|
|
12
|
+
attr_reader messages: Array[String]
|
|
13
|
+
|
|
14
|
+
def initialize: (name: String name, ok: bool ok, messages: Array[String] messages) -> void
|
|
15
|
+
|
|
16
|
+
def ok?: () -> bool
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Raised for invalid test definitions.
|
|
20
|
+
class DefinitionError < Capsium::Error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader name: String
|
|
24
|
+
|
|
25
|
+
def initialize: (name: String name) -> void
|
|
26
|
+
|
|
27
|
+
# Runs the test against the context and returns a Result.
|
|
28
|
+
def run: (Context context) -> Result
|
|
29
|
+
|
|
30
|
+
def self.register: (String type, singleton(TestCase) klass) -> void
|
|
31
|
+
|
|
32
|
+
def self.types: () -> Hash[String, singleton(TestCase)]
|
|
33
|
+
|
|
34
|
+
# Builds a test case from a YAML definition hash (string keys).
|
|
35
|
+
def self.build: (untyped definition) -> TestCase
|
|
36
|
+
|
|
37
|
+
def self.from_h: (Hash[String, untyped] definition) -> TestCase
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
module Testing
|
|
4
|
+
# Loads and runs the package's tests/*.yaml suites (05x-testing).
|
|
5
|
+
# Route tests are served by a reactor on an ephemeral port for the
|
|
6
|
+
# duration of the run.
|
|
7
|
+
class TestSuite
|
|
8
|
+
TESTS_DIR: String
|
|
9
|
+
|
|
10
|
+
attr_reader package: Capsium::Package
|
|
11
|
+
|
|
12
|
+
def initialize: (Capsium::Package package) -> void
|
|
13
|
+
|
|
14
|
+
def test_files: () -> Array[String]
|
|
15
|
+
|
|
16
|
+
def run: () -> Report
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Integrity and digital-signature verification of a loaded package
|
|
4
|
+
# (ARCHITECTURE.md section 6), mixed into Package.
|
|
5
|
+
module Verification
|
|
6
|
+
# Verifies the package against security.json. Returns a list of
|
|
7
|
+
# typed errors; empty when no security.json is present or all
|
|
8
|
+
# checksums match.
|
|
9
|
+
def verify_integrity: () -> Array[Security::ChecksumMismatch | Security::MissingFile | Security::UncheckedFile]
|
|
10
|
+
|
|
11
|
+
def verify_integrity!: () -> void
|
|
12
|
+
|
|
13
|
+
# Whether security.json declares a digital signature for this package.
|
|
14
|
+
def signed?: () -> bool
|
|
15
|
+
|
|
16
|
+
# Verifies the declared digital signature (RSA-SHA256) against the
|
|
17
|
+
# checksum-covered payload. True when the package is unsigned or the
|
|
18
|
+
# signature verifies; false on mismatch.
|
|
19
|
+
def verify_signature: () -> bool
|
|
20
|
+
|
|
21
|
+
def verify_signature!: () -> void
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# A semantic version (MAJOR.MINOR.PATCH[-prerelease][+build]) with
|
|
4
|
+
# semver precedence ordering.
|
|
5
|
+
class Version
|
|
6
|
+
include Comparable
|
|
7
|
+
|
|
8
|
+
PATTERN: Regexp
|
|
9
|
+
|
|
10
|
+
attr_reader major: Integer
|
|
11
|
+
attr_reader minor: Integer
|
|
12
|
+
attr_reader patch: Integer
|
|
13
|
+
attr_reader prerelease: String?
|
|
14
|
+
|
|
15
|
+
def self.parse: (String string) -> Version
|
|
16
|
+
|
|
17
|
+
def initialize: (Integer major, Integer minor, Integer patch,
|
|
18
|
+
?String? prerelease) -> void
|
|
19
|
+
|
|
20
|
+
def <=>: (Version other) -> Integer
|
|
21
|
+
|
|
22
|
+
def to_s: () -> String
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A semver range for metadata.dependencies (ARCHITECTURE.md section
|
|
26
|
+
# 4a): "*", exact versions, wildcards/partials, caret, tilde,
|
|
27
|
+
# comparison operators and comma/space conjunctions.
|
|
28
|
+
class VersionRange
|
|
29
|
+
NUMERIC_PART: Regexp
|
|
30
|
+
WILDCARD_PART: Regexp
|
|
31
|
+
|
|
32
|
+
def self.parse: (String string) -> VersionRange
|
|
33
|
+
|
|
34
|
+
def initialize: (Array[[String, Version]] bounds) -> void
|
|
35
|
+
|
|
36
|
+
def satisfied_by?: (String | Version version) -> bool
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/sig/capsium/package.rbs
CHANGED
|
@@ -2,11 +2,17 @@ module Capsium
|
|
|
2
2
|
# A Capsium package loaded from a directory or a .cap file
|
|
3
3
|
# (ARCHITECTURE.md section 1).
|
|
4
4
|
class Package
|
|
5
|
+
include Verification
|
|
6
|
+
include Composition
|
|
7
|
+
include Preparation
|
|
8
|
+
|
|
5
9
|
MANIFEST_FILE: String
|
|
6
10
|
METADATA_FILE: String
|
|
7
11
|
STORAGE_FILE: String
|
|
8
12
|
ROUTES_FILE: String
|
|
9
13
|
SECURITY_FILE: String
|
|
14
|
+
SIGNATURE_FILE: String
|
|
15
|
+
AUTHENTICATION_FILE: String
|
|
10
16
|
CONTENT_DIR: String
|
|
11
17
|
DATA_DIR: String
|
|
12
18
|
|
|
@@ -18,36 +24,38 @@ module Capsium
|
|
|
18
24
|
attr_reader storage: Storage
|
|
19
25
|
attr_reader security: Security
|
|
20
26
|
attr_reader load_type: Symbol
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
attr_reader resolved_dependencies: Array[ResolvedDependency]
|
|
28
|
+
attr_reader authentication: Authentication
|
|
29
|
+
|
|
30
|
+
# Loads a package directory or .cap file. Composite packages
|
|
31
|
+
# (metadata.dependencies, ARCHITECTURE.md section 4a) resolve against
|
|
32
|
+
# the package store given as `store` (directory path or Store) or via
|
|
33
|
+
# CAPSIUM_STORE. `dependency_chain` is internal: the ancestor GUIDs
|
|
34
|
+
# used for circular-dependency detection during recursive resolution.
|
|
35
|
+
def initialize: (String | Pathname path, ?load_type: Symbol? load_type,
|
|
36
|
+
?decryption_key: String? decryption_key,
|
|
37
|
+
?store: (Store | String)? store,
|
|
38
|
+
?dependency_chain: Array[String] dependency_chain) -> void
|
|
25
39
|
|
|
26
40
|
def solidify: () -> void
|
|
27
41
|
|
|
28
|
-
def decompress_cap_file: (String | Pathname file_path) -> String
|
|
29
|
-
|
|
30
42
|
def load_package: () -> void
|
|
31
43
|
|
|
32
|
-
# Removes the temporary extraction
|
|
33
|
-
#
|
|
44
|
+
# Removes the temporary extraction directories of the package and its
|
|
45
|
+
# resolved dependencies; a no-op for directory loads.
|
|
34
46
|
def cleanup: () -> void
|
|
35
47
|
|
|
36
48
|
def datasets: () -> Array[Dataset]
|
|
37
49
|
|
|
50
|
+
# The merged content view across the storage layers (ARCHITECTURE.md
|
|
51
|
+
# section 5a); exported_only gives the dependent-package view (section 4a).
|
|
52
|
+
# Resolved dependencies act as read-only layers below all own layers.
|
|
53
|
+
def merged_view: (?exported_only: bool exported_only) -> MergedView
|
|
54
|
+
|
|
38
55
|
# The .cap file this package was loaded from, or nil when loaded
|
|
39
56
|
# from a directory.
|
|
40
57
|
def cap_file_path: () -> String?
|
|
41
58
|
|
|
42
59
|
def content_files: () -> Array[String]
|
|
43
|
-
|
|
44
|
-
def determine_load_type: (String | Pathname path) -> Symbol
|
|
45
|
-
|
|
46
|
-
# Verifies the package against security.json (ARCHITECTURE.md
|
|
47
|
-
# section 6). Returns a list of typed errors; empty when no
|
|
48
|
-
# security.json is present or all checksums match.
|
|
49
|
-
def verify_integrity: () -> Array[Security::ChecksumMismatch | Security::MissingFile | Security::UncheckedFile]
|
|
50
|
-
|
|
51
|
-
def verify_integrity!: () -> void
|
|
52
60
|
end
|
|
53
61
|
end
|
data/sig/capsium/packager.rbs
CHANGED
|
@@ -18,6 +18,15 @@ module Capsium
|
|
|
18
18
|
|
|
19
19
|
def compress_package: (Package package, String cap_file) -> void
|
|
20
20
|
|
|
21
|
+
# Unpacks a .cap into a temporary directory, yields it for read-only
|
|
22
|
+
# inspection and returns the block's value.
|
|
23
|
+
def with_unpacked_cap: (String cap_path) { (String dir) -> untyped } -> untyped
|
|
24
|
+
|
|
25
|
+
# Unpacks a .cap into a temporary directory, yields it for
|
|
26
|
+
# modification, then recompresses the result back over cap_path.
|
|
27
|
+
# Returns cap_path.
|
|
28
|
+
def transform_cap: (String cap_path) { (String dir) -> untyped } -> String
|
|
29
|
+
|
|
21
30
|
def relative_path_current: (String absolute_path) -> String
|
|
22
31
|
end
|
|
23
32
|
end
|