capsium 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +46 -7
- data/CHANGELOG.md +183 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.adoc +496 -127
- data/Rakefile +8 -1
- data/bin/console +1 -1
- data/capsium.gemspec +12 -19
- data/exe/capsium +1 -1
- data/lib/capsium/cli/convert.rb +18 -0
- data/lib/capsium/cli/formatting.rb +27 -0
- data/lib/capsium/cli/package.rb +148 -0
- data/lib/capsium/cli/reactor.rb +33 -0
- data/lib/capsium/cli.rb +4 -96
- data/lib/capsium/converters/jekyll.rb +5 -7
- data/lib/capsium/converters.rb +7 -0
- data/lib/capsium/package/authentication.rb +38 -0
- data/lib/capsium/package/authentication_config.rb +78 -0
- data/lib/capsium/package/cipher.rb +197 -0
- data/lib/capsium/package/composition.rb +77 -0
- data/lib/capsium/package/dataset.rb +67 -34
- data/lib/capsium/package/dependency_resolver.rb +61 -0
- data/lib/capsium/package/encryption_config.rb +38 -0
- data/lib/capsium/package/manifest.rb +33 -22
- data/lib/capsium/package/manifest_config.rb +52 -7
- data/lib/capsium/package/merged_view.rb +168 -0
- data/lib/capsium/package/metadata.rb +4 -8
- data/lib/capsium/package/metadata_config.rb +97 -9
- data/lib/capsium/package/preparation.rb +81 -0
- data/lib/capsium/package/routes.rb +39 -41
- data/lib/capsium/package/routes_config.rb +174 -44
- data/lib/capsium/package/security.rb +140 -0
- data/lib/capsium/package/security_config.rb +60 -0
- data/lib/capsium/package/signer.rb +201 -0
- data/lib/capsium/package/storage.rb +20 -45
- data/lib/capsium/package/storage_config.rb +128 -3
- data/lib/capsium/package/store.rb +109 -0
- data/lib/capsium/package/testing/config_test.rb +70 -0
- data/lib/capsium/package/testing/context.rb +11 -0
- data/lib/capsium/package/testing/data_validation_test.rb +63 -0
- data/lib/capsium/package/testing/file_test.rb +45 -0
- data/lib/capsium/package/testing/report.rb +33 -0
- data/lib/capsium/package/testing/route_test.rb +64 -0
- data/lib/capsium/package/testing/test_case.rb +60 -0
- data/lib/capsium/package/testing/test_suite.rb +86 -0
- data/lib/capsium/package/testing.rb +20 -0
- data/lib/capsium/package/validator.rb +160 -0
- data/lib/capsium/package/verification.rb +32 -0
- data/lib/capsium/package/version.rb +204 -0
- data/lib/capsium/package.rb +92 -106
- data/lib/capsium/packager.rb +75 -6
- data/lib/capsium/reactor/authenticator.rb +180 -0
- data/lib/capsium/reactor/deploy.rb +71 -0
- data/lib/capsium/reactor/htpasswd.rb +154 -0
- data/lib/capsium/reactor/introspection.rb +91 -0
- data/lib/capsium/reactor/oauth2.rb +106 -0
- data/lib/capsium/reactor/serving.rb +75 -0
- data/lib/capsium/reactor/session.rb +82 -0
- data/lib/capsium/reactor.rb +93 -43
- data/lib/capsium/thor_ext.rb +1 -1
- data/lib/capsium/version.rb +1 -1
- data/lib/capsium.rb +7 -9
- data/sig/capsium/package/authentication.rbs +23 -0
- data/sig/capsium/package/authentication_config.rbs +71 -0
- data/sig/capsium/package/cipher.rbs +51 -0
- data/sig/capsium/package/composition.rbs +20 -0
- data/sig/capsium/package/dataset.rbs +28 -0
- data/sig/capsium/package/dependency_resolver.rbs +49 -0
- data/sig/capsium/package/encryption_config.rbs +35 -0
- data/sig/capsium/package/manifest.rbs +34 -0
- data/sig/capsium/package/manifest_config.rbs +38 -0
- data/sig/capsium/package/merged_view.rbs +46 -0
- data/sig/capsium/package/metadata.rbs +26 -0
- data/sig/capsium/package/metadata_config.rbs +49 -0
- data/sig/capsium/package/preparation.rbs +23 -0
- data/sig/capsium/package/routes.rbs +37 -0
- data/sig/capsium/package/routes_config.rbs +107 -0
- data/sig/capsium/package/security.rbs +72 -0
- data/sig/capsium/package/security_config.rbs +57 -0
- data/sig/capsium/package/signer.rbs +59 -0
- data/sig/capsium/package/storage.rbs +28 -0
- data/sig/capsium/package/storage_config.rbs +85 -0
- data/sig/capsium/package/store.rbs +37 -0
- data/sig/capsium/package/testing/config_test.rbs +21 -0
- data/sig/capsium/package/testing/context.rbs +14 -0
- data/sig/capsium/package/testing/data_validation_test.rbs +22 -0
- data/sig/capsium/package/testing/file_test.rbs +17 -0
- data/sig/capsium/package/testing/report.rbs +20 -0
- data/sig/capsium/package/testing/route_test.rbs +22 -0
- data/sig/capsium/package/testing/test_case.rbs +41 -0
- data/sig/capsium/package/testing/test_suite.rbs +20 -0
- data/sig/capsium/package/testing.rbs +7 -0
- data/sig/capsium/package/validator.rbs +29 -0
- data/sig/capsium/package/verification.rbs +24 -0
- data/sig/capsium/package/version.rbs +39 -0
- data/sig/capsium/package.rbs +61 -0
- data/sig/capsium/packager.rbs +32 -0
- data/sig/capsium/reactor/authenticator.rbs +40 -0
- data/sig/capsium/reactor/deploy.rbs +34 -0
- data/sig/capsium/reactor/htpasswd.rbs +36 -0
- data/sig/capsium/reactor/introspection.rbs +31 -0
- data/sig/capsium/reactor/oauth2.rbs +31 -0
- data/sig/capsium/reactor/serving.rbs +22 -0
- data/sig/capsium/reactor/session.rbs +36 -0
- data/sig/capsium/reactor.rbs +35 -0
- data/sig/capsium.rbs +3 -1
- metadata +108 -170
- data/lib/capsium/package/dataset_config.rb +0 -28
- data/lib/capsium/protector.rb +0 -103
data/README.adoc
CHANGED
|
@@ -9,27 +9,14 @@ content packages with ease.
|
|
|
9
9
|
This gem provides a structured way to handle content, data, and metadata for
|
|
10
10
|
various applications.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
== Testing out how it works
|
|
14
|
-
|
|
15
|
-
Download the mn-samples-iso built site: https://github.com/metanorma/mn-samples-iso/actions/runs/8862815829/artifacts/1453746303[mn-samples-iso-Linux].
|
|
16
|
-
|
|
17
|
-
Then run these commands:
|
|
12
|
+
== Test it out!
|
|
18
13
|
|
|
19
14
|
[source,bash]
|
|
20
15
|
----
|
|
21
|
-
$
|
|
22
|
-
$
|
|
23
|
-
$
|
|
24
|
-
|
|
25
|
-
$ echo '{"name": "mn-samples-iso","version":"0.1.0"}' > metadata.json
|
|
26
|
-
$ cd ..
|
|
27
|
-
$ bundle exec capsium package pack -f mn-samples-iso-Linux
|
|
28
|
-
Package created: mn-samples-iso-0.1.0.cap
|
|
29
|
-
$ bundle exec capsium reactor serve mn-samples-iso-0.1.0.cap
|
|
30
|
-
Starting server on http://localhost:8864
|
|
31
|
-
[2024-06-05 14:56:41] INFO WEBrick::HTTPServer#start: pid=8234 port=8864
|
|
32
|
-
...
|
|
16
|
+
$ wget https://github.com/capsiums/cap-story/releases/download/v0.9.0/story_of_claire-0.9.0.cap
|
|
17
|
+
$ capsium reactor serve story_of_claire-0.9.0.cap
|
|
18
|
+
$ open http://localhost:8864
|
|
19
|
+
# => Read the story!
|
|
33
20
|
----
|
|
34
21
|
|
|
35
22
|
== Installation
|
|
@@ -57,184 +44,558 @@ gem install capsium
|
|
|
57
44
|
|
|
58
45
|
== What is a Capsium package?
|
|
59
46
|
|
|
60
|
-
A Capsium package is a structured collection of content, data, metadata, and
|
|
47
|
+
A Capsium package is a structured collection of content, data, metadata, and
|
|
48
|
+
routing information, distributed as a single `.cap` file (a ZIP archive, MIME
|
|
49
|
+
`application/vnd.capsium.package`). The canonical layout:
|
|
61
50
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
[source]
|
|
52
|
+
----
|
|
53
|
+
metadata.json # REQUIRED, hand-authored
|
|
54
|
+
manifest.json # auto-generated at load/pack time when absent
|
|
55
|
+
routes.json # auto-generated when absent
|
|
56
|
+
storage.json # optional (only when datasets exist)
|
|
57
|
+
security.json # generated at pack time (SHA-256 checksums)
|
|
58
|
+
authentication.json # optional (user authentication)
|
|
59
|
+
content/ # served content root (URL space mirrors paths below it)
|
|
60
|
+
data/ # datasets (optional)
|
|
61
|
+
----
|
|
62
|
+
|
|
63
|
+
Paths inside the configuration files are package-relative POSIX paths.
|
|
64
|
+
|
|
65
|
+
* **metadata.json**: name (kebab-case), version (semver), description, guid
|
|
66
|
+
(URI) and uuid are required; author, license, repository, dependencies
|
|
67
|
+
(an object of `guid -> semver range`) are optional.
|
|
68
|
+
* **manifest.json**: an object keyed by package-relative resource path, e.g.
|
|
69
|
+
`"content/index.html": { "type": "text/html", "visibility": "exported" }`.
|
|
70
|
+
Generated by scanning `content/` (MIME types via marcel) when absent.
|
|
71
|
+
* **routes.json**: an optional top-level `index` plus a `routes` array. Route
|
|
72
|
+
kinds: `{path, resource}` for static files, `{path, dataset}` for datasets
|
|
73
|
+
(served under `/api/v1/data/<id>`). HTML files get two routes (basename and
|
|
74
|
+
full filename); the index HTML additionally gets `/`.
|
|
75
|
+
* **storage.json**: dataset definitions under `storage.dataSets`, either
|
|
76
|
+
schema-backed files (`source`, `schemaFile`, `schemaType: json-schema`)
|
|
77
|
+
or SQLite (`databaseFile`, `table`). `storage.layers` declares overlay
|
|
78
|
+
layers (see "Layered storage" below).
|
|
79
|
+
* **security.json**: `security.integrityChecks.checksums` holds a SHA-256 hex
|
|
80
|
+
digest for every file in the package except `security.json` itself and
|
|
81
|
+
`signature.sig`. When the package is signed, `security.digitalSignatures`
|
|
82
|
+
records `{ "publicKey", "signatureFile" }`.
|
|
83
|
+
* **authentication.json**: optional user authentication (see
|
|
84
|
+
"Authentication" below): `authentication.basicAuth` and/or
|
|
85
|
+
`authentication.oauth2`.
|
|
86
|
+
|
|
87
|
+
Legacy (pre-0.2) configuration files are still accepted on read and
|
|
88
|
+
normalized to the canonical forms; writers emit only the canonical forms.
|
|
67
89
|
|
|
68
90
|
== What is a Capsium reactor?
|
|
69
91
|
|
|
70
|
-
A Capsium reactor is a runtime environment that serves Capsium packages. It
|
|
92
|
+
A Capsium reactor is a runtime environment that serves Capsium packages. It
|
|
93
|
+
reads the package configuration and starts a server that can handle HTTP
|
|
94
|
+
requests according to the routes defined in the package. The reactor verifies
|
|
95
|
+
the package against `security.json` when present and rejects tampered
|
|
96
|
+
packages. Dataset routes are served as JSON; static resources are served with
|
|
97
|
+
`Cache-Control: public, max-age=31536000` (configurable). While serving, the
|
|
98
|
+
reactor also answers the introspection endpoints described below.
|
|
71
99
|
|
|
72
100
|
== CLI: Package
|
|
73
101
|
|
|
74
|
-
|
|
102
|
+
Capsium provides a CLI to help you pack and manage your packages.
|
|
75
103
|
|
|
76
|
-
|
|
104
|
+
=== Packing a package
|
|
77
105
|
|
|
78
106
|
[source,bash]
|
|
79
107
|
----
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
capsium package new my_package
|
|
83
|
-
# Shorthand
|
|
84
|
-
capsium package my_package
|
|
108
|
+
capsium package pack [--force/-f] path-to-package
|
|
109
|
+
----
|
|
85
110
|
|
|
86
|
-
|
|
87
|
-
|
|
111
|
+
This command generates `manifest.json`/`routes.json` when absent, writes
|
|
112
|
+
`security.json` with SHA-256 checksums of every package file, and packs the
|
|
113
|
+
directory into `{package-name}-{package-version}.cap` (name and version from
|
|
114
|
+
`metadata.json`).
|
|
88
115
|
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
.Sample `pack` command
|
|
117
|
+
====
|
|
118
|
+
[source,bash]
|
|
119
|
+
----
|
|
120
|
+
capsium package pack -f spec/fixtures/bare-package
|
|
121
|
+
----
|
|
122
|
+
====
|
|
91
123
|
|
|
92
|
-
|
|
93
|
-
capsium package add metadata name=sample_package version=0.1.0
|
|
124
|
+
=== Unpacking a package
|
|
94
125
|
|
|
95
|
-
|
|
96
|
-
|
|
126
|
+
[source,bash]
|
|
127
|
+
----
|
|
128
|
+
capsium package unpack bare-package-0.1.0.cap [-o/--output my_bare_package]
|
|
129
|
+
----
|
|
97
130
|
|
|
98
|
-
|
|
99
|
-
|
|
131
|
+
When `-o` is omitted, the package is unpacked into a directory named after
|
|
132
|
+
the `.cap` file.
|
|
133
|
+
|
|
134
|
+
Extraction is zip-slip safe: entries whose names would escape the
|
|
135
|
+
destination (absolute paths, drive letters, `..` segments) are rejected
|
|
136
|
+
with `Capsium::Packager::UnsafeEntryError`.
|
|
137
|
+
|
|
138
|
+
=== Validating a package
|
|
139
|
+
|
|
140
|
+
[source,bash]
|
|
141
|
+
----
|
|
142
|
+
capsium package validate path-to-package-or-cap
|
|
100
143
|
----
|
|
101
144
|
|
|
102
|
-
|
|
145
|
+
Runs a per-check report and exits with status 1 on any failure:
|
|
146
|
+
|
|
147
|
+
* metadata: required fields present and well-formed (kebab-case name, semver
|
|
148
|
+
version, URI guid, UUID)
|
|
149
|
+
* manifest: every resource exists on disk
|
|
150
|
+
* routes: route targets exist; dataset routes live under `/api/v1/data/`;
|
|
151
|
+
the index resolves to an existing HTML file
|
|
152
|
+
* storage: dataset sources (and schemas) exist; dataset data passes
|
|
153
|
+
JSON-schema validation
|
|
154
|
+
* security: checksums match when `security.json` is present
|
|
155
|
+
* content: no external `http(s)` references in `content/` files
|
|
103
156
|
|
|
104
|
-
|
|
157
|
+
=== Inspecting a package
|
|
105
158
|
|
|
106
159
|
[source,bash]
|
|
107
160
|
----
|
|
108
|
-
capsium package
|
|
161
|
+
capsium package info|manifest|routes|metadata|storage path-to-package [--store DIR]
|
|
109
162
|
----
|
|
110
163
|
|
|
111
|
-
|
|
112
|
-
|
|
164
|
+
`info` additionally prints the resolved dependency tree for composite
|
|
165
|
+
packages (declared GUID and range, resolved version, store `.cap`), see
|
|
166
|
+
"Composite packages" below.
|
|
113
167
|
|
|
114
|
-
|
|
115
|
-
* where the name and version both come from `{package-path}/metadata.json`
|
|
168
|
+
=== Signing a package
|
|
116
169
|
|
|
117
|
-
.Sample `pack` command
|
|
118
|
-
====
|
|
119
170
|
[source,bash]
|
|
120
171
|
----
|
|
121
|
-
capsium package
|
|
172
|
+
capsium package sign path-to-package-or-cap --key private.pem [--cert cert.pem]
|
|
173
|
+
----
|
|
174
|
+
|
|
175
|
+
Signs the package with an RSA private key (minimum 2048 bits) using
|
|
176
|
+
RSA-SHA256, per the packaging standard's digital signature clause. Signing
|
|
177
|
+
is a post-pack step: `pack` drops signing artifacts, so sign the packed
|
|
178
|
+
directory or the `.cap` file itself.
|
|
179
|
+
|
|
180
|
+
The signed payload is the concatenation, in sorted package-relative path
|
|
181
|
+
order, of the raw bytes of every file covered by the `security.json`
|
|
182
|
+
integrity checksums — i.e. every package file except `security.json` and
|
|
183
|
+
`signature.sig`. The signature over that payload is stored as raw bytes in
|
|
184
|
+
`signature.sig`, the signer's public key PEM is embedded as
|
|
185
|
+
`signature.pub.pem`, and `security.json` records:
|
|
186
|
+
|
|
187
|
+
[source,json]
|
|
188
|
+
----
|
|
189
|
+
{
|
|
190
|
+
"security": {
|
|
191
|
+
"digitalSignatures": {
|
|
192
|
+
"publicKey": "signature.pub.pem",
|
|
193
|
+
"signatureFile": "signature.sig"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
----
|
|
198
|
+
|
|
199
|
+
When `--cert` is given, the X.509 certificate must match the private key,
|
|
200
|
+
and the certificate's public key is embedded instead.
|
|
201
|
+
|
|
202
|
+
The payload construction is openssl-compatible; to verify a signed package
|
|
203
|
+
independently, concatenate the checksum-covered files in sorted order and
|
|
204
|
+
run:
|
|
205
|
+
|
|
206
|
+
[source,bash]
|
|
207
|
+
----
|
|
208
|
+
openssl dgst -sha256 -verify signature.pub.pem -signature signature.sig payload.bin
|
|
209
|
+
----
|
|
210
|
+
|
|
211
|
+
=== Verifying a package signature
|
|
212
|
+
|
|
213
|
+
[source,bash]
|
|
214
|
+
----
|
|
215
|
+
capsium package verify-signature path-to-package-or-cap [--cert cert-or-public-key.pem]
|
|
216
|
+
----
|
|
217
|
+
|
|
218
|
+
Verifies the declared signature against the package contents (using the
|
|
219
|
+
embedded public key, or the given certificate/public key) and exits with
|
|
220
|
+
status 1 when the package is unsigned or the signature does not match.
|
|
221
|
+
Packages that declare a signature are also verified automatically on load —
|
|
222
|
+
a mismatch raises `Capsium::Package::Signer::SignatureMismatchError`, so a
|
|
223
|
+
tampered signed package cannot be served by the reactor.
|
|
224
|
+
|
|
225
|
+
=== Encrypting a package
|
|
226
|
+
|
|
227
|
+
[source,bash]
|
|
228
|
+
----
|
|
229
|
+
capsium package encrypt path-to-package-or-cap --public-key public.pem -o encrypted.cap
|
|
122
230
|
----
|
|
123
|
-
====
|
|
124
231
|
|
|
232
|
+
Encrypts the whole package for the recipient's RSA public key (or X.509
|
|
233
|
+
certificate), per the packaging standard's encryption clause: the
|
|
234
|
+
encrypted `.cap` is a zip containing `metadata.json` (cleartext, so
|
|
235
|
+
name/version stay readable), `signature.json` (the encryption envelope)
|
|
236
|
+
and `package.enc` — the AES-256-GCM ciphertext of the inner plaintext
|
|
237
|
+
`.cap` zip (content, configuration files, data). A random 256-bit data
|
|
238
|
+
encryption key (DEK) encrypts the inner zip and is wrapped with the
|
|
239
|
+
recipient's public key using RSA-OAEP with SHA-256:
|
|
125
240
|
|
|
241
|
+
[source,json]
|
|
242
|
+
----
|
|
243
|
+
{
|
|
244
|
+
"encryption": {
|
|
245
|
+
"algorithm": "AES-256-GCM",
|
|
246
|
+
"keyManagement": "RSA-OAEP-SHA256",
|
|
247
|
+
"encryptedDek": "<base64>",
|
|
248
|
+
"iv": "<base64>",
|
|
249
|
+
"authTag": "<base64>"
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
----
|
|
253
|
+
|
|
254
|
+
The OCB/OpenPGP alternatives mentioned by the standard are intentionally
|
|
255
|
+
not implemented.
|
|
256
|
+
|
|
257
|
+
=== Decrypting a package
|
|
258
|
+
|
|
259
|
+
[source,bash]
|
|
260
|
+
----
|
|
261
|
+
capsium package decrypt encrypted.cap --private-key private.pem [-o decrypted.cap]
|
|
262
|
+
----
|
|
263
|
+
|
|
264
|
+
Decrypts an encrypted package with the recipient's RSA private key and
|
|
265
|
+
writes the plaintext `.cap` (default output: `<name>-decrypted.cap`).
|
|
266
|
+
Decryption fails with a typed error when the key does not match or the
|
|
267
|
+
ciphertext was tampered with (GCM authentication). Loading an encrypted
|
|
268
|
+
package without a key raises `Capsium::Package::Cipher::KeyRequiredError`
|
|
269
|
+
— the reactor refuses to serve it.
|
|
270
|
+
|
|
271
|
+
=== Testing a package
|
|
272
|
+
|
|
273
|
+
[source,bash]
|
|
274
|
+
----
|
|
275
|
+
capsium package test path-to-package-or-cap
|
|
276
|
+
----
|
|
277
|
+
|
|
278
|
+
Runs the package's test suite declared in the Capsium testing YAML DSL
|
|
279
|
+
(05x-testing): all `tests/*.yaml` files in the package, each holding a
|
|
280
|
+
top-level `tests` list. Every test has a `name` and a `type`; the four
|
|
281
|
+
supported types:
|
|
282
|
+
|
|
283
|
+
* `route` — requests the URL path from a reactor started for the run and
|
|
284
|
+
checks `expected_status`, plus optional `response_contains` (body
|
|
285
|
+
substring) and `expected_content_type`. Absolute URLs are accepted;
|
|
286
|
+
only their path (and query) is used.
|
|
287
|
+
* `file` — the file at `path` must exist in the package; optional
|
|
288
|
+
`contains` checks a content substring.
|
|
289
|
+
* `data_validation` — the rows of `data_file` (format `json` or `yaml`)
|
|
290
|
+
must validate against the JSON schema at `schema_file`. Array data
|
|
291
|
+
validates row by row.
|
|
292
|
+
* `config` — `config_file` (format `json` or `yaml`) must exist and
|
|
293
|
+
parse; the known package configs (`metadata.json`, `manifest.json`,
|
|
294
|
+
`routes.json`, `storage.json`, `security.json`) are additionally
|
|
295
|
+
validated against their canonical models.
|
|
296
|
+
|
|
297
|
+
Example:
|
|
298
|
+
|
|
299
|
+
[source,yaml]
|
|
300
|
+
----
|
|
301
|
+
tests:
|
|
302
|
+
- name: Home Route Test
|
|
303
|
+
type: route
|
|
304
|
+
url: "/home"
|
|
305
|
+
expected_status: 200
|
|
306
|
+
response_contains: "Welcome"
|
|
307
|
+
|
|
308
|
+
- name: Config File Exists
|
|
309
|
+
type: file
|
|
310
|
+
path: "metadata.json"
|
|
311
|
+
|
|
312
|
+
- name: JSON Data Validation
|
|
313
|
+
type: data_validation
|
|
314
|
+
format: json
|
|
315
|
+
data_file: "data/animals.json"
|
|
316
|
+
schema_file: "data/animals.schema.json"
|
|
317
|
+
|
|
318
|
+
- name: Metadata Config Validation
|
|
319
|
+
type: config
|
|
320
|
+
format: json
|
|
321
|
+
config_file: "metadata.json"
|
|
322
|
+
----
|
|
323
|
+
|
|
324
|
+
Each test is reported as `PASS`/`FAIL` with messages; the command exits
|
|
325
|
+
with status 1 when any test fails (or a test definition is invalid).
|
|
126
326
|
|
|
127
327
|
== CLI: Reactor
|
|
128
328
|
|
|
129
329
|
=== Starting a reactor on your package
|
|
130
330
|
|
|
131
|
-
|
|
331
|
+
[source,bash]
|
|
332
|
+
----
|
|
333
|
+
capsium reactor serve my_package.cap [--port 8864] [--store DIR] [--deploy deploy.json]
|
|
334
|
+
----
|
|
335
|
+
|
|
336
|
+
The reactor takes a local package directory or `.cap` file; serving
|
|
337
|
+
directly from a URL is not supported. `--store` (or `CAPSIUM_STORE`)
|
|
338
|
+
resolves composite-package dependencies; `--deploy` (or
|
|
339
|
+
`CAPSIUM_DEPLOY`) points at the reactor-side `deploy.json` with
|
|
340
|
+
authentication secrets (see "Authentication" below).
|
|
341
|
+
|
|
342
|
+
=== Reactor introspection API
|
|
343
|
+
|
|
344
|
+
While serving, the reactor answers the Monitoring HTTP API
|
|
345
|
+
(ARCHITECTURE.md section 7) as `application/json`:
|
|
346
|
+
|
|
347
|
+
[source]
|
|
348
|
+
----
|
|
349
|
+
GET /api/v1/introspect/metadata # => {"packages": [{"name", "version", "author", "description"}]}
|
|
350
|
+
GET /api/v1/introspect/routes # => {"routes": [{"package", "routes": [{"method", "path"}]}]}
|
|
351
|
+
GET /api/v1/introspect/content-hashes # => {"contentHashes": [{"package", "hash"}]}
|
|
352
|
+
GET /api/v1/introspect/content-validity # => {"contentValidity": [{"package", "valid", "lastChecked", "reason"?}]}
|
|
353
|
+
----
|
|
354
|
+
|
|
355
|
+
Non-GET methods on these paths are answered `405 Method Not Allowed`.
|
|
356
|
+
|
|
357
|
+
`content-hashes` is the SHA-256 of the `.cap` blob when the package was
|
|
358
|
+
served from one. For a directory source there is no blob, so the hash
|
|
359
|
+
covers the canonical (sorted-key) JSON serialization of the package
|
|
360
|
+
content checksums — the same data `security.json` integrityChecks carry.
|
|
361
|
+
|
|
362
|
+
`content-validity` re-verifies the package against `security.json` on
|
|
363
|
+
every request and reports the outcome with a UTC `lastChecked`
|
|
364
|
+
timestamp; `reason` lists the integrity errors when `valid` is false.
|
|
365
|
+
The entry also reports `signed` and `encrypted` status, plus
|
|
366
|
+
`signatureValid` when the package declares a signature.
|
|
367
|
+
|
|
368
|
+
== Layered storage
|
|
369
|
+
|
|
370
|
+
`storage.layers` stacks overlay directories over `content/` (bottom ->
|
|
371
|
+
top in declaration order), each mirroring the `content/` tree:
|
|
372
|
+
|
|
373
|
+
[source,json]
|
|
374
|
+
----
|
|
375
|
+
{ "storage": { "layers": [
|
|
376
|
+
{ "path": "base", "writable": false, "visibility": "exported" },
|
|
377
|
+
{ "path": "updates", "writable": true, "visibility": "private" }
|
|
378
|
+
] } }
|
|
379
|
+
----
|
|
380
|
+
|
|
381
|
+
A request resolves against layers from the TOP down; the first hit
|
|
382
|
+
wins. Deletions are recorded as tombstones: a `.capsium-tombstones`
|
|
383
|
+
JSON file (an array of `content/`-relative paths) in a writable layer;
|
|
384
|
+
a tombstoned path resolves 404 even when a lower layer holds the file,
|
|
385
|
+
while a file reappearing above the tombstone is served again. Packages
|
|
386
|
+
without a `layers` config behave exactly as before (single implicit
|
|
387
|
+
`content/` layer). `visibility: private` layers are hidden from
|
|
388
|
+
dependent packages (see below), as are resources whose manifest
|
|
389
|
+
visibility is `private`.
|
|
390
|
+
|
|
391
|
+
== Composite packages
|
|
392
|
+
|
|
393
|
+
`metadata.dependencies` maps a dependency GUID to a semver range
|
|
394
|
+
(`>=1.0.0`, `^1.2.3`, `~1.2.3`, exact, `*`, `1.x`/`1.2.x`, partials,
|
|
395
|
+
and comma/space conjunctions like `>=1.0.0, <2.0.0`). Dependencies
|
|
396
|
+
resolve against a *package store* — a directory of
|
|
397
|
+
`<name>-<version>.cap` files plus an optional `index.json` (GUID ->
|
|
398
|
+
file) — given via `CAPSIUM_STORE` or `--store`; the newest satisfying
|
|
399
|
+
version wins.
|
|
400
|
+
|
|
401
|
+
On load, each resolved dependency's exported content becomes read-only
|
|
402
|
+
layers below all of the dependent's own layers. Routes may address
|
|
403
|
+
dependency content explicitly (`"resource": "<dependency-guid>/content/app.js"`)
|
|
404
|
+
or declare route-inheritance attributes per 05x-routing: `remap`
|
|
405
|
+
(replaces the serving path), `responseRewrite` (`body`, `headers`),
|
|
406
|
+
`responseHeaders` (merged over served headers) and `requestHeaders`
|
|
407
|
+
(parsed and exposed for forwarding reactors; this reactor serves
|
|
408
|
+
statically, so they do not alter its responses). Referencing a
|
|
409
|
+
dependency's private or missing resource is a load-time error, as are
|
|
410
|
+
circular, missing or unsatisfiable dependencies.
|
|
132
411
|
|
|
133
412
|
[source,bash]
|
|
134
413
|
----
|
|
135
|
-
capsium
|
|
414
|
+
capsium package info my-composite-package --store ./store
|
|
415
|
+
capsium reactor serve my-composite-package --store ./store
|
|
136
416
|
----
|
|
137
417
|
|
|
418
|
+
== Authentication
|
|
138
419
|
|
|
139
|
-
|
|
420
|
+
`authentication.json` enables user authentication (05x-authentication):
|
|
140
421
|
|
|
141
|
-
|
|
422
|
+
[source,json]
|
|
423
|
+
----
|
|
424
|
+
{ "authentication": {
|
|
425
|
+
"basicAuth": { "enabled": true, "passwdFile": "auth/.htpasswd", "realm": "capsium" },
|
|
426
|
+
"oauth2": { "enabled": true, "provider": "google", "clientId": "...",
|
|
427
|
+
"authorizationUrl": "...", "tokenUrl": "...", "userinfoUrl": "...",
|
|
428
|
+
"redirectPath": "/auth/callback", "scopes": ["openid", "email"] }
|
|
429
|
+
} }
|
|
430
|
+
----
|
|
142
431
|
|
|
143
|
-
|
|
432
|
+
When enabled, the reactor challenges every unauthenticated request
|
|
433
|
+
(`401`, with `WWW-Authenticate: Basic realm="..."` when basicAuth is
|
|
434
|
+
enabled) and verifies credentials against the htpasswd file. Supported
|
|
435
|
+
hashes: bcrypt (`htpasswd -B`, via the `bcrypt` gem), Apache apr1 MD5
|
|
436
|
+
and md5-crypt (`$apr1$`/`$1$`, pure Ruby), unsalted SHA-1 (`{SHA}`),
|
|
437
|
+
and a platform `crypt(3)` fallback for the rest.
|
|
144
438
|
|
|
145
|
-
|
|
439
|
+
With OAuth2, `/auth/login` redirects to the provider with HMAC-signed
|
|
440
|
+
state; the callback exchanges the code at `tokenUrl`, fetches the
|
|
441
|
+
userinfo claims and establishes an HMAC-SHA256 signed session cookie
|
|
442
|
+
(`capsium_session`, `HttpOnly; SameSite=Lax`). Provider errors during
|
|
443
|
+
the exchange answer `502`; a tampered state answers `401`.
|
|
444
|
+
|
|
445
|
+
Route-level `accessControl` on dataset routes is enforced after
|
|
446
|
+
authentication — `401` unauthenticated, `403` when the identity lacks a
|
|
447
|
+
required role:
|
|
448
|
+
|
|
449
|
+
[source,json]
|
|
450
|
+
----
|
|
451
|
+
{ "path": "/api/v1/data/animals", "dataset": "animals",
|
|
452
|
+
"accessControl": { "roles": ["admin"], "authenticationRequired": true } }
|
|
146
453
|
----
|
|
147
|
-
require 'capsium'
|
|
148
454
|
|
|
149
|
-
package
|
|
150
|
-
|
|
151
|
-
version: '0.1.0',
|
|
152
|
-
content: {
|
|
153
|
-
'example.html' => 'text/html',
|
|
154
|
-
'example.css' => 'text/css',
|
|
155
|
-
'example.js' => 'application/javascript'
|
|
156
|
-
},
|
|
157
|
-
data: {
|
|
158
|
-
'animals.yaml' => {
|
|
159
|
-
'animals' => [
|
|
160
|
-
{ 'name' => 'Lion', 'type' => 'Mammal', 'habitat' => 'Savannah' },
|
|
161
|
-
{ 'name' => 'Eagle', 'type' => 'Bird', 'habitat' => 'Mountains' },
|
|
162
|
-
{ 'name' => 'Shark', 'type' => 'Fish', 'habitat' => 'Ocean' }
|
|
163
|
-
]
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
metadata: {
|
|
167
|
-
'name' => 'sample_package',
|
|
168
|
-
'version' => '0.1.0',
|
|
169
|
-
'dependencies' => []
|
|
170
|
-
},
|
|
171
|
-
routes: {
|
|
172
|
-
'/' => 'example.html',
|
|
173
|
-
'/index' => 'index.html',
|
|
174
|
-
'/index.html' => 'index.html',
|
|
175
|
-
'/example.css' => 'example.css',
|
|
176
|
-
'/example.js' => 'example.js',
|
|
177
|
-
'/api/v1/data/animals' => { 'type' => 'dataset', 'name' => 'animals' }
|
|
178
|
-
},
|
|
179
|
-
storage: {
|
|
180
|
-
'datasets' => [
|
|
181
|
-
{
|
|
182
|
-
'name' => 'animals',
|
|
183
|
-
'source' => 'data/animals.yaml',
|
|
184
|
-
'format' => 'yaml',
|
|
185
|
-
'schema' => 'data/animals_schema.yaml'
|
|
186
|
-
}
|
|
187
|
-
]
|
|
188
|
-
}
|
|
189
|
-
)
|
|
455
|
+
Secrets NEVER come from the package. They live in `deploy.json`
|
|
456
|
+
(`--deploy` or `CAPSIUM_DEPLOY`):
|
|
190
457
|
|
|
191
|
-
|
|
192
|
-
|
|
458
|
+
[source,json]
|
|
459
|
+
----
|
|
460
|
+
{
|
|
461
|
+
"baseUrl": "http://localhost:8864",
|
|
462
|
+
"authentication": {
|
|
463
|
+
"basicAuth": { "passwdFile": "/secure/outside/package/.htpasswd" },
|
|
464
|
+
"oauth2": { "clientSecret": "..." },
|
|
465
|
+
"sessionSecret": "...",
|
|
466
|
+
"roles": { "alice": ["admin"] }
|
|
467
|
+
}
|
|
468
|
+
}
|
|
193
469
|
----
|
|
194
470
|
|
|
195
|
-
|
|
471
|
+
`roles` assigns roles by identity name (basic-auth username, OAuth2
|
|
472
|
+
email or subject); userinfo `roles` claims are honored too. Without a
|
|
473
|
+
configured `sessionSecret`, the reactor generates one and persists it
|
|
474
|
+
(mode `0600`) outside the package.
|
|
196
475
|
|
|
197
|
-
|
|
476
|
+
== Programmatically managing packages
|
|
477
|
+
|
|
478
|
+
The public API below ships with RBS signatures in `sig/`
|
|
479
|
+
(`bundle exec rbs -I sig validate`).
|
|
480
|
+
|
|
481
|
+
=== Loading packages
|
|
198
482
|
|
|
199
483
|
[source,ruby]
|
|
200
484
|
----
|
|
201
485
|
require 'capsium'
|
|
202
486
|
|
|
203
|
-
# Read
|
|
487
|
+
# Read a package directory or a .cap file
|
|
204
488
|
package = Capsium::Package.new(path)
|
|
205
489
|
|
|
206
|
-
#
|
|
207
|
-
|
|
490
|
+
# Read an encrypted .cap (raises Capsium::Package::Cipher::KeyRequiredError
|
|
491
|
+
# without a key, Capsium::Package::Cipher::DecryptionError for a wrong key
|
|
492
|
+
# or tampered ciphertext)
|
|
493
|
+
package = Capsium::Package.new('encrypted.cap', decryption_key: 'private.pem')
|
|
208
494
|
----
|
|
209
495
|
|
|
210
|
-
|
|
496
|
+
When `security.json` is present, the package is verified on load and
|
|
497
|
+
`Capsium::Package::Security::IntegrityError` is raised on mismatch.
|
|
211
498
|
|
|
212
|
-
|
|
499
|
+
=== Using packages in your program
|
|
213
500
|
|
|
214
501
|
[source,ruby]
|
|
215
502
|
----
|
|
216
503
|
# Accessing package metadata
|
|
217
|
-
puts "Package Name: #{package.metadata
|
|
218
|
-
puts "Package Version: #{package.metadata
|
|
504
|
+
puts "Package Name: #{package.metadata.name}"
|
|
505
|
+
puts "Package Version: #{package.metadata.version}"
|
|
219
506
|
|
|
220
|
-
# Accessing
|
|
221
|
-
package.
|
|
222
|
-
puts "
|
|
507
|
+
# Accessing manifest resources
|
|
508
|
+
package.manifest.resources.each do |path, resource|
|
|
509
|
+
puts "Resource: #{path}, Type: #{resource.type}"
|
|
223
510
|
end
|
|
224
511
|
|
|
225
|
-
# Accessing data
|
|
226
|
-
animals_data = package.data['animals.yaml']
|
|
227
|
-
puts "Animals Data: #{animals_data.inspect}"
|
|
228
|
-
|
|
229
512
|
# Accessing routes
|
|
230
|
-
package.routes.
|
|
231
|
-
|
|
513
|
+
route = package.routes.resolve('/')
|
|
514
|
+
puts route.resource
|
|
515
|
+
|
|
516
|
+
# Accessing datasets
|
|
517
|
+
animals = package.storage.dataset('animals')
|
|
518
|
+
puts animals.data.inspect
|
|
519
|
+
|
|
520
|
+
# Verifying integrity (returns a list of typed errors, empty when valid)
|
|
521
|
+
errors = package.verify_integrity
|
|
522
|
+
|
|
523
|
+
# Digital signatures (RSA-SHA256)
|
|
524
|
+
package.signed? # => security.json declares digitalSignatures
|
|
525
|
+
package.verify_signature # => true/false
|
|
526
|
+
|
|
527
|
+
signer = Capsium::Package::Signer.new(package_dir)
|
|
528
|
+
signer.sign('private.pem') # or sign('private.pem', 'cert.pem')
|
|
529
|
+
signer.verify # embedded public key
|
|
530
|
+
signer.verify('cert-or-public-key.pem') # explicit key
|
|
531
|
+
signer.verify! # raises SignatureMismatchError
|
|
532
|
+
|
|
533
|
+
# Whole-package encryption (AES-256-GCM, RSA-OAEP-SHA256 wrapped DEK)
|
|
534
|
+
cipher = Capsium::Package::Cipher.new
|
|
535
|
+
cipher.encrypt('pkg-1.0.0.cap', 'public.pem', 'encrypted.cap')
|
|
536
|
+
cipher.decrypt('encrypted.cap', 'private.pem', 'decrypted.cap')
|
|
537
|
+
Capsium::Package::Cipher.encrypted?('encrypted.cap') # => true
|
|
538
|
+
|
|
539
|
+
# Running the package's tests/*.yaml suite (05x-testing DSL)
|
|
540
|
+
report = Capsium::Package::Testing::TestSuite.new(package).run
|
|
541
|
+
report.ok? # => true/false
|
|
542
|
+
report.failures # => failed TestCase::Result list
|
|
543
|
+
report.summary # => "7 tests, 0 failures"
|
|
544
|
+
|
|
545
|
+
# Layered storage (5a): the merged overlay view shared with the reactor
|
|
546
|
+
view = package.merged_view
|
|
547
|
+
view.resolve('content/app.js') # => absolute path of the topmost hit, or nil
|
|
548
|
+
|
|
549
|
+
# Composite packages (4a): resolve metadata.dependencies against a store
|
|
550
|
+
package = Capsium::Package.new(dir, store: './store') # or CAPSIUM_STORE
|
|
551
|
+
package.resolved_dependencies.each do |dep|
|
|
552
|
+
puts "#{dep.guid} (#{dep.range}) => #{dep.version} [#{dep.path}]"
|
|
232
553
|
end
|
|
554
|
+
view = package.merged_view # own layers over dependency layers
|
|
555
|
+
view = package.merged_view(exported_only: true) # what dependents may see
|
|
233
556
|
|
|
234
|
-
#
|
|
235
|
-
package
|
|
236
|
-
|
|
237
|
-
|
|
557
|
+
# Serving with authentication and a store
|
|
558
|
+
reactor = Capsium::Reactor.new(package: dir, store: './store',
|
|
559
|
+
deploy: 'deploy.json')
|
|
560
|
+
reactor.serve
|
|
561
|
+
----
|
|
562
|
+
|
|
563
|
+
=== Packing and unpacking programmatically
|
|
564
|
+
|
|
565
|
+
[source,ruby]
|
|
566
|
+
----
|
|
567
|
+
packager = Capsium::Packager.new
|
|
568
|
+
cap_file = packager.pack(package, force: true)
|
|
569
|
+
packager.unpack(cap_file, 'output-directory')
|
|
570
|
+
----
|
|
571
|
+
|
|
572
|
+
== Building the mn-samples-iso cap package
|
|
573
|
+
|
|
574
|
+
Download the mn-samples-iso built site: https://github.com/metanorma/mn-samples-iso/actions/runs/8862815829/artifacts/1453746303[mn-samples-iso-Linux].
|
|
575
|
+
|
|
576
|
+
Then run these commands:
|
|
577
|
+
|
|
578
|
+
[source,bash]
|
|
579
|
+
----
|
|
580
|
+
$ unzip mn-samples-iso-Linux.zip
|
|
581
|
+
$ cd mn-samples-iso-Linux
|
|
582
|
+
$ mkdir content
|
|
583
|
+
$ mv index.html documents.xml documents content
|
|
584
|
+
$ cat > metadata.json <<'JSON'
|
|
585
|
+
{
|
|
586
|
+
"name": "mn-samples-iso",
|
|
587
|
+
"version": "0.1.0",
|
|
588
|
+
"description": "Metanorma ISO sample documents",
|
|
589
|
+
"guid": "https://github.com/metanorma/mn-samples-iso",
|
|
590
|
+
"uuid": "123e4567-e89b-12d3-a456-426614174000"
|
|
591
|
+
}
|
|
592
|
+
JSON
|
|
593
|
+
$ cd ..
|
|
594
|
+
$ bundle exec capsium package pack -f mn-samples-iso-Linux
|
|
595
|
+
Package created: mn-samples-iso-0.1.0.cap
|
|
596
|
+
$ bundle exec capsium reactor serve mn-samples-iso-0.1.0.cap
|
|
597
|
+
Starting server on http://localhost:8864
|
|
598
|
+
...
|
|
238
599
|
----
|
|
239
600
|
|
|
240
601
|
== Contributing
|
|
@@ -247,7 +608,16 @@ To run the tests, use the following command:
|
|
|
247
608
|
|
|
248
609
|
[source,bash]
|
|
249
610
|
----
|
|
250
|
-
rspec
|
|
611
|
+
bundle exec rspec
|
|
612
|
+
----
|
|
613
|
+
|
|
614
|
+
The linter and the RBS signature validation are part of the default rake
|
|
615
|
+
task (`bundle exec rake`), or run them individually:
|
|
616
|
+
|
|
617
|
+
[source,bash]
|
|
618
|
+
----
|
|
619
|
+
bundle exec rubocop
|
|
620
|
+
bundle exec rbs -I sig validate
|
|
251
621
|
----
|
|
252
622
|
|
|
253
623
|
== License
|
|
@@ -255,4 +625,3 @@ rspec
|
|
|
255
625
|
Copyright Ribose.
|
|
256
626
|
|
|
257
627
|
Capsium is released under the MIT License. See the LICENSE file for more details.
|
|
258
|
-
|