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
data/README.adoc
CHANGED
|
@@ -42,6 +42,23 @@ Alternatively, you can install the gem directly using:
|
|
|
42
42
|
gem install capsium
|
|
43
43
|
----
|
|
44
44
|
|
|
45
|
+
=== OpenPGP support (librnp)
|
|
46
|
+
|
|
47
|
+
OpenPGP signatures and encryption are provided through the `rnp` gem
|
|
48
|
+
(a declared dependency) bound to the *librnp* shared library, which
|
|
49
|
+
must be installed separately:
|
|
50
|
+
|
|
51
|
+
[source,bash]
|
|
52
|
+
----
|
|
53
|
+
brew install rnp # homebrew-core formula, provides librnp
|
|
54
|
+
# or build librnp from source: https://github.com/rnpgp/rnp
|
|
55
|
+
----
|
|
56
|
+
|
|
57
|
+
The binding is loaded lazily, only when an OpenPGP feature is used, so
|
|
58
|
+
the gem works without librnp until then; OpenPGP commands raise a
|
|
59
|
+
typed `Capsium::Package::OpenPgp::OpenPgpUnavailableError` with
|
|
60
|
+
installation guidance when the library is missing.
|
|
61
|
+
|
|
45
62
|
== What is a Capsium package?
|
|
46
63
|
|
|
47
64
|
A Capsium package is a structured collection of content, data, metadata, and
|
|
@@ -55,6 +72,7 @@ manifest.json # auto-generated at load/pack time when absent
|
|
|
55
72
|
routes.json # auto-generated when absent
|
|
56
73
|
storage.json # optional (only when datasets exist)
|
|
57
74
|
security.json # generated at pack time (SHA-256 checksums)
|
|
75
|
+
authentication.json # optional (user authentication)
|
|
58
76
|
content/ # served content root (URL space mirrors paths below it)
|
|
59
77
|
data/ # datasets (optional)
|
|
60
78
|
----
|
|
@@ -73,9 +91,15 @@ Paths inside the configuration files are package-relative POSIX paths.
|
|
|
73
91
|
full filename); the index HTML additionally gets `/`.
|
|
74
92
|
* **storage.json**: dataset definitions under `storage.dataSets`, either
|
|
75
93
|
schema-backed files (`source`, `schemaFile`, `schemaType: json-schema`)
|
|
76
|
-
or SQLite (`databaseFile`, `table`).
|
|
94
|
+
or SQLite (`databaseFile`, `table`). `storage.layers` declares overlay
|
|
95
|
+
layers (see "Layered storage" below).
|
|
77
96
|
* **security.json**: `security.integrityChecks.checksums` holds a SHA-256 hex
|
|
78
|
-
digest for every file in the package except `security.json` itself
|
|
97
|
+
digest for every file in the package except `security.json` itself and
|
|
98
|
+
`signature.sig`. When the package is signed, `security.digitalSignatures`
|
|
99
|
+
records `{ "publicKey", "signatureFile" }`.
|
|
100
|
+
* **authentication.json**: optional user authentication (see
|
|
101
|
+
"Authentication" below): `authentication.basicAuth` and/or
|
|
102
|
+
`authentication.oauth2`.
|
|
79
103
|
|
|
80
104
|
Legacy (pre-0.2) configuration files are still accepted on read and
|
|
81
105
|
normalized to the canonical forms; writers emit only the canonical forms.
|
|
@@ -98,7 +122,7 @@ Capsium provides a CLI to help you pack and manage your packages.
|
|
|
98
122
|
|
|
99
123
|
[source,bash]
|
|
100
124
|
----
|
|
101
|
-
capsium package pack [--force/-f] path-to-package
|
|
125
|
+
capsium package pack [--force/-f] [--bundle-deps/--bundle] [--store DIR] [--registry REF] path-to-package
|
|
102
126
|
----
|
|
103
127
|
|
|
104
128
|
This command generates `manifest.json`/`routes.json` when absent, writes
|
|
@@ -106,6 +130,15 @@ This command generates `manifest.json`/`routes.json` when absent, writes
|
|
|
106
130
|
directory into `{package-name}-{package-version}.cap` (name and version from
|
|
107
131
|
`metadata.json`).
|
|
108
132
|
|
|
133
|
+
With `--bundle-deps` (alias `--bundle`) a package that declares
|
|
134
|
+
`metadata.dependencies` is packed as an *encapsulated* package: every
|
|
135
|
+
declared dependency is resolved through the store -> registry chain
|
|
136
|
+
(`--store`/`CAPSIUM_STORE`, `--registry`/`CAPSIUM_REGISTRY`) and embedded
|
|
137
|
+
under `packages/`, so the resulting `.cap` activates with no store or
|
|
138
|
+
registry at all (see <<_encapsulated_packages_bundled_dependencies>>).
|
|
139
|
+
A declared dependency that cannot be resolved aborts the pack with a
|
|
140
|
+
typed dependency error.
|
|
141
|
+
|
|
109
142
|
.Sample `pack` command
|
|
110
143
|
====
|
|
111
144
|
[source,bash]
|
|
@@ -151,20 +184,326 @@ Runs a per-check report and exits with status 1 on any failure:
|
|
|
151
184
|
|
|
152
185
|
[source,bash]
|
|
153
186
|
----
|
|
154
|
-
capsium package info|manifest|routes|metadata|storage path-to-package
|
|
187
|
+
capsium package info|manifest|routes|metadata|storage path-to-package [--store DIR]
|
|
188
|
+
----
|
|
189
|
+
|
|
190
|
+
`info` additionally prints the resolved dependency tree for composite
|
|
191
|
+
packages (declared GUID and range, resolved version, store `.cap`), see
|
|
192
|
+
"Composite packages" below.
|
|
193
|
+
|
|
194
|
+
=== Signing a package
|
|
195
|
+
|
|
196
|
+
[source,bash]
|
|
197
|
+
----
|
|
198
|
+
capsium package sign path-to-package-or-cap --key private.pem [--cert cert.pem]
|
|
199
|
+
----
|
|
200
|
+
|
|
201
|
+
Signs the package with an RSA private key (minimum 2048 bits) using
|
|
202
|
+
RSA-SHA256, per the packaging standard's digital signature clause. Signing
|
|
203
|
+
is a post-pack step: `pack` drops signing artifacts, so sign the packed
|
|
204
|
+
directory or the `.cap` file itself.
|
|
205
|
+
|
|
206
|
+
The signed payload is the concatenation, in sorted package-relative path
|
|
207
|
+
order, of the raw bytes of every file covered by the `security.json`
|
|
208
|
+
integrity checksums — i.e. every package file except `security.json` and
|
|
209
|
+
`signature.sig`. The signature over that payload is stored as raw bytes in
|
|
210
|
+
`signature.sig`, the signer's public key PEM is embedded as
|
|
211
|
+
`signature.pub.pem`, and `security.json` records:
|
|
212
|
+
|
|
213
|
+
[source,json]
|
|
214
|
+
----
|
|
215
|
+
{
|
|
216
|
+
"security": {
|
|
217
|
+
"digitalSignatures": {
|
|
218
|
+
"publicKey": "signature.pub.pem",
|
|
219
|
+
"signatureFile": "signature.sig"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
----
|
|
224
|
+
|
|
225
|
+
When `--cert` is given, the X.509 certificate must match the private key,
|
|
226
|
+
and the certificate's public key is embedded instead.
|
|
227
|
+
|
|
228
|
+
The payload construction is openssl-compatible; to verify a signed package
|
|
229
|
+
independently, concatenate the checksum-covered files in sorted order and
|
|
230
|
+
run:
|
|
231
|
+
|
|
232
|
+
[source,bash]
|
|
233
|
+
----
|
|
234
|
+
openssl dgst -sha256 -verify signature.pub.pem -signature signature.sig payload.bin
|
|
235
|
+
----
|
|
236
|
+
|
|
237
|
+
==== OpenPGP signatures
|
|
238
|
+
|
|
239
|
+
[source,bash]
|
|
240
|
+
----
|
|
241
|
+
capsium package sign path-to-package-or-cap --openpgp --key secret.asc
|
|
242
|
+
----
|
|
243
|
+
|
|
244
|
+
With `--openpgp`, `--key` is an OpenPGP secret key file (armored or
|
|
245
|
+
binary, auto-detected) and the package is signed through librnp with the
|
|
246
|
+
same canonical payload: `signature.sig` holds the armored detached
|
|
247
|
+
OpenPGP signature (SHA-256), the armored public key is embedded as
|
|
248
|
+
`signature.pub.asc`, and `security.json` records the scheme:
|
|
249
|
+
|
|
250
|
+
[source,json]
|
|
251
|
+
----
|
|
252
|
+
{
|
|
253
|
+
"security": {
|
|
254
|
+
"digitalSignatures": {
|
|
255
|
+
"certificateType": "OpenPGP",
|
|
256
|
+
"publicKey": "signature.pub.asc",
|
|
257
|
+
"signatureFile": "signature.sig"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
----
|
|
262
|
+
|
|
263
|
+
=== Verifying a package signature
|
|
264
|
+
|
|
265
|
+
[source,bash]
|
|
266
|
+
----
|
|
267
|
+
capsium package verify-signature path-to-package-or-cap [--cert cert-or-public-key] [--openpgp]
|
|
268
|
+
----
|
|
269
|
+
|
|
270
|
+
Verifies the declared signature against the package contents (using the
|
|
271
|
+
embedded public key, or the given certificate/public key) and exits with
|
|
272
|
+
status 1 when the package is unsigned or the signature does not match.
|
|
273
|
+
The signature scheme is auto-detected from the declared
|
|
274
|
+
`certificateType` (RSA-SHA256/X.509 by default, OpenPGP when recorded);
|
|
275
|
+
`--openpgp` forces OpenPGP verification with an OpenPGP public key.
|
|
276
|
+
Packages that declare a signature are also verified automatically on load —
|
|
277
|
+
a mismatch raises `Capsium::Package::Signer::SignatureMismatchError`, so a
|
|
278
|
+
tampered signed package cannot be served by the reactor.
|
|
279
|
+
|
|
280
|
+
=== Encrypting a package
|
|
281
|
+
|
|
282
|
+
[source,bash]
|
|
283
|
+
----
|
|
284
|
+
capsium package encrypt path-to-package-or-cap --public-key public.pem -o encrypted.cap
|
|
285
|
+
----
|
|
286
|
+
|
|
287
|
+
Encrypts the whole package for the recipient's RSA public key (or X.509
|
|
288
|
+
certificate), per the packaging standard's encryption clause: the
|
|
289
|
+
encrypted `.cap` is a zip containing `metadata.json` (cleartext, so
|
|
290
|
+
name/version stay readable), `signature.json` (the encryption envelope)
|
|
291
|
+
and `package.enc` — the AES-256-GCM ciphertext of the inner plaintext
|
|
292
|
+
`.cap` zip (content, configuration files, data). A random 256-bit data
|
|
293
|
+
encryption key (DEK) encrypts the inner zip and is wrapped with the
|
|
294
|
+
recipient's public key using RSA-OAEP with SHA-256:
|
|
295
|
+
|
|
296
|
+
[source,json]
|
|
297
|
+
----
|
|
298
|
+
{
|
|
299
|
+
"encryption": {
|
|
300
|
+
"algorithm": "AES-256-GCM",
|
|
301
|
+
"keyManagement": "RSA-OAEP-SHA256",
|
|
302
|
+
"encryptedDek": "<base64>",
|
|
303
|
+
"iv": "<base64>",
|
|
304
|
+
"authTag": "<base64>"
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
----
|
|
308
|
+
|
|
309
|
+
==== OpenPGP encryption
|
|
310
|
+
|
|
311
|
+
[source,bash]
|
|
312
|
+
----
|
|
313
|
+
capsium package encrypt path-to-package-or-cap --openpgp --recipient public.asc -o encrypted.cap
|
|
314
|
+
----
|
|
315
|
+
|
|
316
|
+
With `--openpgp`, the DEK is protected as an armored OpenPGP message to
|
|
317
|
+
the recipient's public key (through librnp) instead of the RSA-OAEP
|
|
318
|
+
wrap; the content encryption and layout are identical, and the
|
|
319
|
+
envelope records the key management:
|
|
320
|
+
|
|
321
|
+
[source,json]
|
|
322
|
+
----
|
|
323
|
+
{
|
|
324
|
+
"encryption": {
|
|
325
|
+
"algorithm": "AES-256-GCM",
|
|
326
|
+
"keyManagement": "OpenPGP",
|
|
327
|
+
"message": "<armored OpenPGP message containing the DEK>",
|
|
328
|
+
"iv": "<base64>",
|
|
329
|
+
"authTag": "<base64>"
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
----
|
|
333
|
+
|
|
334
|
+
The OCB alternative mentioned by the standard remains out of scope.
|
|
335
|
+
|
|
336
|
+
=== Decrypting a package
|
|
337
|
+
|
|
338
|
+
[source,bash]
|
|
339
|
+
----
|
|
340
|
+
capsium package decrypt encrypted.cap [--private-key private.pem | --key secret.asc] [--openpgp] [-o decrypted.cap]
|
|
341
|
+
----
|
|
342
|
+
|
|
343
|
+
Decrypts an encrypted package with the recipient's private key and
|
|
344
|
+
writes the plaintext `.cap` (default output: `<name>-decrypted.cap`).
|
|
345
|
+
The key management is auto-detected from the envelope — `--private-key`
|
|
346
|
+
for RSA-OAEP, `--key` for an OpenPGP secret key (armored or binary);
|
|
347
|
+
`--openpgp` forces the OpenPGP cipher. Decryption fails with a typed
|
|
348
|
+
error when the key does not match or the ciphertext was tampered with
|
|
349
|
+
(GCM authentication). Loading an encrypted package without a key raises
|
|
350
|
+
`Capsium::Package::Cipher::KeyRequiredError` — the reactor refuses to
|
|
351
|
+
serve it. `Capsium::Package.new(path, decryption_key:)` accepts either
|
|
352
|
+
key format transparently.
|
|
353
|
+
|
|
354
|
+
=== Testing a package
|
|
355
|
+
|
|
356
|
+
[source,bash]
|
|
357
|
+
----
|
|
358
|
+
capsium package test path-to-package-or-cap
|
|
359
|
+
----
|
|
360
|
+
|
|
361
|
+
Runs the package's test suite declared in the Capsium testing YAML DSL
|
|
362
|
+
(05x-testing): all `tests/*.yaml` files in the package, each holding a
|
|
363
|
+
top-level `tests` list. Every test has a `name` and a `type`; the four
|
|
364
|
+
supported types:
|
|
365
|
+
|
|
366
|
+
* `route` — requests the URL path from a reactor started for the run and
|
|
367
|
+
checks `expected_status`, plus optional `response_contains` (body
|
|
368
|
+
substring) and `expected_content_type`. Absolute URLs are accepted;
|
|
369
|
+
only their path (and query) is used.
|
|
370
|
+
* `file` — the file at `path` must exist in the package; optional
|
|
371
|
+
`contains` checks a content substring.
|
|
372
|
+
* `data_validation` — the rows of `data_file` (format `json` or `yaml`)
|
|
373
|
+
must validate against the JSON schema at `schema_file`. Array data
|
|
374
|
+
validates row by row.
|
|
375
|
+
* `config` — `config_file` (format `json` or `yaml`) must exist and
|
|
376
|
+
parse; the known package configs (`metadata.json`, `manifest.json`,
|
|
377
|
+
`routes.json`, `storage.json`, `security.json`) are additionally
|
|
378
|
+
validated against their canonical models.
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
|
|
382
|
+
[source,yaml]
|
|
383
|
+
----
|
|
384
|
+
tests:
|
|
385
|
+
- name: Home Route Test
|
|
386
|
+
type: route
|
|
387
|
+
url: "/home"
|
|
388
|
+
expected_status: 200
|
|
389
|
+
response_contains: "Welcome"
|
|
390
|
+
|
|
391
|
+
- name: Config File Exists
|
|
392
|
+
type: file
|
|
393
|
+
path: "metadata.json"
|
|
394
|
+
|
|
395
|
+
- name: JSON Data Validation
|
|
396
|
+
type: data_validation
|
|
397
|
+
format: json
|
|
398
|
+
data_file: "data/animals.json"
|
|
399
|
+
schema_file: "data/animals.schema.json"
|
|
400
|
+
|
|
401
|
+
- name: Metadata Config Validation
|
|
402
|
+
type: config
|
|
403
|
+
format: json
|
|
404
|
+
config_file: "metadata.json"
|
|
155
405
|
----
|
|
156
406
|
|
|
407
|
+
Each test is reported as `PASS`/`FAIL` with messages; the command exits
|
|
408
|
+
with status 1 when any test fails (or a test definition is invalid).
|
|
409
|
+
|
|
157
410
|
== CLI: Reactor
|
|
158
411
|
|
|
159
412
|
=== Starting a reactor on your package
|
|
160
413
|
|
|
161
414
|
[source,bash]
|
|
162
415
|
----
|
|
163
|
-
capsium reactor serve my_package.cap [--port 8864]
|
|
416
|
+
capsium reactor serve my_package.cap [--port 8864] [--store DIR] [--deploy deploy.json] [--workdir DIR]
|
|
417
|
+
capsium reactor serve capsium://example.com/story [--registry DIR_OR_URL] [--store DIR] [--constraint ">=1.0"]
|
|
418
|
+
----
|
|
419
|
+
|
|
420
|
+
The reactor takes a local package directory or `.cap` file, or a
|
|
421
|
+
`capsium://` GUID which is installed from a registry first (see
|
|
422
|
+
"Static registries" below). `--store` (or `CAPSIUM_STORE`)
|
|
423
|
+
resolves composite-package dependencies; `--deploy` (or
|
|
424
|
+
`CAPSIUM_DEPLOY`) points at the reactor-side `deploy.json` with
|
|
425
|
+
authentication secrets (see "Authentication" below); `--registry`
|
|
426
|
+
(or `CAPSIUM_REGISTRY`) supplies the registry for `capsium://` GUIDs
|
|
427
|
+
and for dependency fallback; `--workdir` holds the writable overlays
|
|
428
|
+
and saved packages (default: a temporary directory — see "Writable
|
|
429
|
+
packages" below).
|
|
430
|
+
|
|
431
|
+
=== Serving multiple packages
|
|
432
|
+
|
|
433
|
+
`capsium reactor serve` accepts multiple sources at once: positional
|
|
434
|
+
arguments, repeatable `--mount PATH=SOURCE` options, a JSON mount
|
|
435
|
+
config via `--config`, or any combination:
|
|
436
|
+
|
|
437
|
+
[source,bash]
|
|
438
|
+
----
|
|
439
|
+
capsium reactor serve first.cap second.cap
|
|
440
|
+
capsium reactor serve --mount /=first.cap --mount /api=second.cap
|
|
441
|
+
capsium reactor serve --config mounts.json # {"mounts": [{"path": "/", "source": "first.cap", "store": "..."}]}
|
|
442
|
+
----
|
|
443
|
+
|
|
444
|
+
Default mount points: the first source mounts at `/`, each additional
|
|
445
|
+
source at `/<metadata.name>/`. Requests dispatch to mounts by
|
|
446
|
+
longest-prefix matching, and every package's routes answer below its
|
|
447
|
+
prefix (a package mounted at `/api` serves its `/index.html` route at
|
|
448
|
+
`/api/index.html`). Two mounts claiming the same prefix fail with a
|
|
449
|
+
`MountConflictError`. The introspection endpoints aggregate ALL
|
|
450
|
+
mounted packages, `/package/<name>/...` resolves by name (404 for
|
|
451
|
+
unknown names), and metrics/logs stay reactor-global. On shutdown the
|
|
452
|
+
reactor cleans up every mounted package.
|
|
453
|
+
|
|
454
|
+
=== Writable packages
|
|
455
|
+
|
|
456
|
+
A mounted package whose metadata does not declare `"readOnly": true`
|
|
457
|
+
is writable: the reactor keeps an append-only overlay layer for it in
|
|
458
|
+
the workdir (`--workdir DIR`, default a temporary directory) which is
|
|
459
|
+
always the topmost layer of the merged view (ARCHITECTURE.md section
|
|
460
|
+
5a). The immutable `.cap` never changes on disk, and every write is
|
|
461
|
+
visible on the next request (hot-swap — no restart needed). Overlay
|
|
462
|
+
state persists in the workdir, so a later reactor over the same
|
|
463
|
+
workdir sees it again.
|
|
464
|
+
|
|
465
|
+
Dataset CRUD (JSON) under the dataset's route:
|
|
466
|
+
|
|
467
|
+
[source]
|
|
164
468
|
----
|
|
469
|
+
POST /api/v1/data/<dataset> # append an item => 201 + Location + stored item
|
|
470
|
+
GET /api/v1/data/<dataset>/<id> # one item => 200 (404 when absent)
|
|
471
|
+
PUT /api/v1/data/<dataset>/<id> # replace an item => 200 (404 absent, 422 schema fail)
|
|
472
|
+
DELETE /api/v1/data/<dataset>/<id> # delete an item => 204 (404 absent)
|
|
473
|
+
GET /api/v1/data/<dataset> # the merged collection (as before)
|
|
474
|
+
----
|
|
475
|
+
|
|
476
|
+
Item ids follow the `id` field when present, else the 1-based index as
|
|
477
|
+
a string; a duplicate `id` on POST (or a mismatched one on PUT) is a
|
|
478
|
+
`409`. The request body must satisfy the dataset's JSON schema when
|
|
479
|
+
one is declared (violations are `422` with the schema errors);
|
|
480
|
+
malformed JSON is `400`. Writes on a `"readOnly": true` package are
|
|
481
|
+
`403` with a clear body; writes on a SQLite dataset are `501`;
|
|
482
|
+
wrong verbs are `405`. Mutations persist as a per-dataset JSON
|
|
483
|
+
operation log in the overlay.
|
|
165
484
|
|
|
166
|
-
|
|
167
|
-
|
|
485
|
+
Content writes work on any route path (text bodies for v1):
|
|
486
|
+
|
|
487
|
+
[source]
|
|
488
|
+
----
|
|
489
|
+
PUT /<route> # create/overwrite a content file (route created on demand) => 200
|
|
490
|
+
DELETE /<route> # tombstone: the path 404s even if a lower layer has it => 204
|
|
491
|
+
----
|
|
492
|
+
|
|
493
|
+
Every mounted package with datasets also answers GraphQL at
|
|
494
|
+
`<mount>/graphql` (POST, or GET with `?query=`): the schema is derived
|
|
495
|
+
from the package's datasets — a query field `<dataset>` (list) with an
|
|
496
|
+
optional `id:` argument (single item) plus `create<Dataset>`,
|
|
497
|
+
`update<Dataset>` and `delete<Dataset>` mutations matching the REST
|
|
498
|
+
semantics. Item types are inferred from the dataset's JSON schema when
|
|
499
|
+
present, else map to a permissive JSON scalar; SQLite datasets are
|
|
500
|
+
skipped. Not-found items, schema violations and read-only mutations
|
|
501
|
+
land in the GraphQL `errors` array (never a 500).
|
|
502
|
+
|
|
503
|
+
`POST /package/<name>/save` folds base plus overlays into a NEW
|
|
504
|
+
versioned `.cap` (`<name>-<version+patch>.cap`) in the workdir and
|
|
505
|
+
returns its path and SHA-256; the saved package passes
|
|
506
|
+
`capsium package validate`.
|
|
168
507
|
|
|
169
508
|
=== Reactor introspection API
|
|
170
509
|
|
|
@@ -189,6 +528,251 @@ content checksums — the same data `security.json` integrityChecks carry.
|
|
|
189
528
|
`content-validity` re-verifies the package against `security.json` on
|
|
190
529
|
every request and reports the outcome with a UTC `lastChecked`
|
|
191
530
|
timestamp; `reason` lists the integrity errors when `valid` is false.
|
|
531
|
+
The entry also reports `signed` and `encrypted` status, plus
|
|
532
|
+
`signatureValid` when the package declares a signature.
|
|
533
|
+
|
|
534
|
+
The reactor also answers reactor-level and per-package introspection
|
|
535
|
+
endpoints (07-reactor follow-ons), likewise GET-only JSON:
|
|
536
|
+
|
|
537
|
+
[source]
|
|
538
|
+
----
|
|
539
|
+
GET /introspect/status # => {"status": "running", "uptime": <seconds>, "packagesLoaded": 1}
|
|
540
|
+
GET /introspect/config # => {"port", "storeDir", "cacheControl", "authEnabled", "registry"}
|
|
541
|
+
GET /introspect/metrics # => {"uptime", "requestsTotal", "requestsByStatus": {"200": n, ...}}
|
|
542
|
+
GET /package/<name>/status # => {"package", "version", "status": "loaded", "valid": true}
|
|
543
|
+
GET /package/<name>/metadata # => {"name", "version", "description", "author", "guid"}
|
|
544
|
+
GET /package/<name>/logs[?lines=N] # => {"package", "logs": ["<utc> GET / -> 200", ...]}
|
|
545
|
+
----
|
|
546
|
+
|
|
547
|
+
`/introspect/config` never exposes secrets (deploy.json values, or
|
|
548
|
+
credentials embedded in a registry URL, are redacted). With multiple
|
|
549
|
+
mounted packages the `/api/v1/introspect/*` endpoints aggregate all of
|
|
550
|
+
them, and `/package/<name>/...` resolves by name (404 for unknown
|
|
551
|
+
names). `logs` returns the last N lines (default 100) from a small
|
|
552
|
+
in-memory ring buffer (`Capsium::LogBuffer`) recording key serving
|
|
553
|
+
events; `metrics` counts requests by status in memory
|
|
554
|
+
(`Capsium::Reactor::Metrics`). When authentication is enabled these
|
|
555
|
+
endpoints are gated like any other route.
|
|
556
|
+
|
|
557
|
+
== Layered storage
|
|
558
|
+
|
|
559
|
+
`storage.layers` stacks overlay directories over `content/` (bottom ->
|
|
560
|
+
top in declaration order), each mirroring the `content/` tree:
|
|
561
|
+
|
|
562
|
+
[source,json]
|
|
563
|
+
----
|
|
564
|
+
{ "storage": { "layers": [
|
|
565
|
+
{ "path": "base", "writable": false, "visibility": "exported" },
|
|
566
|
+
{ "path": "updates", "writable": true, "visibility": "private" }
|
|
567
|
+
] } }
|
|
568
|
+
----
|
|
569
|
+
|
|
570
|
+
A request resolves against layers from the TOP down; the first hit
|
|
571
|
+
wins. Deletions are recorded as tombstones: a `.capsium-tombstones`
|
|
572
|
+
JSON file (an array of `content/`-relative paths) in a writable layer;
|
|
573
|
+
a tombstoned path resolves 404 even when a lower layer holds the file,
|
|
574
|
+
while a file reappearing above the tombstone is served again. Packages
|
|
575
|
+
without a `layers` config behave exactly as before (single implicit
|
|
576
|
+
`content/` layer). `visibility: private` layers are hidden from
|
|
577
|
+
dependent packages (see below), as are resources whose manifest
|
|
578
|
+
visibility is `private`.
|
|
579
|
+
|
|
580
|
+
== Composite packages
|
|
581
|
+
|
|
582
|
+
`metadata.dependencies` maps a dependency GUID to a semver range
|
|
583
|
+
(`>=1.0.0`, `^1.2.3`, `~1.2.3`, exact, `*`, `1.x`/`1.2.x`, partials,
|
|
584
|
+
and comma/space conjunctions like `>=1.0.0, <2.0.0`). Dependencies
|
|
585
|
+
resolve against a *package store* — a directory of
|
|
586
|
+
`<name>-<version>.cap` files plus an optional `index.json` (GUID ->
|
|
587
|
+
file) — given via `CAPSIUM_STORE` or `--store`; the newest satisfying
|
|
588
|
+
version wins.
|
|
589
|
+
|
|
590
|
+
On load, each resolved dependency's exported content becomes read-only
|
|
591
|
+
layers below all of the dependent's own layers. Routes may address
|
|
592
|
+
dependency content explicitly (`"resource": "<dependency-guid>/content/app.js"`)
|
|
593
|
+
or declare route-inheritance attributes per 05x-routing: `remap`
|
|
594
|
+
(replaces the serving path), `responseRewrite` (`body`, `headers`),
|
|
595
|
+
`responseHeaders` (merged over served headers) and `requestHeaders`
|
|
596
|
+
(parsed and exposed for forwarding reactors; this reactor serves
|
|
597
|
+
statically, so they do not alter its responses). Referencing a
|
|
598
|
+
dependency's private or missing resource is a load-time error, as are
|
|
599
|
+
circular, missing or unsatisfiable dependencies.
|
|
600
|
+
|
|
601
|
+
[source,bash]
|
|
602
|
+
----
|
|
603
|
+
capsium package info my-composite-package --store ./store
|
|
604
|
+
capsium reactor serve my-composite-package --store ./store
|
|
605
|
+
----
|
|
606
|
+
|
|
607
|
+
[[_encapsulated_packages_bundled_dependencies]]
|
|
608
|
+
== Encapsulated packages (bundled dependencies)
|
|
609
|
+
|
|
610
|
+
`capsium package pack --bundle-deps` (alias `--bundle`) produces a
|
|
611
|
+
*self-contained* package: the resolved dependency `.cap` files are
|
|
612
|
+
embedded inside the parent, so the whole tree activates with no store
|
|
613
|
+
and no registry. Layout inside the `.cap`:
|
|
614
|
+
|
|
615
|
+
----
|
|
616
|
+
packages/
|
|
617
|
+
index.json # bundled-dependencies manifest
|
|
618
|
+
<name>-<version>.cap # one per declared dependency
|
|
619
|
+
----
|
|
620
|
+
|
|
621
|
+
`packages/index.json` maps each dependency GUID to its embedded file,
|
|
622
|
+
the resolved version and the SHA-256 of the embedded `.cap`:
|
|
623
|
+
|
|
624
|
+
[source,json]
|
|
625
|
+
----
|
|
626
|
+
{
|
|
627
|
+
"https://example.com/capsiums/base-package": {
|
|
628
|
+
"file": "packages/base-package-1.2.0.cap",
|
|
629
|
+
"version": "1.2.0",
|
|
630
|
+
"sha256": "<hex sha256 of the .cap>"
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
----
|
|
634
|
+
|
|
635
|
+
Resolution order on load is *bundle first*: a dependency GUID found in
|
|
636
|
+
`packages/index.json` resolves to the embedded `.cap` (this also works
|
|
637
|
+
when the parent itself was loaded from a `.cap` — the embedded files
|
|
638
|
+
are read out of the parent's extraction); anything not bundled falls
|
|
639
|
+
back to the usual store -> registry chain. The bundle is passed down
|
|
640
|
+
to the dependencies' own resolution, so a dependency's dependency
|
|
641
|
+
resolves from the parent's bundle when the parent re-declares it.
|
|
642
|
+
|
|
643
|
+
Bundling follows a *one-level policy*: only the dependencies declared
|
|
644
|
+
in the parent's own `metadata.dependencies` are embedded. To make a
|
|
645
|
+
whole tree self-contained, the parent's `metadata.dependencies` must
|
|
646
|
+
list the transitive closure; a bundled dependency whose own
|
|
647
|
+
dependencies are not re-declared by the parent still needs a store or
|
|
648
|
+
registry at activation.
|
|
649
|
+
|
|
650
|
+
Security: bundled `.cap` files are covered by the parent package's
|
|
651
|
+
`security.json` checksums like every other file; the manifest SHA-256
|
|
652
|
+
is re-verified when a bundled dependency is resolved
|
|
653
|
+
(`Security::IntegrityError` on mismatch), the bundled version must
|
|
654
|
+
still satisfy the declared range, and each bundled package's own
|
|
655
|
+
`security.json` (and any declared signature) is verified when it is
|
|
656
|
+
loaded — the usual activation checks. Serving (layers, visibility,
|
|
657
|
+
route inheritance) is unchanged: bundled sources behave exactly like
|
|
658
|
+
store-resolved ones.
|
|
659
|
+
|
|
660
|
+
== Static registries
|
|
661
|
+
|
|
662
|
+
A *registry* is a directory or a static https base URL holding an
|
|
663
|
+
`index.json` plus `.cap` files stored relative to the registry root,
|
|
664
|
+
so any static host (GitHub Pages, S3, nginx) can serve one:
|
|
665
|
+
|
|
666
|
+
[source,json]
|
|
667
|
+
----
|
|
668
|
+
{
|
|
669
|
+
"packages": {
|
|
670
|
+
"https://github.com/capsiums/cap-story": {
|
|
671
|
+
"name": "story-of-claire",
|
|
672
|
+
"versions": {
|
|
673
|
+
"1.0.0": {
|
|
674
|
+
"file": "story-of-claire-1.0.0.cap",
|
|
675
|
+
"sha256": "<hex sha256 of the .cap>",
|
|
676
|
+
"size": 642047
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
----
|
|
683
|
+
|
|
684
|
+
`Capsium::Registry.fetch(ref)` returns the implementation for a
|
|
685
|
+
reference: `Registry::Local` for a directory (read-write) or
|
|
686
|
+
`Registry::Remote` for an https base URL (read-only; plain http is
|
|
687
|
+
accepted for loopback hosts only). Both resolve a GUID to the newest
|
|
688
|
+
version satisfying a semver constraint and install it: the `.cap` is
|
|
689
|
+
fetched, verified against the sha256 declared in the index
|
|
690
|
+
(`Registry::ChecksumMismatchError` on mismatch) and recorded in the
|
|
691
|
+
package store as `<name>-<version>.cap` with an updated store
|
|
692
|
+
`index.json`. Failures are typed (`RegistryError` subclasses):
|
|
693
|
+
`RegistryNotConfiguredError`, `InvalidRegistryError`,
|
|
694
|
+
`InvalidPackageError`, `PackageNotFoundError`,
|
|
695
|
+
`UnsatisfiableConstraintError`, `ChecksumMismatchError`, `FetchError`.
|
|
696
|
+
|
|
697
|
+
[source,bash]
|
|
698
|
+
----
|
|
699
|
+
# Validate a .cap, copy it into a registry directory and record it in
|
|
700
|
+
# index.json (atomically rewritten; sha256+size recomputed):
|
|
701
|
+
capsium package push story-of-claire-1.0.0.cap --registry ./registry
|
|
702
|
+
|
|
703
|
+
# Resolve, download, sha256-verify and install into the package store:
|
|
704
|
+
capsium install https://github.com/capsiums/cap-story \
|
|
705
|
+
[--constraint ">=1.0"] [--registry DIR_OR_URL] [--store DIR]
|
|
706
|
+
|
|
707
|
+
# Install-then-serve a capsium:// GUID:
|
|
708
|
+
capsium reactor serve capsium://example.com/story \
|
|
709
|
+
[--registry DIR_OR_URL] [--store DIR] [--constraint ">=1.0"]
|
|
710
|
+
----
|
|
711
|
+
|
|
712
|
+
The registry comes from `--registry` or `CAPSIUM_REGISTRY` (a typed
|
|
713
|
+
error when neither is set); the store from `--store` or
|
|
714
|
+
`CAPSIUM_STORE`. Composite-package dependency resolution uses the same
|
|
715
|
+
fallback chain: when the store has no package for a dependency GUID,
|
|
716
|
+
the resolver installs it from the configured registry into the store
|
|
717
|
+
(store -> registry -> typed error).
|
|
718
|
+
|
|
719
|
+
== Authentication
|
|
720
|
+
|
|
721
|
+
`authentication.json` enables user authentication (05x-authentication):
|
|
722
|
+
|
|
723
|
+
[source,json]
|
|
724
|
+
----
|
|
725
|
+
{ "authentication": {
|
|
726
|
+
"basicAuth": { "enabled": true, "passwdFile": "auth/.htpasswd", "realm": "capsium" },
|
|
727
|
+
"oauth2": { "enabled": true, "provider": "google", "clientId": "...",
|
|
728
|
+
"authorizationUrl": "...", "tokenUrl": "...", "userinfoUrl": "...",
|
|
729
|
+
"redirectPath": "/auth/callback", "scopes": ["openid", "email"] }
|
|
730
|
+
} }
|
|
731
|
+
----
|
|
732
|
+
|
|
733
|
+
When enabled, the reactor challenges every unauthenticated request
|
|
734
|
+
(`401`, with `WWW-Authenticate: Basic realm="..."` when basicAuth is
|
|
735
|
+
enabled) and verifies credentials against the htpasswd file. Supported
|
|
736
|
+
hashes: bcrypt (`htpasswd -B`, via the `bcrypt` gem), Apache apr1 MD5
|
|
737
|
+
and md5-crypt (`$apr1$`/`$1$`, pure Ruby), unsalted SHA-1 (`{SHA}`),
|
|
738
|
+
and a platform `crypt(3)` fallback for the rest.
|
|
739
|
+
|
|
740
|
+
With OAuth2, `/auth/login` redirects to the provider with HMAC-signed
|
|
741
|
+
state; the callback exchanges the code at `tokenUrl`, fetches the
|
|
742
|
+
userinfo claims and establishes an HMAC-SHA256 signed session cookie
|
|
743
|
+
(`capsium_session`, `HttpOnly; SameSite=Lax`). Provider errors during
|
|
744
|
+
the exchange answer `502`; a tampered state answers `401`.
|
|
745
|
+
|
|
746
|
+
Route-level `accessControl` on dataset routes is enforced after
|
|
747
|
+
authentication — `401` unauthenticated, `403` when the identity lacks a
|
|
748
|
+
required role:
|
|
749
|
+
|
|
750
|
+
[source,json]
|
|
751
|
+
----
|
|
752
|
+
{ "path": "/api/v1/data/animals", "dataset": "animals",
|
|
753
|
+
"accessControl": { "roles": ["admin"], "authenticationRequired": true } }
|
|
754
|
+
----
|
|
755
|
+
|
|
756
|
+
Secrets NEVER come from the package. They live in `deploy.json`
|
|
757
|
+
(`--deploy` or `CAPSIUM_DEPLOY`):
|
|
758
|
+
|
|
759
|
+
[source,json]
|
|
760
|
+
----
|
|
761
|
+
{
|
|
762
|
+
"baseUrl": "http://localhost:8864",
|
|
763
|
+
"authentication": {
|
|
764
|
+
"basicAuth": { "passwdFile": "/secure/outside/package/.htpasswd" },
|
|
765
|
+
"oauth2": { "clientSecret": "..." },
|
|
766
|
+
"sessionSecret": "...",
|
|
767
|
+
"roles": { "alice": ["admin"] }
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
----
|
|
771
|
+
|
|
772
|
+
`roles` assigns roles by identity name (basic-auth username, OAuth2
|
|
773
|
+
email or subject); userinfo `roles` claims are honored too. Without a
|
|
774
|
+
configured `sessionSecret`, the reactor generates one and persists it
|
|
775
|
+
(mode `0600`) outside the package.
|
|
192
776
|
|
|
193
777
|
== Programmatically managing packages
|
|
194
778
|
|
|
@@ -203,6 +787,11 @@ require 'capsium'
|
|
|
203
787
|
|
|
204
788
|
# Read a package directory or a .cap file
|
|
205
789
|
package = Capsium::Package.new(path)
|
|
790
|
+
|
|
791
|
+
# Read an encrypted .cap (raises Capsium::Package::Cipher::KeyRequiredError
|
|
792
|
+
# without a key, Capsium::Package::Cipher::DecryptionError for a wrong key
|
|
793
|
+
# or tampered ciphertext)
|
|
794
|
+
package = Capsium::Package.new('encrypted.cap', decryption_key: 'private.pem')
|
|
206
795
|
----
|
|
207
796
|
|
|
208
797
|
When `security.json` is present, the package is verified on load and
|
|
@@ -231,6 +820,45 @@ puts animals.data.inspect
|
|
|
231
820
|
|
|
232
821
|
# Verifying integrity (returns a list of typed errors, empty when valid)
|
|
233
822
|
errors = package.verify_integrity
|
|
823
|
+
|
|
824
|
+
# Digital signatures (RSA-SHA256)
|
|
825
|
+
package.signed? # => security.json declares digitalSignatures
|
|
826
|
+
package.verify_signature # => true/false
|
|
827
|
+
|
|
828
|
+
signer = Capsium::Package::Signer.new(package_dir)
|
|
829
|
+
signer.sign('private.pem') # or sign('private.pem', 'cert.pem')
|
|
830
|
+
signer.verify # embedded public key
|
|
831
|
+
signer.verify('cert-or-public-key.pem') # explicit key
|
|
832
|
+
signer.verify! # raises SignatureMismatchError
|
|
833
|
+
|
|
834
|
+
# Whole-package encryption (AES-256-GCM, RSA-OAEP-SHA256 wrapped DEK)
|
|
835
|
+
cipher = Capsium::Package::Cipher.new
|
|
836
|
+
cipher.encrypt('pkg-1.0.0.cap', 'public.pem', 'encrypted.cap')
|
|
837
|
+
cipher.decrypt('encrypted.cap', 'private.pem', 'decrypted.cap')
|
|
838
|
+
Capsium::Package::Cipher.encrypted?('encrypted.cap') # => true
|
|
839
|
+
|
|
840
|
+
# Running the package's tests/*.yaml suite (05x-testing DSL)
|
|
841
|
+
report = Capsium::Package::Testing::TestSuite.new(package).run
|
|
842
|
+
report.ok? # => true/false
|
|
843
|
+
report.failures # => failed TestCase::Result list
|
|
844
|
+
report.summary # => "7 tests, 0 failures"
|
|
845
|
+
|
|
846
|
+
# Layered storage (5a): the merged overlay view shared with the reactor
|
|
847
|
+
view = package.merged_view
|
|
848
|
+
view.resolve('content/app.js') # => absolute path of the topmost hit, or nil
|
|
849
|
+
|
|
850
|
+
# Composite packages (4a): resolve metadata.dependencies against a store
|
|
851
|
+
package = Capsium::Package.new(dir, store: './store') # or CAPSIUM_STORE
|
|
852
|
+
package.resolved_dependencies.each do |dep|
|
|
853
|
+
puts "#{dep.guid} (#{dep.range}) => #{dep.version} [#{dep.path}]"
|
|
854
|
+
end
|
|
855
|
+
view = package.merged_view # own layers over dependency layers
|
|
856
|
+
view = package.merged_view(exported_only: true) # what dependents may see
|
|
857
|
+
|
|
858
|
+
# Serving with authentication and a store
|
|
859
|
+
reactor = Capsium::Reactor.new(package: dir, store: './store',
|
|
860
|
+
deploy: 'deploy.json')
|
|
861
|
+
reactor.serve
|
|
234
862
|
----
|
|
235
863
|
|
|
236
864
|
=== Packing and unpacking programmatically
|
|
@@ -293,6 +921,14 @@ bundle exec rubocop
|
|
|
293
921
|
bundle exec rbs -I sig validate
|
|
294
922
|
----
|
|
295
923
|
|
|
924
|
+
The OpenPGP specs run only when librnp is available (`brew install rnp`,
|
|
925
|
+
or built from https://github.com/rnpgp/rnp); without it they skip
|
|
926
|
+
cleanly, so environments without librnp (e.g. CI images that do not
|
|
927
|
+
install it) stay green. The CI workflow is the shared
|
|
928
|
+
cimas/metanorma `generic-rake` workflow and does not install librnp —
|
|
929
|
+
install it in a custom workflow step (macOS: `brew install rnp`) to run
|
|
930
|
+
the OpenPGP specs there.
|
|
931
|
+
|
|
296
932
|
== License
|
|
297
933
|
|
|
298
934
|
Copyright Ribose.
|