capsium 0.3.0 → 0.6.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 +7 -3
- data/CHANGELOG.md +197 -0
- data/README.adoc +323 -14
- data/capsium.gemspec +2 -0
- data/lib/capsium/cli/package.rb +62 -25
- data/lib/capsium/cli/reactor.rb +112 -6
- data/lib/capsium/cli.rb +29 -0
- data/lib/capsium/log_buffer.rb +47 -0
- data/lib/capsium/package/bundle.rb +136 -0
- data/lib/capsium/package/cipher.rb +73 -17
- data/lib/capsium/package/composition.rb +28 -12
- data/lib/capsium/package/dataset.rb +20 -0
- data/lib/capsium/package/dependency_resolver.rb +39 -9
- data/lib/capsium/package/encryption_config.rb +7 -3
- data/lib/capsium/package/manifest.rb +1 -1
- data/lib/capsium/package/merged_view.rb +4 -2
- data/lib/capsium/package/metadata.rb +1 -1
- data/lib/capsium/package/metadata_config.rb +9 -1
- data/lib/capsium/package/open_pgp.rb +65 -0
- data/lib/capsium/package/open_pgp_cipher.rb +66 -0
- data/lib/capsium/package/open_pgp_signer.rb +176 -0
- data/lib/capsium/package/security_config.rb +6 -2
- data/lib/capsium/package/signer.rb +18 -3
- data/lib/capsium/package/store.rb +27 -0
- data/lib/capsium/package/validator.rb +27 -2
- data/lib/capsium/package/verification.rb +7 -5
- data/lib/capsium/package.rb +24 -5
- data/lib/capsium/packager.rb +64 -22
- data/lib/capsium/reactor/content_api.rb +110 -0
- data/lib/capsium/reactor/data_api.rb +154 -0
- data/lib/capsium/reactor/endpoints.rb +37 -0
- data/lib/capsium/reactor/graphql_api.rb +123 -0
- data/lib/capsium/reactor/graphql_schema.rb +138 -0
- data/lib/capsium/reactor/introspection.rb +151 -34
- data/lib/capsium/reactor/metrics.rb +30 -0
- data/lib/capsium/reactor/mount.rb +206 -0
- data/lib/capsium/reactor/overlay.rb +248 -0
- data/lib/capsium/reactor/package_saver.rb +99 -0
- data/lib/capsium/reactor/responses.rb +41 -0
- data/lib/capsium/reactor/serving.rb +90 -11
- data/lib/capsium/reactor.rb +96 -49
- data/lib/capsium/registry/local.rb +120 -0
- data/lib/capsium/registry/remote.rb +105 -0
- data/lib/capsium/registry.rb +190 -0
- data/lib/capsium/version.rb +1 -1
- data/lib/capsium.rb +2 -0
- data/sig/capsium/log_buffer.rbs +30 -0
- data/sig/capsium/package/bundle.rbs +43 -0
- data/sig/capsium/package/cipher.rbs +12 -1
- data/sig/capsium/package/composition.rbs +4 -3
- data/sig/capsium/package/dataset.rbs +8 -0
- data/sig/capsium/package/dependency_resolver.rbs +8 -4
- data/sig/capsium/package/encryption_config.rbs +6 -2
- data/sig/capsium/package/merged_view.rbs +2 -1
- data/sig/capsium/package/metadata.rbs +1 -0
- data/sig/capsium/package/open_pgp.rbs +38 -0
- data/sig/capsium/package/open_pgp_cipher.rbs +15 -0
- data/sig/capsium/package/open_pgp_signer.rbs +46 -0
- data/sig/capsium/package/security_config.rbs +6 -3
- data/sig/capsium/package/signer.rbs +8 -1
- data/sig/capsium/package/store.rbs +6 -0
- data/sig/capsium/package/verification.rbs +5 -3
- data/sig/capsium/package.rbs +8 -3
- data/sig/capsium/reactor/content_api.rbs +19 -0
- data/sig/capsium/reactor/data_api.rbs +23 -0
- data/sig/capsium/reactor/endpoints.rbs +16 -0
- data/sig/capsium/reactor/graphql_api.rbs +27 -0
- data/sig/capsium/reactor/graphql_schema.rbs +20 -0
- data/sig/capsium/reactor/introspection.rbs +27 -7
- data/sig/capsium/reactor/metrics.rbs +13 -0
- data/sig/capsium/reactor/mount.rbs +83 -0
- data/sig/capsium/reactor/overlay.rbs +72 -0
- data/sig/capsium/reactor/package_saver.rbs +12 -0
- data/sig/capsium/reactor/responses.rbs +23 -0
- data/sig/capsium/reactor/serving.rbs +28 -4
- data/sig/capsium/reactor.rbs +17 -2
- data/sig/capsium/registry/local.rbs +16 -0
- data/sig/capsium/registry/remote.rbs +18 -0
- data/sig/capsium/registry.rbs +83 -0
- data/sig/graphql.rbs +12 -0
- metadata +66 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Bundled dependencies of an encapsulated (self-contained) package:
|
|
4
|
+
# dependency .cap files embedded under packages/ with a
|
|
5
|
+
# packages/index.json manifest mapping each dependency GUID to its
|
|
6
|
+
# file, version and SHA-256. Bundled dependencies resolve first at
|
|
7
|
+
# load time, before the store -> registry chain.
|
|
8
|
+
class Bundle
|
|
9
|
+
DIRECTORY: String
|
|
10
|
+
INDEX_FILE: String
|
|
11
|
+
|
|
12
|
+
# One bundled dependency: the absolute .cap path, its version and
|
|
13
|
+
# the SHA-256 recorded in the manifest.
|
|
14
|
+
class Entry
|
|
15
|
+
def self.new: (String file, String version, String sha256) -> void
|
|
16
|
+
|
|
17
|
+
def file: () -> String
|
|
18
|
+
def version: () -> String
|
|
19
|
+
def sha256: () -> String
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader entries: Hash[String, Entry]
|
|
23
|
+
|
|
24
|
+
def initialize: (String? package_path, ?Hash[String, Entry]? entries) -> void
|
|
25
|
+
|
|
26
|
+
# Embeds the resolved dependency .cap files (guid => .cap path
|
|
27
|
+
# pairs) into the package directory at package_path and writes the
|
|
28
|
+
# packages/index.json manifest. Returns the manifest hash.
|
|
29
|
+
def self.write: (String package_path, Array[[String, String]] resolutions) -> Hash[String, Hash[String, String]]
|
|
30
|
+
|
|
31
|
+
# Whether this bundle embeds any dependency.
|
|
32
|
+
def present?: () -> bool
|
|
33
|
+
|
|
34
|
+
# Merges an inherited (ancestor) bundle underneath this one; own
|
|
35
|
+
# entries win on GUID conflicts.
|
|
36
|
+
def merged_with: (Bundle? other) -> Bundle
|
|
37
|
+
|
|
38
|
+
# The absolute .cap path for a bundled dependency, or nil when the
|
|
39
|
+
# GUID is not bundled.
|
|
40
|
+
def resolve_path: (String guid, String range, chain: Array[String] chain) -> String?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -32,6 +32,15 @@ module Capsium
|
|
|
32
32
|
# encrypted package, i.e. contains package.enc.
|
|
33
33
|
def self.encrypted?: (String | Pathname path) -> bool
|
|
34
34
|
|
|
35
|
+
# The keyManagement declared by the encryption envelope of the
|
|
36
|
+
# path (.cap file or uncompressed directory), or nil when the
|
|
37
|
+
# envelope is absent or unreadable.
|
|
38
|
+
def self.key_management: (String | Pathname path) -> String?
|
|
39
|
+
|
|
40
|
+
# The cipher instance matching the envelope's keyManagement:
|
|
41
|
+
# OpenPgpCipher for OpenPGP envelopes, the RSA cipher otherwise.
|
|
42
|
+
def self.for_encrypted: (String | Pathname path) -> Cipher
|
|
43
|
+
|
|
35
44
|
# Encrypts the package at source_path (a .cap file, or a package
|
|
36
45
|
# directory which is packed first) for the recipient's RSA public
|
|
37
46
|
# key (or X.509 certificate) and writes the encrypted .cap to
|
|
@@ -44,7 +53,9 @@ module Capsium
|
|
|
44
53
|
def decrypt: (String | Pathname encrypted_path, String private_key_path, String output_path) -> String
|
|
45
54
|
|
|
46
55
|
# Decrypts an encrypted package into a fresh temporary directory
|
|
47
|
-
# and returns the directory path.
|
|
56
|
+
# and returns the directory path. The cipher is selected from the
|
|
57
|
+
# envelope's keyManagement: OpenPGP envelopes decrypt through
|
|
58
|
+
# OpenPgpCipher, everything else through the RSA cipher.
|
|
48
59
|
def self.decrypt_to_directory: (String | Pathname source_path, String private_key_path) -> String
|
|
49
60
|
end
|
|
50
61
|
end
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
module Capsium
|
|
2
2
|
class Package
|
|
3
3
|
# Composite-package support (ARCHITECTURE.md section 4a), mixed into
|
|
4
|
-
# Package: metadata.dependencies resolution
|
|
5
|
-
#
|
|
4
|
+
# Package: metadata.dependencies resolution — bundled dependencies
|
|
5
|
+
# (Capsium::Package::Bundle) first, then the package store — and
|
|
6
|
+
# load-time validation of dependency resource references.
|
|
6
7
|
module Composition
|
|
7
8
|
private
|
|
8
9
|
|
|
9
|
-
def resolve_dependencies: ((Store | String)? store, Array[String] chain) -> Array[ResolvedDependency]
|
|
10
|
+
def resolve_dependencies: ((Store | String)? store, (Capsium::Registry | String)? registry, Array[String] chain, ?Bundle? bundle) -> Array[ResolvedDependency]
|
|
10
11
|
|
|
11
12
|
def validate_dependency_references!: () -> void
|
|
12
13
|
|
|
@@ -19,6 +19,14 @@ module Capsium
|
|
|
19
19
|
|
|
20
20
|
def validate: () -> bool
|
|
21
21
|
|
|
22
|
+
# The parsed JSON schema for this dataset, or nil when none is
|
|
23
|
+
# declared or the file is unreadable.
|
|
24
|
+
def json_schema: () -> Hash[String, untyped]?
|
|
25
|
+
|
|
26
|
+
# Schema validation of an arbitrary candidate document (used by
|
|
27
|
+
# the reactor's writable overlay); empty when valid or schema-less.
|
|
28
|
+
def schema_errors_for: (untyped data) -> Array[String]
|
|
29
|
+
|
|
22
30
|
# File-existence and schema validations; empty when valid.
|
|
23
31
|
def validation_errors: () -> Array[String]
|
|
24
32
|
|
|
@@ -35,14 +35,18 @@ module Capsium
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Orchestrates metadata.dependencies resolution against a package
|
|
38
|
-
# store: circularity checks plus store lookup
|
|
38
|
+
# store: circularity checks plus store lookup, with a registry as
|
|
39
|
+
# install fallback for store misses.
|
|
39
40
|
class DependencyResolver
|
|
40
41
|
attr_reader store: Store
|
|
42
|
+
attr_reader registry: Capsium::Registry?
|
|
41
43
|
|
|
42
|
-
def initialize: (Store | String? store
|
|
44
|
+
def initialize: (Store | String? store,
|
|
45
|
+
?registry: (Capsium::Registry | String)? registry) -> void
|
|
43
46
|
|
|
44
|
-
# The newest store .cap satisfying the dependency,
|
|
45
|
-
#
|
|
47
|
+
# The newest store .cap satisfying the dependency, installing from
|
|
48
|
+
# the configured registry on a store miss and raising typed errors
|
|
49
|
+
# for circular, missing or unsatisfiable dependencies.
|
|
46
50
|
def resolve_path: (String guid, String range, chain: Array[String] chain) -> String
|
|
47
51
|
end
|
|
48
52
|
end
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
module Capsium
|
|
2
2
|
class Package
|
|
3
3
|
# The "encryption" object of signature.json in an encrypted package:
|
|
4
|
-
# the AES-256-GCM envelope
|
|
5
|
-
#
|
|
4
|
+
# the AES-256-GCM envelope. keyManagement selects how the DEK is
|
|
5
|
+
# protected: "RSA-OAEP-SHA256" carries it Base64-wrapped in
|
|
6
|
+
# encryptedDek; "OpenPGP" carries an armored OpenPGP message
|
|
7
|
+
# containing the DEK in message. iv/authTag are Base64.
|
|
6
8
|
class EncryptionEnvelope
|
|
7
9
|
attr_accessor algorithm: String?
|
|
8
10
|
attr_accessor key_management: String?
|
|
9
11
|
attr_accessor encrypted_dek: String?
|
|
12
|
+
attr_accessor message: String?
|
|
10
13
|
attr_accessor iv: String?
|
|
11
14
|
attr_accessor auth_tag: String?
|
|
12
15
|
|
|
13
16
|
def initialize: (?algorithm: String? algorithm,
|
|
14
17
|
?key_management: String? key_management,
|
|
15
18
|
?encrypted_dek: String? encrypted_dek,
|
|
19
|
+
?message: String? message,
|
|
16
20
|
?iv: String? iv,
|
|
17
21
|
?auth_tag: String? auth_tag) -> void
|
|
18
22
|
|
|
@@ -29,7 +29,8 @@ module Capsium
|
|
|
29
29
|
def initialize: (String package_path, storage: Storage storage,
|
|
30
30
|
manifest: Manifest manifest,
|
|
31
31
|
?dependency_views: Array[[String, MergedView]] dependency_views,
|
|
32
|
-
?exported_only: bool exported_only
|
|
32
|
+
?exported_only: bool exported_only,
|
|
33
|
+
?extra_layers: Array[Layer] extra_layers) -> void
|
|
33
34
|
|
|
34
35
|
# The absolute filesystem path serving a resource reference: a
|
|
35
36
|
# package-relative content path ("content/app.js"), or a dependency
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# OpenPGP support (signatures and encryption) through the rnp gem's
|
|
4
|
+
# binding to librnp. rnp is a soft dependency: it is only required
|
|
5
|
+
# when an OpenPGP feature is used, and this module is the single
|
|
6
|
+
# place that loads it.
|
|
7
|
+
module OpenPgp
|
|
8
|
+
# The rnp gem or the librnp shared library is not available.
|
|
9
|
+
class OpenPgpUnavailableError < Capsium::Error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# A key file is unreadable or holds no suitable OpenPGP key.
|
|
13
|
+
class KeyError < Capsium::Error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
UNAVAILABLE_MESSAGE: String
|
|
17
|
+
|
|
18
|
+
# One loaded key file: the Rnp context holding the key material
|
|
19
|
+
# and the selected key within it.
|
|
20
|
+
class LoadedKey
|
|
21
|
+
def self.new: (untyped rnp, untyped key) -> void
|
|
22
|
+
|
|
23
|
+
def rnp: () -> untyped
|
|
24
|
+
def key: () -> untyped
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A fresh Rnp context. Raises OpenPgpUnavailableError when the rnp
|
|
28
|
+
# gem or librnp cannot be loaded.
|
|
29
|
+
def self.rnp: () -> untyped
|
|
30
|
+
|
|
31
|
+
# Loads the OpenPGP key at key_path (armored or binary, public or
|
|
32
|
+
# secret — the format is auto-detected). With secret: true the
|
|
33
|
+
# selected key must hold secret key material. Raises KeyError when
|
|
34
|
+
# the file is unreadable or holds no suitable key.
|
|
35
|
+
def self.load_key: (String key_path, ?secret: bool secret) -> LoadedKey
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Encrypts and decrypts whole Capsium packages with OpenPGP key
|
|
4
|
+
# management (ARCHITECTURE.md section 6b) through librnp: the DEK
|
|
5
|
+
# travels as an armored OpenPGP message in the signature.json
|
|
6
|
+
# envelope (keyManagement "OpenPGP"); the AES-256-GCM content
|
|
7
|
+
# encryption and the encrypted .cap layout are the Cipher's.
|
|
8
|
+
class OpenPgpCipher < Cipher
|
|
9
|
+
KEY_MANAGEMENT: String
|
|
10
|
+
|
|
11
|
+
# The OpenPGP symmetric cipher protecting the DEK message.
|
|
12
|
+
CIPHER_NAME: String
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Signs and verifies Capsium packages with OpenPGP detached
|
|
4
|
+
# signatures (ARCHITECTURE.md section 6a) through librnp. Parallel
|
|
5
|
+
# to the RSA-SHA256/X.509 Signer with the same construction
|
|
6
|
+
# semantics and the same error taxonomy.
|
|
7
|
+
class OpenPgpSigner
|
|
8
|
+
ALGORITHM: String
|
|
9
|
+
CERTIFICATE_TYPE: String
|
|
10
|
+
HASH: String
|
|
11
|
+
|
|
12
|
+
# Package-relative name of the embedded armored OpenPGP public key
|
|
13
|
+
# recorded in security.json digitalSignatures.publicKey.
|
|
14
|
+
PUBLIC_KEY_FILE: String
|
|
15
|
+
|
|
16
|
+
attr_reader package_path: String
|
|
17
|
+
|
|
18
|
+
def initialize: (String package_path) -> void
|
|
19
|
+
|
|
20
|
+
# Signs a package directory in place, or a .cap file (unpacked,
|
|
21
|
+
# signed, recompressed), with the OpenPGP secret key at
|
|
22
|
+
# secret_key_path. Returns the signed path.
|
|
23
|
+
def self.sign_package: (String path, String secret_key_path) -> String
|
|
24
|
+
|
|
25
|
+
# Verifies the declared OpenPGP signature of a package directory
|
|
26
|
+
# or .cap file (false on mismatch; raises typed
|
|
27
|
+
# Signer::SignatureError subclasses on structural problems).
|
|
28
|
+
def self.verify_package: (String path, ?String? public_key_path) -> bool
|
|
29
|
+
|
|
30
|
+
# Signs the package directory in place: embeds the armored public
|
|
31
|
+
# key, regenerates security.json (checksums plus digitalSignatures
|
|
32
|
+
# with certificateType "OpenPGP") and writes the armored detached
|
|
33
|
+
# OpenPGP signature to signature.sig. Returns the path of the
|
|
34
|
+
# written signature file.
|
|
35
|
+
def sign: (String secret_key_path) -> String
|
|
36
|
+
|
|
37
|
+
# True when the declared signature verifies against the payload;
|
|
38
|
+
# false on mismatch.
|
|
39
|
+
def verify: (?String? public_key_path) -> bool
|
|
40
|
+
|
|
41
|
+
def verify!: (?String? public_key_path) -> bool
|
|
42
|
+
|
|
43
|
+
def signed?: () -> bool
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -15,13 +15,16 @@ module Capsium
|
|
|
15
15
|
def to_hash: () -> Hash[String, untyped]
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# The "digitalSignatures" object of security.json
|
|
19
|
-
#
|
|
18
|
+
# The "digitalSignatures" object of security.json: certificateType
|
|
19
|
+
# selects the signature scheme (absent = RSA-SHA256/X.509,
|
|
20
|
+
# "OpenPGP" = OpenPGP).
|
|
20
21
|
class DigitalSignatures
|
|
22
|
+
attr_accessor certificate_type: String?
|
|
21
23
|
attr_accessor public_key: String?
|
|
22
24
|
attr_accessor signature_file: String?
|
|
23
25
|
|
|
24
|
-
def initialize: (?
|
|
26
|
+
def initialize: (?certificate_type: String? certificate_type,
|
|
27
|
+
?public_key: String? public_key,
|
|
25
28
|
?signature_file: String? signature_file) -> void
|
|
26
29
|
|
|
27
30
|
def to_json: (*untyped args) -> String
|
|
@@ -37,9 +37,16 @@ module Capsium
|
|
|
37
37
|
|
|
38
38
|
# Verifies the declared signature of a package directory or .cap
|
|
39
39
|
# file (false on mismatch; raises typed SignatureError subclasses
|
|
40
|
-
# on structural problems, e.g. an unsigned package).
|
|
40
|
+
# on structural problems, e.g. an unsigned package). The signer is
|
|
41
|
+
# selected from the declared digitalSignatures certificateType:
|
|
42
|
+
# OpenPGP packages verify through OpenPgpSigner.
|
|
41
43
|
def self.verify_package: (String path, ?String? public_key_path) -> bool
|
|
42
44
|
|
|
45
|
+
# The signer class matching a package's declared signature scheme:
|
|
46
|
+
# OpenPgpSigner when security.json declares
|
|
47
|
+
# digitalSignatures.certificateType "OpenPGP", Signer otherwise.
|
|
48
|
+
def self.signer_class_for: (String package_path) -> (singleton(Signer) | singleton(OpenPgpSigner))
|
|
49
|
+
|
|
43
50
|
# Signs the package directory in place: embeds the public key PEM,
|
|
44
51
|
# regenerates security.json (checksums plus digitalSignatures) and
|
|
45
52
|
# writes the raw RSA-SHA256 signature to signature.sig. Returns the
|
|
@@ -32,6 +32,12 @@ module Capsium
|
|
|
32
32
|
|
|
33
33
|
# Every .cap in the store with its metadata identity (memoized).
|
|
34
34
|
def catalog: () -> Array[CatalogEntry]
|
|
35
|
+
|
|
36
|
+
# Installs the .cap at source_path into the store as file_name,
|
|
37
|
+
# records guid -> file_name in index.json (atomically) and returns
|
|
38
|
+
# the installed path.
|
|
39
|
+
def install: (String source_path, guid: String guid,
|
|
40
|
+
file_name: String file_name) -> String
|
|
35
41
|
end
|
|
36
42
|
end
|
|
37
43
|
end
|
|
@@ -13,9 +13,11 @@ module Capsium
|
|
|
13
13
|
# Whether security.json declares a digital signature for this package.
|
|
14
14
|
def signed?: () -> bool
|
|
15
15
|
|
|
16
|
-
# Verifies the declared digital signature
|
|
17
|
-
# checksum-covered payload
|
|
18
|
-
#
|
|
16
|
+
# Verifies the declared digital signature against the
|
|
17
|
+
# checksum-covered payload, through the signer matching the
|
|
18
|
+
# declared certificateType (OpenPgpSigner for OpenPGP, the
|
|
19
|
+
# RSA-SHA256 Signer otherwise). True when the package is unsigned
|
|
20
|
+
# or the signature verifies; false on mismatch.
|
|
19
21
|
def verify_signature: () -> bool
|
|
20
22
|
|
|
21
23
|
def verify_signature!: () -> void
|
data/sig/capsium/package.rbs
CHANGED
|
@@ -30,12 +30,16 @@ module Capsium
|
|
|
30
30
|
# Loads a package directory or .cap file. Composite packages
|
|
31
31
|
# (metadata.dependencies, ARCHITECTURE.md section 4a) resolve against
|
|
32
32
|
# the package store given as `store` (directory path or Store) or via
|
|
33
|
-
# CAPSIUM_STORE
|
|
33
|
+
# CAPSIUM_STORE, falling back to the registry given as `registry`
|
|
34
|
+
# or via CAPSIUM_REGISTRY when the store has no package for a
|
|
35
|
+
# dependency GUID. `dependency_chain` is internal: the ancestor GUIDs
|
|
34
36
|
# used for circular-dependency detection during recursive resolution.
|
|
35
37
|
def initialize: (String | Pathname path, ?load_type: Symbol? load_type,
|
|
36
38
|
?decryption_key: String? decryption_key,
|
|
37
39
|
?store: (Store | String)? store,
|
|
38
|
-
?
|
|
40
|
+
?registry: (Capsium::Registry | String)? registry,
|
|
41
|
+
?dependency_chain: Array[String] dependency_chain,
|
|
42
|
+
?bundle: Bundle? bundle) -> void
|
|
39
43
|
|
|
40
44
|
def solidify: () -> void
|
|
41
45
|
|
|
@@ -50,7 +54,8 @@ module Capsium
|
|
|
50
54
|
# The merged content view across the storage layers (ARCHITECTURE.md
|
|
51
55
|
# section 5a); exported_only gives the dependent-package view (section 4a).
|
|
52
56
|
# Resolved dependencies act as read-only layers below all own layers.
|
|
53
|
-
|
|
57
|
+
# extra_layers stack above everything (the reactor's writable overlay).
|
|
58
|
+
def merged_view: (?exported_only: bool exported_only, ?extra_layers: Array[MergedView::Layer] extra_layers) -> MergedView
|
|
54
59
|
|
|
55
60
|
# The .cap file this package was loaded from, or nil when loaded
|
|
56
61
|
# from a directory.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# Content writes over the mount's overlay (ARCHITECTURE.md section
|
|
4
|
+
# 5a): PUT creates/overwrites a content file, DELETE records a
|
|
5
|
+
# tombstone. Text bodies only for v1.
|
|
6
|
+
class ContentApi
|
|
7
|
+
include Responses
|
|
8
|
+
|
|
9
|
+
WRITE_METHODS: Array[String]
|
|
10
|
+
CONTENT_PREFIX: String
|
|
11
|
+
|
|
12
|
+
def self.write_method?: (String method) -> bool
|
|
13
|
+
|
|
14
|
+
def initialize: (Mount mount) -> void
|
|
15
|
+
|
|
16
|
+
def handle: (String inner_path, untyped request, untyped response) -> untyped
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# REST CRUD over a mount's datasets (ARCHITECTURE.md sections 5 and
|
|
4
|
+
# 5a): JSON item append/read/replace/delete against the
|
|
5
|
+
# overlay-merged collection.
|
|
6
|
+
class DataApi
|
|
7
|
+
include Responses
|
|
8
|
+
|
|
9
|
+
PREFIX: String
|
|
10
|
+
COLLECTION_PATTERN: Regexp
|
|
11
|
+
ITEM_PATTERN: Regexp
|
|
12
|
+
|
|
13
|
+
def self.path?: (String inner_path) -> bool
|
|
14
|
+
|
|
15
|
+
def initialize: (Mount mount) -> void
|
|
16
|
+
|
|
17
|
+
# The declared dataset route a data path addresses, or nil.
|
|
18
|
+
def route_for: (String inner_path) -> Package::Route?
|
|
19
|
+
|
|
20
|
+
def handle: (String inner_path, untyped request, untyped response) -> untyped
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# Serving of the reactor-level endpoints (introspection reports,
|
|
4
|
+
# per-package save), mixed into Reactor.
|
|
5
|
+
module Endpoints
|
|
6
|
+
include Responses
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def serve_introspection: (untyped request, untyped response) -> untyped
|
|
11
|
+
|
|
12
|
+
# POST /package/<name>/save: folds base plus overlay into a new .cap.
|
|
13
|
+
def serve_package_save: (untyped request, untyped response) -> untyped
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# GraphQL over a mount's datasets (POST and GET <mount>/graphql):
|
|
4
|
+
# executes the derived schema and maps typed overlay errors into
|
|
5
|
+
# the "errors" array.
|
|
6
|
+
class GraphqlApi
|
|
7
|
+
include Responses
|
|
8
|
+
|
|
9
|
+
PATH: String
|
|
10
|
+
|
|
11
|
+
def self.path?: (String inner_path) -> bool
|
|
12
|
+
|
|
13
|
+
def initialize: (Mount mount) -> void
|
|
14
|
+
|
|
15
|
+
def handle: (untyped request, untyped response) -> untyped
|
|
16
|
+
|
|
17
|
+
# Query/mutation resolvers called from the derived schema.
|
|
18
|
+
def collection: (Package::Dataset dataset, ?id: String? id) -> Array[untyped]
|
|
19
|
+
|
|
20
|
+
def create_item: (Package::Dataset dataset, item: untyped item) -> untyped
|
|
21
|
+
|
|
22
|
+
def update_item: (Package::Dataset dataset, id: String id, item: untyped item) -> untyped
|
|
23
|
+
|
|
24
|
+
def remove_item: (Package::Dataset dataset, id: String id) -> bool
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# Builds the GraphQL schema for a mounted package: per dataset a
|
|
4
|
+
# query field "<dataset>" (list, optional "id:") plus
|
|
5
|
+
# create<Dataset>/update<Dataset>/delete<Dataset> mutations. Item
|
|
6
|
+
# types are inferred from the dataset's JSON schema when present.
|
|
7
|
+
class GraphqlSchema
|
|
8
|
+
# Permissive scalar carrying any JSON value.
|
|
9
|
+
class JsonScalar < GraphQL::Schema::Scalar
|
|
10
|
+
def self.coerce_input: (untyped input_value, untyped _context) -> untyped
|
|
11
|
+
|
|
12
|
+
def self.coerce_result: (untyped ruby_value, untyped _context) -> untyped
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize: (Mount mount, GraphqlApi api) -> void
|
|
16
|
+
|
|
17
|
+
def build: () -> singleton(GraphQL::Schema)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
module Capsium
|
|
2
2
|
class Reactor
|
|
3
|
-
# Monitoring HTTP API reports
|
|
4
|
-
#
|
|
3
|
+
# Monitoring HTTP API reports (ARCHITECTURE.md section 7):
|
|
4
|
+
# /api/v1/introspect/* aggregates every mounted package, plus
|
|
5
|
+
# reactor-level /introspect/* and per-package /package/:id/*
|
|
6
|
+
# endpoints resolved by package name.
|
|
5
7
|
class Introspection
|
|
6
8
|
METADATA_PATH: String
|
|
7
9
|
ROUTES_PATH: String
|
|
8
10
|
CONTENT_HASHES_PATH: String
|
|
9
11
|
CONTENT_VALIDITY_PATH: String
|
|
10
12
|
PATHS: Array[String]
|
|
13
|
+
STATUS_PATH: String
|
|
14
|
+
CONFIG_PATH: String
|
|
15
|
+
METRICS_PATH: String
|
|
16
|
+
REACTOR_PATHS: Array[String]
|
|
17
|
+
PACKAGE_MOUNT: String
|
|
18
|
+
PACKAGE_PATH_PATTERN: Regexp
|
|
19
|
+
DEFAULT_LOG_LINES: Integer
|
|
20
|
+
MAX_LOG_LINES: Integer
|
|
11
21
|
|
|
12
|
-
attr_reader
|
|
22
|
+
attr_reader packages: Array[Package]
|
|
13
23
|
|
|
14
|
-
def initialize: (Package
|
|
24
|
+
def initialize: (Array[Package] packages, ?reactor: Reactor? reactor) -> void
|
|
25
|
+
|
|
26
|
+
# The first mounted package (the single-package view).
|
|
27
|
+
def package: () -> Package
|
|
15
28
|
|
|
16
29
|
def endpoint?: (String path) -> bool
|
|
17
30
|
|
|
18
|
-
# The report body for an
|
|
19
|
-
#
|
|
20
|
-
def report_for: (String path) -> Hash[Symbol, untyped]?
|
|
31
|
+
# The report body for an endpoint, or nil when the path is not an
|
|
32
|
+
# endpoint or the package name does not match (404).
|
|
33
|
+
def report_for: (String path, ?params: Hash[String, String] params) -> Hash[Symbol, untyped]?
|
|
21
34
|
|
|
22
35
|
def metadata_report: () -> Hash[Symbol, untyped]
|
|
23
36
|
|
|
@@ -26,6 +39,13 @@ module Capsium
|
|
|
26
39
|
def content_hashes_report: () -> Hash[Symbol, untyped]
|
|
27
40
|
|
|
28
41
|
def content_validity_report: () -> Hash[Symbol, untyped]
|
|
42
|
+
|
|
43
|
+
def status_report: () -> Hash[Symbol, untyped]
|
|
44
|
+
|
|
45
|
+
# Reactor configuration; secrets are never exposed.
|
|
46
|
+
def config_report: () -> Hash[Symbol, untyped]
|
|
47
|
+
|
|
48
|
+
def metrics_report: () -> Hash[Symbol, untyped]
|
|
29
49
|
end
|
|
30
50
|
end
|
|
31
51
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# Minimal in-memory request counters for the /introspect/metrics
|
|
4
|
+
# endpoint, thread-safe via Mutex.
|
|
5
|
+
class Metrics
|
|
6
|
+
def initialize: () -> void
|
|
7
|
+
|
|
8
|
+
def record: (Integer status) -> Metrics
|
|
9
|
+
|
|
10
|
+
def snapshot: () -> Hash[Symbol, untyped]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# Raised when two mounts claim the same URL prefix, whether given
|
|
4
|
+
# explicitly or derived from package names.
|
|
5
|
+
class MountConflictError < Capsium::Error
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# One mounted package: the URL prefix it answers under plus its
|
|
9
|
+
# serving state. The reactor serves a list of mounts resolved by
|
|
10
|
+
# longest-prefix matching; default mount points are "/" for the
|
|
11
|
+
# first source and "/<metadata.name>/" for each additional one.
|
|
12
|
+
class Mount
|
|
13
|
+
ROOT_PATH: String
|
|
14
|
+
SPEC_SEPARATOR: String
|
|
15
|
+
|
|
16
|
+
# A mount entry as accepted from the CLI or a JSON config file:
|
|
17
|
+
# "path" may be nil (the default is assigned on build), "source"
|
|
18
|
+
# is a package directory, .cap file or Package, "store"
|
|
19
|
+
# optionally overrides the global package store for this mount.
|
|
20
|
+
class Entry
|
|
21
|
+
def self.new: (path: String? path, source: (Package | String) source, store: (Package::Store | String)? store) -> void
|
|
22
|
+
|
|
23
|
+
def path: () -> String?
|
|
24
|
+
|
|
25
|
+
def source: () -> (Package | String)
|
|
26
|
+
|
|
27
|
+
def store: () -> (Package::Store | String)?
|
|
28
|
+
|
|
29
|
+
def with: (?path: String? path, ?source: (Package | String) source, ?store: (Package::Store | String)? store) -> Entry
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_reader path: String
|
|
33
|
+
attr_reader package: Package
|
|
34
|
+
attr_reader overlay: Overlay?
|
|
35
|
+
|
|
36
|
+
def self.parse_spec: (String spec) -> Entry
|
|
37
|
+
|
|
38
|
+
def self.config_entries: (String config_file) -> Array[Entry]
|
|
39
|
+
|
|
40
|
+
def self.build: (Array[Entry] entries, ?store: (Package::Store | String)? store, ?registry: (Capsium::Registry | String)? registry) -> Array[Mount]
|
|
41
|
+
|
|
42
|
+
def self.load_package: ((Package | String) source, (Package::Store | String)? store, (Capsium::Registry | String)? registry) -> Package
|
|
43
|
+
|
|
44
|
+
def self.normalize_path: (String? path) -> String
|
|
45
|
+
|
|
46
|
+
def initialize: (path: String? path, package: (Package | String) package, ?store: (Package::Store | String)? store, ?registry: (Capsium::Registry | String)? registry, ?workdir: String? workdir) -> void
|
|
47
|
+
|
|
48
|
+
# Attaches the reactor workdir (creates the writable overlay).
|
|
49
|
+
def attach_workdir: (String workdir) -> void
|
|
50
|
+
|
|
51
|
+
# Writable unless the package declares "readOnly": true.
|
|
52
|
+
def writable?: () -> bool
|
|
53
|
+
|
|
54
|
+
def matches?: (String request_path) -> bool
|
|
55
|
+
|
|
56
|
+
def inner_path: (String request_path) -> String
|
|
57
|
+
|
|
58
|
+
def routes: () -> Package::Routes
|
|
59
|
+
|
|
60
|
+
def merged_view: () -> Package::MergedView
|
|
61
|
+
|
|
62
|
+
# The dataset as served: base data merged with overlay mutations.
|
|
63
|
+
def dataset_data: (Package::Dataset dataset) -> untyped
|
|
64
|
+
|
|
65
|
+
def data_api: () -> DataApi
|
|
66
|
+
|
|
67
|
+
def content_api: () -> ContentApi
|
|
68
|
+
|
|
69
|
+
# Whether this mount exposes a GraphQL API (any file-backed dataset).
|
|
70
|
+
def graphql?: () -> bool
|
|
71
|
+
|
|
72
|
+
def graphql_api: () -> GraphqlApi
|
|
73
|
+
|
|
74
|
+
# The one-line "name version" summary used in reactor logs.
|
|
75
|
+
def summary: () -> String
|
|
76
|
+
|
|
77
|
+
# The WEBrick mount points serving this mount.
|
|
78
|
+
def server_paths: () -> Array[String]
|
|
79
|
+
|
|
80
|
+
def reload: () -> Package
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|