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!
$ wget https://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 http://localhost:8864
# => Read the story!Installation
To install the Capsium gem, add it to your Gemfile:
gem 'capsium'Then, run the following command to install it:
bundle installAlternatively, you can install the gem directly using:
gem install capsiumOpenPGP support (librnp)
OpenPGP signatures and encryption are provided through the rnp gem
(a declared dependency) bound to the librnp shared library, which
must be installed separately:
brew install rnp # homebrew-core formula, provides librnp
# or build librnp from source: https://github.com/rnpgp/rnpThe 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::OpenPgpUnavailableError with
installation guidance when the library is missing.
What is a Capsium package?
A Capsium package is a structured collection of content, data, metadata, and
routing information, distributed as a single .cap file (a ZIP archive, MIME
application/vnd.capsium.package). The canonical layout:
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 scanningcontent/(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 Capsium reactor?
A Capsium reactor is a runtime environment that serves Capsium packages. 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 against security.json when 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
Capsium provides a CLI to help you pack and manage your packages.
Packing a package
capsium package pack [--force/-f] [--bundle-deps/--bundle] [--store DIR] [--registry REF] path-to-packageThis command generates manifest.json/routes.json when absent, writes
security.json with SHA-256 checksums of every package file, and packs the
directory into {package-name}-{package-version}.cap (name and version from
metadata.json).
With --bundle-deps (alias --bundle) a package that declares
metadata.dependencies is packed as an encapsulated package: every
declared dependency is resolved through the store → registry chain
(--store/CAPSIUM_STORE, --registry/CAPSIUM_REGISTRY) and embedded
under packages/, so the resulting .cap activates 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.
pack commandcapsium package pack -f spec/fixtures/bare-packageUnpacking a package
capsium package unpack bare-package-0.1.0.cap [-o/--output my_bare_package]When -o is omitted, the package is unpacked into a directory named after
the .cap file.
Extraction is zip-slip safe: entries whose names would escape the
destination (absolute paths, drive letters, .. segments) are rejected
with Capsium::Packager::UnsafeEntryError.
Validating a package
capsium package validate path-to-package-or-capRuns 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 incontent/files
Inspecting a package
capsium package info|manifest|routes|metadata|storage path-to-package [--store DIR]info additionally prints the resolved dependency tree for composite
packages (declared GUID and range, resolved version, store .cap), see
"Composite packages" below.
Signing a package
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: pack drops signing artifacts, so sign the packed
directory or the .cap file itself.
The signed payload is the concatenation, in sorted package-relative path
order, of the raw bytes of every file covered by the security.json
integrity checksums — i.e. every package file except security.json and
signature.sig. The signature over that payload is stored as raw bytes in
signature.sig, the signer’s public key PEM is embedded as
signature.pub.pem, and security.json records:
{
"security": {
"digitalSignatures": {
"publicKey": "signature.pub.pem",
"signatureFile": "signature.sig"
}
}
}When --cert is 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:
openssl dgst -sha256 -verify signature.pub.pem -signature signature.sig payload.binOpenPGP signatures
capsium package sign path-to-package-or-cap --openpgp --key secret.ascWith --openpgp, --key is an OpenPGP secret key file (armored or
binary, auto-detected) and the package is signed through librnp with the
same canonical payload: signature.sig holds the armored detached
OpenPGP signature (SHA-256), the armored public key is embedded as
signature.pub.asc, and security.json records the scheme:
{
"security": {
"digitalSignatures": {
"certificateType": "OpenPGP",
"publicKey": "signature.pub.asc",
"signatureFile": "signature.sig"
}
}
}Verifying a package signature
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);
--openpgp forces OpenPGP verification with an OpenPGP public key.
Packages that declare a signature are also verified automatically on load —
a mismatch raises Capsium::Package::Signer::SignatureMismatchError, so a
tampered signed package cannot be served by the reactor.
Encrypting a package
capsium package encrypt path-to-package-or-cap --public-key public.pem -o encrypted.capEncrypts the whole package for the recipient’s RSA public key (or X.509
certificate), per the packaging standard’s encryption clause: the
encrypted .cap is a zip containing metadata.json (cleartext, so
name/version stay readable), signature.json (the encryption envelope)
and package.enc — the AES-256-GCM ciphertext of the inner plaintext
.cap zip (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:
{
"encryption": {
"algorithm": "AES-256-GCM",
"keyManagement": "RSA-OAEP-SHA256",
"encryptedDek": "<base64>",
"iv": "<base64>",
"authTag": "<base64>"
}
}OpenPGP encryption
capsium package encrypt path-to-package-or-cap --openpgp --recipient public.asc -o encrypted.capWith --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:
{
"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
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-key
for RSA-OAEP, --key for an OpenPGP secret key (armored or binary);
--openpgp forces 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 raises
Capsium::Package::Cipher::KeyRequiredError — the reactor refuses to
serve it. Capsium::Package.new(path, decryption_key:) accepts either
key format transparently.
Testing a package
capsium package test path-to-package-or-capRuns the package’s test suite declared in the Capsium testing YAML DSL
(05x-testing): all tests/*.yaml files in the package, each holding a
top-level tests list. Every test has a name and a type; 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:
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/FAIL with 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
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 .cap file, or a
capsium:// GUID which is installed from a registry first (see
"Static registries" below). --store (or CAPSIUM_STORE)
resolves composite-package dependencies; --deploy (or
CAPSIUM_DEPLOY) points at the reactor-side deploy.json with
authentication secrets (see "Authentication" below); --registry
(or CAPSIUM_REGISTRY) supplies the registry for capsium:// GUIDs
and for dependency fallback; --workdir holds 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:
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 /api serves its /index.html route at
/api/index.html). Two mounts claiming the same prefix fail with a
MountConflictError. 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 .cap never 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:
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 id field when present, else the 1-based index as
a string; a duplicate id on POST (or a mismatched one on PUT) is a
409. The request body must satisfy the dataset’s JSON schema when
one is declared (violations are 422 with the schema errors);
malformed JSON is 400. Writes on a "readOnly": true package are
403 with a clear body; writes on a SQLite dataset are 501;
wrong verbs are 405. Mutations persist as a per-dataset JSON
operation log in the overlay.
Content writes work on any route path (text bodies for v1):
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 => 204Every 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 GraphQL errors array (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:
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-hashes is the SHA-256 of the .cap blob 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 data security.json integrityChecks carry.
content-validity re-verifies the package against security.json on
every request and reports the outcome with a UTC lastChecked
timestamp; reason lists the integrity errors when valid is false.
The entry also reports signed and encrypted status, plus
signatureValid when the package declares a signature.
The reactor also answers reactor-level and per-package introspection endpoints (07-reactor follow-ons), likewise GET-only JSON:
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[?lines=N] # => {"package", "logs": ["<utc> GET / -> 200", ...]}/introspect/config never 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). logs returns the last N lines (default 100) from a small
in-memory ring buffer (Capsium::LogBuffer) recording key serving
events; metrics counts requests by status in memory
(Capsium::Reactor::Metrics). When authentication is enabled these
endpoints are gated like any other route.
Layered storage
storage.layers stacks overlay directories over content/ (bottom →
top in declaration order), each mirroring the content/ tree:
{ "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-tombstones
JSON file (an array of content/-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 a layers config behave exactly as before (single implicit
content/ layer). visibility: private layers are hidden from
dependent packages (see below), as are resources whose manifest
visibility is private.
Composite packages
metadata.dependencies maps 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 optional index.json (GUID →
file) — given via CAPSIUM_STORE or --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) and requestHeaders
(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.
capsium package info my-composite-package --store ./store
capsium reactor serve my-composite-package --store ./storeEncapsulated packages (bundled dependencies)
capsium package pack --bundle-deps (alias --bundle) produces a
self-contained package: the resolved dependency .cap files 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.json maps each dependency GUID to its embedded file,
the resolved version and the SHA-256 of the embedded .cap:
{
"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.json resolves 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.dependencies are embedded. To make a
whole tree self-contained, the parent’s metadata.dependencies must
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 .cap files are covered by the parent package’s
security.json checksums like every other file; the manifest SHA-256
is re-verified when a bundled dependency is resolved
(Security::IntegrityError on mismatch), the bundled version must
still satisfy the declared range, and each bundled package’s own
security.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.json plus .cap files stored relative to the registry root,
so any static host (GitHub Pages, S3, nginx) can serve one:
{
"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::Local for a directory (read-write) or
Registry::Remote for 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 .cap is
fetched, verified against the sha256 declared in the index
(Registry::ChecksumMismatchError on mismatch) and recorded in the
package store as <name>-<version>.cap with an updated store
index.json. Failures are typed (RegistryError subclasses):
RegistryNotConfiguredError, InvalidRegistryError,
InvalidPackageError, PackageNotFoundError,
UnsatisfiableConstraintError, ChecksumMismatchError, FetchError.
# 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 https://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 --registry or CAPSIUM_REGISTRY (a typed
error when neither is set); the store from --store or
CAPSIUM_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.json enables user authentication (05x-authentication):
{ "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 the bcrypt gem), 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/login redirects to the provider with HMAC-signed
state; the callback exchanges the code at tokenUrl, fetches the
userinfo claims and establishes an HMAC-SHA256 signed session cookie
(capsium_session, HttpOnly; SameSite=Lax). Provider errors during
the exchange answer 502; a tampered state answers 401.
Route-level accessControl on dataset routes is enforced after
authentication — 401 unauthenticated, 403 when the identity lacks a
required role:
{ "path": "/api/v1/data/animals", "dataset": "animals",
"accessControl": { "roles": ["admin"], "authenticationRequired": true } }Secrets NEVER come from the package. They live in deploy.json
(--deploy or CAPSIUM_DEPLOY):
{
"baseUrl": "http://localhost:8864",
"authentication": {
"basicAuth": { "passwdFile": "/secure/outside/package/.htpasswd" },
"oauth2": { "clientSecret": "..." },
"sessionSecret": "...",
"roles": { "alice": ["admin"] }
}
}roles assigns roles by identity name (basic-auth username, OAuth2
email or subject); userinfo roles claims are honored too. Without a
configured sessionSecret, the reactor generates one and persists it
(mode 0600) outside the package.
Programmatically managing packages
The public API below ships with RBS signatures in sig/
(bundle exec rbs -I sig validate).
Loading packages
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::DecryptionError for a wrong key
# or tampered ciphertext)
package = Capsium::Package.new('encrypted.cap', decryption_key: 'private.pem')When security.json is present, the package is verified on load and
Capsium::Package::Security::IntegrityError is raised on mismatch.
Using packages in your program
# 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.servePacking and unpacking programmatically
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: mn-samples-iso-Linux.
Then run these commands:
$ 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 http://localhost:8864
...Contributing
We welcome contributions to the Capsium gem. 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:
bundle exec rspecThe linter and the RBS signature validation are part of the default rake
task (bundle exec rake), or run them individually:
bundle exec rubocop
bundle exec rbs -I sig validateThe OpenPGP specs run only when librnp is available (brew install rnp,
or built from https://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-rake workflow 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.
Capsium is released under the MIT License. See the LICENSE file for more details.