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,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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# Apache htpasswd verification (05x-authentication). Supported hash
|
|
4
|
+
# formats: bcrypt ($2a$/$2b$/$2y$), Apache apr1 MD5 and md5-crypt
|
|
5
|
+
# ($apr1$/$1$), unsalted SHA-1 ({SHA}), and a crypt(3) fallback for
|
|
6
|
+
# the rest (platform-dependent).
|
|
7
|
+
class Htpasswd
|
|
8
|
+
# md5-crypt as deployed by Apache htpasswd, OpenSSL and glibc.
|
|
9
|
+
module Md5Crypt
|
|
10
|
+
ITOA64: String
|
|
11
|
+
DIGEST_GROUPS: Array[Array[Integer]]
|
|
12
|
+
|
|
13
|
+
def self.verify: (String hash, String password) -> bool
|
|
14
|
+
|
|
15
|
+
def self.digest: (String password, String salt, String magic) -> String
|
|
16
|
+
|
|
17
|
+
def self.initial_digest: (String password, String salt, String magic) -> String
|
|
18
|
+
|
|
19
|
+
def self.stretch_digest: (Integer iteration, String password, String salt, String final) -> String
|
|
20
|
+
|
|
21
|
+
def self.to64: (String final) -> String
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader path: String
|
|
25
|
+
|
|
26
|
+
def initialize: (String path) -> void
|
|
27
|
+
|
|
28
|
+
def usernames: () -> Array[String]
|
|
29
|
+
|
|
30
|
+
def verify?: (String username, String password) -> bool
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Constant-time string comparison (length-check first).
|
|
34
|
+
def self.secure_compare: (String own, String theirs) -> bool
|
|
35
|
+
end
|
|
36
|
+
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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# OAuth2 authorization-code flow client (05x-authentication), used by
|
|
4
|
+
# the Authenticator. Talks to the provider over net/http; the client
|
|
5
|
+
# secret never comes from the package.
|
|
6
|
+
class OAuth2
|
|
7
|
+
# The token exchange or userinfo fetch failed at the provider.
|
|
8
|
+
class FlowError < Capsium::Error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
GRANT_TYPE: String
|
|
12
|
+
|
|
13
|
+
attr_reader config: Package::OAuth2Config
|
|
14
|
+
|
|
15
|
+
def initialize: (Package::OAuth2Config config, client_secret: String client_secret,
|
|
16
|
+
session: Session session, ?base_url: String? base_url) -> void
|
|
17
|
+
|
|
18
|
+
# The provider URL the login endpoint redirects the browser to.
|
|
19
|
+
def authorization_url: (untyped request) -> String
|
|
20
|
+
|
|
21
|
+
def valid_state?: (String? state) -> bool
|
|
22
|
+
|
|
23
|
+
# Exchanges the authorization code and fetches the userinfo
|
|
24
|
+
# claims. Returns the raw userinfo hash; raises FlowError on
|
|
25
|
+
# provider errors.
|
|
26
|
+
def complete: (String? code, untyped request) -> Hash[String, untyped]
|
|
27
|
+
|
|
28
|
+
def callback_uri: (untyped request) -> String
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# The writable top overlay layer of a mounted package
|
|
4
|
+
# (ARCHITECTURE.md section 5a): an append-only record of writes in
|
|
5
|
+
# the reactor workdir (content files, tombstones, per-dataset
|
|
6
|
+
# mutation logs). The immutable package never changes on disk.
|
|
7
|
+
class Overlay
|
|
8
|
+
# Base class for the typed overlay errors the APIs map to HTTP
|
|
9
|
+
# statuses.
|
|
10
|
+
class Error < Capsium::Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The addressed item does not exist in the merged collection (404).
|
|
14
|
+
class ItemNotFoundError < Error
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The write conflicts with existing state (409).
|
|
18
|
+
class ConflictError < Error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The dataset cannot take item writes or the path is unsafe.
|
|
22
|
+
class UnsupportedDatasetError < Error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The candidate document violates the dataset's JSON schema (422).
|
|
26
|
+
class SchemaViolationError < Error
|
|
27
|
+
attr_reader messages: Array[String]
|
|
28
|
+
|
|
29
|
+
def initialize: (Array[String] messages) -> void
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
TOMBSTONE_FILE: String
|
|
33
|
+
SAFE_NAME_PATTERN: Regexp
|
|
34
|
+
|
|
35
|
+
attr_reader root: String
|
|
36
|
+
attr_reader tombstones: Set[String]
|
|
37
|
+
|
|
38
|
+
def initialize: (root: String root) -> void
|
|
39
|
+
|
|
40
|
+
def content_root: () -> String
|
|
41
|
+
|
|
42
|
+
def data_root: () -> String
|
|
43
|
+
|
|
44
|
+
# The overlay as the topmost MergedView layer.
|
|
45
|
+
def layer: () -> Package::MergedView::Layer
|
|
46
|
+
|
|
47
|
+
def write_content: (String content_relative, String body) -> void
|
|
48
|
+
|
|
49
|
+
def delete_content: (String content_relative) -> void
|
|
50
|
+
|
|
51
|
+
# The dataset as served: base data merged with the mutation log.
|
|
52
|
+
def dataset_data: (Package::Dataset dataset) -> untyped
|
|
53
|
+
|
|
54
|
+
# The merged item collection.
|
|
55
|
+
def items: (Package::Dataset dataset) -> Array[untyped]
|
|
56
|
+
|
|
57
|
+
def item: (Package::Dataset dataset, String id) -> untyped
|
|
58
|
+
|
|
59
|
+
def append_item: (Package::Dataset dataset, untyped item) -> String
|
|
60
|
+
|
|
61
|
+
def replace_item: (Package::Dataset dataset, String id, untyped item) -> void
|
|
62
|
+
|
|
63
|
+
def delete_item: (Package::Dataset dataset, String id) -> nil
|
|
64
|
+
|
|
65
|
+
# Whether the dataset has recorded mutations.
|
|
66
|
+
def mutations?: (String name) -> bool
|
|
67
|
+
|
|
68
|
+
# The item id convention: an "id" field, else nil (positional).
|
|
69
|
+
def item_id: (untyped item) -> String?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# Folds a mounted package's base plus its overlay into a NEW
|
|
4
|
+
# versioned .cap (name-version+patch.cap) in the reactor workdir.
|
|
5
|
+
class PackageSaver
|
|
6
|
+
def initialize: (Mount mount) -> void
|
|
7
|
+
|
|
8
|
+
# Saves and returns the {"path", "sha256", "name", "version"} hash.
|
|
9
|
+
def save: (String workdir) -> Hash[String, String]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# Plain HTTP response writers shared by the reactor dispatch, the
|
|
4
|
+
# route serving and the API handlers (data, GraphQL, save).
|
|
5
|
+
module Responses
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def respond_not_found: (untyped response) -> untyped
|
|
9
|
+
|
|
10
|
+
def respond_forbidden: (untyped response) -> untyped
|
|
11
|
+
|
|
12
|
+
def respond_not_implemented: (untyped response) -> untyped
|
|
13
|
+
|
|
14
|
+
def respond_method_not_allowed: (untyped response) -> untyped
|
|
15
|
+
|
|
16
|
+
def respond_text: (untyped response, Integer status, String body) -> untyped
|
|
17
|
+
|
|
18
|
+
def respond_json: (untyped response, Integer status, untyped body) -> untyped
|
|
19
|
+
|
|
20
|
+
def respond_error: (untyped response, Integer status, String message, ?messages: Array[String]? messages) -> untyped
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# Serving of resolved routes (static files through the merged view,
|
|
4
|
+
# datasets, handler-route 501s), mixed into Reactor. Includes route
|
|
5
|
+
# inheritance processing per 05x-routing.
|
|
6
|
+
module Serving
|
|
7
|
+
include Responses
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# One request metric and one log line per served request.
|
|
12
|
+
def record_request: (untyped request, untyped response) -> untyped
|
|
13
|
+
|
|
14
|
+
# The mount answering a request path: longest matching prefix wins.
|
|
15
|
+
def resolve_mount: (String path) -> Mount?
|
|
16
|
+
|
|
17
|
+
def mounts_by_length: () -> Array[Mount]
|
|
18
|
+
|
|
19
|
+
# The root ("/") mount, or the first mount when nothing is mounted
|
|
20
|
+
# at the root.
|
|
21
|
+
def root_mount: () -> Mount
|
|
22
|
+
|
|
23
|
+
# Serves a request that matched a mount.
|
|
24
|
+
def serve_mounted_request: (Mount mount, untyped identity, untyped request, untyped response) -> untyped
|
|
25
|
+
|
|
26
|
+
def serve_data_api: (Mount mount, untyped identity, String inner, untyped request, untyped response) -> untyped
|
|
27
|
+
|
|
28
|
+
def serve_content_api: (Mount mount, untyped identity, String inner, untyped request, untyped response) -> untyped
|
|
29
|
+
|
|
30
|
+
# Route-level access control; false when the response was written.
|
|
31
|
+
def authorized?: (untyped identity, Package::Route route, untyped response) -> bool
|
|
32
|
+
|
|
33
|
+
def serve_route: (Mount mount, Package::Route route, untyped response) -> untyped
|
|
34
|
+
|
|
35
|
+
def serve_file: (Mount mount, Package::Route route, untyped response) -> untyped
|
|
36
|
+
|
|
37
|
+
def inherited_processing: (Package::Route route, String body, Hash[String, String] headers) -> [String, Hash[String, String]]
|
|
38
|
+
|
|
39
|
+
def content_type_for: (Mount mount, Package::Route route, String content_path) -> String
|
|
40
|
+
|
|
41
|
+
def headers_for: (Package::Route route) -> Hash[String, String]
|
|
42
|
+
|
|
43
|
+
def serve_dataset: (Mount mount, String dataset_name, untyped response) -> untyped
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Reactor
|
|
3
|
+
# HMAC-SHA256 signed session cookies (05x-authentication). The
|
|
4
|
+
# signing secret comes from deploy.json (sessionSecret) or is
|
|
5
|
+
# generated and persisted reactor-side (never in the package).
|
|
6
|
+
class Session
|
|
7
|
+
COOKIE_NAME: String
|
|
8
|
+
SECRET_BYTES: Integer
|
|
9
|
+
|
|
10
|
+
def self.hmac: (String secret, String data) -> String
|
|
11
|
+
|
|
12
|
+
# The persisted reactor-side secret: loaded from state_file when
|
|
13
|
+
# present, generated and written (mode 0600) otherwise.
|
|
14
|
+
def self.load_or_generate_secret: (String? state_file) -> String
|
|
15
|
+
|
|
16
|
+
attr_reader secret: String
|
|
17
|
+
|
|
18
|
+
def initialize: (?secret: String? secret, ?state_file: String? state_file) -> void
|
|
19
|
+
|
|
20
|
+
# HMAC-SHA256 of data with the session secret (hex).
|
|
21
|
+
def sign: (String data) -> String
|
|
22
|
+
|
|
23
|
+
def encode: (Hash[String, untyped] payload) -> String
|
|
24
|
+
|
|
25
|
+
# The verified payload, or nil when the cookie is missing or the
|
|
26
|
+
# signature does not match.
|
|
27
|
+
def decode: (String? cookie_value) -> Hash[String, untyped]?
|
|
28
|
+
|
|
29
|
+
# The identity for a request's Cookie header, or nil.
|
|
30
|
+
def identity_from: (untyped request) -> Hash[String, untyped]?
|
|
31
|
+
|
|
32
|
+
# A Set-Cookie value establishing the identity session.
|
|
33
|
+
def cookie_for: (Hash[String, untyped] identity) -> String
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/sig/capsium/reactor.rbs
CHANGED
|
@@ -3,8 +3,13 @@ module Capsium
|
|
|
3
3
|
# Monitoring HTTP API (ARCHITECTURE.md section 7) and reloads the
|
|
4
4
|
# package on filesystem changes.
|
|
5
5
|
class Reactor
|
|
6
|
+
include Responses
|
|
7
|
+
include Endpoints
|
|
8
|
+
include Serving
|
|
9
|
+
|
|
6
10
|
DEFAULT_PORT: Integer
|
|
7
11
|
DEFAULT_CACHE_CONTROL: String
|
|
12
|
+
PACKAGE_SAVE_PATTERN: Regexp
|
|
8
13
|
|
|
9
14
|
attr_reader package: Package
|
|
10
15
|
attr_reader package_path: String
|
|
@@ -14,10 +19,23 @@ module Capsium
|
|
|
14
19
|
attr_reader server: untyped
|
|
15
20
|
attr_reader server_thread: Thread?
|
|
16
21
|
attr_reader introspection: Introspection
|
|
22
|
+
attr_reader authenticator: Authenticator
|
|
23
|
+
attr_reader store: (Package::Store | String)?
|
|
24
|
+
attr_reader registry: (Capsium::Registry | String)?
|
|
25
|
+
attr_reader started_at: Time
|
|
26
|
+
attr_reader metrics: Metrics
|
|
27
|
+
attr_reader log_buffer: Capsium::LogBuffer
|
|
28
|
+
attr_reader mounts: Array[Mount]
|
|
29
|
+
attr_reader workdir: String
|
|
17
30
|
|
|
18
|
-
def initialize: (package: Package | String package, ?
|
|
31
|
+
def initialize: (?package: (Package | String)? package, ?mounts: Array[Mount]? mounts,
|
|
32
|
+
?port: Integer port,
|
|
19
33
|
?cache_control: String? cache_control,
|
|
20
|
-
?do_not_listen: bool do_not_listen
|
|
34
|
+
?do_not_listen: bool do_not_listen,
|
|
35
|
+
?store: (Package::Store | String)? store,
|
|
36
|
+
?deploy: (Hash[String, untyped] | String)? deploy,
|
|
37
|
+
?registry: (Capsium::Registry | String)? registry,
|
|
38
|
+
?workdir: String? workdir) -> void
|
|
21
39
|
|
|
22
40
|
def serve: () -> untyped
|
|
23
41
|
|
|
@@ -26,5 +44,7 @@ module Capsium
|
|
|
26
44
|
def mount_routes: () -> void
|
|
27
45
|
|
|
28
46
|
def restart_server: () -> Thread
|
|
47
|
+
|
|
48
|
+
def cleanup: () -> void
|
|
29
49
|
end
|
|
30
50
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Registry
|
|
3
|
+
# A static registry in a local directory (read-write).
|
|
4
|
+
class Local < Registry
|
|
5
|
+
attr_reader dir: String
|
|
6
|
+
|
|
7
|
+
def initialize: (String dir) -> void
|
|
8
|
+
|
|
9
|
+
def location: () -> String
|
|
10
|
+
|
|
11
|
+
# Validates cap_path, copies it into the registry and records it
|
|
12
|
+
# in index.json. Returns the recorded Entry.
|
|
13
|
+
def push: (String cap_path) -> Entry
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Registry
|
|
3
|
+
# A read-only static registry behind an https base URL (plain http
|
|
4
|
+
# for loopback hosts only).
|
|
5
|
+
class Remote < Registry
|
|
6
|
+
OPEN_TIMEOUT: Integer
|
|
7
|
+
READ_TIMEOUT: Integer
|
|
8
|
+
MAX_REDIRECTS: Integer
|
|
9
|
+
LOOPBACK_NETWORKS: Array[untyped]
|
|
10
|
+
|
|
11
|
+
attr_reader base_url: String
|
|
12
|
+
|
|
13
|
+
def initialize: (String base_url) -> void
|
|
14
|
+
|
|
15
|
+
def location: () -> String
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
# A static package registry (directory or https base URL with an
|
|
3
|
+
# index.json): Capsium::Registry::Local (read-write) and
|
|
4
|
+
# Capsium::Registry::Remote (read-only). See lib/capsium/registry.rb.
|
|
5
|
+
class Registry
|
|
6
|
+
INDEX_FILE: String
|
|
7
|
+
ENV_VAR: String
|
|
8
|
+
|
|
9
|
+
# Base error for every registry failure.
|
|
10
|
+
class RegistryError < Capsium::Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# No registry reference was given (neither flag nor CAPSIUM_REGISTRY).
|
|
14
|
+
class RegistryNotConfiguredError < RegistryError
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The registry itself is unusable (bad path or URL, unreadable index).
|
|
18
|
+
class InvalidRegistryError < RegistryError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A .cap offered to push failed package validation.
|
|
22
|
+
class InvalidPackageError < RegistryError
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The index has no entry for the requested package GUID.
|
|
26
|
+
class PackageNotFoundError < RegistryError
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The registry has the GUID but no version satisfies the constraint.
|
|
30
|
+
class UnsatisfiableConstraintError < RegistryError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A fetched .cap does not match the sha256 declared in the index.
|
|
34
|
+
class ChecksumMismatchError < RegistryError
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A registry file could not be retrieved (network or HTTP failure).
|
|
38
|
+
class FetchError < RegistryError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# One indexed version of a registry package.
|
|
42
|
+
class Entry
|
|
43
|
+
def self.new: (guid: String, name: String, version: Package::Version,
|
|
44
|
+
file: String, sha256: String, size: Integer) -> void
|
|
45
|
+
|
|
46
|
+
def guid: () -> String
|
|
47
|
+
def name: () -> String
|
|
48
|
+
def version: () -> Package::Version
|
|
49
|
+
def file: () -> String
|
|
50
|
+
def sha256: () -> String
|
|
51
|
+
def size: () -> Integer
|
|
52
|
+
|
|
53
|
+
# The canonical store file name for this entry's .cap.
|
|
54
|
+
def cap_file_name: () -> String
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The registry at the given reference: a Local directory or a Remote
|
|
58
|
+
# http(s) base URL. Raises RegistryNotConfiguredError when no
|
|
59
|
+
# reference is given.
|
|
60
|
+
def self.fetch: (String? ref) -> Registry
|
|
61
|
+
|
|
62
|
+
# The registry named by the CAPSIUM_REGISTRY environment variable,
|
|
63
|
+
# or nil when unset.
|
|
64
|
+
def self.default: () -> Registry?
|
|
65
|
+
|
|
66
|
+
def initialize: () -> void
|
|
67
|
+
|
|
68
|
+
# Where this registry lives (directory path or base URL).
|
|
69
|
+
def location: () -> String
|
|
70
|
+
|
|
71
|
+
# The newest indexed version of the package GUID satisfying the
|
|
72
|
+
# semver constraint.
|
|
73
|
+
def resolve: (String guid, ?String constraint) -> Entry
|
|
74
|
+
|
|
75
|
+
# Resolves, fetches, checksum-verifies and installs the newest
|
|
76
|
+
# satisfying .cap into the package store, returning the store path.
|
|
77
|
+
def install: (String guid, ?String constraint,
|
|
78
|
+
store: (Package::Store | String) store) -> String
|
|
79
|
+
|
|
80
|
+
# Only Local registries are writable.
|
|
81
|
+
def push: (String cap_path) -> Entry
|
|
82
|
+
end
|
|
83
|
+
end
|
data/sig/graphql.rbs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Minimal RBS stubs for the graphql gem: only the constants capsium's
|
|
2
|
+
# own signatures reference (the gem ships full sigs, but this project
|
|
3
|
+
# does not use an RBS collection).
|
|
4
|
+
module GraphQL
|
|
5
|
+
class Schema
|
|
6
|
+
def self.execute: (String query, ?variables: Hash[String, untyped] variables, ?operation_name: String? operation_name) -> untyped
|
|
7
|
+
|
|
8
|
+
class Scalar
|
|
9
|
+
def self.graphql_name: (?String name) -> void
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|