capsium 0.2.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 +17 -1
- data/CHANGELOG.md +322 -0
- data/README.adoc +643 -7
- data/capsium.gemspec +3 -0
- data/lib/capsium/cli/formatting.rb +27 -0
- data/lib/capsium/cli/package.rb +106 -7
- data/lib/capsium/cli/reactor.rb +120 -6
- data/lib/capsium/cli.rb +30 -0
- data/lib/capsium/log_buffer.rb +47 -0
- data/lib/capsium/package/authentication.rb +38 -0
- data/lib/capsium/package/authentication_config.rb +78 -0
- data/lib/capsium/package/bundle.rb +136 -0
- data/lib/capsium/package/cipher.rb +253 -0
- data/lib/capsium/package/composition.rb +93 -0
- data/lib/capsium/package/dataset.rb +20 -0
- data/lib/capsium/package/dependency_resolver.rb +91 -0
- data/lib/capsium/package/encryption_config.rb +42 -0
- data/lib/capsium/package/manifest.rb +1 -1
- data/lib/capsium/package/merged_view.rb +170 -0
- 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/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/security_config.rb +6 -2
- data/lib/capsium/package/signer.rb +216 -0
- data/lib/capsium/package/storage_config.rb +29 -1
- data/lib/capsium/package/store.rb +136 -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 +39 -3
- data/lib/capsium/package/verification.rb +34 -0
- data/lib/capsium/package/version.rb +204 -0
- data/lib/capsium/package.rb +78 -75
- data/lib/capsium/packager.rb +97 -22
- data/lib/capsium/reactor/authenticator.rb +180 -0
- data/lib/capsium/reactor/content_api.rb +110 -0
- data/lib/capsium/reactor/data_api.rb +154 -0
- data/lib/capsium/reactor/deploy.rb +71 -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/htpasswd.rb +154 -0
- data/lib/capsium/reactor/introspection.rb +151 -31
- data/lib/capsium/reactor/metrics.rb +30 -0
- data/lib/capsium/reactor/mount.rb +206 -0
- data/lib/capsium/reactor/oauth2.rb +106 -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 +154 -0
- data/lib/capsium/reactor/session.rb +82 -0
- data/lib/capsium/reactor.rb +116 -67
- 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/thor_ext.rb +1 -1
- 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/authentication.rbs +23 -0
- data/sig/capsium/package/authentication_config.rbs +71 -0
- data/sig/capsium/package/bundle.rbs +43 -0
- data/sig/capsium/package/cipher.rbs +62 -0
- data/sig/capsium/package/composition.rbs +21 -0
- data/sig/capsium/package/dataset.rbs +8 -0
- data/sig/capsium/package/dependency_resolver.rbs +53 -0
- data/sig/capsium/package/encryption_config.rbs +39 -0
- data/sig/capsium/package/merged_view.rbs +47 -0
- 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/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/security_config.rbs +6 -3
- data/sig/capsium/package/signer.rbs +66 -0
- data/sig/capsium/package/storage_config.rbs +26 -2
- data/sig/capsium/package/store.rbs +43 -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 +26 -0
- data/sig/capsium/package/version.rbs +39 -0
- data/sig/capsium/package.rbs +29 -16
- data/sig/capsium/packager.rbs +9 -0
- data/sig/capsium/reactor/authenticator.rbs +40 -0
- data/sig/capsium/reactor/content_api.rbs +19 -0
- data/sig/capsium/reactor/data_api.rbs +23 -0
- data/sig/capsium/reactor/deploy.rbs +34 -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/htpasswd.rbs +36 -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/oauth2.rbs +31 -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 +46 -0
- data/sig/capsium/reactor/session.rbs +36 -0
- data/sig/capsium/reactor.rbs +22 -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 +135 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
# A small thread-safe ring buffer of timestamped log entries with a
|
|
3
|
+
# fixed capacity: when full, the oldest entry is dropped.
|
|
4
|
+
class LogBuffer
|
|
5
|
+
DEFAULT_CAPACITY: Integer
|
|
6
|
+
|
|
7
|
+
# One buffer entry: a UTC timestamp and a message.
|
|
8
|
+
class Entry
|
|
9
|
+
def self.new: (timestamp: Time, message: String) -> void
|
|
10
|
+
|
|
11
|
+
def timestamp: () -> Time
|
|
12
|
+
def message: () -> String
|
|
13
|
+
|
|
14
|
+
# "2026-07-19T15:00:00Z message"
|
|
15
|
+
def line: () -> String
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader capacity: Integer
|
|
19
|
+
|
|
20
|
+
def initialize: (?capacity: Integer capacity) -> void
|
|
21
|
+
|
|
22
|
+
def add: (String message, ?timestamp: Time timestamp) -> LogBuffer
|
|
23
|
+
|
|
24
|
+
# The last n entries, oldest first.
|
|
25
|
+
def last: (Integer count) -> Array[Entry]
|
|
26
|
+
|
|
27
|
+
# The last n entries as formatted lines, oldest first.
|
|
28
|
+
def lines: (Integer count) -> Array[String]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Loads a package's authentication.json (ARCHITECTURE.md section 4b).
|
|
4
|
+
class Authentication
|
|
5
|
+
attr_reader path: String
|
|
6
|
+
attr_reader config: AuthenticationConfig?
|
|
7
|
+
|
|
8
|
+
def initialize: (String path) -> void
|
|
9
|
+
|
|
10
|
+
def present?: () -> bool
|
|
11
|
+
|
|
12
|
+
def basic_auth: () -> BasicAuthConfig?
|
|
13
|
+
|
|
14
|
+
def oauth2: () -> OAuth2Config?
|
|
15
|
+
|
|
16
|
+
# Whether any authentication method is enabled.
|
|
17
|
+
def enabled?: () -> bool
|
|
18
|
+
|
|
19
|
+
def to_json: (*untyped args) -> String
|
|
20
|
+
def to_hash: () -> Hash[String, untyped]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# The "basicAuth" object of authentication.json (ARCHITECTURE.md
|
|
4
|
+
# section 4b): Apache-style Basic authentication verified against an
|
|
5
|
+
# htpasswd file.
|
|
6
|
+
class BasicAuthConfig
|
|
7
|
+
attr_accessor enabled: bool
|
|
8
|
+
attr_accessor passwd_file: String?
|
|
9
|
+
attr_accessor realm: String
|
|
10
|
+
|
|
11
|
+
def initialize: (?enabled: bool enabled, ?passwd_file: String? passwd_file,
|
|
12
|
+
?realm: String? realm) -> void
|
|
13
|
+
|
|
14
|
+
def enabled?: () -> bool
|
|
15
|
+
|
|
16
|
+
def to_json: (*untyped args) -> String
|
|
17
|
+
def to_hash: () -> Hash[String, untyped]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The "oauth2" object of authentication.json: an OAuth2
|
|
21
|
+
# authorization-code flow. The client secret is NEVER read from the
|
|
22
|
+
# package — it comes from deploy.json or reactor configuration.
|
|
23
|
+
class OAuth2Config
|
|
24
|
+
attr_accessor enabled: bool
|
|
25
|
+
attr_accessor provider: String?
|
|
26
|
+
attr_accessor client_id: String?
|
|
27
|
+
attr_accessor authorization_url: String?
|
|
28
|
+
attr_accessor token_url: String?
|
|
29
|
+
attr_accessor userinfo_url: String?
|
|
30
|
+
attr_accessor redirect_path: String
|
|
31
|
+
attr_accessor scopes: Array[String]
|
|
32
|
+
|
|
33
|
+
def initialize: (?enabled: bool enabled, ?provider: String? provider,
|
|
34
|
+
?client_id: String? client_id,
|
|
35
|
+
?authorization_url: String? authorization_url,
|
|
36
|
+
?token_url: String? token_url,
|
|
37
|
+
?userinfo_url: String? userinfo_url,
|
|
38
|
+
?redirect_path: String? redirect_path,
|
|
39
|
+
?scopes: Array[String] scopes) -> void
|
|
40
|
+
|
|
41
|
+
def enabled?: () -> bool
|
|
42
|
+
|
|
43
|
+
def to_json: (*untyped args) -> String
|
|
44
|
+
def to_hash: () -> Hash[String, untyped]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The "authentication" object of authentication.json.
|
|
48
|
+
class AuthenticationData
|
|
49
|
+
attr_accessor basic_auth: BasicAuthConfig?
|
|
50
|
+
attr_accessor oauth2: OAuth2Config?
|
|
51
|
+
|
|
52
|
+
def initialize: (?basic_auth: BasicAuthConfig? basic_auth,
|
|
53
|
+
?oauth2: OAuth2Config? oauth2) -> void
|
|
54
|
+
|
|
55
|
+
def to_json: (*untyped args) -> String
|
|
56
|
+
def to_hash: () -> Hash[String, untyped]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Canonical authentication.json model (ARCHITECTURE.md section 4b).
|
|
60
|
+
class AuthenticationConfig
|
|
61
|
+
attr_accessor authentication: AuthenticationData?
|
|
62
|
+
|
|
63
|
+
def initialize: (?authentication: AuthenticationData? authentication) -> void
|
|
64
|
+
|
|
65
|
+
def self.from_json: (String json) -> instance
|
|
66
|
+
|
|
67
|
+
def to_json: (*untyped args) -> String
|
|
68
|
+
def to_hash: () -> Hash[String, untyped]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Encrypts and decrypts whole Capsium packages (05x-packaging
|
|
4
|
+
# "Encryption", 05x-security "Encrypted information"). An encrypted
|
|
5
|
+
# .cap is a zip containing metadata.json (cleartext), signature.json
|
|
6
|
+
# (the encryption envelope) and package.enc (AES-256-GCM of the inner
|
|
7
|
+
# plaintext .cap zip). The random DEK is wrapped with the recipient's
|
|
8
|
+
# RSA public key using OAEP with SHA-256. OCB/OpenPGP are out of
|
|
9
|
+
# scope.
|
|
10
|
+
class Cipher
|
|
11
|
+
ALGORITHM: String
|
|
12
|
+
KEY_MANAGEMENT: String
|
|
13
|
+
CIPHER_NAME: String
|
|
14
|
+
ENCRYPTED_FILE: String
|
|
15
|
+
ENVELOPE_FILE: String
|
|
16
|
+
RSA_OPTIONS: Hash[String, String]
|
|
17
|
+
|
|
18
|
+
# Structural problems: unreadable input, missing zip entries,
|
|
19
|
+
# unsupported envelope algorithms, unloadable keys.
|
|
20
|
+
class CipherError < Capsium::Error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# An encrypted package was opened without a private key.
|
|
24
|
+
class KeyRequiredError < CipherError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Decryption failed: wrong private key or tampered ciphertext.
|
|
28
|
+
class DecryptionError < CipherError
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Whether the path (.cap file or uncompressed directory) is an
|
|
32
|
+
# encrypted package, i.e. contains package.enc.
|
|
33
|
+
def self.encrypted?: (String | Pathname path) -> bool
|
|
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
|
+
|
|
44
|
+
# Encrypts the package at source_path (a .cap file, or a package
|
|
45
|
+
# directory which is packed first) for the recipient's RSA public
|
|
46
|
+
# key (or X.509 certificate) and writes the encrypted .cap to
|
|
47
|
+
# output_path. Returns output_path.
|
|
48
|
+
def encrypt: (String source_path, String public_key_path, String output_path) -> String
|
|
49
|
+
|
|
50
|
+
# Decrypts the encrypted package at encrypted_path (.cap file or
|
|
51
|
+
# uncompressed directory) with the recipient's RSA private key and
|
|
52
|
+
# writes the plaintext .cap to output_path. Returns output_path.
|
|
53
|
+
def decrypt: (String | Pathname encrypted_path, String private_key_path, String output_path) -> String
|
|
54
|
+
|
|
55
|
+
# Decrypts an encrypted package into a fresh temporary directory
|
|
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.
|
|
59
|
+
def self.decrypt_to_directory: (String | Pathname source_path, String private_key_path) -> String
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Composite-package support (ARCHITECTURE.md section 4a), mixed into
|
|
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.
|
|
7
|
+
module Composition
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def resolve_dependencies: ((Store | String)? store, (Capsium::Registry | String)? registry, Array[String] chain, ?Bundle? bundle) -> Array[ResolvedDependency]
|
|
11
|
+
|
|
12
|
+
def validate_dependency_references!: () -> void
|
|
13
|
+
|
|
14
|
+
def validate_dependency_reference: (Route route, ResolvedDependency dependency, String reference) -> void
|
|
15
|
+
|
|
16
|
+
def dependency_for: (String reference) -> ResolvedDependency?
|
|
17
|
+
|
|
18
|
+
def dependency_views: () -> Array[[String, MergedView]]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -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
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Base error for composite-package dependency resolution
|
|
4
|
+
# (ARCHITECTURE.md section 4a).
|
|
5
|
+
class DependencyError < Capsium::Error
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# No package in the store provides the dependency GUID.
|
|
9
|
+
class DependencyNotFoundError < DependencyError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The store provides the GUID but no version satisfies the range.
|
|
13
|
+
class UnsatisfiableDependencyError < DependencyError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A dependency cycle was detected while resolving.
|
|
17
|
+
class CircularDependencyError < DependencyError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# A dependent route references a dependency resource that is not
|
|
21
|
+
# exported (private) and therefore not visible to dependents.
|
|
22
|
+
class DependencyVisibilityError < DependencyError
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# One resolved dependency: the declared GUID and range, the .cap it
|
|
26
|
+
# resolved to and the loaded package.
|
|
27
|
+
class ResolvedDependency
|
|
28
|
+
def self.new: (String guid, String range, String path, Package package) -> void
|
|
29
|
+
|
|
30
|
+
def guid: () -> String
|
|
31
|
+
def range: () -> String
|
|
32
|
+
def path: () -> String
|
|
33
|
+
def package: () -> Package
|
|
34
|
+
def version: () -> String?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Orchestrates metadata.dependencies resolution against a package
|
|
38
|
+
# store: circularity checks plus store lookup, with a registry as
|
|
39
|
+
# install fallback for store misses.
|
|
40
|
+
class DependencyResolver
|
|
41
|
+
attr_reader store: Store
|
|
42
|
+
attr_reader registry: Capsium::Registry?
|
|
43
|
+
|
|
44
|
+
def initialize: (Store | String? store,
|
|
45
|
+
?registry: (Capsium::Registry | String)? registry) -> void
|
|
46
|
+
|
|
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.
|
|
50
|
+
def resolve_path: (String guid, String range, chain: Array[String] chain) -> String
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# The "encryption" object of signature.json in an encrypted package:
|
|
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.
|
|
8
|
+
class EncryptionEnvelope
|
|
9
|
+
attr_accessor algorithm: String?
|
|
10
|
+
attr_accessor key_management: String?
|
|
11
|
+
attr_accessor encrypted_dek: String?
|
|
12
|
+
attr_accessor message: String?
|
|
13
|
+
attr_accessor iv: String?
|
|
14
|
+
attr_accessor auth_tag: String?
|
|
15
|
+
|
|
16
|
+
def initialize: (?algorithm: String? algorithm,
|
|
17
|
+
?key_management: String? key_management,
|
|
18
|
+
?encrypted_dek: String? encrypted_dek,
|
|
19
|
+
?message: String? message,
|
|
20
|
+
?iv: String? iv,
|
|
21
|
+
?auth_tag: String? auth_tag) -> void
|
|
22
|
+
|
|
23
|
+
def to_json: (*untyped args) -> String
|
|
24
|
+
def to_hash: () -> Hash[String, untyped]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The signature.json file of an encrypted package.
|
|
28
|
+
class EncryptionConfig
|
|
29
|
+
attr_accessor encryption: EncryptionEnvelope?
|
|
30
|
+
|
|
31
|
+
def initialize: (?encryption: EncryptionEnvelope? encryption) -> void
|
|
32
|
+
|
|
33
|
+
def self.from_json: (String json) -> instance
|
|
34
|
+
|
|
35
|
+
def to_json: (*untyped args) -> String
|
|
36
|
+
def to_hash: () -> Hash[String, untyped]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
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,
|
|
33
|
+
?extra_layers: Array[Layer] extra_layers) -> void
|
|
34
|
+
|
|
35
|
+
# The absolute filesystem path serving a resource reference: a
|
|
36
|
+
# package-relative content path ("content/app.js"), or a dependency
|
|
37
|
+
# reference ("<dependency-guid>/content/app.js"). nil when no layer
|
|
38
|
+
# provides the path or it is tombstoned. Non-content paths never
|
|
39
|
+
# resolve through the view.
|
|
40
|
+
def resolve: (String? reference) -> String?
|
|
41
|
+
|
|
42
|
+
# The [GUID, view] pair whose GUID prefixes the reference (longest
|
|
43
|
+
# GUID first, so nested GUIDs address the innermost dependency).
|
|
44
|
+
def dependency_pair_for: (String reference) -> [String, MergedView]?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -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
|
|
@@ -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]
|
|
@@ -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
|