Capsium
Capsium: Common architecture for portable secure information interchange and unified management.
Capsium is designed to facilitate the creation, management and deployment of content packages with ease.
This gem provides a structured way to handle content, data, and metadata for various applications.
Test it out!
- source,bash
$ wget github.com/capsiums/cap-story/releases/download/v0.9.0/story_of_claire-0.9.0.cap $ capsium reactor serve story_of_claire-0.9.0.cap $ open localhost:8864 # => Read the story!
Installation
To install the
Capsiumgem, add it to your Gemfile:- source,ruby
gem ‘capsium’
Then, run the following command to install it:
- source,bash
bundle install
Alternatively, you can install the gem directly using:
- source,bash
gem install capsium
OpenPGP support (librnp)
OpenPGP signatures and encryption are provided through the
rnpgem (a declared dependency) bound to the librnp shared library, which must be installed separately:- source,bash
brew install rnp # homebrew-core formula, provides librnp # or build librnp from source: github.com/rnpgp/rnp
The binding is loaded lazily, only when an OpenPGP feature is used, so the gem works without librnp until then; OpenPGP commands raise a typed
Capsium::Package::OpenPgp::OpenPgpUnavailableErrorwith installation guidance when the library is missing.What is a
Capsiumpackage?A
Capsiumpackage is a structured collection of content, data, metadata, and routing information, distributed as a single.capfile (a ZIP archive, MIMEapplication/vnd.capsium.package). The canonical layout:- source
metadata.json # REQUIRED, hand-authored manifest.json # auto-generated at load/pack time when absent routes.json # auto-generated when absent storage.json # optional (only when datasets exist) security.json # generated at pack time (SHA-256 checksums) authentication.json # optional (user authentication) content/ # served content root (URL space mirrors paths below it) data/ # datasets (optional)
Paths inside the configuration files are package-relative POSIX paths.
-
metadata.json: name (kebab-case), version (semver), description, guid (URI) and uuid are required; author, license, repository, dependencies (an object of ‘guid -> semver range`) are optional.
-
manifest.json: an object keyed by package-relative resource path, e.g. ‘“content/index.html”: { “type”: “text/html”, “visibility”: “exported” }`. Generated by scanning
content/(MIME types via marcel) when absent. -
routes.json: an optional top-level
indexplus aroutesarray. Route kinds: ‘{path, resource}` for static files, `{path, dataset}` for datasets (served under `/api/v1/data/<id>`). HTML files get two routes (basename and full filename); the index HTML additionally gets/. -
storage.json: dataset definitions under
storage.dataSets, either schema-backed files (source,schemaFile, ‘schemaType: json-schema`) or SQLite (databaseFile,table).storage.layersdeclares overlay layers (see “Layered storage” below). -
security.json:
security.integrityChecks.checksumsholds a SHA-256 hex digest for every file in the package exceptsecurity.jsonitself andsignature.sig. When the package is signed,security.digitalSignaturesrecords ‘{ “publicKey”, “signatureFile” }`. -
authentication.json: optional user authentication (see “Authentication” below):
authentication.basicAuthand/orauthentication.oauth2.
Legacy (pre-0.2) configuration files are still accepted on read and normalized to the canonical forms; writers emit only the canonical forms.
What is a
Capsiumreactor?A
Capsiumreactor is a runtime environment that servesCapsiumpackages. It reads the package configuration and starts a server that can handle HTTP requests according to the routes defined in the package. The reactor verifies the package againstsecurity.jsonwhen present and rejects tampered packages. Dataset routes are served as JSON; static resources are served with ‘Cache-Control: public, max-age=31536000` (configurable). While serving, the reactor also answers the introspection endpoints described below.CLI: Package
Capsiumprovides a CLI to help you pack and manage your packages.Packing a package
- source,bash
capsium package pack [–force/-f] [–bundle-deps/–bundle] [–store DIR] [–registry REF] path-to-package
This command generates
manifest.json/routes.jsonwhen absent, writessecurity.jsonwith SHA-256 checksums of every package file, and packs the directory into ‘{package-name}-{package-version}.cap` (name and version frommetadata.json).With
--bundle-deps(alias--bundle) a package that declaresmetadata.dependenciesis packed as an encapsulated package: every declared dependency is resolved through the store -> registry chain (--store/CAPSIUM_STORE,--registry/CAPSIUM_REGISTRY) and embedded underpackages/, so the resulting.capactivates with no store or registry at all (see <<_encapsulated_packages_bundled_dependencies>>). A declared dependency that cannot be resolved aborts the pack with a typed dependency error..Sample
packcommand- source,bash
capsium package pack -f spec/fixtures/bare-package
Unpacking a package
- source,bash
capsium package unpack bare-package-0.1.0.cap [-o/–output my_bare_package]
When
-ois omitted, the package is unpacked into a directory named after the.capfile.Extraction is zip-slip safe: entries whose names would escape the destination (absolute paths, drive letters,
..segments) are rejected withCapsium::Packager::UnsafeEntryError.Validating a package
- source,bash
capsium package validate path-to-package-or-cap
Runs a per-check report and exits with status 1 on any failure:
-
metadata: required fields present and well-formed (kebab-case name, semver version, URI guid, UUID)
-
manifest: every resource exists on disk
-
routes: route targets exist; dataset routes live under
/api/v1/data/; the index resolves to an existing HTML file -
storage: dataset sources (and schemas) exist; dataset data passes JSON-schema validation
-
security: checksums match when
security.jsonis present -
content: no external ‘http(s)` references in
content/files
Inspecting a package
- source,bash
capsium package info|manifest|routes|metadata|storage path-to-package [–store DIR]
infoadditionally prints the resolved dependency tree for composite packages (declared GUID and range, resolved version, store.cap), see “Composite packages” below.Signing a package
- source,bash
capsium package sign path-to-package-or-cap –key private.pem [–cert cert.pem]
Signs the package with an RSA private key (minimum 2048 bits) using RSA-SHA256, per the packaging standard’s digital signature clause. Signing is a post-pack step:
packdrops signing artifacts, so sign the packed directory or the.capfile itself.The signed payload is the concatenation, in sorted package-relative path order, of the raw bytes of every file covered by the
security.jsonintegrity checksums — i.e. every package file exceptsecurity.jsonandsignature.sig. The signature over that payload is stored as raw bytes insignature.sig, the signer’s public key PEM is embedded assignature.pub.pem, andsecurity.jsonrecords:- source,json
{
"security": { "digitalSignatures": { "publicKey": "signature.pub.pem", "signatureFile": "signature.sig" } }}
When
--certis given, the X.509 certificate must match the private key, and the certificate’s public key is embedded instead.The payload construction is openssl-compatible; to verify a signed package independently, concatenate the checksum-covered files in sorted order and run:
- source,bash
openssl dgst -sha256 -verify signature.pub.pem -signature signature.sig payload.bin
OpenPGP signatures
- source,bash
capsium package sign path-to-package-or-cap –openpgp –key secret.asc
With
--openpgp,--keyis an OpenPGP secret key file (armored or binary, auto-detected) and the package is signed through librnp with the same canonical payload:signature.sigholds the armored detached OpenPGP signature (SHA-256), the armored public key is embedded assignature.pub.asc, andsecurity.jsonrecords the scheme:- source,json
{
"security": { "digitalSignatures": { "certificateType": "OpenPGP", "publicKey": "signature.pub.asc", "signatureFile": "signature.sig" } }}
Verifying a package signature
- source,bash
capsium package verify-signature path-to-package-or-cap [–cert cert-or-public-key] [–openpgp]
Verifies the declared signature against the package contents (using the embedded public key, or the given certificate/public key) and exits with status 1 when the package is unsigned or the signature does not match. The signature scheme is auto-detected from the declared
certificateType(RSA-SHA256/X.509 by default, OpenPGP when recorded);--openpgpforces OpenPGP verification with an OpenPGP public key. Packages that declare a signature are also verified automatically on load —a mismatch raisesCapsium::Package::Signer::SignatureMismatchError, so a tampered signed package cannot be served by the reactor.Encrypting a package
- source,bash
capsium package encrypt path-to-package-or-cap –public-key public.pem -o encrypted.cap
Encrypts the whole package for the recipient’s RSA public key (or X.509 certificate), per the packaging standard’s encryption clause: the encrypted
.capis a zip containingmetadata.json(cleartext, so name/version stay readable),signature.json(the encryption envelope) andpackage.enc— the AES-256-GCM ciphertext of the inner plaintext.capzip (content, configuration files, data). A random 256-bit data encryption key (DEK) encrypts the inner zip and is wrapped with the recipient’s public key using RSA-OAEP with SHA-256:- source,json
{
"encryption": { "algorithm": "AES-256-GCM", "keyManagement": "RSA-OAEP-SHA256", "encryptedDek": "<base64>", "iv": "<base64>", "authTag": "<base64>" }}
OpenPGP encryption
- source,bash
capsium package encrypt path-to-package-or-cap –openpgp –recipient public.asc -o encrypted.cap
With
--openpgp, the DEK is protected as an armored OpenPGP message to the recipient’s public key (through librnp) instead of the RSA-OAEP wrap; the content encryption and layout are identical, and the envelope records the key management:- source,json
{
"encryption": { "algorithm": "AES-256-GCM", "keyManagement": "OpenPGP", "message": "<armored OpenPGP message containing the DEK>", "iv": "<base64>", "authTag": "<base64>" }}
The OCB alternative mentioned by the standard remains out of scope.
Decrypting a package
- source,bash
capsium package decrypt encrypted.cap [–private-key private.pem | –key secret.asc] [–openpgp] [-o decrypted.cap]
Decrypts an encrypted package with the recipient’s private key and writes the plaintext
.cap(default output: ‘<name>-decrypted.cap`). The key management is auto-detected from the envelope —--private-keyfor RSA-OAEP,--keyfor an OpenPGP secret key (armored or binary);--openpgpforces the OpenPGP cipher. Decryption fails with a typed error when the key does not match or the ciphertext was tampered with (GCM authentication). Loading an encrypted package without a key raisesCapsium::Package::Cipher::KeyRequiredError— the reactor refuses to serve it. `Capsium::Package.new(path, decryption_key:)` accepts either key format transparently.Testing a package
- source,bash
capsium package test path-to-package-or-cap
Runs the package’s test suite declared in the
Capsiumtesting YAML DSL (05x-testing): all ‘tests/*.yaml` files in the package, each holding a top-leveltestslist. Every test has anameand atype; the four supported types:-
route— requests the URL path from a reactor started for the run and checksexpected_status, plus optionalresponse_contains(body substring) andexpected_content_type. Absolute URLs are accepted; only their path (and query) is used. -
file— the file atpathmust exist in the package; optionalcontainschecks a content substring. -
data_validation— the rows ofdata_file(formatjsonoryaml) must validate against the JSON schema atschema_file. Array data validates row by row. -
config—config_file(formatjsonoryaml) must exist and parse; the known package configs (metadata.json,manifest.json,routes.json,storage.json,security.json) are additionally validated against their canonical models.
Example:
- source,yaml
tests:
- name: Home Route Test type: route url: "/home" expected_status: 200 response_contains: "Welcome" - name: Config File Exists type: file path: "metadata.json" - name: JSON Data Validation type: data_validation format: json data_file: "data/animals.json" schema_file: "data/animals.schema.json" - name: Metadata Config Validation type: config format: json config_file: "metadata.json"
Each test is reported as
PASS/FAILwith messages; the command exits with status 1 when any test fails (or a test definition is invalid).CLI: Reactor
Starting a reactor on your package
- source,bash
capsium reactor serve my_package.cap [–port 8864] [–store DIR] [–deploy deploy.json] [–workdir DIR] capsium reactor serve capsium://example.com/story [–registry DIR_OR_URL] [–store DIR] [–constraint “>=1.0”]
The reactor takes a local package directory or
.capfile, or acapsium://GUID which is installed from a registry first (see “Static registries” below).--store(orCAPSIUM_STORE) resolves composite-package dependencies;--deploy(orCAPSIUM_DEPLOY) points at the reactor-sidedeploy.jsonwith authentication secrets (see “Authentication” below);--registry(orCAPSIUM_REGISTRY) supplies the registry forcapsium://GUIDs and for dependency fallback;--workdirholds the writable overlays and saved packages (default: a temporary directory — see “Writable packages” below).Serving multiple packages
‘capsium reactor serve` accepts multiple sources at once: positional arguments, repeatable `–mount PATH=SOURCE` options, a JSON mount config via
--config, or any combination:- source,bash
capsium reactor serve first.cap second.cap capsium reactor serve –mount /=first.cap –mount /api=second.cap capsium reactor serve –config mounts.json # {“mounts”: [{“path”: “/”, “source”: “first.cap”, “store”: “…”}]}
Default mount points: the first source mounts at
/, each additional source at ‘/<metadata.name>/`. Requests dispatch to mounts by longest-prefix matching, and every package’s routes answer below its prefix (a package mounted at/apiserves its/index.htmlroute at/api/index.html). Two mounts claiming the same prefix fail with aMountConflictError. The introspection endpoints aggregate ALL mounted packages, ‘/package/<name>/…` resolves by name (404 for unknown names), and metrics/logs stay reactor-global. On shutdown the reactor cleans up every mounted package.Writable packages
A mounted package whose metadata does not declare ‘“readOnly”: true` is writable: the reactor keeps an append-only overlay layer for it in the workdir (`–workdir DIR`, default a temporary directory) which is always the topmost layer of the merged view (ARCHITECTURE.md section 5a). The immutable
.capnever changes on disk, and every write is visible on the next request (hot-swap — no restart needed). Overlay state persists in the workdir, so a later reactor over the same workdir sees it again.Dataset CRUD (JSON) under the dataset’s route:
- source
POST /api/v1/data/<dataset> # append an item => 201 + Location + stored item GET /api/v1/data/<dataset>/<id> # one item => 200 (404 when absent) PUT /api/v1/data/<dataset>/<id> # replace an item => 200 (404 absent, 422 schema fail) DELETE /api/v1/data/<dataset>/<id> # delete an item => 204 (404 absent) GET /api/v1/data/<dataset> # the merged collection (as before)
Item ids follow the
idfield when present, else the 1-based index as a string; a duplicateidon POST (or a mismatched one on PUT) is a409. The request body must satisfy the dataset’s JSON schema when one is declared (violations are422with the schema errors); malformed JSON is400. Writes on a ‘“readOnly”: true` package are403with a clear body; writes on a SQLite dataset are501; wrong verbs are405. Mutations persist as a per-dataset JSON operation log in the overlay.Content writes work on any route path (text bodies for v1):
- source
PUT /<route> # create/overwrite a content file (route created on demand) => 200 DELETE /<route> # tombstone: the path 404s even if a lower layer has it => 204
Every mounted package with datasets also answers GraphQL at ‘<mount>/graphql` (POST, or GET with `?query=`): the schema is derived from the package’s datasets — a query field ‘<dataset>` (list) with an optional
id:argument (single item) plus `create<Dataset>`, `update<Dataset>` and `delete<Dataset>` mutations matching the REST semantics. Item types are inferred from the dataset’s JSON schema when present, else map to a permissive JSON scalar; SQLite datasets are skipped. Not-found items, schema violations and read-only mutations land in the GraphQLerrorsarray (never a 500).‘POST /package/<name>/save` folds base plus overlays into a NEW versioned
.cap(`<name>-<version+patch>.cap`) in the workdir and returns its path and SHA-256; the saved package passes `capsium package validate`.Reactor introspection API
While serving, the reactor answers the Monitoring HTTP API (ARCHITECTURE.md section 7) as
application/json:- source
GET /api/v1/introspect/metadata # => {“packages”: [{“name”, “version”, “author”, “description”}]} GET /api/v1/introspect/routes # => {“routes”: [{“package”, “routes”: [{“method”, “path”}]}]} GET /api/v1/introspect/content-hashes # => {“contentHashes”: [{“package”, “hash”}]} GET /api/v1/introspect/content-validity # => {“contentValidity”: [{“package”, “valid”, “lastChecked”, “reason”?}]}
Non-GET methods on these paths are answered ‘405 Method Not Allowed`.
content-hashesis the SHA-256 of the.capblob when the package was served from one. For a directory source there is no blob, so the hash covers the canonical (sorted-key) JSON serialization of the package content checksums — the same datasecurity.jsonintegrityChecks carry.content-validityre-verifies the package againstsecurity.jsonon every request and reports the outcome with a UTClastCheckedtimestamp;reasonlists the integrity errors whenvalidis false. The entry also reportssignedandencryptedstatus, plussignatureValidwhen the package declares a signature.The reactor also answers reactor-level and per-package introspection endpoints (07-reactor follow-ons), likewise GET-only JSON:
- source
GET /introspect/status # => {“status”: “running”, “uptime”: <seconds>, “packagesLoaded”: 1} GET /introspect/config # => {“port”, “storeDir”, “cacheControl”, “authEnabled”, “registry”} GET /introspect/metrics # => {“uptime”, “requestsTotal”, “requestsByStatus”: {“200”: n, …}} GET /package/<name>/status # => {“package”, “version”, “status”: “loaded”, “valid”: true} GET /package/<name>/metadata # => {“name”, “version”, “description”, “author”, “guid”} GET /package/<name>/logs # => {“package”, “logs”: [“<utc> GET / -> 200”, …]}
/introspect/confignever exposes secrets (deploy.json values, or credentials embedded in a registry URL, are redacted). With multiple mounted packages the/api/v1/introspect/*endpoints aggregate all of them, and ‘/package/<name>/…` resolves by name (404 for unknown names).logsreturns the last N lines (default 100) from a small in-memory ring buffer (Capsium::LogBuffer) recording key serving events;metricscounts requests by status in memory (Capsium::Reactor::Metrics). When authentication is enabled these endpoints are gated like any other route.Layered storage
storage.layersstacks overlay directories overcontent/(bottom -> top in declaration order), each mirroring thecontent/tree:- source,json
{ “storage”: { “layers”: [
{ "path": "base", "writable": false, "visibility": "exported" }, { "path": "updates", "writable": true, "visibility": "private" }] } }
A request resolves against layers from the TOP down; the first hit wins. Deletions are recorded as tombstones: a
.capsium-tombstonesJSON file (an array ofcontent/-relative paths) in a writable layer; a tombstoned path resolves 404 even when a lower layer holds the file, while a file reappearing above the tombstone is served again. Packages without alayersconfig behave exactly as before (single implicitcontent/layer). ‘visibility: private` layers are hidden from dependent packages (see below), as are resources whose manifest visibility isprivate.Composite packages
metadata.dependenciesmaps a dependency GUID to a semver range (‘>=1.0.0`, `^1.2.3`, `~1.2.3`, exact, `*`,1.x/1.2.x, partials, and comma/space conjunctions like `>=1.0.0, <2.0.0`). Dependencies resolve against a *package store* — a directory of `<name>-<version>.cap` files plus an optionalindex.json(GUID -> file) — given viaCAPSIUM_STOREor--store; the newest satisfying version wins.On load, each resolved dependency’s exported content becomes read-only layers below all of the dependent’s own layers. Routes may address dependency content explicitly (‘“resource”: “<dependency-guid>/content/app.js”`) or declare route-inheritance attributes per 05x-routing:
remap(replaces the serving path),responseRewrite(body,headers),responseHeaders(merged over served headers) andrequestHeaders(parsed and exposed for forwarding reactors; this reactor serves statically, so they do not alter its responses). Referencing a dependency’s private or missing resource is a load-time error, as are circular, missing or unsatisfiable dependencies.- source,bash
capsium package info my-composite-package –store ./store capsium reactor serve my-composite-package –store ./store
- [_encapsulated_packages_bundled_dependencies]
-
Encapsulated packages (bundled dependencies)
‘capsium package pack –bundle-deps` (alias
--bundle) produces a self-contained package: the resolved dependency.capfiles are embedded inside the parent, so the whole tree activates with no store and no registry. Layout inside the.cap:
packages/
index.json # bundled-dependencies manifest <name>-<version>.cap # one per declared dependency
packages/index.jsonmaps each dependency GUID to its embedded file, the resolved version and the SHA-256 of the embedded.cap:- source,json
{
"https://example.com/capsiums/base-package": { "file": "packages/base-package-1.2.0.cap", "version": "1.2.0", "sha256": "<hex sha256 of the .cap>" }}
Resolution order on load is *bundle first*: a dependency GUID found in
packages/index.jsonresolves to the embedded.cap(this also works when the parent itself was loaded from a.cap— the embedded files are read out of the parent’s extraction); anything not bundled falls back to the usual store -> registry chain. The bundle is passed down to the dependencies’ own resolution, so a dependency’s dependency resolves from the parent’s bundle when the parent re-declares it.Bundling follows a *one-level policy*: only the dependencies declared in the parent’s own
metadata.dependenciesare embedded. To make a whole tree self-contained, the parent’smetadata.dependenciesmust list the transitive closure; a bundled dependency whose own dependencies are not re-declared by the parent still needs a store or registry at activation.Security: bundled
.capfiles are covered by the parent package’ssecurity.jsonchecksums like every other file; the manifest SHA-256 is re-verified when a bundled dependency is resolved (Security::IntegrityErroron mismatch), the bundled version must still satisfy the declared range, and each bundled package’s ownsecurity.json(and any declared signature) is verified when it is loaded — the usual activation checks. Serving (layers, visibility, route inheritance) is unchanged: bundled sources behave exactly like store-resolved ones.Static registries
A registry is a directory or a static https base URL holding an
index.jsonplus.capfiles stored relative to the registry root, so any static host (GitHub Pages, S3, nginx) can serve one:- source,json
{
"packages": { "https://github.com/capsiums/cap-story": { "name": "story-of-claire", "versions": { "1.0.0": { "file": "story-of-claire-1.0.0.cap", "sha256": "<hex sha256 of the .cap>", "size": 642047 } } } }}
‘Capsium::Registry.fetch(ref)` returns the implementation for a reference:
Registry::Localfor a directory (read-write) orRegistry::Remotefor an https base URL (read-only; plain http is accepted for loopback hosts only). Both resolve a GUID to the newest version satisfying a semver constraint and install it: the.capis fetched, verified against the sha256 declared in the index (Registry::ChecksumMismatchErroron mismatch) and recorded in the package store as `<name>-<version>.cap` with an updated storeindex.json. Failures are typed (RegistryErrorsubclasses):RegistryNotConfiguredError,InvalidRegistryError,InvalidPackageError,PackageNotFoundError,UnsatisfiableConstraintError,ChecksumMismatchError,FetchError.- source,bash
# Validate a .cap, copy it into a registry directory and record it in # index.json (atomically rewritten; sha256+size recomputed): capsium package push story-of-claire-1.0.0.cap –registry ./registry
# Resolve, download, sha256-verify and install into the package store: capsium install github.com/capsiums/cap-story \
[--constraint ">=1.0"] [--registry DIR_OR_URL] [--store DIR]
# Install-then-serve a capsium:// GUID: capsium reactor serve capsium://example.com/story \
[--registry DIR_OR_URL] [--store DIR] [--constraint ">=1.0"]
The registry comes from
--registryorCAPSIUM_REGISTRY(a typed error when neither is set); the store from--storeorCAPSIUM_STORE. Composite-package dependency resolution uses the same fallback chain: when the store has no package for a dependency GUID, the resolver installs it from the configured registry into the store (store -> registry -> typed error).Authentication
authentication.jsonenables user authentication (05x-authentication):- source,json
{ “authentication”: {
"basicAuth": { "enabled": true, "passwdFile": "auth/.htpasswd", "realm": "capsium" }, "oauth2": { "enabled": true, "provider": "google", "clientId": "...", "authorizationUrl": "...", "tokenUrl": "...", "userinfoUrl": "...", "redirectPath": "/auth/callback", "scopes": ["openid", "email"] }} }
When enabled, the reactor challenges every unauthenticated request (
401, with ‘WWW-Authenticate: Basic realm=“…”` when basicAuth is enabled) and verifies credentials against the htpasswd file. Supported hashes: bcrypt (`htpasswd -B`, via thebcryptgem), Apache apr1 MD5 and md5-crypt (`$apr1$/$1$`, pure Ruby), unsalted SHA-1 (`{SHA}`), and a platform `crypt(3)` fallback for the rest.With OAuth2,
/auth/loginredirects to the provider with HMAC-signed state; the callback exchanges the code attokenUrl, fetches the userinfo claims and establishes an HMAC-SHA256 signed session cookie (capsium_session, ‘HttpOnly; SameSite=Lax`). Provider errors during the exchange answer502; a tampered state answers401.Route-level
accessControlon dataset routes is enforced after authentication —401unauthenticated,403when the identity lacks a required role:- source,json
{ “path”: “/api/v1/data/animals”, “dataset”: “animals”,
"accessControl": { "roles": ["admin"], "authenticationRequired": true } }
Secrets NEVER come from the package. They live in
deploy.json(--deployorCAPSIUM_DEPLOY):- source,json
{
"baseUrl": "http://localhost:8864", "authentication": { "basicAuth": { "passwdFile": "/secure/outside/package/.htpasswd" }, "oauth2": { "clientSecret": "..." }, "sessionSecret": "...", "roles": { "alice": ["admin"] } }}
rolesassigns roles by identity name (basic-auth username, OAuth2 email or subject); userinforolesclaims are honored too. Without a configuredsessionSecret, the reactor generates one and persists it (mode0600) outside the package.Programmatically managing packages
The public API below ships with RBS signatures in
sig/(‘bundle exec rbs -I sig validate`).Loading packages
- source,ruby
require ‘capsium’
# Read a package directory or a .cap file package =
Capsium::Package.new(path)# Read an encrypted .cap (raises
Capsium::Package::Cipher::KeyRequiredError# without a key,Capsium::Package::Cipher::DecryptionErrorfor a wrong key # or tampered ciphertext) package =Capsium::Package.new(‘encrypted.cap’, decryption_key: ‘private.pem’)
When
security.jsonis present, the package is verified on load andCapsium::Package::Security::IntegrityErroris raised on mismatch.Using packages in your program
- source,ruby
# Accessing package metadata puts “Package Name: #{package.metadata.name}” puts “Package Version: #{package.metadata.version}”
# Accessing manifest resources package.manifest.resources.each do |path, resource|
puts "Resource: #{path}, Type: #{resource.type}"
end
# Accessing routes route = package.routes.resolve(‘/’) puts route.resource
# Accessing datasets animals = package.storage.dataset(‘animals’) puts animals.data.inspect
# Verifying integrity (returns a list of typed errors, empty when valid) errors = package.verify_integrity
# Digital signatures (RSA-SHA256) package.signed? # => security.json declares digitalSignatures package.verify_signature # => true/false
signer =
Capsium::Package::Signer.new(package_dir)signer.sign(‘private.pem’) # or sign(‘private.pem’, ‘cert.pem’) signer.verify # embedded public key signer.verify(‘cert-or-public-key.pem’) # explicit key signer.verify! # raises SignatureMismatchError# Whole-package encryption (AES-256-GCM, RSA-OAEP-SHA256 wrapped DEK) cipher = Capsium::Package::Cipher.new cipher.encrypt(‘pkg-1.0.0.cap’, ‘public.pem’, ‘encrypted.cap’) cipher.decrypt(‘encrypted.cap’, ‘private.pem’, ‘decrypted.cap’)
Capsium::Package::Cipher.encrypted?(‘encrypted.cap’) # => true# Running the package’s tests/*.yaml suite (05x-testing DSL) report =
Capsium::Package::Testing::TestSuite.new(package).run report.ok? # => true/false report.failures # => failed TestCase::Result list report.summary # => “7 tests, 0 failures”# Layered storage (5a): the merged overlay view shared with the reactor view = package.merged_view view.resolve(‘content/app.js’) # => absolute path of the topmost hit, or nil
# Composite packages (4a): resolve metadata.dependencies against a store package =
Capsium::Package.new(dir, store: ‘./store’) # or CAPSIUM_STORE package.resolved_dependencies.each do |dep|puts "#{dep.guid} (#{dep.range}) => #{dep.version} [#{dep.path}]"
end view = package.merged_view # own layers over dependency layers view = package.merged_view(exported_only: true) # what dependents may see
# Serving with authentication and a store reactor =
Capsium::Reactor.new(package: dir, store: ‘./store’,deploy: 'deploy.json')
reactor.serve
Packing and unpacking programmatically
- source,ruby
packager = Capsium::Packager.new cap_file = packager.pack(package, force: true) packager.unpack(cap_file, ‘output-directory’)
Building the mn-samples-iso cap package
Download the mn-samples-iso built site: github.com/metanorma/mn-samples-iso/actions/runs/8862815829/artifacts/1453746303.
Then run these commands:
- source,bash
$ unzip mn-samples-iso-Linux.zip $ cd mn-samples-iso-Linux $ mkdir content $ mv index.html documents.xml documents content $ cat > metadata.json <<‘JSON’ {
"name": "mn-samples-iso", "version": "0.1.0", "description": "Metanorma ISO sample documents", "guid": "https://github.com/metanorma/mn-samples-iso", "uuid": "123e4567-e89b-12d3-a456-426614174000"
} JSON $ cd .. $ bundle exec capsium package pack -f mn-samples-iso-Linux Package created: mn-samples-iso-0.1.0.cap $ bundle exec capsium reactor serve mn-samples-iso-0.1.0.cap Starting server on localhost:8864 …
Contributing
We welcome contributions to the
Capsiumgem. If you would like to contribute, please fork the repository and submit a pull request.Running tests
To run the tests, use the following command:
- source,bash
bundle exec rspec
The linter and the RBS signature validation are part of the default rake task (‘bundle exec rake`), or run them individually:
- source,bash
bundle exec rubocop bundle exec rbs -I sig validate
The OpenPGP specs run only when librnp is available (‘brew install rnp`, or built from github.com/rnpgp/rnp); without it they skip cleanly, so environments without librnp (e.g. CI images that do not install it) stay green. The CI workflow is the shared cimas/metanorma
generic-rakeworkflow and does not install librnp —install it in a custom workflow step (macOS: `brew install rnp`) to run the OpenPGP specs there.License
Copyright Ribose.
Capsiumis released under the MIT License. See the LICENSE file for more details.
-
-
-