capsium 0.1.2 → 0.3.0

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