capsium 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -3
- data/CHANGELOG.md +197 -0
- data/README.adoc +323 -14
- data/capsium.gemspec +2 -0
- data/lib/capsium/cli/package.rb +62 -25
- data/lib/capsium/cli/reactor.rb +112 -6
- data/lib/capsium/cli.rb +29 -0
- data/lib/capsium/log_buffer.rb +47 -0
- data/lib/capsium/package/bundle.rb +136 -0
- data/lib/capsium/package/cipher.rb +73 -17
- data/lib/capsium/package/composition.rb +28 -12
- data/lib/capsium/package/dataset.rb +20 -0
- data/lib/capsium/package/dependency_resolver.rb +39 -9
- data/lib/capsium/package/encryption_config.rb +7 -3
- data/lib/capsium/package/manifest.rb +1 -1
- data/lib/capsium/package/merged_view.rb +4 -2
- data/lib/capsium/package/metadata.rb +1 -1
- data/lib/capsium/package/metadata_config.rb +9 -1
- data/lib/capsium/package/open_pgp.rb +65 -0
- data/lib/capsium/package/open_pgp_cipher.rb +66 -0
- data/lib/capsium/package/open_pgp_signer.rb +176 -0
- data/lib/capsium/package/security_config.rb +6 -2
- data/lib/capsium/package/signer.rb +18 -3
- data/lib/capsium/package/store.rb +27 -0
- data/lib/capsium/package/validator.rb +27 -2
- data/lib/capsium/package/verification.rb +7 -5
- data/lib/capsium/package.rb +24 -5
- data/lib/capsium/packager.rb +64 -22
- data/lib/capsium/reactor/content_api.rb +110 -0
- data/lib/capsium/reactor/data_api.rb +154 -0
- data/lib/capsium/reactor/endpoints.rb +37 -0
- data/lib/capsium/reactor/graphql_api.rb +123 -0
- data/lib/capsium/reactor/graphql_schema.rb +138 -0
- data/lib/capsium/reactor/introspection.rb +151 -34
- data/lib/capsium/reactor/metrics.rb +30 -0
- data/lib/capsium/reactor/mount.rb +206 -0
- data/lib/capsium/reactor/overlay.rb +248 -0
- data/lib/capsium/reactor/package_saver.rb +99 -0
- data/lib/capsium/reactor/responses.rb +41 -0
- data/lib/capsium/reactor/serving.rb +90 -11
- data/lib/capsium/reactor.rb +96 -49
- data/lib/capsium/registry/local.rb +120 -0
- data/lib/capsium/registry/remote.rb +105 -0
- data/lib/capsium/registry.rb +190 -0
- data/lib/capsium/version.rb +1 -1
- data/lib/capsium.rb +2 -0
- data/sig/capsium/log_buffer.rbs +30 -0
- data/sig/capsium/package/bundle.rbs +43 -0
- data/sig/capsium/package/cipher.rbs +12 -1
- data/sig/capsium/package/composition.rbs +4 -3
- data/sig/capsium/package/dataset.rbs +8 -0
- data/sig/capsium/package/dependency_resolver.rbs +8 -4
- data/sig/capsium/package/encryption_config.rbs +6 -2
- data/sig/capsium/package/merged_view.rbs +2 -1
- data/sig/capsium/package/metadata.rbs +1 -0
- data/sig/capsium/package/open_pgp.rbs +38 -0
- data/sig/capsium/package/open_pgp_cipher.rbs +15 -0
- data/sig/capsium/package/open_pgp_signer.rbs +46 -0
- data/sig/capsium/package/security_config.rbs +6 -3
- data/sig/capsium/package/signer.rbs +8 -1
- data/sig/capsium/package/store.rbs +6 -0
- data/sig/capsium/package/verification.rbs +5 -3
- data/sig/capsium/package.rbs +8 -3
- data/sig/capsium/reactor/content_api.rbs +19 -0
- data/sig/capsium/reactor/data_api.rbs +23 -0
- data/sig/capsium/reactor/endpoints.rbs +16 -0
- data/sig/capsium/reactor/graphql_api.rbs +27 -0
- data/sig/capsium/reactor/graphql_schema.rbs +20 -0
- data/sig/capsium/reactor/introspection.rbs +27 -7
- data/sig/capsium/reactor/metrics.rbs +13 -0
- data/sig/capsium/reactor/mount.rbs +83 -0
- data/sig/capsium/reactor/overlay.rbs +72 -0
- data/sig/capsium/reactor/package_saver.rbs +12 -0
- data/sig/capsium/reactor/responses.rbs +23 -0
- data/sig/capsium/reactor/serving.rbs +28 -4
- data/sig/capsium/reactor.rbs +17 -2
- data/sig/capsium/registry/local.rbs +16 -0
- data/sig/capsium/registry/remote.rbs +18 -0
- data/sig/capsium/registry.rbs +83 -0
- data/sig/graphql.rbs +12 -0
- metadata +66 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d5f0f655cb66b0e6b49edcb70ad68b8e6670e0ca2cb0d5a77aef84c609a3c6b
|
|
4
|
+
data.tar.gz: 3f4f17221db2c72db65c14faa2b5e3a31cf026e5c5b1e0ffc331af872b8b0111
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e3b9777dc6f0c498784959ab66cb89228657b6f8e85a43818c804c311371d86f78d9ffd3cf7f61e1ad743db08608fafdbe35d49d3936073f8d8281d66cd9816
|
|
7
|
+
data.tar.gz: 7cfb11cce09e70b03f94057c68adea6389022f50aa5b9573311f0cf0a8d606013bf60115e4f11bfb45a07460fd3dd5f22e93b34ef5dcb84cb052bc25c93cc6bb
|
data/.rubocop.yml
CHANGED
|
@@ -32,14 +32,18 @@ Metrics/ModuleLength:
|
|
|
32
32
|
- 'spec/**/*'
|
|
33
33
|
|
|
34
34
|
# Spec helpers take the parameters the fixtures need; the reactor
|
|
35
|
-
# constructor takes one keyword per deploy-time concern
|
|
35
|
+
# constructor takes one keyword per deploy-time concern (keyword args
|
|
36
|
+
# are self-documenting and not counted).
|
|
36
37
|
Metrics/ParameterLists:
|
|
37
|
-
Max:
|
|
38
|
+
Max: 7
|
|
39
|
+
CountKeywordArgs: false
|
|
38
40
|
Exclude:
|
|
39
41
|
- 'spec/**/*'
|
|
40
42
|
|
|
41
43
|
Metrics/ClassLength:
|
|
42
|
-
|
|
44
|
+
# The reactor (request dispatch + lifecycle) and the introspection
|
|
45
|
+
# endpoint set grew with the phase-3 follow-ons.
|
|
46
|
+
Max: 150
|
|
43
47
|
|
|
44
48
|
Metrics/MethodLength:
|
|
45
49
|
Max: 25
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,202 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- OpenPGP signatures and encryption through librnp (new `rnp` gem
|
|
8
|
+
dependency, loaded lazily — a typed
|
|
9
|
+
`Capsium::Package::OpenPgp::OpenPgpUnavailableError` with
|
|
10
|
+
installation guidance is raised when the gem or librnp is missing):
|
|
11
|
+
- `Capsium::Package::OpenPgpSigner` (parallel to the RSA-SHA256/X.509
|
|
12
|
+
`Signer`, same construction semantics and `Signer::SignatureError`
|
|
13
|
+
taxonomy): `capsium package sign PATH --openpgp --key SEC.asc`
|
|
14
|
+
writes an armored detached OpenPGP signature (SHA-256) over the
|
|
15
|
+
canonical section-6a payload to `signature.sig`, embeds the armored
|
|
16
|
+
public key as `signature.pub.asc` and records
|
|
17
|
+
`"digitalSignatures": {"certificateType": "OpenPGP", ...}` in
|
|
18
|
+
security.json. Verification auto-detects the scheme from
|
|
19
|
+
`certificateType` (`Signer.verify_package`,
|
|
20
|
+
`Package#verify_signature`, the load-time signature gate and
|
|
21
|
+
`capsium package verify-signature` all dispatch); `--openpgp
|
|
22
|
+
--cert PUB.asc` verifies with an explicit OpenPGP public key.
|
|
23
|
+
- `Capsium::Package::OpenPgpCipher < Cipher` (same encrypted .cap
|
|
24
|
+
layout and AES-256-GCM content encryption): `capsium package
|
|
25
|
+
encrypt PATH --openpgp --recipient PUB.asc -o OUT.cap` protects the
|
|
26
|
+
DEK as an armored OpenPGP message
|
|
27
|
+
(`signature.json`: `{"encryption": {"algorithm": "AES-256-GCM",
|
|
28
|
+
"keyManagement": "OpenPGP", "message", "iv", "authTag"}}`).
|
|
29
|
+
Decryption auto-detects the key management from the envelope
|
|
30
|
+
(`Cipher.for_encrypted`, `Cipher.key_management`), and
|
|
31
|
+
`Package.new(decryption_key:)` accepts OpenPGP secret keys
|
|
32
|
+
(armored or binary) transparently. `Cipher`'s envelope/DEK handling
|
|
33
|
+
was refactored into subclass seams; the RSA-OAEP-SHA256 path is
|
|
34
|
+
unchanged.
|
|
35
|
+
- `Capsium::Package::OpenPgp` is the single lazy loader of the rnp
|
|
36
|
+
binding and the key-file loader (armor/format auto-detection,
|
|
37
|
+
`OpenPgp::KeyError` for unreadable or unsuitable keys).
|
|
38
|
+
- The OpenPGP specs skip cleanly when librnp cannot load, so CI
|
|
39
|
+
without librnp stays green; see README "OpenPGP support (librnp)".
|
|
40
|
+
- Encapsulated packages (bundled dependencies): `capsium package pack
|
|
41
|
+
--bundle-deps` (alias `--bundle`; `Packager#pack` option
|
|
42
|
+
`bundle_deps: true`) resolves every declared `metadata.dependencies`
|
|
43
|
+
entry through the store -> registry chain and embeds the resolved
|
|
44
|
+
`.cap` files inside the parent under `packages/`, producing a fully
|
|
45
|
+
self-contained package that activates with no store or registry. The
|
|
46
|
+
`packages/index.json` manifest maps each dependency GUID to its
|
|
47
|
+
embedded file, resolved version and SHA-256
|
|
48
|
+
(`Capsium::Package::Bundle`). Loading resolves bundled dependencies
|
|
49
|
+
FIRST — including from inside a parent `.cap` — before the store ->
|
|
50
|
+
registry chain, and passes the bundle down so re-declared transitive
|
|
51
|
+
dependencies resolve from it (one-level bundling policy: the parent's
|
|
52
|
+
`metadata.dependencies` must list the transitive closure). The
|
|
53
|
+
manifest SHA-256 is re-verified at resolution
|
|
54
|
+
(`Security::IntegrityError` on mismatch) and each bundled package's
|
|
55
|
+
own `security.json`/signature is verified at activation. `pack`
|
|
56
|
+
gained `--store`/`--registry` options; `capsium package validate`
|
|
57
|
+
resolves bundled dependency layers, so fallthrough routes of
|
|
58
|
+
encapsulated packages validate.
|
|
59
|
+
|
|
60
|
+
## 0.5.0
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- Multiple mounted packages per reactor: `capsium reactor serve`
|
|
65
|
+
accepts multiple sources — positional arguments, repeatable
|
|
66
|
+
`--mount PATH=SOURCE` options and/or a JSON mount config
|
|
67
|
+
(`--config FILE`: `{"mounts": [{"path", "source", "store"?}]}`).
|
|
68
|
+
Default mount points: the first source at `/`, each additional one at
|
|
69
|
+
`/<metadata.name>/`; duplicate prefixes raise
|
|
70
|
+
`Capsium::Reactor::MountConflictError`. Requests dispatch by
|
|
71
|
+
longest-prefix mount matching (`Capsium::Reactor::Mount`); the
|
|
72
|
+
`/api/v1/introspect/*` endpoints aggregate ALL mounted packages,
|
|
73
|
+
`/package/<name>/{status,metadata,logs}` resolve by package name
|
|
74
|
+
(404 for unknown names), metrics/logs stay reactor-global, and
|
|
75
|
+
`Capsium::Reactor#cleanup` cleans up every mounted package.
|
|
76
|
+
Single-source invocation is unchanged.
|
|
77
|
+
- Writable packages (ARCHITECTURE.md section 5a): a mounted package
|
|
78
|
+
whose metadata does not declare `"readOnly": true` gets an
|
|
79
|
+
append-only overlay layer in the reactor workdir (`--workdir DIR`,
|
|
80
|
+
default a temporary directory removed on cleanup), always the
|
|
81
|
+
topmost layer of the merged view (`MergedView` gained
|
|
82
|
+
`extra_layers`). The immutable base never changes on disk and every
|
|
83
|
+
write is visible on the next request (hot-swap); overlay state
|
|
84
|
+
(content files, `.capsium-tombstones`, per-dataset JSON operation
|
|
85
|
+
logs) persists in the workdir.
|
|
86
|
+
- REST CRUD over datasets (`Capsium::Reactor::DataApi`):
|
|
87
|
+
`POST /api/v1/data/<dataset>` appends an item (201 + Location +
|
|
88
|
+
stored item; id convention is the `id` field else the 1-based index
|
|
89
|
+
as a string), `GET /<id>` reads one item, `PUT /<id>` replaces and
|
|
90
|
+
`DELETE /<id>` deletes. Statuses: 400 for malformed JSON, 403 with a
|
|
91
|
+
clear body on read-only packages, 404 for unknown datasets/items,
|
|
92
|
+
405 for wrong verbs, 409 for duplicate/mismatched ids, 422 with the
|
|
93
|
+
schema errors when the candidate document violates the dataset's
|
|
94
|
+
JSON schema, 501 for SQLite datasets. Undeclared datasets are not
|
|
95
|
+
served and route-level `accessControl` still applies.
|
|
96
|
+
- Content writes (`Capsium::Reactor::ContentApi`): `PUT <route>`
|
|
97
|
+
creates/overwrites a content file (creating its route on demand;
|
|
98
|
+
text bodies only for v1) and `DELETE <route>` records a tombstone so
|
|
99
|
+
the path 404s even when a lower layer holds the file (404 when
|
|
100
|
+
nothing to delete).
|
|
101
|
+
- GraphQL (new `graphql` dependency): every mounted package with
|
|
102
|
+
datasets answers `<mount>/graphql` (POST, or GET with `?query=`)
|
|
103
|
+
with a schema auto-derived from the package's storage
|
|
104
|
+
(`Capsium::Reactor::GraphqlSchema`): a query field `<dataset>`
|
|
105
|
+
(list) with an optional `id:` argument (single item) plus
|
|
106
|
+
`create<Dataset>`/`update<Dataset>`/`delete<Dataset>` mutations
|
|
107
|
+
matching the REST semantics including schema validation. Item types
|
|
108
|
+
are inferred from the dataset's JSON schema when present, else map
|
|
109
|
+
to a permissive JSON scalar; SQLite datasets are skipped. Not-found
|
|
110
|
+
items, schema violations, duplicates and read-only mutations land in
|
|
111
|
+
the `errors` array (no 500s on user error).
|
|
112
|
+
- Save composite: `POST /package/<name>/save`
|
|
113
|
+
(`Capsium::Reactor::PackageSaver`) folds the base package plus its
|
|
114
|
+
overlay into a NEW versioned `.cap` (`<name>-<version+patch>.cap`)
|
|
115
|
+
in the workdir — overlay content files replace, tombstones delete,
|
|
116
|
+
dataset mutation logs replay into the dataset files, and manifest,
|
|
117
|
+
routes and security.json are regenerated — returning the `.cap`
|
|
118
|
+
path and its SHA-256. The saved package passes
|
|
119
|
+
`capsium package validate`. Read-only packages get 403, unknown
|
|
120
|
+
names 404, non-POST 405.
|
|
121
|
+
|
|
122
|
+
### Changed
|
|
123
|
+
|
|
124
|
+
- `Capsium::Reactor::Introspection.new` now takes the list of served
|
|
125
|
+
packages (aggregation); `/introspect/status` `packagesLoaded`
|
|
126
|
+
reflects the mount count.
|
|
127
|
+
- The reactor dispatch, endpoint and response handling are split into
|
|
128
|
+
`Capsium::Reactor::Serving` (mount request serving),
|
|
129
|
+
`Capsium::Reactor::Endpoints` (reactor-level endpoints) and
|
|
130
|
+
`Capsium::Reactor::Responses` (HTTP response writers).
|
|
131
|
+
- WEBrick's `ProcHandler` (GET/POST/PUT only) is extended with
|
|
132
|
+
`do_DELETE`/`do_PATCH` aliases so the writable-package REST verbs
|
|
133
|
+
reach the mounted procs.
|
|
134
|
+
- `Metrics/ParameterLists` rubocop no longer counts keyword args (the
|
|
135
|
+
reactor constructor takes one keyword per deploy-time concern).
|
|
136
|
+
|
|
137
|
+
## 0.4.0
|
|
138
|
+
|
|
139
|
+
### Added
|
|
140
|
+
|
|
141
|
+
- Static registries (ARCHITECTURE.md section 4a follow-on): a registry
|
|
142
|
+
is a directory or a static https base URL holding an `index.json`
|
|
143
|
+
(`packages` -> GUID -> `name` + `versions` -> version -> `file`,
|
|
144
|
+
`sha256`, `size`) with `.cap` files stored relative to the registry
|
|
145
|
+
root, so any static host (GitHub Pages, S3, nginx) can serve one.
|
|
146
|
+
`Capsium::Registry.fetch(ref)` returns `Registry::Local` (read-write
|
|
147
|
+
directory) or `Registry::Remote` (read-only https base URL over
|
|
148
|
+
net/http with redirect following and timeouts; plain http for
|
|
149
|
+
loopback hosts only). `Registry#resolve` picks the newest version
|
|
150
|
+
satisfying a semver constraint via the built-in
|
|
151
|
+
`Version`/`VersionRange` matcher.
|
|
152
|
+
- `Registry::Local#push`: validates the `.cap` with
|
|
153
|
+
`Capsium::Package::Validator` (`Registry::InvalidPackageError` on
|
|
154
|
+
failure), copies it into the registry directory and atomically
|
|
155
|
+
rewrites `index.json` (tmp + rename) with recomputed sha256 and size.
|
|
156
|
+
- `Registry#install`: fetches the resolved `.cap`, verifies it against
|
|
157
|
+
the sha256 declared in the index (`Registry::ChecksumMismatchError`
|
|
158
|
+
on mismatch) and installs it into the package store as
|
|
159
|
+
`<name>-<version>.cap`, atomically updating the store's own
|
|
160
|
+
`index.json` (`Package::Store#install`). All registry failures are
|
|
161
|
+
typed `Registry::RegistryError` subclasses
|
|
162
|
+
(`RegistryNotConfiguredError`, `InvalidRegistryError`,
|
|
163
|
+
`InvalidPackageError`, `PackageNotFoundError`,
|
|
164
|
+
`UnsatisfiableConstraintError`, `ChecksumMismatchError`,
|
|
165
|
+
`FetchError`).
|
|
166
|
+
- `capsium package push PACKAGE --registry DIR` and
|
|
167
|
+
`capsium install GUID [--constraint RANGE] [--registry DIR_OR_URL]
|
|
168
|
+
[--store DIR]`; the registry defaults to `CAPSIUM_REGISTRY`, the
|
|
169
|
+
store to `CAPSIUM_STORE` (typed errors when unconfigured).
|
|
170
|
+
- `capsium reactor serve capsium://GUID` (install-then-serve with
|
|
171
|
+
`--registry`/`--store`/`--constraint`), and dependency-resolution
|
|
172
|
+
fallback for composite packages: `Package::DependencyResolver` (and
|
|
173
|
+
`Capsium::Package.new`/`Capsium::Reactor.new` through their new
|
|
174
|
+
`registry:` keyword) installs a dependency from the configured
|
|
175
|
+
registry when the store has no package for its GUID — fallback chain
|
|
176
|
+
store -> registry -> typed error (`DependencyNotFoundError` /
|
|
177
|
+
`UnsatisfiableDependencyError`).
|
|
178
|
+
- Reactor-level and per-package introspection endpoints (ARCHITECTURE.md
|
|
179
|
+
section 7 follow-ons), extending `Capsium::Reactor::Introspection`:
|
|
180
|
+
`GET /introspect/status` (`{status, uptime, packagesLoaded}`),
|
|
181
|
+
`GET /introspect/config` (port, store dir, cacheControl, authEnabled,
|
|
182
|
+
registry — deploy.json values and registry URL credentials are never
|
|
183
|
+
exposed) and `GET /introspect/metrics`
|
|
184
|
+
(`{uptime, requestsTotal, requestsByStatus}`) backed by the new
|
|
185
|
+
thread-safe in-memory `Capsium::Reactor::Metrics` counter wired into
|
|
186
|
+
the serving path, plus `GET /package/<name>/status`,
|
|
187
|
+
`GET /package/<name>/metadata` and `GET /package/<name>/logs` (the
|
|
188
|
+
reactor serves one package; any other name is a 404). `logs` returns
|
|
189
|
+
the last N lines (`?lines=N`, default 100) from the new
|
|
190
|
+
`Capsium::LogBuffer`, a small thread-safe ring buffer recording key
|
|
191
|
+
serving events (reactor start, package reload, one line per served
|
|
192
|
+
request). All endpoints are GET-only JSON (405 otherwise) and gated
|
|
193
|
+
when `authentication.json` enables authentication.
|
|
194
|
+
|
|
195
|
+
### Changed
|
|
196
|
+
|
|
197
|
+
- `Metrics/ClassLength` rubocop budget raised to 150 for the reactor
|
|
198
|
+
and introspection classes grown by the phase-3 endpoints.
|
|
199
|
+
|
|
3
200
|
## 0.3.0
|
|
4
201
|
|
|
5
202
|
### Added
|
data/README.adoc
CHANGED
|
@@ -42,6 +42,23 @@ Alternatively, you can install the gem directly using:
|
|
|
42
42
|
gem install capsium
|
|
43
43
|
----
|
|
44
44
|
|
|
45
|
+
=== OpenPGP support (librnp)
|
|
46
|
+
|
|
47
|
+
OpenPGP signatures and encryption are provided through the `rnp` gem
|
|
48
|
+
(a declared dependency) bound to the *librnp* shared library, which
|
|
49
|
+
must be installed separately:
|
|
50
|
+
|
|
51
|
+
[source,bash]
|
|
52
|
+
----
|
|
53
|
+
brew install rnp # homebrew-core formula, provides librnp
|
|
54
|
+
# or build librnp from source: https://github.com/rnpgp/rnp
|
|
55
|
+
----
|
|
56
|
+
|
|
57
|
+
The binding is loaded lazily, only when an OpenPGP feature is used, so
|
|
58
|
+
the gem works without librnp until then; OpenPGP commands raise a
|
|
59
|
+
typed `Capsium::Package::OpenPgp::OpenPgpUnavailableError` with
|
|
60
|
+
installation guidance when the library is missing.
|
|
61
|
+
|
|
45
62
|
== What is a Capsium package?
|
|
46
63
|
|
|
47
64
|
A Capsium package is a structured collection of content, data, metadata, and
|
|
@@ -105,7 +122,7 @@ Capsium provides a CLI to help you pack and manage your packages.
|
|
|
105
122
|
|
|
106
123
|
[source,bash]
|
|
107
124
|
----
|
|
108
|
-
capsium package pack [--force/-f] path-to-package
|
|
125
|
+
capsium package pack [--force/-f] [--bundle-deps/--bundle] [--store DIR] [--registry REF] path-to-package
|
|
109
126
|
----
|
|
110
127
|
|
|
111
128
|
This command generates `manifest.json`/`routes.json` when absent, writes
|
|
@@ -113,6 +130,15 @@ This command generates `manifest.json`/`routes.json` when absent, writes
|
|
|
113
130
|
directory into `{package-name}-{package-version}.cap` (name and version from
|
|
114
131
|
`metadata.json`).
|
|
115
132
|
|
|
133
|
+
With `--bundle-deps` (alias `--bundle`) a package that declares
|
|
134
|
+
`metadata.dependencies` is packed as an *encapsulated* package: every
|
|
135
|
+
declared dependency is resolved through the store -> registry chain
|
|
136
|
+
(`--store`/`CAPSIUM_STORE`, `--registry`/`CAPSIUM_REGISTRY`) and embedded
|
|
137
|
+
under `packages/`, so the resulting `.cap` activates with no store or
|
|
138
|
+
registry at all (see <<_encapsulated_packages_bundled_dependencies>>).
|
|
139
|
+
A declared dependency that cannot be resolved aborts the pack with a
|
|
140
|
+
typed dependency error.
|
|
141
|
+
|
|
116
142
|
.Sample `pack` command
|
|
117
143
|
====
|
|
118
144
|
[source,bash]
|
|
@@ -208,16 +234,45 @@ run:
|
|
|
208
234
|
openssl dgst -sha256 -verify signature.pub.pem -signature signature.sig payload.bin
|
|
209
235
|
----
|
|
210
236
|
|
|
237
|
+
==== OpenPGP signatures
|
|
238
|
+
|
|
239
|
+
[source,bash]
|
|
240
|
+
----
|
|
241
|
+
capsium package sign path-to-package-or-cap --openpgp --key secret.asc
|
|
242
|
+
----
|
|
243
|
+
|
|
244
|
+
With `--openpgp`, `--key` is an OpenPGP secret key file (armored or
|
|
245
|
+
binary, auto-detected) and the package is signed through librnp with the
|
|
246
|
+
same canonical payload: `signature.sig` holds the armored detached
|
|
247
|
+
OpenPGP signature (SHA-256), the armored public key is embedded as
|
|
248
|
+
`signature.pub.asc`, and `security.json` records the scheme:
|
|
249
|
+
|
|
250
|
+
[source,json]
|
|
251
|
+
----
|
|
252
|
+
{
|
|
253
|
+
"security": {
|
|
254
|
+
"digitalSignatures": {
|
|
255
|
+
"certificateType": "OpenPGP",
|
|
256
|
+
"publicKey": "signature.pub.asc",
|
|
257
|
+
"signatureFile": "signature.sig"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
----
|
|
262
|
+
|
|
211
263
|
=== Verifying a package signature
|
|
212
264
|
|
|
213
265
|
[source,bash]
|
|
214
266
|
----
|
|
215
|
-
capsium package verify-signature path-to-package-or-cap [--cert cert-or-public-key
|
|
267
|
+
capsium package verify-signature path-to-package-or-cap [--cert cert-or-public-key] [--openpgp]
|
|
216
268
|
----
|
|
217
269
|
|
|
218
270
|
Verifies the declared signature against the package contents (using the
|
|
219
271
|
embedded public key, or the given certificate/public key) and exits with
|
|
220
272
|
status 1 when the package is unsigned or the signature does not match.
|
|
273
|
+
The signature scheme is auto-detected from the declared
|
|
274
|
+
`certificateType` (RSA-SHA256/X.509 by default, OpenPGP when recorded);
|
|
275
|
+
`--openpgp` forces OpenPGP verification with an OpenPGP public key.
|
|
221
276
|
Packages that declare a signature are also verified automatically on load —
|
|
222
277
|
a mismatch raises `Capsium::Package::Signer::SignatureMismatchError`, so a
|
|
223
278
|
tampered signed package cannot be served by the reactor.
|
|
@@ -251,22 +306,50 @@ recipient's public key using RSA-OAEP with SHA-256:
|
|
|
251
306
|
}
|
|
252
307
|
----
|
|
253
308
|
|
|
254
|
-
|
|
255
|
-
|
|
309
|
+
==== OpenPGP encryption
|
|
310
|
+
|
|
311
|
+
[source,bash]
|
|
312
|
+
----
|
|
313
|
+
capsium package encrypt path-to-package-or-cap --openpgp --recipient public.asc -o encrypted.cap
|
|
314
|
+
----
|
|
315
|
+
|
|
316
|
+
With `--openpgp`, the DEK is protected as an armored OpenPGP message to
|
|
317
|
+
the recipient's public key (through librnp) instead of the RSA-OAEP
|
|
318
|
+
wrap; the content encryption and layout are identical, and the
|
|
319
|
+
envelope records the key management:
|
|
320
|
+
|
|
321
|
+
[source,json]
|
|
322
|
+
----
|
|
323
|
+
{
|
|
324
|
+
"encryption": {
|
|
325
|
+
"algorithm": "AES-256-GCM",
|
|
326
|
+
"keyManagement": "OpenPGP",
|
|
327
|
+
"message": "<armored OpenPGP message containing the DEK>",
|
|
328
|
+
"iv": "<base64>",
|
|
329
|
+
"authTag": "<base64>"
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
----
|
|
333
|
+
|
|
334
|
+
The OCB alternative mentioned by the standard remains out of scope.
|
|
256
335
|
|
|
257
336
|
=== Decrypting a package
|
|
258
337
|
|
|
259
338
|
[source,bash]
|
|
260
339
|
----
|
|
261
|
-
capsium package decrypt encrypted.cap --private-key private.pem [-o decrypted.cap]
|
|
340
|
+
capsium package decrypt encrypted.cap [--private-key private.pem | --key secret.asc] [--openpgp] [-o decrypted.cap]
|
|
262
341
|
----
|
|
263
342
|
|
|
264
|
-
Decrypts an encrypted package with the recipient's
|
|
343
|
+
Decrypts an encrypted package with the recipient's private key and
|
|
265
344
|
writes the plaintext `.cap` (default output: `<name>-decrypted.cap`).
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
345
|
+
The key management is auto-detected from the envelope — `--private-key`
|
|
346
|
+
for RSA-OAEP, `--key` for an OpenPGP secret key (armored or binary);
|
|
347
|
+
`--openpgp` forces the OpenPGP cipher. Decryption fails with a typed
|
|
348
|
+
error when the key does not match or the ciphertext was tampered with
|
|
349
|
+
(GCM authentication). Loading an encrypted package without a key raises
|
|
350
|
+
`Capsium::Package::Cipher::KeyRequiredError` — the reactor refuses to
|
|
351
|
+
serve it. `Capsium::Package.new(path, decryption_key:)` accepts either
|
|
352
|
+
key format transparently.
|
|
270
353
|
|
|
271
354
|
=== Testing a package
|
|
272
355
|
|
|
@@ -330,14 +413,97 @@ with status 1 when any test fails (or a test definition is invalid).
|
|
|
330
413
|
|
|
331
414
|
[source,bash]
|
|
332
415
|
----
|
|
333
|
-
capsium reactor serve my_package.cap [--port 8864] [--store DIR] [--deploy deploy.json]
|
|
416
|
+
capsium reactor serve my_package.cap [--port 8864] [--store DIR] [--deploy deploy.json] [--workdir DIR]
|
|
417
|
+
capsium reactor serve capsium://example.com/story [--registry DIR_OR_URL] [--store DIR] [--constraint ">=1.0"]
|
|
334
418
|
----
|
|
335
419
|
|
|
336
|
-
The reactor takes a local package directory or `.cap` file
|
|
337
|
-
|
|
420
|
+
The reactor takes a local package directory or `.cap` file, or a
|
|
421
|
+
`capsium://` GUID which is installed from a registry first (see
|
|
422
|
+
"Static registries" below). `--store` (or `CAPSIUM_STORE`)
|
|
338
423
|
resolves composite-package dependencies; `--deploy` (or
|
|
339
424
|
`CAPSIUM_DEPLOY`) points at the reactor-side `deploy.json` with
|
|
340
|
-
authentication secrets (see "Authentication" below)
|
|
425
|
+
authentication secrets (see "Authentication" below); `--registry`
|
|
426
|
+
(or `CAPSIUM_REGISTRY`) supplies the registry for `capsium://` GUIDs
|
|
427
|
+
and for dependency fallback; `--workdir` holds the writable overlays
|
|
428
|
+
and saved packages (default: a temporary directory — see "Writable
|
|
429
|
+
packages" below).
|
|
430
|
+
|
|
431
|
+
=== Serving multiple packages
|
|
432
|
+
|
|
433
|
+
`capsium reactor serve` accepts multiple sources at once: positional
|
|
434
|
+
arguments, repeatable `--mount PATH=SOURCE` options, a JSON mount
|
|
435
|
+
config via `--config`, or any combination:
|
|
436
|
+
|
|
437
|
+
[source,bash]
|
|
438
|
+
----
|
|
439
|
+
capsium reactor serve first.cap second.cap
|
|
440
|
+
capsium reactor serve --mount /=first.cap --mount /api=second.cap
|
|
441
|
+
capsium reactor serve --config mounts.json # {"mounts": [{"path": "/", "source": "first.cap", "store": "..."}]}
|
|
442
|
+
----
|
|
443
|
+
|
|
444
|
+
Default mount points: the first source mounts at `/`, each additional
|
|
445
|
+
source at `/<metadata.name>/`. Requests dispatch to mounts by
|
|
446
|
+
longest-prefix matching, and every package's routes answer below its
|
|
447
|
+
prefix (a package mounted at `/api` serves its `/index.html` route at
|
|
448
|
+
`/api/index.html`). Two mounts claiming the same prefix fail with a
|
|
449
|
+
`MountConflictError`. The introspection endpoints aggregate ALL
|
|
450
|
+
mounted packages, `/package/<name>/...` resolves by name (404 for
|
|
451
|
+
unknown names), and metrics/logs stay reactor-global. On shutdown the
|
|
452
|
+
reactor cleans up every mounted package.
|
|
453
|
+
|
|
454
|
+
=== Writable packages
|
|
455
|
+
|
|
456
|
+
A mounted package whose metadata does not declare `"readOnly": true`
|
|
457
|
+
is writable: the reactor keeps an append-only overlay layer for it in
|
|
458
|
+
the workdir (`--workdir DIR`, default a temporary directory) which is
|
|
459
|
+
always the topmost layer of the merged view (ARCHITECTURE.md section
|
|
460
|
+
5a). The immutable `.cap` never changes on disk, and every write is
|
|
461
|
+
visible on the next request (hot-swap — no restart needed). Overlay
|
|
462
|
+
state persists in the workdir, so a later reactor over the same
|
|
463
|
+
workdir sees it again.
|
|
464
|
+
|
|
465
|
+
Dataset CRUD (JSON) under the dataset's route:
|
|
466
|
+
|
|
467
|
+
[source]
|
|
468
|
+
----
|
|
469
|
+
POST /api/v1/data/<dataset> # append an item => 201 + Location + stored item
|
|
470
|
+
GET /api/v1/data/<dataset>/<id> # one item => 200 (404 when absent)
|
|
471
|
+
PUT /api/v1/data/<dataset>/<id> # replace an item => 200 (404 absent, 422 schema fail)
|
|
472
|
+
DELETE /api/v1/data/<dataset>/<id> # delete an item => 204 (404 absent)
|
|
473
|
+
GET /api/v1/data/<dataset> # the merged collection (as before)
|
|
474
|
+
----
|
|
475
|
+
|
|
476
|
+
Item ids follow the `id` field when present, else the 1-based index as
|
|
477
|
+
a string; a duplicate `id` on POST (or a mismatched one on PUT) is a
|
|
478
|
+
`409`. The request body must satisfy the dataset's JSON schema when
|
|
479
|
+
one is declared (violations are `422` with the schema errors);
|
|
480
|
+
malformed JSON is `400`. Writes on a `"readOnly": true` package are
|
|
481
|
+
`403` with a clear body; writes on a SQLite dataset are `501`;
|
|
482
|
+
wrong verbs are `405`. Mutations persist as a per-dataset JSON
|
|
483
|
+
operation log in the overlay.
|
|
484
|
+
|
|
485
|
+
Content writes work on any route path (text bodies for v1):
|
|
486
|
+
|
|
487
|
+
[source]
|
|
488
|
+
----
|
|
489
|
+
PUT /<route> # create/overwrite a content file (route created on demand) => 200
|
|
490
|
+
DELETE /<route> # tombstone: the path 404s even if a lower layer has it => 204
|
|
491
|
+
----
|
|
492
|
+
|
|
493
|
+
Every mounted package with datasets also answers GraphQL at
|
|
494
|
+
`<mount>/graphql` (POST, or GET with `?query=`): the schema is derived
|
|
495
|
+
from the package's datasets — a query field `<dataset>` (list) with an
|
|
496
|
+
optional `id:` argument (single item) plus `create<Dataset>`,
|
|
497
|
+
`update<Dataset>` and `delete<Dataset>` mutations matching the REST
|
|
498
|
+
semantics. Item types are inferred from the dataset's JSON schema when
|
|
499
|
+
present, else map to a permissive JSON scalar; SQLite datasets are
|
|
500
|
+
skipped. Not-found items, schema violations and read-only mutations
|
|
501
|
+
land in the GraphQL `errors` array (never a 500).
|
|
502
|
+
|
|
503
|
+
`POST /package/<name>/save` folds base plus overlays into a NEW
|
|
504
|
+
versioned `.cap` (`<name>-<version+patch>.cap`) in the workdir and
|
|
505
|
+
returns its path and SHA-256; the saved package passes
|
|
506
|
+
`capsium package validate`.
|
|
341
507
|
|
|
342
508
|
=== Reactor introspection API
|
|
343
509
|
|
|
@@ -365,6 +531,29 @@ timestamp; `reason` lists the integrity errors when `valid` is false.
|
|
|
365
531
|
The entry also reports `signed` and `encrypted` status, plus
|
|
366
532
|
`signatureValid` when the package declares a signature.
|
|
367
533
|
|
|
534
|
+
The reactor also answers reactor-level and per-package introspection
|
|
535
|
+
endpoints (07-reactor follow-ons), likewise GET-only JSON:
|
|
536
|
+
|
|
537
|
+
[source]
|
|
538
|
+
----
|
|
539
|
+
GET /introspect/status # => {"status": "running", "uptime": <seconds>, "packagesLoaded": 1}
|
|
540
|
+
GET /introspect/config # => {"port", "storeDir", "cacheControl", "authEnabled", "registry"}
|
|
541
|
+
GET /introspect/metrics # => {"uptime", "requestsTotal", "requestsByStatus": {"200": n, ...}}
|
|
542
|
+
GET /package/<name>/status # => {"package", "version", "status": "loaded", "valid": true}
|
|
543
|
+
GET /package/<name>/metadata # => {"name", "version", "description", "author", "guid"}
|
|
544
|
+
GET /package/<name>/logs[?lines=N] # => {"package", "logs": ["<utc> GET / -> 200", ...]}
|
|
545
|
+
----
|
|
546
|
+
|
|
547
|
+
`/introspect/config` never exposes secrets (deploy.json values, or
|
|
548
|
+
credentials embedded in a registry URL, are redacted). With multiple
|
|
549
|
+
mounted packages the `/api/v1/introspect/*` endpoints aggregate all of
|
|
550
|
+
them, and `/package/<name>/...` resolves by name (404 for unknown
|
|
551
|
+
names). `logs` returns the last N lines (default 100) from a small
|
|
552
|
+
in-memory ring buffer (`Capsium::LogBuffer`) recording key serving
|
|
553
|
+
events; `metrics` counts requests by status in memory
|
|
554
|
+
(`Capsium::Reactor::Metrics`). When authentication is enabled these
|
|
555
|
+
endpoints are gated like any other route.
|
|
556
|
+
|
|
368
557
|
== Layered storage
|
|
369
558
|
|
|
370
559
|
`storage.layers` stacks overlay directories over `content/` (bottom ->
|
|
@@ -415,6 +604,118 @@ capsium package info my-composite-package --store ./store
|
|
|
415
604
|
capsium reactor serve my-composite-package --store ./store
|
|
416
605
|
----
|
|
417
606
|
|
|
607
|
+
[[_encapsulated_packages_bundled_dependencies]]
|
|
608
|
+
== Encapsulated packages (bundled dependencies)
|
|
609
|
+
|
|
610
|
+
`capsium package pack --bundle-deps` (alias `--bundle`) produces a
|
|
611
|
+
*self-contained* package: the resolved dependency `.cap` files are
|
|
612
|
+
embedded inside the parent, so the whole tree activates with no store
|
|
613
|
+
and no registry. Layout inside the `.cap`:
|
|
614
|
+
|
|
615
|
+
----
|
|
616
|
+
packages/
|
|
617
|
+
index.json # bundled-dependencies manifest
|
|
618
|
+
<name>-<version>.cap # one per declared dependency
|
|
619
|
+
----
|
|
620
|
+
|
|
621
|
+
`packages/index.json` maps each dependency GUID to its embedded file,
|
|
622
|
+
the resolved version and the SHA-256 of the embedded `.cap`:
|
|
623
|
+
|
|
624
|
+
[source,json]
|
|
625
|
+
----
|
|
626
|
+
{
|
|
627
|
+
"https://example.com/capsiums/base-package": {
|
|
628
|
+
"file": "packages/base-package-1.2.0.cap",
|
|
629
|
+
"version": "1.2.0",
|
|
630
|
+
"sha256": "<hex sha256 of the .cap>"
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
----
|
|
634
|
+
|
|
635
|
+
Resolution order on load is *bundle first*: a dependency GUID found in
|
|
636
|
+
`packages/index.json` resolves to the embedded `.cap` (this also works
|
|
637
|
+
when the parent itself was loaded from a `.cap` — the embedded files
|
|
638
|
+
are read out of the parent's extraction); anything not bundled falls
|
|
639
|
+
back to the usual store -> registry chain. The bundle is passed down
|
|
640
|
+
to the dependencies' own resolution, so a dependency's dependency
|
|
641
|
+
resolves from the parent's bundle when the parent re-declares it.
|
|
642
|
+
|
|
643
|
+
Bundling follows a *one-level policy*: only the dependencies declared
|
|
644
|
+
in the parent's own `metadata.dependencies` are embedded. To make a
|
|
645
|
+
whole tree self-contained, the parent's `metadata.dependencies` must
|
|
646
|
+
list the transitive closure; a bundled dependency whose own
|
|
647
|
+
dependencies are not re-declared by the parent still needs a store or
|
|
648
|
+
registry at activation.
|
|
649
|
+
|
|
650
|
+
Security: bundled `.cap` files are covered by the parent package's
|
|
651
|
+
`security.json` checksums like every other file; the manifest SHA-256
|
|
652
|
+
is re-verified when a bundled dependency is resolved
|
|
653
|
+
(`Security::IntegrityError` on mismatch), the bundled version must
|
|
654
|
+
still satisfy the declared range, and each bundled package's own
|
|
655
|
+
`security.json` (and any declared signature) is verified when it is
|
|
656
|
+
loaded — the usual activation checks. Serving (layers, visibility,
|
|
657
|
+
route inheritance) is unchanged: bundled sources behave exactly like
|
|
658
|
+
store-resolved ones.
|
|
659
|
+
|
|
660
|
+
== Static registries
|
|
661
|
+
|
|
662
|
+
A *registry* is a directory or a static https base URL holding an
|
|
663
|
+
`index.json` plus `.cap` files stored relative to the registry root,
|
|
664
|
+
so any static host (GitHub Pages, S3, nginx) can serve one:
|
|
665
|
+
|
|
666
|
+
[source,json]
|
|
667
|
+
----
|
|
668
|
+
{
|
|
669
|
+
"packages": {
|
|
670
|
+
"https://github.com/capsiums/cap-story": {
|
|
671
|
+
"name": "story-of-claire",
|
|
672
|
+
"versions": {
|
|
673
|
+
"1.0.0": {
|
|
674
|
+
"file": "story-of-claire-1.0.0.cap",
|
|
675
|
+
"sha256": "<hex sha256 of the .cap>",
|
|
676
|
+
"size": 642047
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
----
|
|
683
|
+
|
|
684
|
+
`Capsium::Registry.fetch(ref)` returns the implementation for a
|
|
685
|
+
reference: `Registry::Local` for a directory (read-write) or
|
|
686
|
+
`Registry::Remote` for an https base URL (read-only; plain http is
|
|
687
|
+
accepted for loopback hosts only). Both resolve a GUID to the newest
|
|
688
|
+
version satisfying a semver constraint and install it: the `.cap` is
|
|
689
|
+
fetched, verified against the sha256 declared in the index
|
|
690
|
+
(`Registry::ChecksumMismatchError` on mismatch) and recorded in the
|
|
691
|
+
package store as `<name>-<version>.cap` with an updated store
|
|
692
|
+
`index.json`. Failures are typed (`RegistryError` subclasses):
|
|
693
|
+
`RegistryNotConfiguredError`, `InvalidRegistryError`,
|
|
694
|
+
`InvalidPackageError`, `PackageNotFoundError`,
|
|
695
|
+
`UnsatisfiableConstraintError`, `ChecksumMismatchError`, `FetchError`.
|
|
696
|
+
|
|
697
|
+
[source,bash]
|
|
698
|
+
----
|
|
699
|
+
# Validate a .cap, copy it into a registry directory and record it in
|
|
700
|
+
# index.json (atomically rewritten; sha256+size recomputed):
|
|
701
|
+
capsium package push story-of-claire-1.0.0.cap --registry ./registry
|
|
702
|
+
|
|
703
|
+
# Resolve, download, sha256-verify and install into the package store:
|
|
704
|
+
capsium install https://github.com/capsiums/cap-story \
|
|
705
|
+
[--constraint ">=1.0"] [--registry DIR_OR_URL] [--store DIR]
|
|
706
|
+
|
|
707
|
+
# Install-then-serve a capsium:// GUID:
|
|
708
|
+
capsium reactor serve capsium://example.com/story \
|
|
709
|
+
[--registry DIR_OR_URL] [--store DIR] [--constraint ">=1.0"]
|
|
710
|
+
----
|
|
711
|
+
|
|
712
|
+
The registry comes from `--registry` or `CAPSIUM_REGISTRY` (a typed
|
|
713
|
+
error when neither is set); the store from `--store` or
|
|
714
|
+
`CAPSIUM_STORE`. Composite-package dependency resolution uses the same
|
|
715
|
+
fallback chain: when the store has no package for a dependency GUID,
|
|
716
|
+
the resolver installs it from the configured registry into the store
|
|
717
|
+
(store -> registry -> typed error).
|
|
718
|
+
|
|
418
719
|
== Authentication
|
|
419
720
|
|
|
420
721
|
`authentication.json` enables user authentication (05x-authentication):
|
|
@@ -620,6 +921,14 @@ bundle exec rubocop
|
|
|
620
921
|
bundle exec rbs -I sig validate
|
|
621
922
|
----
|
|
622
923
|
|
|
924
|
+
The OpenPGP specs run only when librnp is available (`brew install rnp`,
|
|
925
|
+
or built from https://github.com/rnpgp/rnp); without it they skip
|
|
926
|
+
cleanly, so environments without librnp (e.g. CI images that do not
|
|
927
|
+
install it) stay green. The CI workflow is the shared
|
|
928
|
+
cimas/metanorma `generic-rake` workflow and does not install librnp —
|
|
929
|
+
install it in a custom workflow step (macOS: `brew install rnp`) to run
|
|
930
|
+
the OpenPGP specs there.
|
|
931
|
+
|
|
623
932
|
== License
|
|
624
933
|
|
|
625
934
|
Copyright Ribose.
|
data/capsium.gemspec
CHANGED
|
@@ -35,11 +35,13 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
|
|
36
36
|
spec.add_dependency "bcrypt"
|
|
37
37
|
spec.add_dependency "csv"
|
|
38
|
+
spec.add_dependency "graphql"
|
|
38
39
|
spec.add_dependency "json"
|
|
39
40
|
spec.add_dependency "json-schema"
|
|
40
41
|
spec.add_dependency "listen"
|
|
41
42
|
spec.add_dependency "lutaml-model", ">= 0.8", "< 1.0"
|
|
42
43
|
spec.add_dependency "marcel"
|
|
44
|
+
spec.add_dependency "rnp", ">= 1.0"
|
|
43
45
|
spec.add_dependency "rubyzip", ">= 2.3.2", "< 4.0"
|
|
44
46
|
spec.add_dependency "sqlite3"
|
|
45
47
|
spec.add_dependency "thor"
|