From 0ce13cf04314b99e40f737a3de6c168de56aa33e Mon Sep 17 00:00:00 2001
From: Lee Hannigan <29015211+LeeroyHannigan@users.noreply.github.com>
Date: Tue, 10 Nov 2026 13:55:36 +0000
Subject: [PATCH] build: make the workspace crates packageable for crates.io
All 14 workspace crates now pass `cargo package`, the step before a
publish to crates.io. This change publishes nothing.
- Each internal entry of `[workspace.dependencies]` requires the
workspace version exactly, as in `version = "=0.1.14"`. The crates
change their APIs between patch releases. An exact pin stops a fresh
install from mixing crates of two releases.
- Internal dev-dependencies use a path and no version, so cargo removes
them from the published manifests. Packaging then no longer stops at
the dev-dependency cycle between extenddb-engine and
extenddb-storage-sqlite.
- The workspace uses `resolver = "3"`. On Rust 1.88, our minimum
version, resolver 2 picked uuid 1.29.2 for an install without
`--locked`. That version needs Rust 1.89. Resolver 3 picks uuid
1.26.1, and the build passes.
- Each crate has a description, a repository and a homepage. The
extenddb crate also has a README for its crates.io page, keywords and
categories.
- Each crate directory links LICENSE and NOTICE to the root files. This
change regenerates the four license notices, and they list the
workspace crates.
- Cargo.lock moves spin to 0.9.9 and chacha20 to 0.10.2. crates.io lists
the old versions as yanked.
- cdrs-tokio stays on 9.0.x and cassandra-protocol on 4.0.x.
cassandra-protocol 4.1.0 uses num-bigint 0.5 for its public BigInt,
and the Cassandra backend then fails to compile. To hold the bound,
extenddb-storage-cassandra now depends on cassandra-protocol directly.
- crates/bin/build.rs reads the packaged commit from
.cargo_vcs_info.json. A build from crates.io then reports its commit
instead of "unknown".
- The new devtools/check-crate-packages checks the pins, the
dev-dependencies, the license files and the metadata. It also checks
the field rules and size limits that crates.io applies at upload. A
new `package` job in the Test workflow runs it, and the required
`test` gate needs that job.
- The backup-store README and config.rs drop one S3 server from their
examples.
- ADR-0029 records the decision. CONTRIBUTING.md describes the
dependency rules, and packaging/README.md adds the pins to the release
bump.
---
.github/workflows/test.yml | 27 +-
CONTRIBUTING.md | 16 +
Cargo.lock | 13 +-
Cargo.toml | 53 +++-
SOFTWARE-LICENSE-NOTICES-CASSANDRA.html | 232 +++++++++++++-
SOFTWARE-LICENSE-NOTICES-DEV.html | 232 +++++++++++++-
SOFTWARE-LICENSE-NOTICES-MONGODB.html | 232 +++++++++++++-
SOFTWARE-LICENSE-NOTICES.html | 230 +++++++++++++-
crates/app/Cargo.toml | 3 +
crates/app/LICENSE | 1 +
crates/app/NOTICE | 1 +
crates/auth/Cargo.toml | 5 +-
crates/auth/LICENSE | 1 +
crates/auth/NOTICE | 1 +
crates/backup-store/Cargo.toml | 4 +-
crates/backup-store/LICENSE | 1 +
crates/backup-store/NOTICE | 1 +
crates/backup-store/README.md | 2 +-
crates/backup-store/src/config.rs | 2 +-
crates/bin/Cargo.toml | 8 +-
crates/bin/LICENSE | 1 +
crates/bin/NOTICE | 1 +
crates/bin/README.md | 91 ++++++
crates/bin/build.rs | 39 ++-
crates/bin/src/main.rs | 2 +-
crates/cache/Cargo.toml | 4 +-
crates/cache/LICENSE | 1 +
crates/cache/NOTICE | 1 +
crates/config/Cargo.toml | 3 +
crates/config/LICENSE | 1 +
crates/config/NOTICE | 1 +
crates/core/Cargo.toml | 3 +
crates/core/LICENSE | 1 +
crates/core/NOTICE | 1 +
crates/engine/Cargo.toml | 5 +-
crates/engine/LICENSE | 1 +
crates/engine/NOTICE | 1 +
crates/server/Cargo.toml | 9 +-
crates/server/LICENSE | 1 +
crates/server/NOTICE | 1 +
crates/server/src/serve.rs | 4 +-
crates/storage-cassandra/Cargo.toml | 8 +-
crates/storage-cassandra/LICENSE | 1 +
crates/storage-cassandra/NOTICE | 1 +
crates/storage-mongodb/Cargo.toml | 5 +-
crates/storage-mongodb/LICENSE | 1 +
crates/storage-mongodb/NOTICE | 1 +
crates/storage-postgres/Cargo.toml | 9 +-
crates/storage-postgres/LICENSE | 1 +
crates/storage-postgres/NOTICE | 1 +
crates/storage-sqlite/Cargo.toml | 9 +-
crates/storage-sqlite/LICENSE | 1 +
crates/storage-sqlite/NOTICE | 1 +
crates/storage/Cargo.toml | 3 +
crates/storage/LICENSE | 1 +
crates/storage/NOTICE | 1 +
devtools/check-crate-packages | 267 ++++++++++++++++
devtools/check_crate_packages_test.py | 396 ++++++++++++++++++++++++
docs/adr/0029-crates-io-packaging.md | 136 ++++++++
docs/adr/README.md | 1 +
packaging/README.md | 11 +-
61 files changed, 1993 insertions(+), 99 deletions(-)
create mode 120000 crates/app/LICENSE
create mode 120000 crates/app/NOTICE
create mode 120000 crates/auth/LICENSE
create mode 120000 crates/auth/NOTICE
create mode 120000 crates/backup-store/LICENSE
create mode 120000 crates/backup-store/NOTICE
create mode 120000 crates/bin/LICENSE
create mode 120000 crates/bin/NOTICE
create mode 100644 crates/bin/README.md
create mode 120000 crates/cache/LICENSE
create mode 120000 crates/cache/NOTICE
create mode 120000 crates/config/LICENSE
create mode 120000 crates/config/NOTICE
create mode 120000 crates/core/LICENSE
create mode 120000 crates/core/NOTICE
create mode 120000 crates/engine/LICENSE
create mode 120000 crates/engine/NOTICE
create mode 120000 crates/server/LICENSE
create mode 120000 crates/server/NOTICE
create mode 120000 crates/storage-cassandra/LICENSE
create mode 120000 crates/storage-cassandra/NOTICE
create mode 120000 crates/storage-mongodb/LICENSE
create mode 120000 crates/storage-mongodb/NOTICE
create mode 120000 crates/storage-postgres/LICENSE
create mode 120000 crates/storage-postgres/NOTICE
create mode 120000 crates/storage-sqlite/LICENSE
create mode 120000 crates/storage-sqlite/NOTICE
create mode 120000 crates/storage/LICENSE
create mode 120000 crates/storage/NOTICE
create mode 100755 devtools/check-crate-packages
create mode 100644 devtools/check_crate_packages_test.py
create mode 100644 docs/adr/0029-crates-io-packaging.md
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ef1b68230..b951c035e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -105,13 +105,36 @@ jobs:
if: failure()
run: cat moto.log || true
+ # crates.io accepts only what `cargo package` can write. Each dependency
+ # needs a version, and the dev-dependency cycle between extenddb-engine and
+ # extenddb-storage-sqlite must stay out of the published manifests.
+ # --no-verify writes the packages without compiling them, in seconds, and
+ # runs no build script. devtools/check-crate-packages then checks the pins,
+ # the LICENSE and NOTICE files, the metadata and crates.io's size limits.
+ # Compiling each package from its tarball takes minutes. That build belongs
+ # to the crates.io release workflow (follow-up work), before the upload, and
+ # not to each PR.
+ package:
+ runs-on: extenddb_ubuntu-2404_4-core
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ with:
+ persist-credentials: false
+ - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master
+ with:
+ toolchain: stable
+ - run: cargo package --workspace --no-verify --locked
+ - run: devtools/check-crate-packages
+
test:
runs-on: extenddb_ubuntu-2404_4-core
timeout-minutes: 15
- needs: run-tests
+ needs: [run-tests, package]
if: always()
steps:
- run: |
- if [ "${{ needs.run-tests.result }}" != "success" ]; then
+ if [ "${{ needs.run-tests.result }}" != "success" ] \
+ || [ "${{ needs.package.result }}" != "success" ]; then
exit 1
fi
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e321d02e0..f32f5ec26 100755
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -82,6 +82,22 @@ cargo clippy -- -W clippy::pedantic -W clippy::unwrap_used -W clippy::expect_use
- Address all clippy warnings or add `#[allow(...)]` with a justification comment.
- Avoid `.unwrap()` and `.expect()` in library crates — use `?` or explicit error handling.
+### Dependencies between workspace crates
+
+A crate depends on another workspace crate with `{ workspace = true }`. The
+root `[workspace.dependencies]` pins each workspace crate to the workspace
+version exactly (`=X.Y.Z`), because the crates publish to crates.io together
+([ADR-0029](docs/adr/0029-crates-io-packaging.md)). Declare a dev-dependency
+on a workspace crate by path with no version, for example
+`extenddb-storage = { path = "../storage", features = ["test-util"] }`, so
+that cargo drops it from the published manifest. The `package` job of the
+Test workflow checks both rules. To run the same check locally, on a tree
+with uncommitted changes too (the script needs Python 3.11 or later):
+
+```bash
+cargo package --workspace --no-verify --locked --allow-dirty && devtools/check-crate-packages
+```
+
## When you need an ADR or an RFC
ExtendDB uses two lightweight processes to keep core decisions reviewable:
diff --git a/Cargo.lock b/Cargo.lock
index faabdd021..c052ecd60 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1016,9 +1016,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
@@ -1869,6 +1869,7 @@ dependencies = [
"base64 0.22.1",
"bcrypt",
"bigdecimal",
+ "cassandra-protocol",
"cdrs-tokio",
"chrono",
"crc32fast",
@@ -2018,7 +2019,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"futures-core",
"futures-sink",
- "spin 0.9.8",
+ "spin 0.9.9",
]
[[package]]
@@ -2865,7 +2866,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
- "spin 0.9.8",
+ "spin 0.9.9",
]
[[package]]
@@ -4214,9 +4215,9 @@ dependencies = [
[[package]]
name = "spin"
-version = "0.9.8"
+version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
dependencies = [
"lock_api",
]
diff --git a/Cargo.toml b/Cargo.toml
index 2ee723c20..9c0c459f0 100755
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,12 @@
# Copyright 2026 ExtendDB contributors
# SPDX-License-Identifier: Apache-2.0
[workspace]
-resolver = "2"
+# Resolver 3 prefers dependency versions whose rust-version fits ours. The
+# published crates use it too. (With resolver 2, cargo writes
+# `resolver = "2"` into each of them.) Without `--locked`, `cargo install`
+# then resolves for Rust 1.88 and does not fail on a dependency that needs a
+# newer Rust.
+resolver = "3"
members = [
"crates/core",
"crates/cache",
@@ -24,22 +29,32 @@ version = "0.1.14"
edition = "2024"
rust-version = "1.88"
license = "Apache-2.0"
+repository = "https://github.com/ExtendDB/extenddb"
+homepage = "https://extenddb.org/"
[workspace.dependencies]
-# Internal crates
-extenddb-core = { path = "crates/core" }
-extenddb-cache = { path = "crates/cache" }
-extenddb-backup-store = { path = "crates/backup-store" }
-extenddb-engine = { path = "crates/engine" }
-extenddb-storage = { path = "crates/storage" }
-extenddb-config = { path = "crates/config" }
-extenddb-storage-cassandra = { path = "crates/storage-cassandra" }
-extenddb-storage-mongodb = { path = "crates/storage-mongodb" }
-extenddb-storage-postgres = { path = "crates/storage-postgres" }
-extenddb-storage-sqlite = { path = "crates/storage-sqlite" }
-extenddb-auth = { path = "crates/auth" }
-extenddb-server = { path = "crates/server" }
-extenddb-app = { path = "crates/app" }
+# Internal crates. crates.io needs a version on each of them. Each version is
+# an exact pin to the workspace version, because the crates change their APIs
+# between patch releases. A crate of one release must never resolve against a
+# crate of another. A release bump changes each pin together with `version`
+# above. A pin left at the old version makes every cargo command that
+# resolves dependencies fail, and `devtools/check-crate-packages` fails on a
+# pin that is not exact.
+# Internal dev-dependencies are declared by path in each crate instead, with
+# no version, so that cargo drops them from the published manifests.
+extenddb-core = { path = "crates/core", version = "=0.1.14" }
+extenddb-cache = { path = "crates/cache", version = "=0.1.14" }
+extenddb-backup-store = { path = "crates/backup-store", version = "=0.1.14" }
+extenddb-engine = { path = "crates/engine", version = "=0.1.14" }
+extenddb-storage = { path = "crates/storage", version = "=0.1.14" }
+extenddb-config = { path = "crates/config", version = "=0.1.14" }
+extenddb-storage-cassandra = { path = "crates/storage-cassandra", version = "=0.1.14" }
+extenddb-storage-mongodb = { path = "crates/storage-mongodb", version = "=0.1.14" }
+extenddb-storage-postgres = { path = "crates/storage-postgres", version = "=0.1.14" }
+extenddb-storage-sqlite = { path = "crates/storage-sqlite", version = "=0.1.14" }
+extenddb-auth = { path = "crates/auth", version = "=0.1.14" }
+extenddb-server = { path = "crates/server", version = "=0.1.14" }
+extenddb-app = { path = "crates/app", version = "=0.1.14" }
# Serialization
serde = { version = "1", features = ["derive"] }
@@ -70,7 +85,13 @@ urlencoding = { version = "2.1" }
moka = { version = "0.12", features = ["future"] }
# Database
-cdrs-tokio = { version = "9.0", features = ["derive"] }
+# cdrs-tokio stays on 9.0.x and cassandra-protocol on 4.0.x. cassandra-protocol
+# 4.1.0 moved its public BigInt to num-bigint 0.5, while bigdecimal 0.4 uses
+# num-bigint 0.4. extenddb-storage-cassandra then fails to compile. The pins
+# keep a fresh resolution, such as `cargo install` without `--locked`, on
+# versions that build.
+cdrs-tokio = { version = "~9.0.2", features = ["derive"] }
+cassandra-protocol = "~4.0.1"
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls-aws-lc-rs", "postgres", "json", "time", "uuid", "bigdecimal", "derive"] }
# SQLite's C API, for the online backup API that sqlx does not expose. The
# version is the one sqlx-sqlite depends on: the crate declares
diff --git a/SOFTWARE-LICENSE-NOTICES-CASSANDRA.html b/SOFTWARE-LICENSE-NOTICES-CASSANDRA.html
index 8a5f1b144..73ced0490 100644
--- a/SOFTWARE-LICENSE-NOTICES-CASSANDRA.html
+++ b/SOFTWARE-LICENSE-NOTICES-CASSANDRA.html
@@ -1939,6 +1939,225 @@
Used by:
Copyright [yyyy] [name of copyright owner]
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+
+ Apache License 2.0
+ Used by:
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright ©2026 Amazon.com Inc. or Affiliates (“Amazon”).
+
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -6261,7 +6480,7 @@ Used by:
+
+ Apache License 2.0
+ Used by:
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright ©2026 Amazon.com Inc. or Affiliates (“Amazon”).
+
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -7720,17 +7939,6 @@ Used by:
Apache License 2.0
Used by:
The MIT License (MIT)
diff --git a/SOFTWARE-LICENSE-NOTICES-MONGODB.html b/SOFTWARE-LICENSE-NOTICES-MONGODB.html
index 7920a20c8..ac2ff9fe8 100644
--- a/SOFTWARE-LICENSE-NOTICES-MONGODB.html
+++ b/SOFTWARE-LICENSE-NOTICES-MONGODB.html
@@ -1732,6 +1732,225 @@ Used by:
Copyright [yyyy] [name of copyright owner]
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+
+ Apache License 2.0
+ Used by:
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright ©2026 Amazon.com Inc. or Affiliates (“Amazon”).
+
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -6061,7 +6280,7 @@ Used by:
block-buffer 0.10.4
block-buffer 0.12.1
blowfish 0.9.1
- chacha20 0.10.1
+ chacha20 0.10.2
cipher 0.4.4
const-oid 0.10.2
cpufeatures 0.2.17
@@ -7538,17 +7757,6 @@ Used by:
Apache License 2.0
Used by:
- - extenddb-app 0.1.14
- - extenddb-auth 0.1.14
- - extenddb-backup-store 0.1.14
- - extenddb 0.1.14
- - extenddb-cache 0.1.14
- - extenddb-config 0.1.14
- - extenddb-core 0.1.14
- - extenddb-engine 0.1.14
- - extenddb-server 0.1.14
- - extenddb-storage 0.1.14
- - extenddb-storage-mongodb 0.1.14
- allocator-api2 0.2.21
- anyhow 1.0.103
- arraydeque 0.5.1
diff --git a/SOFTWARE-LICENSE-NOTICES.html b/SOFTWARE-LICENSE-NOTICES.html
index 9103a0534..1b429178d 100644
--- a/SOFTWARE-LICENSE-NOTICES.html
+++ b/SOFTWARE-LICENSE-NOTICES.html
@@ -1730,6 +1730,225 @@ Used by:
Copyright [yyyy] [name of copyright owner]
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+ -
+
Apache License 2.0
+ Used by:
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright ©2026 Amazon.com Inc. or Affiliates (“Amazon”).
+
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -7302,17 +7521,6 @@ Used by:
Apache License 2.0
Used by:
- - extenddb-app 0.1.14
- - extenddb-auth 0.1.14
- - extenddb-backup-store 0.1.14
- - extenddb 0.1.14
- - extenddb-cache 0.1.14
- - extenddb-config 0.1.14
- - extenddb-core 0.1.14
- - extenddb-engine 0.1.14
- - extenddb-server 0.1.14
- - extenddb-storage 0.1.14
- - extenddb-storage-postgres 0.1.14
- allocator-api2 0.2.21
- anyhow 1.0.103
- arraydeque 0.5.1
diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml
index fef29af1e..a31527d90 100644
--- a/crates/app/Cargo.toml
+++ b/crates/app/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "Command-line interface of the ExtendDB server, shared by every backend build. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[features]
# Developer mode: plain HTTP on loopback, open authorization (SigV4 still
diff --git a/crates/app/LICENSE b/crates/app/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/app/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/app/NOTICE b/crates/app/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/app/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml
index a71126ef2..b936b9e52 100755
--- a/crates/auth/Cargo.toml
+++ b/crates/auth/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "SigV4 authentication and IAM-compatible authorization of the ExtendDB server. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[dependencies]
extenddb-core = { workspace = true }
@@ -23,7 +26,7 @@ tracing = { workspace = true }
zeroize = { workspace = true }
[dev-dependencies]
-extenddb-cache = { workspace = true, features = ["test-util"] }
+extenddb-cache = { path = "../cache", features = ["test-util"] }
tokio = { workspace = true, features = ["test-util", "macros", "rt-multi-thread"] }
# The SigV4 differential tests (tests/sigv4_differential) sign requests with
# the SDK's signer and generate them from a fixed seed.
diff --git a/crates/auth/LICENSE b/crates/auth/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/auth/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/auth/NOTICE b/crates/auth/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/auth/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/backup-store/Cargo.toml b/crates/backup-store/Cargo.toml
index 12723cb46..73be71b87 100644
--- a/crates/backup-store/Cargo.toml
+++ b/crates/backup-store/Cargo.toml
@@ -6,7 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
-description = "Backup object store abstraction with filesystem and S3 implementations."
+description = "Backup object store of the ExtendDB server, with filesystem and S3 implementations. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[dependencies]
aws-config = { workspace = true }
diff --git a/crates/backup-store/LICENSE b/crates/backup-store/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/backup-store/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/backup-store/NOTICE b/crates/backup-store/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/backup-store/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/backup-store/README.md b/crates/backup-store/README.md
index ae741ecfa..d1192bb95 100644
--- a/crates/backup-store/README.md
+++ b/crates/backup-store/README.md
@@ -349,7 +349,7 @@ AWS_REGION=us-east-1 \
cargo test -p extenddb-backup-store
```
-Another S3-compatible server (MinIO built from source, Garage, LocalStack)
+Another S3-compatible server (MinIO built from source, or Garage)
should work the same way through the endpoint and credential variables,
but none has been run; see the checksum note in "Behavior". What moto
cannot produce is synthesised in the suite (a 301, a 403 with its error
diff --git a/crates/backup-store/src/config.rs b/crates/backup-store/src/config.rs
index 927329603..59bcc0da9 100644
--- a/crates/backup-store/src/config.rs
+++ b/crates/backup-store/src/config.rs
@@ -22,7 +22,7 @@
//! bucket = "my-extenddb-backups" # required when kind = "s3"; must not be empty
//! prefix = "extenddb/" # optional; at most 512 bytes
//! region = "us-east-1" # optional; SDK chain otherwise
-//! endpoint = "http://127.0.0.1:9000" # optional; moto, Garage, LocalStack, or any S3-compatible endpoint
+//! endpoint = "http://127.0.0.1:9000" # optional; moto, Garage, or any S3-compatible endpoint
//! allow_insecure_endpoint = false # an http:// endpoint must be loopback unless true
//! force_path_style = true # false when absent; an http:// endpoint needs true
//! upload_part_size_bytes = 8388608 # 5 MiB to 5 GiB (S3's part size limits)
diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml
index df58be389..f1c1148c0 100755
--- a/crates/bin/Cargo.toml
+++ b/crates/bin/Cargo.toml
@@ -6,6 +6,12 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "A DynamoDB-compatible API server that stores data in PostgreSQL, MongoDB, Cassandra or SQLite."
+repository.workspace = true
+homepage.workspace = true
+readme = "README.md"
+keywords = ["dynamodb", "database", "nosql", "server", "postgresql"]
+categories = ["database-implementations", "command-line-utilities"]
[[bin]]
name = "extenddb"
@@ -45,4 +51,4 @@ extenddb-config = { workspace = true }
sqlx = { workspace = true }
tokio = { workspace = true }
uuid = { workspace = true }
-extenddb-storage = { workspace = true, features = ["test-util"] }
+extenddb-storage = { path = "../storage", features = ["test-util"] }
diff --git a/crates/bin/LICENSE b/crates/bin/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/bin/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/bin/NOTICE b/crates/bin/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/bin/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/bin/README.md b/crates/bin/README.md
new file mode 100644
index 000000000..e1e898e91
--- /dev/null
+++ b/crates/bin/README.md
@@ -0,0 +1,91 @@
+# ExtendDB
+
+> **ExtendDB is an independent open source project managed by Amazon Web Services. It is not Amazon DynamoDB and does
+> not contain any DynamoDB source code.** "DynamoDB" is a trademark of Amazon.com, Inc. ExtendDB is a clean-room
+> implementation that speaks the DynamoDB wire protocol. Behavioral differences from the real service are documented
+> in [Differences from DynamoDB](https://github.com/ExtendDB/extenddb/blob/main/docs/differences-from-dynamodb.md).
+
+ExtendDB is a DynamoDB-compatible API server. Any AWS SDK, CLI or tool that works with DynamoDB works with ExtendDB.
+This crate builds the `extenddb` binary.
+
+## Install
+
+You need Rust 1.88 or later and a C compiler. Each build contains one storage backend, which a Cargo feature selects:
+
+| Backend | Command |
+|---|---|
+| PostgreSQL (the default) | `cargo install extenddb --locked` |
+| MongoDB | `cargo install extenddb --locked --no-default-features --features mongodb` |
+| Apache Cassandra (experimental) | `cargo install extenddb --locked --no-default-features --features cassandra` |
+| SQLite dev server | `cargo install extenddb --locked --no-default-features --features sqlite,dev-mode` |
+
+Every build installs a binary named `extenddb`, so you can have one of them installed at a time.
+
+With `--locked`, Cargo builds from the `Cargo.lock` inside the crate, which holds the dependency versions that the
+project tests. Without it, Cargo resolves every dependency again and can choose versions that the project never
+tested.
+
+## Run the dev server
+
+The SQLite dev build needs no database and no configuration file:
+
+```bash
+extenddb serve --foreground
+```
+
+It serves plain HTTP on `127.0.0.1:18443`. It refuses a bind address other than loopback unless the environment sets
+`EXTENDDB_DEV_ALLOW_ANY_BIND=1`, as the dev container image does for its own network.
+
+When it starts with no database, it creates `extenddb.sqlite` and a `backups/` directory in the working directory. It
+also adds an `admin` user to the database and prints the admin password. At startup it prints the AWS example key pair,
+which it always accepts:
+
+```bash
+AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE \
+AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
+aws dynamodb list-tables --region us-east-1 --endpoint-url http://127.0.0.1:18443
+```
+
+The dev build has no TLS, and its authorization is open: a request signed with any key that the server holds can run
+every operation. Use it for local development and tests only, and never store data in it that you need to keep
+private.
+
+Without `--foreground`, `extenddb serve` runs as a daemon and writes its log to syslog. Daemon mode works on Unix
+systems only, so on Windows always pass `--foreground`.
+
+## Run a server build
+
+The PostgreSQL, MongoDB and Cassandra builds each need a running database:
+
+- PostgreSQL 14 or later
+- MongoDB 7.0 or later, configured as a replica set
+- Apache Cassandra 4.1 or later
+
+Run `extenddb init` once to create the catalog, the admin user, a TLS certificate and `extenddb.toml`. Then run
+`extenddb serve`, which listens with TLS on port 18443.
+[Getting started](https://github.com/ExtendDB/extenddb/blob/main/docs/getting-started.md) describes each step and
+each backend's options.
+
+## Other ways to run ExtendDB
+
+- Docker images, with no Rust toolchain needed:
+ [`extenddb/extenddb-postgres`](https://hub.docker.com/r/extenddb/extenddb-postgres),
+ [`extenddb/extenddb-mongodb`](https://hub.docker.com/r/extenddb/extenddb-mongodb) and
+ [`extenddb/extenddb-dev`](https://hub.docker.com/r/extenddb/extenddb-dev).
+- [`@extenddb/dev`](https://www.npmjs.com/package/@extenddb/dev) on npm starts a prebuilt dev server from a Node.js
+ test suite.
+
+The [documentation](https://github.com/ExtendDB/extenddb/tree/main/docs) covers configuration, operations and the API
+surface.
+
+## The library crates
+
+The `extenddb-*` library crates on crates.io are the parts of this binary. Their Rust APIs carry no stability promise,
+and any release can change them. Each release publishes every crate at the same version, and each crate requires its
+ExtendDB dependencies at exactly that version.
+
+## License
+
+ExtendDB is licensed under the Apache License 2.0. See
+[LICENSE](https://github.com/ExtendDB/extenddb/blob/main/LICENSE) and
+[NOTICE](https://github.com/ExtendDB/extenddb/blob/main/NOTICE).
diff --git a/crates/bin/build.rs b/crates/bin/build.rs
index 302d0118b..e973f4375 100755
--- a/crates/bin/build.rs
+++ b/crates/bin/build.rs
@@ -7,6 +7,8 @@
//! time so `extenddb version` can report them without runtime git access.
use std::env;
+use std::fs;
+use std::path::Path;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -14,12 +16,15 @@ fn main() {
println!("cargo:rerun-if-env-changed=EXTENDDB_GIT_HASH");
println!("cargo:rerun-if-env-changed=EXTENDDB_BUILD_TIME");
- // Git commit hash (short). Container builds exclude .git from the context,
- // so the release workflow injects EXTENDDB_GIT_HASH explicitly. Local builds
- // retain the convenient git fallback.
+ // Git commit hash. Container builds exclude .git from the context, so the
+ // release workflow injects EXTENDDB_GIT_HASH (the full SHA) explicitly. A
+ // build of the published crate has no git repository either, so the
+ // commit `cargo package` recorded is read next. Local builds retain the
+ // convenient git fallback (short form).
let git_hash = env::var("EXTENDDB_GIT_HASH")
.ok()
.filter(|value| !value.trim().is_empty())
+ .or_else(packaged_commit)
.unwrap_or_else(|| {
Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
@@ -78,3 +83,31 @@ fn main() {
// file in the package changes, while the env directives above make metadata
// overrides participate in Cargo's normal rebuild tracking.
}
+
+/// The commit of a build from the published crate.
+///
+/// `cargo package` writes `.cargo_vcs_info.json` beside the manifest, with
+/// `"sha1": "<40 hex digits>"` for the packaged commit and `"dirty": true`
+/// when the tree had uncommitted changes. The file exists only in a packaged
+/// crate, so a build in the repository returns `None` here and falls back to
+/// git. A file without a well-formed SHA also returns `None`.
+fn packaged_commit() -> Option {
+ let dir = env::var_os("CARGO_MANIFEST_DIR")?;
+ let text = fs::read_to_string(Path::new(&dir).join(".cargo_vcs_info.json")).ok()?;
+ let after_key = &text[text.find("\"sha1\"")? + "\"sha1\"".len()..];
+ let value = &after_key[after_key.find('"')? + 1..];
+ let sha = value.get(..40)?;
+ if !sha.bytes().all(|b| b.is_ascii_hexdigit()) || !value[40..].starts_with('"') {
+ return None;
+ }
+ let dirty = text.find("\"dirty\"").is_some_and(|at| {
+ let rest = text[at + "\"dirty\"".len()..].trim_start();
+ rest.strip_prefix(':')
+ .is_some_and(|value| value.trim_start().starts_with("true"))
+ });
+ Some(if dirty {
+ format!("{sha}-dirty")
+ } else {
+ sha.to_owned()
+ })
+}
diff --git a/crates/bin/src/main.rs b/crates/bin/src/main.rs
index 15876a052..46b78f315 100755
--- a/crates/bin/src/main.rs
+++ b/crates/bin/src/main.rs
@@ -28,7 +28,7 @@
compile_error!(
"the `postgres`, `mongodb`, `sqlite`, and `cassandra` features are mutually exclusive: \
a thin bin installs exactly one backend (e.g. build the MongoDB binary with \
- `--no-default-features --features mongodb)"
+ `--no-default-features --features mongodb`)"
);
#[cfg(not(any(
diff --git a/crates/cache/Cargo.toml b/crates/cache/Cargo.toml
index 3b180df9d..a4d0db686 100644
--- a/crates/cache/Cargo.toml
+++ b/crates/cache/Cargo.toml
@@ -6,7 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
-description = "In-memory stale-while-revalidate cache primitive used by the auth and storage layers."
+description = "In-memory stale-while-revalidate cache used by the ExtendDB auth and server layers. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[features]
# `test-util` exposes test-only helpers like `SwrCache::run_pending_tasks`
diff --git a/crates/cache/LICENSE b/crates/cache/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/cache/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/cache/NOTICE b/crates/cache/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/cache/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml
index f9f086124..841ffb944 100644
--- a/crates/config/Cargo.toml
+++ b/crates/config/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "Configuration types and loading of the ExtendDB server. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[features]
# Developer mode: `is_tls_enabled` reports plain HTTP regardless of the
diff --git a/crates/config/LICENSE b/crates/config/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/config/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/config/NOTICE b/crates/config/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/config/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 75495dda6..9b8e71939 100755
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "Core types, validation, errors and limits of the ExtendDB server. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[dependencies]
serde = { workspace = true }
diff --git a/crates/core/LICENSE b/crates/core/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/core/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/core/NOTICE b/crates/core/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/core/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml
index bc33e7ff3..40682b374 100755
--- a/crates/engine/Cargo.toml
+++ b/crates/engine/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "Operation dispatch, input validation and DynamoDB error mapping of the ExtendDB server. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[features]
default = ["partiql"]
@@ -44,4 +47,4 @@ futures = { workspace = true }
# `start_paused` for the tests of timers that must not depend on the wall clock.
tokio = { workspace = true, features = ["test-util"] }
# A storage engine for the tests that need an `OperationContext`.
-extenddb-storage-sqlite = { workspace = true }
+extenddb-storage-sqlite = { path = "../storage-sqlite" }
diff --git a/crates/engine/LICENSE b/crates/engine/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/engine/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/engine/NOTICE b/crates/engine/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/engine/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml
index 02e5c992b..635ea2130 100755
--- a/crates/server/Cargo.toml
+++ b/crates/server/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "HTTP layer of the ExtendDB server, which serves the DynamoDB wire protocol. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[features]
mongodb-test-hooks = []
@@ -60,11 +63,11 @@ syslog-tracing = { workspace = true }
libc = { workspace = true }
[dev-dependencies]
-extenddb-cache = { workspace = true, features = ["test-util"] }
+extenddb-cache = { path = "../cache", features = ["test-util"] }
# The login tests count bcrypt checks with `count_verifications` against an
# in-memory SQLite catalog.
-extenddb-storage = { workspace = true, features = ["test-util"] }
-extenddb-storage-sqlite = { workspace = true }
+extenddb-storage = { path = "../storage", features = ["test-util"] }
+extenddb-storage-sqlite = { path = "../storage-sqlite" }
sqlx = { workspace = true, features = ["sqlite"] }
tempfile = "3"
tokio = { workspace = true, features = ["test-util"] }
diff --git a/crates/server/LICENSE b/crates/server/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/server/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/server/NOTICE b/crates/server/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/server/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/server/src/serve.rs b/crates/server/src/serve.rs
index 8505c0b75..64f015b1b 100644
--- a/crates/server/src/serve.rs
+++ b/crates/server/src/serve.rs
@@ -47,7 +47,9 @@ pub struct BuildInfo {
/// Package version of the deployed binary (e.g. `env!("CARGO_PKG_VERSION")`
/// from the bin crate — not from a library crate, whose version may drift).
pub version: &'static str,
- /// Short git commit hash of the build (e.g. `env!("EXTENDDB_GIT_HASH")`).
+ /// Git commit hash of the build (e.g. `env!("EXTENDDB_GIT_HASH")`): the
+ /// full SHA in release images and in builds from crates.io, the short form
+ /// in local builds.
pub git_hash: &'static str,
/// Build timestamp (e.g. `env!("EXTENDDB_BUILD_TIME")`).
pub build_time: &'static str,
diff --git a/crates/storage-cassandra/Cargo.toml b/crates/storage-cassandra/Cargo.toml
index ad9bb4b7e..fdcebf3dd 100644
--- a/crates/storage-cassandra/Cargo.toml
+++ b/crates/storage-cassandra/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "Apache Cassandra storage backend of the ExtendDB server (experimental). Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[dependencies]
extenddb-core = { workspace = true }
@@ -27,6 +30,9 @@ bigdecimal = { workspace = true }
crc32fast = { workspace = true }
zeroize = { workspace = true }
cdrs-tokio = { workspace = true }
+# Not used in the code. It holds cdrs-tokio's protocol crate on 4.0.x. The
+# root Cargo.toml gives the reason.
+cassandra-protocol = { workspace = true }
chrono = { workspace = true }
[features]
@@ -39,6 +45,6 @@ test-util = ["extenddb-storage/test-util"]
test-hooks = ["extenddb-storage/test-hooks"]
[dev-dependencies]
-extenddb-storage = { workspace = true, features = ["test-util"] }
+extenddb-storage = { path = "../storage", features = ["test-util"] }
extenddb-storage-cassandra = { path = ".", features = ["test-util"] }
tokio = { workspace = true, features = ["test-util"] }
diff --git a/crates/storage-cassandra/LICENSE b/crates/storage-cassandra/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/storage-cassandra/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/storage-cassandra/NOTICE b/crates/storage-cassandra/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/storage-cassandra/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/storage-mongodb/Cargo.toml b/crates/storage-mongodb/Cargo.toml
index 510805d92..b34203e39 100644
--- a/crates/storage-mongodb/Cargo.toml
+++ b/crates/storage-mongodb/Cargo.toml
@@ -7,6 +7,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "MongoDB storage backend of the ExtendDB server. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[features]
# Enables deterministic coordination hooks used only by the MongoDB integration
@@ -75,7 +78,7 @@ proptest = "1"
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
# The checksum test checks this backend's migration table with the framework's
# test helper.
-extenddb-storage = { workspace = true, features = ["test-util"] }
+extenddb-storage = { path = "../storage", features = ["test-util"] }
# The crate's snapshot tests need `snapshot-test-hooks` and its migration tests
# need `test-util`; a dev-dependency on itself turns both on for test builds
# only.
diff --git a/crates/storage-mongodb/LICENSE b/crates/storage-mongodb/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/storage-mongodb/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/storage-mongodb/NOTICE b/crates/storage-mongodb/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/storage-mongodb/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/storage-postgres/Cargo.toml b/crates/storage-postgres/Cargo.toml
index b2c47b590..fda1a04b6 100755
--- a/crates/storage-postgres/Cargo.toml
+++ b/crates/storage-postgres/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "PostgreSQL storage backend of the ExtendDB server. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[features]
# Compiles the snapshot page-size setter the consistency test uses and the
@@ -22,14 +25,14 @@ test-util = ["extenddb-storage/test-util"]
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
# The checksum test checks this backend's migration table with the framework's
# test helper.
-extenddb-storage = { workspace = true, features = ["test-util"] }
+extenddb-storage = { path = "../storage", features = ["test-util"] }
# The crate's own tests need `test-hooks` and `test-util`; a dev-dependency on
# itself turns both features on for test builds only.
extenddb-storage-postgres = { path = ".", features = ["test-hooks", "test-util"] }
# The store identity tests run the engine's checks over two engines on one
# scratch catalog and two filesystem stores.
-extenddb-engine = { workspace = true }
-extenddb-backup-store = { workspace = true }
+extenddb-engine = { path = "../engine" }
+extenddb-backup-store = { path = "../backup-store" }
tempfile = "3"
[dependencies]
diff --git a/crates/storage-postgres/LICENSE b/crates/storage-postgres/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/storage-postgres/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/storage-postgres/NOTICE b/crates/storage-postgres/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/storage-postgres/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/storage-sqlite/Cargo.toml b/crates/storage-sqlite/Cargo.toml
index 96a432a1a..e9a6ec2b8 100644
--- a/crates/storage-sqlite/Cargo.toml
+++ b/crates/storage-sqlite/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "SQLite storage backend of the ExtendDB server. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[features]
# Compile the in-memory (`:memory:`) database in as the default storage path,
@@ -41,12 +44,12 @@ libc = { workspace = true }
libsqlite3-sys = { workspace = true }
[dev-dependencies]
-extenddb-storage = { workspace = true, features = ["test-util"] }
+extenddb-storage = { path = "../storage", features = ["test-util"] }
# `tests/backup_job_panic.rs` drives the engine's CreateBackup job and
# reconciler against a `SqliteEngine` (the engine depends on `extenddb-storage`,
# not on this crate, so the edge is acyclic).
-extenddb-engine = { workspace = true }
-extenddb-backup-store = { workspace = true }
+extenddb-engine = { path = "../engine" }
+extenddb-backup-store = { path = "../backup-store" }
# The slow-statement test sets sqlx's threshold, a `log` level, and prints
# every tracing event of a child process.
log = "0.4"
diff --git a/crates/storage-sqlite/LICENSE b/crates/storage-sqlite/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/storage-sqlite/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/storage-sqlite/NOTICE b/crates/storage-sqlite/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/storage-sqlite/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml
index a024143df..f151c3206 100755
--- a/crates/storage/Cargo.toml
+++ b/crates/storage/Cargo.toml
@@ -6,6 +6,9 @@ version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
+description = "Storage traits that every ExtendDB backend implements, with the catalog migration framework and the storage code the backends share. Internal crate of the extenddb binary, with no API stability promise."
+repository.workspace = true
+homepage.workspace = true
[features]
# `test-util` exposes `migrations::check_released`, the checksum test each
diff --git a/crates/storage/LICENSE b/crates/storage/LICENSE
new file mode 120000
index 000000000..30cff7403
--- /dev/null
+++ b/crates/storage/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/storage/NOTICE b/crates/storage/NOTICE
new file mode 120000
index 000000000..fb376cfaa
--- /dev/null
+++ b/crates/storage/NOTICE
@@ -0,0 +1 @@
+../../NOTICE
\ No newline at end of file
diff --git a/devtools/check-crate-packages b/devtools/check-crate-packages
new file mode 100755
index 000000000..8eae2ed5a
--- /dev/null
+++ b/devtools/check-crate-packages
@@ -0,0 +1,267 @@
+#!/usr/bin/env python3
+# Copyright 2026 ExtendDB contributors
+# SPDX-License-Identifier: Apache-2.0
+"""Check the workspace crates for crates.io before anything is published.
+
+ cargo package --workspace --no-verify --locked
+ devtools/check-crate-packages [--root DIR] [--packages DIR]
+
+The first command writes one `.crate` file per published member into the
+`package` directory of cargo's target directory. On a tree with uncommitted
+changes, it also needs `--allow-dirty`. This script needs Python 3.11 or
+later. It reads the members from `cargo metadata`, so cargo resolves globs,
+`exclude`, inherited fields and renamed dependencies. It then fails, with
+one line per problem, unless all of the following hold for each member that
+crates.io receives (one without `publish = false`):
+
+- Each dependency on another workspace crate, normal or build, requires
+ that crate's version exactly (`=X.Y.Z`). A caret requirement would let a
+ fresh install combine crates of two releases. A pin left at the previous
+ version after a release bump also fails every cargo command that resolves
+ dependencies.
+- Each dev-dependency on a workspace crate has a path and no version, so
+ cargo drops it from the published manifest. With a version, the
+ dev-dependencies between extenddb-engine and extenddb-storage-sqlite form
+ a cycle, and crates.io can publish neither crate first.
+- The member has a package at its version.
+- The package holds LICENSE and NOTICE with the bytes of the root files.
+ Each crate links both from the root, and the Apache License 2.0 requires a
+ redistributor to pass both on.
+- The packaged manifest has a description, a license and a repository. It
+ requires each workspace crate it depends on at exactly that crate's
+ version, and it has no dev-dependency on a workspace crate.
+- The packaged manifest passes the field rules of crates.io's publish
+ handler: a description of at most 1000 bytes, at most 5 keywords of at
+ most 20 bytes in crates.io's keyword format, and at most 5 categories.
+ The category slugs are not checked against crates.io's list.
+- The package is inside the default limits in crates.io's
+ `src/config/publish_limits.rs`. The limits cover the upload size, the
+ unpacked size, the number of tarball entries, the manifest size, and the
+ number of dependencies and features.
+
+Exit 0 when every check passes, 1 otherwise.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import subprocess
+import sys
+import tarfile
+from pathlib import Path
+
+try:
+ import tomllib
+except ModuleNotFoundError: # Python 3.10 and older
+ sys.exit("error: devtools/check-crate-packages needs Python 3.11 or later (for tomllib)")
+
+# crates.io's PublishLimitsConfig defaults (src/config/publish_limits.rs).
+UPLOAD_SIZE = 10 * 1024 * 1024
+UNPACK_SIZE = 512 * 1024 * 1024
+TARBALL_ENTRIES = 30_000
+METADATA_FILE_SIZE = 1024 * 1024
+DEPENDENCIES = 500
+FEATURES = 300
+
+# crates.io's publish handler (src/controllers/krate/publish.rs, and
+# Keyword::valid_name in crates/crates_io_database/src/models/keyword.rs).
+DESCRIPTION_LENGTH = 1000
+KEYWORDS = 5
+KEYWORD_LENGTH = 20
+CATEGORIES = 5
+
+DEPENDENCY_TABLES = ("dependencies", "build-dependencies", "dev-dependencies")
+
+
+def cargo_metadata(root: Path) -> dict:
+ """The workspace as cargo resolves it, without the dependency graph."""
+ cargo = os.environ.get("CARGO", "cargo")
+ try:
+ result = subprocess.run(
+ [cargo, "metadata", "--no-deps", "--format-version", "1"],
+ cwd=root, capture_output=True, text=True, check=False,
+ )
+ except OSError as error:
+ sys.exit(f"error: cannot run `{cargo} metadata` in {root}: {error}")
+ if result.returncode != 0:
+ sys.exit(f"error: `cargo metadata` failed in {root}:\n{result.stderr}")
+ return json.loads(result.stdout)
+
+
+def is_published(package: dict) -> bool:
+ """`publish = false` reaches cargo metadata as an empty registry list."""
+ return package.get("publish") != []
+
+
+def check(root: Path, packages: Path | None = None) -> list[str]:
+ """Return one message per problem; an empty list means every check passed."""
+ metadata = cargo_metadata(root)
+ members = {package["name"]: package for package in metadata["packages"]}
+ versions = {name: package["version"] for name, package in members.items()}
+ if packages is None:
+ packages = Path(metadata["target_directory"]) / "package"
+ problems: list[str] = []
+
+ for name, package in sorted(members.items()):
+ if not is_published(package):
+ continue
+ manifest = os.path.relpath(package["manifest_path"], root)
+ for dependency in package["dependencies"]:
+ target = dependency["name"]
+ if target not in members:
+ continue
+ key = dependency.get("rename") or target
+ kind = dependency.get("kind") or "normal"
+ if kind == "dev":
+ if dependency.get("path") is None or dependency["req"] != "*":
+ problems.append(
+ f"{manifest}: dev-dependency {key} requires {dependency['req']!r}; declare it "
+ f"by path with no version or workspace key, so cargo drops it from the "
+ f"published manifest"
+ )
+ elif dependency["req"] != f"={versions[target]}":
+ problems.append(
+ f"{manifest}: {kind} dependency {key} requires {dependency['req']!r}; it must be "
+ f'"={versions[target]}" (in the root [workspace.dependencies] for a '
+ f"`workspace = true` entry)"
+ )
+
+ try:
+ root_files = {f: (root / f).read_bytes() for f in ("LICENSE", "NOTICE")}
+ except OSError as error:
+ return problems + [f"cannot read the root LICENSE or NOTICE: {error}"]
+ for name, package in sorted(members.items()):
+ if not is_published(package):
+ continue
+ crate = packages / f"{name}-{package['version']}.crate"
+ if not crate.is_file():
+ problems.append(f"{crate}: missing; run `cargo package --workspace --no-verify --locked` first")
+ continue
+ problems.extend(check_package(crate, name, package["version"], versions, root_files))
+ return problems
+
+
+def dependency_tables(manifest: dict):
+ """Yield (table label, table) for each dependency table of a manifest,
+ including the target-specific ones."""
+ for kind in DEPENDENCY_TABLES:
+ if kind in manifest:
+ yield kind, manifest[kind]
+ for target, tables in manifest.get("target", {}).items():
+ for kind in DEPENDENCY_TABLES:
+ if kind in tables:
+ yield f"target.{target}.{kind}", tables[kind]
+
+
+def check_package(crate: Path, name: str, version: str, versions: dict[str, str], root_files: dict[str, bytes]) -> list[str]:
+ """Check one `.crate` file against the root files and crates.io's limits.
+
+ `versions` maps each workspace crate to its version."""
+ problems: list[str] = []
+ size = crate.stat().st_size
+ if size > UPLOAD_SIZE:
+ problems.append(f"{crate.name}: {size} bytes, over the crates.io upload limit of {UPLOAD_SIZE}")
+ prefix = f"{name}-{version}/"
+ with tarfile.open(crate, "r:gz") as tar:
+ entries = tar.getmembers()
+ if len(entries) > TARBALL_ENTRIES:
+ problems.append(f"{crate.name}: {len(entries)} entries, over the crates.io limit of {TARBALL_ENTRIES}")
+ unpacked = sum(entry.size for entry in entries)
+ if unpacked > UNPACK_SIZE:
+ problems.append(f"{crate.name}: {unpacked} bytes unpacked, over the crates.io limit of {UNPACK_SIZE}")
+ files = {entry.name: entry for entry in entries if entry.isfile()}
+ for filename, expected in root_files.items():
+ entry = files.get(prefix + filename)
+ if entry is None:
+ problems.append(f"{crate.name}: no {filename}; link crates//{filename} to the root file")
+ elif tar.extractfile(entry).read() != expected:
+ problems.append(f"{crate.name}: {filename} differs from the root {filename}")
+ entry = files.get(prefix + "Cargo.toml")
+ if entry is None:
+ return problems + [f"{crate.name}: no Cargo.toml"]
+ if entry.size > METADATA_FILE_SIZE:
+ problems.append(f"{crate.name}: Cargo.toml is {entry.size} bytes, over the crates.io limit of {METADATA_FILE_SIZE}")
+ manifest = tomllib.loads(tar.extractfile(entry).read().decode())
+ package = manifest.get("package", {})
+ for field in ("description", "license", "repository"):
+ if not str(package.get(field, "")).strip():
+ problems.append(f"{crate.name}: the packaged manifest has no {field}")
+ problems.extend(check_metadata(crate.name, package))
+ count = 0
+ for label, table in dependency_tables(manifest):
+ count += len(table)
+ for key, spec in table.items():
+ target = spec.get("package", key) if isinstance(spec, dict) else key
+ if target not in versions:
+ continue
+ if label.endswith("dev-dependencies"):
+ problems.append(
+ f"{crate.name}: [{label}] {key}: cargo kept this dev-dependency in the published "
+ f"manifest; declare it by path with no version"
+ )
+ continue
+ found = spec.get("version") if isinstance(spec, dict) else spec
+ if found != f"={versions[target]}":
+ problems.append(f'{crate.name}: [{label}] {key} requires {found!r}, not "={versions[target]}"')
+ if count > DEPENDENCIES:
+ problems.append(f"{crate.name}: {count} dependencies, over the crates.io limit of {DEPENDENCIES}")
+ features = manifest.get("features", {})
+ if len(features) > FEATURES:
+ problems.append(f"{crate.name}: {len(features)} features, over the crates.io limit of {FEATURES}")
+ for feature, enables in features.items():
+ if len(enables) > FEATURES:
+ problems.append(f"{crate.name}: feature {feature} enables {len(enables)} entries, over the crates.io limit of {FEATURES}")
+ return problems
+
+
+def valid_keyword(keyword: str) -> bool:
+ """crates.io's Keyword::valid_name: an ASCII letter or digit, then ASCII
+ letters, digits, `_`, `-` or `+`."""
+ def allowed(c: str) -> bool:
+ return c.isascii() and (c.isalnum() or c in "_-+")
+ return bool(keyword) and keyword[0].isascii() and keyword[0].isalnum() and all(allowed(c) for c in keyword[1:])
+
+
+def check_metadata(crate: str, package: dict) -> list[str]:
+ """The [package] fields crates.io refuses at upload. Lengths are in bytes,
+ as crates.io counts them. Category slugs are not checked against
+ crates.io's list here."""
+ problems: list[str] = []
+ description = str(package.get("description", ""))
+ if len(description.encode()) > DESCRIPTION_LENGTH:
+ problems.append(f"{crate}: the description is {len(description.encode())} bytes, over the crates.io limit of {DESCRIPTION_LENGTH}")
+ keywords = package.get("keywords", [])
+ if len(keywords) > KEYWORDS:
+ problems.append(f"{crate}: {len(keywords)} keywords, over the crates.io limit of {KEYWORDS}")
+ for keyword in keywords:
+ if len(keyword.encode()) > KEYWORD_LENGTH:
+ problems.append(f"{crate}: keyword {keyword!r} is over the crates.io limit of {KEYWORD_LENGTH} bytes")
+ elif not valid_keyword(keyword):
+ problems.append(f"{crate}: keyword {keyword!r} is not a valid crates.io keyword")
+ categories = package.get("categories", [])
+ if len(categories) > CATEGORIES:
+ problems.append(f"{crate}: {len(categories)} categories, over the crates.io limit of {CATEGORIES}")
+ return problems
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
+ parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent,
+ help="workspace root (default: this repository)")
+ parser.add_argument("--packages", type=Path,
+ help="directory of .crate files (default: the package directory of cargo's target directory)")
+ args = parser.parse_args()
+ problems = check(args.root.resolve(), args.packages)
+ for problem in problems:
+ print(f"error: {problem}")
+ if problems:
+ print(f"{len(problems)} problem(s) found")
+ return 1
+ print("ok: every published workspace crate is ready for crates.io")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/devtools/check_crate_packages_test.py b/devtools/check_crate_packages_test.py
new file mode 100644
index 000000000..4e12c23ff
--- /dev/null
+++ b/devtools/check_crate_packages_test.py
@@ -0,0 +1,396 @@
+# Copyright 2026 ExtendDB contributors
+# SPDX-License-Identifier: Apache-2.0
+"""Tests of devtools/check-crate-packages.
+
+Each test builds a two-crate cargo workspace and its `.crate` files in a
+temporary directory, then runs the script, which reads the workspace through
+`cargo metadata`. The script passes the complete workspace and names each
+problem a test introduces. The crates.io limits are tested by calling
+`check_package` with the limits lowered.
+
+ python3 -m unittest discover -s devtools -p '*_test.py'
+"""
+import importlib.util
+import io
+import os
+import subprocess
+import sys
+import tarfile
+import tempfile
+import textwrap
+import unittest
+from importlib.machinery import SourceFileLoader
+from pathlib import Path
+from unittest import mock
+
+SCRIPT = Path(__file__).resolve().parent / "check-crate-packages"
+VERSION = "1.2.3"
+
+ROOT_MANIFEST = f"""\
+[workspace]
+resolver = "3"
+members = ["crates/*"]
+
+[workspace.package]
+version = "{VERSION}"
+edition = "2021"
+
+[workspace.dependencies]
+example-a = {{ path = "crates/a", version = "={VERSION}" }}
+example-b = {{ path = "crates/b", version = "={VERSION}" }}
+"""
+
+MEMBER_A = """\
+[package]
+name = "example-a"
+version.workspace = true
+edition.workspace = true
+
+[dev-dependencies]
+example-b = { path = "../b" }
+"""
+
+MEMBER_B = """\
+[package]
+name = "example-b"
+version.workspace = true
+edition.workspace = true
+
+[dependencies]
+example-a = { workspace = true }
+"""
+
+
+def load_script():
+ """Import the script, which has no .py suffix, as a module."""
+ loader = SourceFileLoader("check_crate_packages", str(SCRIPT))
+ spec = importlib.util.spec_from_loader(loader.name, loader)
+ module = importlib.util.module_from_spec(spec)
+ loader.exec_module(module)
+ return module
+
+
+def packaged_manifest(name: str, extra: str = "", version: str = VERSION, **package) -> str:
+ """A manifest in the form `cargo package` writes. `package` overrides or,
+ with None, removes a [package] field; `extra` is appended as is."""
+ fields = {"name": name, "version": version, "description": "d", "license": "Apache-2.0",
+ "repository": "https://example.com"}
+ fields.update(package)
+ lines = ["[package]"] + [f'{k} = "{v}"' for k, v in fields.items() if v is not None]
+ return "\n".join(lines) + "\n" + textwrap.dedent(extra)
+
+
+B_DEPENDENCY = f'\n[dependencies.example-a]\nversion = "={VERSION}"\n'
+
+
+class CheckCratePackagesTest(unittest.TestCase):
+ def setUp(self) -> None:
+ self.tmp = tempfile.TemporaryDirectory()
+ self.root = Path(self.tmp.name)
+ self.packages = self.root / "target" / "package"
+ self.packages.mkdir(parents=True)
+ self.write("Cargo.toml", ROOT_MANIFEST)
+ self.member("a", MEMBER_A)
+ self.member("b", MEMBER_B)
+ self.write("LICENSE", "license text\n")
+ self.write("NOTICE", "notice text\n")
+ self.package("example-a", {"Cargo.toml": packaged_manifest("example-a")})
+ self.package("example-b", {"Cargo.toml": packaged_manifest("example-b", B_DEPENDENCY)})
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+
+ def write(self, relative: str, text: str) -> None:
+ path = self.root / relative
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(text)
+
+ def member(self, directory: str, manifest: str) -> None:
+ self.write(f"crates/{directory}/Cargo.toml", manifest)
+ self.write(f"crates/{directory}/src/lib.rs", "")
+
+ def package(self, name: str, files: dict, licenses: bool = True, version: str = VERSION,
+ into: Path | None = None) -> None:
+ """Write -.crate holding `files`, plus the root LICENSE
+ and NOTICE unless `licenses` is false."""
+ contents = dict(files)
+ if licenses:
+ contents.setdefault("LICENSE", (self.root / "LICENSE").read_bytes())
+ contents.setdefault("NOTICE", (self.root / "NOTICE").read_bytes())
+ directory = into or self.packages
+ directory.mkdir(parents=True, exist_ok=True)
+ with tarfile.open(directory / f"{name}-{version}.crate", "w:gz") as tar:
+ for filename, data in contents.items():
+ raw = data.encode() if isinstance(data, str) else data
+ info = tarfile.TarInfo(f"{name}-{version}/{filename}")
+ info.size = len(raw)
+ tar.addfile(info, io.BytesIO(raw))
+
+ def run_check(self, env: dict | None = None) -> tuple[int, str]:
+ environment = {k: v for k, v in os.environ.items() if k != "CARGO_TARGET_DIR"}
+ environment.update(env or {})
+ result = subprocess.run(
+ [sys.executable, str(SCRIPT), "--root", str(self.root)],
+ capture_output=True, text=True, env=environment, check=False,
+ )
+ return result.returncode, result.stdout + result.stderr
+
+ def assert_passes(self, env: dict | None = None) -> None:
+ code, output = self.run_check(env)
+ self.assertEqual(code, 0, output)
+ self.assertIn("ok: every published workspace crate is ready for crates.io", output)
+
+ def assert_fails_with(self, *fragments: str) -> None:
+ code, output = self.run_check()
+ self.assertEqual(code, 1, output)
+ for fragment in fragments:
+ self.assertIn(fragment, output)
+
+ # The workspace as cargo reads it.
+
+ def test_a_complete_workspace_passes(self) -> None:
+ self.assert_passes()
+
+ def test_a_workspace_pin_that_is_not_exact_fails(self) -> None:
+ self.write("Cargo.toml", ROOT_MANIFEST.replace(f'"crates/a", version = "={VERSION}"', f'"crates/a", version = "{VERSION}"'))
+ self.assert_fails_with(
+ f"crates/b/Cargo.toml: normal dependency example-a requires '^{VERSION}'", f'"={VERSION}"')
+
+ def test_a_workspace_pin_left_at_the_last_release_fails(self) -> None:
+ self.write("Cargo.toml", ROOT_MANIFEST.replace(f'"crates/a", version = "={VERSION}"', '"crates/a", version = "=1.2.2"'))
+ self.assert_fails_with("normal dependency example-a requires '=1.2.2'")
+
+ def test_a_renamed_dependency_is_checked_under_its_package_name(self) -> None:
+ self.member("b", MEMBER_B.replace("example-a = { workspace = true }",
+ f'alias = {{ package = "example-a", path = "../a", version = "{VERSION}" }}'))
+ self.assert_fails_with(f"normal dependency alias requires '^{VERSION}'")
+
+ def test_a_build_dependency_needs_the_exact_pin(self) -> None:
+ self.member("b", MEMBER_B + f'\n[build-dependencies]\nexample-a = {{ path = "../a", version = "{VERSION}" }}\n')
+ self.assert_fails_with(f"build dependency example-a requires '^{VERSION}'")
+
+ def test_an_internal_dev_dependency_through_the_workspace_fails(self) -> None:
+ self.member("a", MEMBER_A.replace('{ path = "../b" }', "{ workspace = true }"))
+ self.assert_fails_with(f"crates/a/Cargo.toml: dev-dependency example-b requires '={VERSION}'")
+
+ def test_an_internal_dev_dependency_with_a_version_fails(self) -> None:
+ self.member("a", MEMBER_A.replace('{ path = "../b" }', f'{{ path = "../b", version = "={VERSION}" }}'))
+ self.assert_fails_with("dev-dependency example-b requires")
+
+ def test_an_internal_dev_dependency_without_a_path_fails(self) -> None:
+ self.member("a", MEMBER_A.replace('{ path = "../b" }', '"*"'))
+ self.assert_fails_with("dev-dependency example-b requires '*'; declare it by path")
+
+ def test_a_target_specific_dev_dependency_is_checked(self) -> None:
+ self.member("a", MEMBER_A + "\n[target.'cfg(unix)'.dev-dependencies]\nexample-b = { workspace = true }\n")
+ self.assert_fails_with(f"dev-dependency example-b requires '={VERSION}'")
+
+ def test_a_missing_package_fails(self) -> None:
+ (self.packages / f"example-b-{VERSION}.crate").unlink()
+ self.assert_fails_with(f"example-b-{VERSION}.crate: missing")
+
+ def test_an_unpublished_member_needs_no_package_and_no_pins(self) -> None:
+ self.member("b", MEMBER_B.replace("edition.workspace = true", "edition.workspace = true\npublish = false")
+ .replace("{ workspace = true }", '{ path = "../a" }'))
+ (self.packages / f"example-b-{VERSION}.crate").unlink()
+ self.assert_passes()
+
+ def test_publish_inherited_from_the_workspace_is_read(self) -> None:
+ self.write("Cargo.toml", ROOT_MANIFEST.replace('edition = "2021"', 'edition = "2021"\npublish = false'))
+ self.member("b", MEMBER_B.replace("edition.workspace = true", "edition.workspace = true\npublish.workspace = true"))
+ (self.packages / f"example-b-{VERSION}.crate").unlink()
+ self.assert_passes()
+
+ def test_an_excluded_directory_is_not_a_member(self) -> None:
+ self.write("Cargo.toml", ROOT_MANIFEST.replace('members = ["crates/*"]', 'members = ["crates/*"]\nexclude = ["crates/x"]'))
+ self.write("crates/x/Cargo.toml", '[package]\nname = "example-x"\nversion = "0.1.0"\nedition = "2021"\n')
+ self.write("crates/x/src/lib.rs", "")
+ self.assert_passes()
+
+ def test_a_member_with_its_own_version_is_packaged_and_pinned_at_it(self) -> None:
+ self.write("Cargo.toml", ROOT_MANIFEST.replace(f'"crates/a", version = "={VERSION}"', '"crates/a", version = "=2.0.0"'))
+ self.member("a", MEMBER_A.replace("version.workspace = true", 'version = "2.0.0"'))
+ (self.packages / f"example-a-{VERSION}.crate").unlink()
+ self.package("example-a", {"Cargo.toml": packaged_manifest("example-a", version="2.0.0")}, version="2.0.0")
+ self.package("example-b", {"Cargo.toml": packaged_manifest("example-b", '\n[dependencies.example-a]\nversion = "=2.0.0"\n')})
+ self.assert_passes()
+
+ def test_cargo_target_dir_moves_the_packages(self) -> None:
+ moved = self.root / "elsewhere"
+ for crate in self.packages.iterdir():
+ moved.joinpath("package").mkdir(parents=True, exist_ok=True)
+ crate.rename(moved / "package" / crate.name)
+ self.assert_fails_with(f"example-a-{VERSION}.crate: missing")
+ self.assert_passes({"CARGO_TARGET_DIR": str(moved)})
+
+ # The packages.
+
+ def test_a_package_without_the_license_files_fails(self) -> None:
+ self.package("example-a", {"Cargo.toml": packaged_manifest("example-a")}, licenses=False)
+ self.assert_fails_with(f"example-a-{VERSION}.crate: no LICENSE", f"example-a-{VERSION}.crate: no NOTICE")
+
+ def test_a_license_file_that_differs_from_the_root_fails(self) -> None:
+ self.package("example-a", {"Cargo.toml": packaged_manifest("example-a"), "LICENSE": "other\n", "NOTICE": "other\n"})
+ self.assert_fails_with(f"example-a-{VERSION}.crate: LICENSE differs from the root LICENSE",
+ f"example-a-{VERSION}.crate: NOTICE differs from the root NOTICE")
+
+ def test_a_package_without_a_manifest_fails(self) -> None:
+ self.package("example-a", {"README.md": "r\n"})
+ self.assert_fails_with(f"example-a-{VERSION}.crate: no Cargo.toml")
+
+ def test_each_required_manifest_field_is_checked(self) -> None:
+ for field in ("description", "license", "repository"):
+ with self.subTest(field=field):
+ self.package("example-a", {"Cargo.toml": packaged_manifest("example-a", **{field: None})})
+ self.assert_fails_with(f"example-a-{VERSION}.crate: the packaged manifest has no {field}")
+
+ def test_each_packaged_dependency_table_needs_the_exact_pin(self) -> None:
+ tables = {
+ "dependencies": f'\n[dependencies.example-a]\nversion = "^{VERSION}"\n',
+ "build-dependencies": f'\n[build-dependencies.example-a]\nversion = "^{VERSION}"\n',
+ "target.cfg(unix).dependencies": f'\n[target."cfg(unix)".dependencies.example-a]\nversion = "^{VERSION}"\n',
+ "dependencies (renamed)": f'\n[dependencies.alias]\npackage = "example-a"\nversion = "^{VERSION}"\n',
+ "dependencies (string form)": f'\n[dependencies]\nexample-a = "^{VERSION}"\n',
+ }
+ for label, extra in tables.items():
+ with self.subTest(table=label):
+ self.package("example-b", {"Cargo.toml": packaged_manifest("example-b", extra)})
+ key = "alias" if "renamed" in label else "example-a"
+ table = label.split(" ")[0]
+ self.assert_fails_with(f"example-b-{VERSION}.crate: [{table}] {key} requires '^{VERSION}'")
+
+ def test_a_dev_dependency_cargo_kept_in_the_package_fails(self) -> None:
+ tables = {
+ "dev-dependencies": '\n[dev-dependencies.example-a]\nversion = "*"\n',
+ "target.cfg(unix).dev-dependencies": f'\n[target."cfg(unix)".dev-dependencies.example-a]\nversion = "={VERSION}"\n',
+ }
+ for label, extra in tables.items():
+ with self.subTest(table=label):
+ self.package("example-b", {"Cargo.toml": packaged_manifest("example-b", B_DEPENDENCY + extra)})
+ self.assert_fails_with(
+ f"example-b-{VERSION}.crate: [{label}] example-a: cargo kept this dev-dependency")
+
+ def test_a_blank_description_fails(self) -> None:
+ self.package("example-a", {"Cargo.toml": packaged_manifest("example-a", description=" ")})
+ self.assert_fails_with(f"example-a-{VERSION}.crate: the packaged manifest has no description")
+
+ def test_a_failing_cargo_metadata_fails(self) -> None:
+ self.write("Cargo.toml", "[workspace\n")
+ self.assert_fails_with("error: `cargo metadata` failed in")
+
+ def test_a_missing_root_fails_with_an_error_line(self) -> None:
+ self.root = self.root / "does-not-exist"
+ code, output = self.run_check()
+ self.assertEqual(code, 1, output)
+ self.assertIn("error: cannot run `cargo metadata`", output)
+ self.assertNotIn("Traceback", output)
+
+
+class LimitsTest(unittest.TestCase):
+ """check_package against crates.io's limits, lowered so a fixture can
+ cross each one."""
+
+ def setUp(self) -> None:
+ self.module = load_script()
+ self.tmp = tempfile.TemporaryDirectory()
+ self.crate = Path(self.tmp.name) / f"example-a-{VERSION}.crate"
+ self.root_files = {"LICENSE": b"l\n", "NOTICE": b"n\n"}
+
+ def tearDown(self) -> None:
+ self.tmp.cleanup()
+
+ def write_crate(self, manifest: str, extra_files: int = 0) -> None:
+ files = {"Cargo.toml": manifest.encode(), **self.root_files}
+ files.update({f"f{i}.rs": b"x" * 100 for i in range(extra_files)})
+ with tarfile.open(self.crate, "w:gz") as tar:
+ for filename, raw in files.items():
+ info = tarfile.TarInfo(f"example-a-{VERSION}/{filename}")
+ info.size = len(raw)
+ tar.addfile(info, io.BytesIO(raw))
+
+ def check(self, **limits) -> list[str]:
+ """check_package on the crate written last, with `limits` patched in."""
+ def run() -> list[str]:
+ return self.module.check_package(self.crate, "example-a", VERSION, {"example-a": VERSION}, self.root_files)
+
+ if not limits:
+ return run()
+ with mock.patch.multiple(self.module, **limits):
+ return run()
+
+ def problems(self, manifest: str, extra_files: int = 0, **limits) -> list[str]:
+ self.write_crate(manifest, extra_files)
+ return self.check(**limits)
+
+ def test_a_package_exactly_at_each_limit_passes_and_one_over_fails(self) -> None:
+ extra = ('\n[dependencies.x]\nversion = "1"\n\n[dev-dependencies.y]\nversion = "1"\n'
+ '\n[features]\none = ["two", "dep:x"]\ntwo = []\n')
+ manifest = packaged_manifest("example-a", extra)
+ self.write_crate(manifest, 3)
+ with tarfile.open(self.crate, "r:gz") as tar:
+ entries = tar.getmembers()
+ measured = {
+ "UPLOAD_SIZE": self.crate.stat().st_size,
+ "TARBALL_ENTRIES": len(entries),
+ "UNPACK_SIZE": sum(entry.size for entry in entries),
+ "METADATA_FILE_SIZE": len(manifest.encode()),
+ "DEPENDENCIES": 2,
+ # Two features, and feature `one` enables two entries.
+ "FEATURES": 2,
+ }
+ for limit, value in measured.items():
+ with self.subTest(limit=limit):
+ self.assertEqual(self.check(**{limit: value}), [])
+ self.assertNotEqual(self.check(**{limit: value - 1}), [])
+
+ def test_the_crates_io_field_rules_are_checked(self) -> None:
+ at_the_limits = {"description": "d" * 1000, "keywords": ["a", "b+c", "d_e", "f-g", "h" * 20],
+ "categories": ["a", "b", "c", "d", "e"]}
+ self.assertEqual(self.module.check_metadata("x.crate", at_the_limits), [])
+ cases = {
+ "the description is 1001 bytes": {"description": "d" * 1001},
+ "6 keywords, over": {"keywords": list("abcdef")},
+ "is over the crates.io limit of 20 bytes": {"keywords": ["k" * 21]},
+ "'-k' is not a valid crates.io keyword": {"keywords": ["-k"]},
+ "'a b' is not a valid crates.io keyword": {"keywords": ["a b"]},
+ "'\u00e9t\u00e9' is not a valid crates.io keyword": {"keywords": ["\u00e9t\u00e9"]},
+ "'caf\u00e9' is not a valid crates.io keyword": {"keywords": ["caf\u00e9"]},
+ "6 categories, over": {"categories": list("abcdef")},
+ }
+ for fragment, package in cases.items():
+ with self.subTest(rule=fragment):
+ problems = self.module.check_metadata("x.crate", package)
+ self.assertTrue(any(fragment in p for p in problems), problems)
+
+ def test_the_field_rules_run_on_each_package(self) -> None:
+ manifest = packaged_manifest("example-a", 'keywords = ["a", "b", "c", "d", "e", "f"]\n')
+ self.assertEqual(self.problems(manifest), [f"example-a-{VERSION}.crate: 6 keywords, over the crates.io limit of 5"])
+
+ def test_a_package_inside_every_limit_passes(self) -> None:
+ self.assertEqual(self.problems(packaged_manifest("example-a")), [])
+
+ def test_each_limit_is_enforced(self) -> None:
+ features = '\n[features]\none = ["two", "three"]\ntwo = []\nthree = []\n'
+ dependencies = '\n[dependencies.x]\nversion = "1"\n\n[dev-dependencies.y]\nversion = "1"\n'
+ cases = {
+ "upload limit": ({"UPLOAD_SIZE": 10}, "", 0),
+ "entries, over the crates.io limit of 2": ({"TARBALL_ENTRIES": 2}, "", 0),
+ "bytes unpacked": ({"UNPACK_SIZE": 500}, "", 10),
+ "Cargo.toml is": ({"METADATA_FILE_SIZE": 20}, "", 0),
+ "2 dependencies, over": ({"DEPENDENCIES": 1}, dependencies, 0),
+ "3 features, over": ({"FEATURES": 2}, features, 0),
+ }
+ for fragment, (limits, extra, files) in cases.items():
+ with self.subTest(limit=fragment):
+ problems = self.problems(packaged_manifest("example-a", extra), files, **limits)
+ self.assertTrue(any(fragment in p for p in problems), problems)
+
+ def test_the_entries_one_feature_enables_are_counted(self) -> None:
+ features = '\n[features]\none = ["two", "dep:x", "x/serde"]\ntwo = []\n'
+ problems = self.problems(packaged_manifest("example-a", features), FEATURES=2)
+ self.assertEqual(
+ problems, [f"example-a-{VERSION}.crate: feature one enables 3 entries, over the crates.io limit of 2"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/docs/adr/0029-crates-io-packaging.md b/docs/adr/0029-crates-io-packaging.md
new file mode 100644
index 000000000..3db2c36e4
--- /dev/null
+++ b/docs/adr/0029-crates-io-packaging.md
@@ -0,0 +1,136 @@
+# ADR-0029: The workspace crates go to crates.io together, at one version with exact pins
+
+- Status: Accepted
+- Date: 2026-11-10
+- Deciders: @LeeroyHannigan
+
+## Context
+
+Today a user builds ExtendDB from a clone with `cargo build --release`, or
+runs a container image or the npm launcher. Publishing to crates.io adds the
+short form `cargo install extenddb --locked`, an entry in the registry's
+search, and docs.rs pages for the library crates. These facts constrain how
+the workspace can publish:
+
+- crates.io refuses a dependency that is not on crates.io. The one exception
+ is a dev-dependency without a version, which Cargo removes from the
+ published manifest. Cargo drops the path of a path dependency when it
+ packages a crate, so each path dependency also needs a version. The `extenddb` binary
+ depends on the other 13 workspace crates, so publishing the binary means
+ publishing all 14.
+- On main at e681b8bf2, `cargo package --workspace` stopped at its first crate
+ with an internal dependency: "all dependencies must have a version
+ requirement specified when packaging. dependency `extenddb-cache` does not
+ specify a version".
+- `extenddb-engine` and `extenddb-storage-sqlite` are dev-dependencies of each
+ other. With a version on every internal dependency, packaging stopped with
+ "no matching package named `extenddb-engine` found". Each of the two crates
+ must exist on crates.io before the other.
+- ExtendDB patch releases change the Rust APIs between its crates. For a 0.x
+ version, Cargo reads the requirement `0.1.14` as "0.1.14 or any later
+ 0.1.x".
+- A build contains exactly one storage backend. `crates/bin/src/main.rs`
+ rejects two backend features at compile time.
+- Without `--locked`, `cargo install` resolves every dependency again. Measured
+ on v0.1.14 from an empty directory: on Rust 1.88, the minimum supported
+ version, resolution chose `uuid` 1.29.2, which needs Rust 1.89, and the build
+ failed. With `resolver = "3"`, cargo resolved for Rust 1.88, chose `uuid`
+ 1.26.1, and the build passed. With either resolver, the fresh lock changed
+ the version of 173 of the 425 third-party crates that it shares with the
+ tested lock.
+- No package contained LICENSE or NOTICE. Section 4 of the Apache License 2.0
+ requires each redistributor to pass on a copy of the license and the NOTICE
+ attributions. A crate from crates.io would have reached its user without
+ either file.
+
+## Options Considered
+
+1. **Publish only the binary**: crates.io refuses it, because its
+ dependencies are path dependencies.
+2. **One binary crate per backend**, named like the container images:
+ `extenddb-postgres`, `extenddb-mongodb`, `extenddb-cassandra` and
+ `extenddb-dev`. Each installs with one short command, and the four binaries
+ can be installed together. The workspace gets 4 more crates, 18 in all. The rate
+ limit on new crates adds about 40 minutes to the first publish, and each
+ crate needs its own owners and trusted publisher entry. Two database tools
+ with millions of downloads each, diesel_cli and sqlx-cli, publish one
+ binary crate each and select databases with Cargo features.
+3. **One binary crate, `extenddb`, with a Cargo feature that selects the
+ backend**, as the repository builds it today. The crate README gives one
+ install command per backend.
+
+For the version requirements between the workspace crates:
+
+- Caret requirements (`0.1.14`): after the next release exists, a fresh
+ install of an `extenddb` release resolves each library crate to the next
+ release. The binary's own `main.rs` then builds against the next release's
+ `extenddb-app`, and their APIs differ.
+- Exact pins (`=0.1.14`): each release resolves only its own crates.
+
+## Decision
+
+All 14 workspace crates publish together at the workspace version, and each
+internal dependency requires exactly that version. `extenddb` stays the one
+binary crate, and a Cargo feature selects its backend.
+
+## Rationale
+
+- With exact pins, a fresh resolution of any release builds only the crates
+ of that release.
+- One binary crate follows the pattern of the two database tools above, and
+ it adds no crate, owner or trusted publisher entry.
+- Each crate declares its internal dev-dependencies by path, with no version.
+ Cargo drops them from the published manifests, which removes the cycle between
+ `extenddb-engine` and `extenddb-storage-sqlite`. Each crate's tests still
+ build in the repository with the same dependencies.
+- `resolver = "3"` makes a fresh resolution prefer versions that build on
+ Rust 1.88. Install instructions still say `--locked`, because the fresh
+ resolution is not the tested set.
+- Each crate directory links LICENSE and NOTICE to the root files. Cargo packs
+ the content of a linked file, and a link cannot drift from the root file as
+ a copy can.
+- The descriptions of the 13 library crates say that each is internal to the
+ `extenddb` binary, with no API stability promise. The versioning draft in
+ #897 lists the crate APIs under "Not covered", and RFC-0002 keeps the
+ stability policy of the `Storage` trait as its unresolved question 4.
+
+## Consequences
+
+- Each pull request runs `cargo package --workspace --no-verify --locked` and
+ `devtools/check-crate-packages`. The check fails on a pin that is not
+ exact, or on an internal dev-dependency with a version. It also fails on a
+ missing LICENSE, NOTICE or description, and on a package or manifest field
+ over crates.io's limits. A pin left at an older version also makes every
+ cargo command that resolves dependencies fail.
+- Compiling each package from its tarball takes minutes, so that build
+ belongs to the release workflow, before the upload, and not to each pull
+ request.
+- A release bump changes the 13 pins together with the workspace version.
+- Every release publishes all 14 crates, including the crates that did not
+ change.
+- A user can have one backend build of `extenddb` installed at a time.
+- A fresh resolution can choose a dependency release that breaks the build.
+ cassandra-protocol 4.1.0 did this for the Cassandra backend, so it and
+ cdrs-tokio carry upper bounds. `--locked` avoids the problem.
+- The tests of the published library crates build only in the repository.
+ Cargo drops the internal dev-dependencies, and some tests read fixtures
+ from other crates.
+- The license notices now list the workspace crates under the text of the
+ repository's LICENSE file, with a link to the repository.
+- Publishing is follow-up work. It needs a release workflow, and a first
+ publish with an API token, because trusted publishing cannot create a crate.
+ Each crate then needs its owners and a trusted publisher entry. Each crate
+ added to the workspace later needs its own first publish.
+- A version on crates.io can never be replaced. A yank only stops new lock
+ files from choosing it.
+
+---
+
+## License
+
+Copyright 2026 ExtendDB contributors. Licensed under the Apache License, Version 2.0.
+See [LICENSE](../../LICENSE) for the full text.
+
+This software is provided "as is" without warranty of any kind. ExtendDB is not
+affiliated with, endorsed by, or sponsored by Amazon Web Services. "DynamoDB" is
+a trademark of Amazon.com, Inc.
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 1fe3fe104..7b3963e0a 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -52,3 +52,4 @@ decision, write a new ADR.
| [0023](0023-catalog-migrations.md) | One list of catalog migrations for every backend, with a ledger in every store | Accepted |
| [0026](0026-credential-crypto-on-aws-lc.md) | Credential cryptography uses AWS-LC through aws-lc-rs | Accepted |
| [0027](0027-cassandra-index-row-version-stamps.md) | Cassandra: index rows carry the base-row version as their write timestamp | Accepted |
+| [0029](0029-crates-io-packaging.md) | The workspace crates go to crates.io together, at one version with exact pins | Accepted |
diff --git a/packaging/README.md b/packaging/README.md
index 85da98149..dc945f86a 100644
--- a/packaging/README.md
+++ b/packaging/README.md
@@ -95,11 +95,12 @@ cargo build --release --locked --no-default-features --features sqlite,dev-mode
Flow (mirrors the container release: candidate first, promotion is a
deliberate second step):
-1. Land the release state on `main` with the workspace version bumped and
- `npm/package.json` bumped to match, then tag that commit `vX.Y.Z` and push
- the tag. This is the same tag the container release consumes; the two
- publish workflows are dispatched independently against it. When the
- release ships a catalog migration, the same state raises
+1. Land the release state on `main`. Bump the workspace version and the 13
+ `=X.Y.Z` pins in the root `[workspace.dependencies]`, regenerate the four
+ license notices, and bump `npm/package.json` to match. Tag that commit
+ `vX.Y.Z` and push the tag. This is the same tag the container release
+ consumes; the two publish workflows are dispatched independently against
+ it. When the release ships a catalog migration, the same state raises
`RELEASED_THROUGH` to the last migration and pastes the checksum lines the
checksum test prints (CONTRIBUTING.md, "Adding a catalog migration"); the
gate jobs of the two container release workflows run