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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -3
  3. data/CHANGELOG.md +197 -0
  4. data/README.adoc +323 -14
  5. data/capsium.gemspec +2 -0
  6. data/lib/capsium/cli/package.rb +62 -25
  7. data/lib/capsium/cli/reactor.rb +112 -6
  8. data/lib/capsium/cli.rb +29 -0
  9. data/lib/capsium/log_buffer.rb +47 -0
  10. data/lib/capsium/package/bundle.rb +136 -0
  11. data/lib/capsium/package/cipher.rb +73 -17
  12. data/lib/capsium/package/composition.rb +28 -12
  13. data/lib/capsium/package/dataset.rb +20 -0
  14. data/lib/capsium/package/dependency_resolver.rb +39 -9
  15. data/lib/capsium/package/encryption_config.rb +7 -3
  16. data/lib/capsium/package/manifest.rb +1 -1
  17. data/lib/capsium/package/merged_view.rb +4 -2
  18. data/lib/capsium/package/metadata.rb +1 -1
  19. data/lib/capsium/package/metadata_config.rb +9 -1
  20. data/lib/capsium/package/open_pgp.rb +65 -0
  21. data/lib/capsium/package/open_pgp_cipher.rb +66 -0
  22. data/lib/capsium/package/open_pgp_signer.rb +176 -0
  23. data/lib/capsium/package/security_config.rb +6 -2
  24. data/lib/capsium/package/signer.rb +18 -3
  25. data/lib/capsium/package/store.rb +27 -0
  26. data/lib/capsium/package/validator.rb +27 -2
  27. data/lib/capsium/package/verification.rb +7 -5
  28. data/lib/capsium/package.rb +24 -5
  29. data/lib/capsium/packager.rb +64 -22
  30. data/lib/capsium/reactor/content_api.rb +110 -0
  31. data/lib/capsium/reactor/data_api.rb +154 -0
  32. data/lib/capsium/reactor/endpoints.rb +37 -0
  33. data/lib/capsium/reactor/graphql_api.rb +123 -0
  34. data/lib/capsium/reactor/graphql_schema.rb +138 -0
  35. data/lib/capsium/reactor/introspection.rb +151 -34
  36. data/lib/capsium/reactor/metrics.rb +30 -0
  37. data/lib/capsium/reactor/mount.rb +206 -0
  38. data/lib/capsium/reactor/overlay.rb +248 -0
  39. data/lib/capsium/reactor/package_saver.rb +99 -0
  40. data/lib/capsium/reactor/responses.rb +41 -0
  41. data/lib/capsium/reactor/serving.rb +90 -11
  42. data/lib/capsium/reactor.rb +96 -49
  43. data/lib/capsium/registry/local.rb +120 -0
  44. data/lib/capsium/registry/remote.rb +105 -0
  45. data/lib/capsium/registry.rb +190 -0
  46. data/lib/capsium/version.rb +1 -1
  47. data/lib/capsium.rb +2 -0
  48. data/sig/capsium/log_buffer.rbs +30 -0
  49. data/sig/capsium/package/bundle.rbs +43 -0
  50. data/sig/capsium/package/cipher.rbs +12 -1
  51. data/sig/capsium/package/composition.rbs +4 -3
  52. data/sig/capsium/package/dataset.rbs +8 -0
  53. data/sig/capsium/package/dependency_resolver.rbs +8 -4
  54. data/sig/capsium/package/encryption_config.rbs +6 -2
  55. data/sig/capsium/package/merged_view.rbs +2 -1
  56. data/sig/capsium/package/metadata.rbs +1 -0
  57. data/sig/capsium/package/open_pgp.rbs +38 -0
  58. data/sig/capsium/package/open_pgp_cipher.rbs +15 -0
  59. data/sig/capsium/package/open_pgp_signer.rbs +46 -0
  60. data/sig/capsium/package/security_config.rbs +6 -3
  61. data/sig/capsium/package/signer.rbs +8 -1
  62. data/sig/capsium/package/store.rbs +6 -0
  63. data/sig/capsium/package/verification.rbs +5 -3
  64. data/sig/capsium/package.rbs +8 -3
  65. data/sig/capsium/reactor/content_api.rbs +19 -0
  66. data/sig/capsium/reactor/data_api.rbs +23 -0
  67. data/sig/capsium/reactor/endpoints.rbs +16 -0
  68. data/sig/capsium/reactor/graphql_api.rbs +27 -0
  69. data/sig/capsium/reactor/graphql_schema.rbs +20 -0
  70. data/sig/capsium/reactor/introspection.rbs +27 -7
  71. data/sig/capsium/reactor/metrics.rbs +13 -0
  72. data/sig/capsium/reactor/mount.rbs +83 -0
  73. data/sig/capsium/reactor/overlay.rbs +72 -0
  74. data/sig/capsium/reactor/package_saver.rbs +12 -0
  75. data/sig/capsium/reactor/responses.rbs +23 -0
  76. data/sig/capsium/reactor/serving.rbs +28 -4
  77. data/sig/capsium/reactor.rbs +17 -2
  78. data/sig/capsium/registry/local.rbs +16 -0
  79. data/sig/capsium/registry/remote.rbs +18 -0
  80. data/sig/capsium/registry.rbs +83 -0
  81. data/sig/graphql.rbs +12 -0
  82. 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 against a package store
5
- # and load-time validation of dependency resource references.
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) -> void
44
+ def initialize: (Store | String? store,
45
+ ?registry: (Capsium::Registry | String)? registry) -> void
43
46
 
44
- # The newest store .cap satisfying the dependency, raising typed
45
- # errors for circular, missing or unsatisfiable dependencies.
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 with the RSA-OAEP-SHA256 wrapped DEK, all
5
- # binary values Base64-encoded.
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) -> void
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
@@ -21,6 +21,7 @@ module Capsium
21
21
  def license: () -> String?
22
22
  def repository: () -> Repository?
23
23
  def dependencies: () -> Hash[String, String]
24
+ def read_only: () -> bool?
24
25
  end
25
26
  end
26
27
  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
@@ -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 (parsed only;
19
- # signing is a later phase).
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: (?public_key: String? public_key,
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 (RSA-SHA256) against the
17
- # checksum-covered payload. True when the package is unsigned or the
18
- # signature verifies; false on mismatch.
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
@@ -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. `dependency_chain` is internal: the ancestor GUIDs
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
- ?dependency_chain: Array[String] dependency_chain) -> void
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
- def merged_view: (?exported_only: bool exported_only) -> MergedView
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 for the package this reactor serves
4
- # (ARCHITECTURE.md section 7).
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 package: Package
22
+ attr_reader packages: Array[Package]
13
23
 
14
- def initialize: (Package package) -> void
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 introspection endpoint, or nil when the
19
- # path is not an introspection endpoint.
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